jolicode / automapper Goto Github PK
View Code? Open in Web Editor NEW:rocket: Very FAST :rocket: PHP AutoMapper with on the fly code generation
Home Page: https://automapper.jolicode.com/
License: MIT License
:rocket: Very FAST :rocket: PHP AutoMapper with on the fly code generation
Home Page: https://automapper.jolicode.com/
License: MIT License
This PR made me realize there is another problem, a little bit more complex to mitigate:
readonly class SomeClass
{
public function __construct(public int|null $foo = 1, public int $bar = 0)
{
}
}
$autoMapper->map([], SomeClass::class);
two problems here:
foo
defaults to null
whereas it should default to 1
bar
is not present, although the property has a default valueI'm not sure how we could fix this... maybe by adding the default value in the WriteMutator
?
Description
when using #[MapToAttribute]
, when the getter type is different from the property type, the automapper guesses the type from the property, although it should guess it from the getter's return type
How to reproduce
class SomeClass
{
public function __construct(private SomeEnum $enum){}
public function getEnum(#[MapToAttribute('foo) string $foo]): string
{
return "{$foo}-{$this->enum->value}";
}
}
$automapper->map(new SomeClass(SomeEnum::FOO, 'array'));
this code produces an error, because the generated mapper thinks SomeClass::getEnum()
will return an enum
Possible Solution
don't know yet, need to dig into the generator
Inspired from https://docs.automapper.org/en/stable/Flattening.html
The idea is to makes simpler DTO compared to your source entities (or whatever your source is).
Let's do a quick example:
// source
class User
{
public string $email;
}
class Order
{
public string $reference;
public User $customer;
}
// target
class OrderDTO
{
public string $reference;
public string $customerEmail;
}
$customer = new User();
$customer->email = '[email protected]';
$source = new Order();
$source->reference = 'FAC9123';
$source->customer = $customer;
dump($autoMapper->map($source, OrderDTO::class));
// Will output:
//
// OrderDTO {#14 ๐ฝ
// +reference: "FAC9123"
// +customerEmail: "[email protected]"
// }
Idea is to flatten the objects so they can be mapped to a property with a name matching their path, in this example we have "Order -> customer (User) -> email" that is flatten to "Order -> customerEmail"
Original issue: janephp/janephp#749
Reported by: @nikophil
Jane version(s) affected: 7.5.3
Description
a private property in the constructor without any getter/setter will not be mapped
How to reproduce
The following code will create an error, because the generated mapper will be empty
class SomeClass
{
public function __construct(
private string $someProp
){}
}
$automapper->map(['someProp' => 'foo'], SomeClass::class);
Possible Solution
dig in the generator and allow this kind of props
I think what is done by valinor with attributes is pretty nice:
https://twitter.com/Rommsteinz/status/1739673469724299392?t=5oVgUTC1atb1UacsHkB_bw
We should be able to leverage some concept like this in automapper
Today we have mappers that are generated based on their metadata. For each mapper we make a hash that is used to check if we need to re-generate it or not. This behavior is called hot-reload. It's enabled by default because in dev you want that feature.
Depending on the AutoMapper version, you probably have some changes on the mappers and want them to be re-generated that's why we think it would be nice to have the AutoMapper version within the generated hash so when the library version changes, it will force a mapper generation (see #11 (comment)).
When generating a Mapper, we will have stuff like this: \AutoMapper\MapperContext::isAllowedAttribute($context, 'scoreOnCreation', $fieldvalue_20 = $value->scoreOnCreation)
.
But if the field is not initialized before it will fail violently. We should handle non-initiliazed properties a better way, isset
can tell if a property is initialized or not.
Is it possible to transform an object/DTO to multiple target attributes ?
Source:
{
"name": "John DOE",
"job": {
"company": "MyCompany",
"title": "I am a developer"
}
}
// MainDTO
final class ProfileDTO
{
public string $name;
#[MapTo(name: '????', transformer: JobTransformer::class)]
#[ApiProperty(description: 'Job information.')]
public ?JobDTO $job = null;
}
// Sub DTO
final class JobDTO
{
#[ApiProperty(description: 'Company name.')]
public ?string $company = null;
public ?string $title = null;
}
// My entity
class Profile
{
#[ORM\Column(length: 255, nullable: true)]
private ?string $name = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $jobTitle = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $jobCompany = null;
}
class JobTransformer implements PropertyTransformerInterface
{
/**
* @param JobDTO $value
*/
public function transform(mixed $value, array|object $source, array $context): mixed
{
// ???
}
}
If we're using some native PHP type that wasn't handled in one of the current transformers, the error is not very explicit.
Tried recently with stuff like \DateInterval
or \Generate
and it failed violently.
Idea: try to improve errors so it's more explicit on what is the error and how to fix it.
Second time: add more native PHP stuff in the base AutoMapper transformers.
Original issue: janephp/janephp#666
Reported by: @ihmels
Jane version(s) affected: 7.3.1
Description
If no WriteMutator can be determined via MappingExtractor::getWriteMutator()
, but the property is writable according to PropertyInfoExtractorInterface::isWriteable()
, an error occurs in line 73 of SourceTargetMappingExtractor
, because $targetMutatorConstruct
is not checked for null
.
How to reproduce
In the following code snippet there is the property Attribute::$values
and the corresponding setter Attribute::setValues()
(actually this should be called "setValue", but I made a typo).
The PropertyInfoExtractorInterface
finds out with the ReflectionExtractor
that AttributeValue
s can be added with addValues()
, so PropertyInfoExtractorInterface:isWriteable()
returns true, but MappingExtractor::getWriteMutator()
return null
.
class Attribute
{
private Collection $values;
public function __construct()
{
$this->values = new ArrayCollection();
}
/**
* @return Collection<int, AttributeValue>
*/
public function getValues(): Collection
{
return $this->values;
}
public function addValues(AttributeValue $values): void
{
if (!$this->values->contains($values)) {
$this->values->add($values);
$values->setAttribute($this);
}
}
public function removeValues(AttributeValue $values): void
{
if ($this->values->removeElement($values)) {
// set the owning side to null (unless already changed)
if ($values->getAttribute() === $this) {
$values->setAttribute(null);
}
}
}
}
class AttributeValue {
// ...
}
Symfony 7
Php 8.2
Today, I attempted to test the package but encountered difficulty installing version 8.2.2.
When I executed composer require jolicode/automapper-bundle
, it installed version 8.1. However, I attempted to enforce version ^8.2 and encountered the following error:
Problem 1
- Root composer.json requires jolicode/automapper-bundle 8.2.2 (exact version match: 8.2.2 or 8.2.2.0), found jolicode/automapper-bundle[dev-feat/make-it-works-with-last-version, dev-release/v8.1.0, dev-fix/disallow-8.2, dev-main, 8.0.0, ..., 8.1.0] but it does not match the constraint.
I'm unsure how to configure the mapping in the YAML file with version 8.1.
Should I consider using the latest 9 beta instead?
See #56 (comment) for context
Idea is to have an object with exactly what we could have in context instead of using an array
Here is a list of things that would be nice to be available for a first version of the 9.0 :
List of things that could be nice, but could be added after the first release :
Is there a way to transform attributes in a specific order and retrieve a transformed field from a previous transformer to another one ?
Consider we have this DTO
class ProfileDTO
{
#[MapTo(name: 'source', transformer: SourceTransformer::class)]
public int $sourceId;
#[MapTo(name: 'city', transformer: CityTransformer::class)]
public string $cityName;
}
$profileDTO = new ProfileDTO();
$profileDTO->sourceId = 1;
$profileDTO->cityName = 'Nice';
$automapper = AutoMapper::create(propertyTransformers: [$sourceTransformer, $cityTransformer]);
/** @var Profile $profile */
$profile = $automapper->map($profileDTO, Profile::class);
class SourceTransformer implements PropertyTransformerInterface
{
public function __construct(private SourceRepository $sourceRepository) {}
/**
* @param int $value
* @return Source
*/
public function transform(mixed $value, array|object $source, array $context): mixed
{
return $this->sourceRepository->find($value);
}
}
class SourceTransformer implements PropertyTransformerInterface
{
public function __construct(private CityRepository $cityRepository) {}
/**
* @param CityDTO $value
* @return City|null
*/
public function transform(mixed $value, array|object $source, array $context): mixed
{
// Here I want to apply some logic regarding to the Source entity <------
}
}
We could implement a debug mode for the automapper, here are some ideas:
Hello,
I'm trying to transform an array of objects (DTOs) into an array(entities), Is that something that the library is capable of handling?
Here's a snippet of the code:
/** @var ColourApi[] */
#[MapTo(transformer: ColourTransformer::class)]
public array $colours = [];
When looking at the automapper tab in the profiler although it attaches AutoMapper\Transformer\PropertyTransformer\PropertyTransformer there's no code inside.
I've the same setup for simple properties, so I know the problem is probably not the transformer.
Let me know if you need more context.
Thank you!
Hello guys,
I'm currently blocked from using this library properly in a Symfony application.
From documentation: https://jolicode.github.io/automapper/#/?id=map-manually-a-whole-object
In order to customize the mapping of a whole object, you can leverage
AutoMapper\Transformer\CustomTransformer\CustomModelTransformerInterface.
Even though the code for custom transformers is in a main
branch, it still doesn't work when doing normal installation as it was implemented after latest release 8.1.0
.
And since my project directly depends on automapper-bundle
I cannot just Composer override automapper package version to dev-main
.
It would be awesome if we could have a new version with this feature out.
Thanks
Currently getting:
- jolicode/automapper[8.0.0, ..., 8.2.0] require nikic/php-parser ^4.0 -> found nikic/php-parser[v4.0.0, ..., v4.18.0] but the package is fixed to v5.0.2 (lock file version) by a partial update and that version does not match. Make sure you list it as an argument for the update command.
Using nikic/php-parser ^4.0 || ^5.0
in your dep tree would fix this.
Synopsis:
abstract class Animal {}
class Dog extends Animal {}
class Cat extends Animal {}
class Fish extends Animal {}
abstract class AnimalDto {}
class DogDto extends AnimalDto {}
class CatDto extends AnimalDto {}
class FishDto extends AnimalDto {}
$dog = new Dog();
$cat = new Cat();
$fish = new Fish();
$dogDto = $autoMapper->map($dog, AnimalDto::class);
$catDto = $autoMapper->map($cat, AnimalDto::class);
$fishDto = $autoMapper->map($fish, AnimalDto::class);
$dog = $autoMapper->map($dogDto, Animal::class);
$cat = $autoMapper->map($catDto, Animal::class);
$fish = $autoMapper->map($fishDto, Animal::class);
My proposal for attaching metadata using attribute:
#[InheritanceMapping([
Dog::class => DogDto::class,
Cat::class => CatDto::class,
Fish::class => FishDto::class,
])]
abstract class AnimalDto {}
Logic:
InheritanceMapping
attribute, then the mapper uses it directlyInheritanceMapping
attribute in the parents of the source class, starting from the immediate parent. If found, array_flip()
the mapping and use it.presently, the only way to ignore a property is to use serializer's #[Ignore]
attribute (or to use automapper context's IGNORED_ATTRIBUTES
, but this forces to rely on properties names, and cannot be used in as a generic mechanism)
Myabe we could then introduce its own ignore attribute for the automapper?
Hello,
Let's take this example:
<?php
use App\AddressDTO;
final readonly class FooTransformer implements CustomModelTransformerInterface
{
public function supports(array $sourceTypes, array $targetTypes): bool
{
return true;
}
public function transform(mixed $input): mixed
{
$addressDTO = new AddressDTO();
$addressDTO->city = "{$input['city']} from custom model transformer";
return $addressDTO;
}
}
We can see that we are instanciating an AddressDTO
, when extracting code, we don't extract that this statement needs the use App\AddressDTO;
, we should do it to avoid issues when using CustomTransformers.
On how to do it, I think we need to make it in 3 steps:
use
statements in the filetransform
method codeuse
statementsHey,
The objective is to make AutoMapper easier to work with for developpers.
In this issue I'll try to list all stuff developpers needs to fit their needs when using AutoMapper.
All the following stuff are examples and subject to change in their implementation.
class Foo
{
public function __construct(
#[MapTo(('array', 'phrase')]
#[MapTo(Bar::class, 'phrase')]
public string $sentence,
) {
}
}
class Foo
{
public function __construct(
#[MapFrom(('array', 'phrase')]
#[MapFrom(Bar::class, 'phrase')]
public string $sentence,
) {
}
}
class Foo
{
public function __construct(
#[MapIf(('array', fn() => true)]
#[MapIf(Bar::class, fn() => false)]
public string $sentence,
) {
}
}
final readonly class FromSourceCustomPropertyTransformer implements CustomPropertyTransformerInterface
{
public function supports(string $source, string $target, string $propertyName): bool
{
return $source === UserDTO::class && $target === 'array' && $propertyName === 'name';
}
public function transform(mixed $input): mixed
{
return "{$input} set by custom property transformer";
}
}
final readonly class FromTargetCustomModelTransformer implements CustomModelTransformerInterface
{
public function supports(string $source, string $target): bool
{
return $source === 'array' && $target === AddressDTO::class;
}
public function transform(mixed $input): mixed
{
$addressDTO = new \AutoMapper\Tests\Fixtures\AddressDTO();
$addressDTO->city = "{$input['city']} from custom model transformer";
return $addressDTO;
}
}
beforeInitialization
afterInitialization
afterHydratation
class Foo
{
public function __construct(
public string $sentence,
) {
}
#[MapEvent(Event::AFTER_INITIALIZATION)]
public function doctrineHook(mixed $object): void
{
// link my entity to Doctrine UOW
}
}
Original issue: janephp/janephp#745
Reported by: @weaverryan
Jane version(s) affected: 7.5.3
Description
Hi!
In the AutoMapper bundle, if you create a custom MapperConfigurationInterface
and call $metadata->getPropertiesMapping()
, the mappings will be generated BEFORE the ObjectTransformer
was loaded into the system. The problem comes with how the objects are instantiated:
A) When the container creates theJane\Component\AutoMapper\Transformer\ChainTransformerFactory
service, in the compiled container, it eventually calls:
$instance->addTransformerFactory(new \Jane\Component\AutoMapper\Transformer\ObjectTransformerFactory(($container->services['Jane\\Component\\AutoMapper\\AutoMapperInterface'] ?? $container->load('getAutoMapperInterfaceService'))));
In order to create the ObjectTransformerFactory
, it needs to load the AutoMapperInterface
service so it can be passed to it.
B) But, when the AutoMapperInterface
is being created, in the cached container, it will call:
$instance->addMapperConfiguration(new \App\Automapper\UserToUserApiMapperConfiguration());
(where UserToUserApiMapperConfiguration
is the custom mapper configuration class).
C) The AutoMapper::addMapperConfiguration()
method - https://github.com/janephp/janephp/blob/next/src/Bundle/AutoMapperBundle/AutoMapper.php#L13 - causes the process()
method to be called on the custom mapper configuration. However, at this point, the ObjectTransformerFactory
hasn't yet finished being added to the system.
This is a minor issue - it can just cause WTF moments. I couldn't figure out why a property was NOT apparently being mapped... but then WOULD be mapped in the end.
How to reproduce
symfony new reproducing_jane_missing_object_transformer
cd reproducing_jane_missing_object_transformer
composer require jane-php/automapper-bundle
Create a mapping config class - throw an exception:
<?php
namespace App;
use Jane\Bundle\AutoMapperBundle\Configuration\MapperConfigurationInterface;
use Jane\Component\AutoMapper\MapperGeneratorMetadataInterface;
class TestingMapperConfiguration implements MapperConfigurationInterface
{
public function process(MapperGeneratorMetadataInterface $metadata): void
{
throw new \Exception();
}
public function getSource(): string
{
return 'foo';
}
public function getTarget(): string
{
return 'bar';
}
}
Then, use the AutoMapper somewhere so that it's not removed from the container:
<?php
namespace App\Controller;
use Jane\Component\AutoMapper\AutoMapperInterface;
class AnyController
{
public function __construct(AutoMapperInterface $autoMapper)
{
}
}
Finally:
php bin/console cache:clear -vvv
If you look at the stacktrace, you'll see something like:
ContainerTWxKxjp\App_KernelDevDebugContainer->load() at /Users/weaverryan/Sites/os/reproducers/reproducing_jane_missing_object_transformer/var/cache/de_/ContainerTWxKxjp/getChainTransformerFactoryService.php:42
If you look at that line, you'll see it starting the instantiation of the AutoMapperInterface service before the ObjectTransformerFactory is complete, which proves that the custom mapper config class is called before the ObjectTransformerFactory
is present.
Possible Solution
One possibility is to inject the AutoMapperInterface
dependency into ObjectTransformerFactory
lazily in some way so that the chain transformer factory can finish instantiating without triggering the instantiation of the auto mapper.
Cheers!
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.