From 264f637eef59dd82c12d34f7cf6a3e07638bfce4 Mon Sep 17 00:00:00 2001 From: Joel Wurtz Date: Wed, 27 Sep 2023 16:05:06 +0200 Subject: [PATCH] chore(doc): document all ast code by explaining what it generates --- src/Extractor/ReadAccessor.php | 50 +++- src/Extractor/WriteMutator.php | 28 ++ src/Generator/Generator.php | 273 +++++++++++++++++- src/Transformer/AbstractArrayTransformer.php | 16 + src/Transformer/ArrayTransformer.php | 5 + src/Transformer/BuiltinTransformer.php | 14 +- src/Transformer/CallbackTransformer.php | 11 +- src/Transformer/CopyEnumTransformer.php | 1 + src/Transformer/CopyTransformer.php | 1 + .../DateTimeImmutableToMutableTransformer.php | 5 + .../DateTimeMutableToImmutableTransformer.php | 5 + .../DateTimeToStringTransformer.php | 5 + src/Transformer/DictionaryTransformer.php | 5 + src/Transformer/MultipleTransformer.php | 12 + src/Transformer/NullableTransformer.php | 12 + src/Transformer/ObjectTransformer.php | 5 + src/Transformer/SourceEnumTransformer.php | 1 + .../StringToDateTimeTransformer.php | 5 + .../StringToSymfonyUidTransformer.php | 5 + src/Transformer/SymfonyUidCopyTransformer.php | 5 + .../SymfonyUidToStringTransformer.php | 5 + src/Transformer/TargetEnumTransformer.php | 5 + 22 files changed, 459 insertions(+), 15 deletions(-) diff --git a/src/Extractor/ReadAccessor.php b/src/Extractor/ReadAccessor.php index da026a63..d8c77de0 100644 --- a/src/Extractor/ReadAccessor.php +++ b/src/Extractor/ReadAccessor.php @@ -53,11 +53,11 @@ public function getExpression(Expr\Variable $input): Expr foreach ($parameters as $parameter) { if ($attribute = ($parameter->getAttributes(MapToContext::class)[0] ?? null)) { - // generates code similar to: - // $value->getValue( - // $context['map_to_accessor_parameter']['some_key'] ?? throw new \InvalidArgumentException('error message'); - // ) - + /* + * Create method call argument to read value from context and throw exception if not found + * + * $context['map_to_accessor_parameter']['some_key'] ?? throw new \InvalidArgumentException('error message'); + */ $methodCallArguments[] = new Arg( new Expr\BinaryOp\Coalesce( new Expr\ArrayDimFetch( @@ -88,6 +88,13 @@ public function getExpression(Expr\Variable $input): Expr } if ($this->private) { + /* + * When the method is private we use the extract callback that can read this value + * + * @see \AutoMapper\Extractor\ReadAccessor::getExtractCallback() + * + * $this->extractCallbacks['method_name']($input) + */ return new Expr\FuncCall( new Expr\ArrayDimFetch(new Expr\PropertyFetch(new Expr\Variable('this'), 'extractCallbacks'), new Scalar\String_($this->name ?? $this->accessor)), [ @@ -96,11 +103,23 @@ public function getExpression(Expr\Variable $input): Expr ); } + /* + * Use the method call to read the value + * + * $input->method_name(...$args) + */ return new Expr\MethodCall($input, $this->accessor, $methodCallArguments); } if (self::TYPE_PROPERTY === $this->type) { if ($this->private) { + /* + * When the property is private we use the extract callback that can read this value + * + * @see \AutoMapper\Extractor\ReadAccessor::getExtractCallback() + * + * $this->extractCallbacks['property_name']($input) + */ return new Expr\FuncCall( new Expr\ArrayDimFetch(new Expr\PropertyFetch(new Expr\Variable('this'), 'extractCallbacks'), new Scalar\String_($this->accessor)), [ @@ -109,10 +128,20 @@ public function getExpression(Expr\Variable $input): Expr ); } + /* + * Use the property fetch to read the value + * + * $input->property_name + */ return new Expr\PropertyFetch($input, $this->accessor); } if (self::TYPE_ARRAY_DIMENSION === $this->type) { + /* + * Use the array dim fetch to read the value + * + * $input['property_name'] + */ return new Expr\ArrayDimFetch($input, new Scalar\String_($this->accessor)); } @@ -132,6 +161,17 @@ public function getExtractCallback(string $className): ?Expr return null; } + /* + * Create extract callback for this accessor + * + * \Closure::bind(function ($object) { + * return $object->property_name; + * }, null, $className) + * + * \Closure::bind(function ($object) { + * return $object->method_name(); + * }, null, $className) + */ return new Expr\StaticCall(new Name\FullyQualified(\Closure::class), 'bind', [ new Arg( new Expr\Closure([ diff --git a/src/Extractor/WriteMutator.php b/src/Extractor/WriteMutator.php index 70481ca5..e9f5692d 100644 --- a/src/Extractor/WriteMutator.php +++ b/src/Extractor/WriteMutator.php @@ -41,6 +41,11 @@ public function __construct( public function getExpression(Expr\Variable $output, Expr $value, bool $byRef = false): ?Expr { if (self::TYPE_METHOD === $this->type || self::TYPE_ADDER_AND_REMOVER === $this->type) { + /* + * Create method call expression to write value + * + * $output->method($value); + */ return new Expr\MethodCall($output, $this->name, [ new Arg($value), ]); @@ -48,6 +53,11 @@ public function getExpression(Expr\Variable $output, Expr $value, bool $byRef = if (self::TYPE_PROPERTY === $this->type) { if ($this->private) { + /* + * Use hydrate callback to write value + * + * $this->hydrateCallbacks['propertyName']($output, $value); + */ return new Expr\FuncCall( new Expr\ArrayDimFetch(new Expr\PropertyFetch(new Expr\Variable('this'), 'hydrateCallbacks'), new Scalar\String_($this->name)), [ @@ -56,6 +66,12 @@ public function getExpression(Expr\Variable $output, Expr $value, bool $byRef = ] ); } + + /* + * Create property expression to write value + * + * $output->propertyName &= $value; + */ if ($byRef) { return new Expr\AssignRef(new Expr\PropertyFetch($output, $this->name), $value); } @@ -64,6 +80,11 @@ public function getExpression(Expr\Variable $output, Expr $value, bool $byRef = } if (self::TYPE_ARRAY_DIMENSION === $this->type) { + /* + * Create array write expression to write value + * + * $output['propertyName'] &= $value; + */ if ($byRef) { return new Expr\AssignRef(new Expr\ArrayDimFetch($output, new Scalar\String_($this->name)), $value); } @@ -83,6 +104,13 @@ public function getHydrateCallback(string $className): ?Expr return null; } + /* + * Create hydrate callback for this mutator + * + * \Closure::bind(function ($object, $value) { + * $object->propertyName = $value; + * }, null, $className) + */ return new Expr\StaticCall(new Name\FullyQualified(\Closure::class), 'bind', [ new Arg(new Expr\Closure([ 'params' => [ diff --git a/src/Generator/Generator.php b/src/Generator/Generator.php index 74917125..9568c023 100644 --- a/src/Generator/Generator.php +++ b/src/Generator/Generator.php @@ -58,6 +58,13 @@ public function generate(MapperGeneratorMetadataInterface $mapperGeneratorMetada $addedDependencies = []; $canHaveCircularDependency = $mapperGeneratorMetadata->canHaveCircularReference() && 'array' !== $mapperGeneratorMetadata->getSource(); + /** + * First statement is to check if the source is null, if so, return null. + * + * if (null === $source) { + * return $source; + * ] + */ $statements = [ new Stmt\If_(new Expr\BinaryOp\Identical(new Expr\ConstFetch(new Name('null')), $sourceInput), [ 'stmts' => [new Stmt\Return_($sourceInput)], @@ -65,6 +72,14 @@ public function generate(MapperGeneratorMetadataInterface $mapperGeneratorMetada ]; if ($canHaveCircularDependency) { + /* + * When there can be circular dependency in the mapping, the following statements try to use the reference for the source if it's available + * + * $sourceHash = spl_object_hash($source) . $target; + * if (MapperContext::shouldHandleCircularReference($context, $sourceHash, $source)) { + * return MapperContext::handleCircularReference($context, $sourceHash, $source, $this->circularReferenceLimit, $this->circularReferenceHandler); + * } + */ $statements[] = new Stmt\Expression(new Expr\Assign($hashVariable, new Expr\BinaryOp\Concat(new Expr\FuncCall(new Name('spl_object_hash'), [ new Arg($sourceInput), ]), @@ -87,15 +102,36 @@ public function generate(MapperGeneratorMetadataInterface $mapperGeneratorMetada ]); } + /** + * Get statements about how to create the object. + * + * $createObjectStmts : Statements to create the object + * $inConstructor : Field to set in the constructor, this allow to transform them before the constructor is called + * $constructStatementsForCreateObjects : Additional statements to add in the constructor + * $injectMapperStatements : Additional statements to add in the injectMappers method, this allow to inject mappers for dependencies + */ [$createObjectStmts, $inConstructor, $constructStatementsForCreateObjects, $injectMapperStatements] = $this->getCreateObjectStatements($mapperGeneratorMetadata, $result, $contextVariable, $sourceInput, $uniqueVariableScope); $constructStatements = array_merge($constructStatements, $constructStatementsForCreateObjects); $targetToPopulate = new Expr\ArrayDimFetch($contextVariable, new Scalar\String_(MapperContext::TARGET_TO_POPULATE)); + + /* + * Get result from context if available, otherwise set it to null + * + * $result = $context[MapperContext::TARGET_TO_POPULATE] ?? null; + */ $statements[] = new Stmt\Expression(new Expr\Assign($result, new Expr\BinaryOp\Coalesce( $targetToPopulate, new Expr\ConstFetch(new Name('null')) ))); if (!$this->allowReadOnlyTargetToPopulate && $mapperGeneratorMetadata->isTargetReadOnlyClass()) { + /* + * If the target is a read-only class, we throw an exception if the target is not null + * + * if ($contextVariable[MapperContext::ALLOW_READONLY_TARGET_TO_POPULATE] ?? false && is_object($targetToPopulate)) { + * throw new ReadOnlyTargetException(); + * } + */ $statements[] = new Stmt\If_( new Expr\BinaryOp\BooleanAnd( new Expr\BooleanNot(new Expr\BinaryOp\Coalesce(new Expr\ArrayDimFetch($contextVariable, new Scalar\String_(MapperContext::ALLOW_READONLY_TARGET_TO_POPULATE)), new Expr\ConstFetch(new Name('false')))), @@ -105,6 +141,13 @@ public function generate(MapperGeneratorMetadataInterface $mapperGeneratorMetada ]); } + /* + * If the result is null, we create the object + * + * if (null === $result) { + * ... // create object statements @see getCreateObjectStatements + * } + */ $statements[] = new Stmt\If_(new Expr\BinaryOp\Identical(new Expr\ConstFetch(new Name('null')), $result), [ 'stmts' => $createObjectStmts, ]); @@ -119,6 +162,12 @@ public function generate(MapperGeneratorMetadataInterface $mapperGeneratorMetada continue; } + /* + * If the transformer has dependencies, we inject the mappers for the dependencies + * This allows to inject mappers when creating the service instead of resolving them at runtime which is faster + * + * $this->mappers[$dependency->name] = $autoMapperRegistry->getMapper($dependency->source, $dependency->target); + */ $injectMapperStatements[] = new Stmt\Expression(new Expr\Assign( new Expr\ArrayDimFetch(new Expr\PropertyFetch(new Expr\Variable('this'), 'mappers'), new Scalar\String_($dependency->name)), new Expr\MethodCall(new Expr\Variable('autoMapperRegistry'), 'getMapper', [ @@ -133,6 +182,11 @@ public function generate(MapperGeneratorMetadataInterface $mapperGeneratorMetada $addedDependenciesStatements = []; if ($addedDependencies) { if ($canHaveCircularDependency) { + /* + * Here we register the result into the context to allow circular dependency, it's done before mapping so if there is a circular dependency, it will be correctly handled + * + * $context = MapperContext::withReference($context, $sourceHash, $result); + */ $addedDependenciesStatements[] = new Stmt\Expression(new Expr\Assign( $contextVariable, new Expr\StaticCall(new Name\FullyQualified(MapperContext::class), 'withReference', [ @@ -143,6 +197,11 @@ public function generate(MapperGeneratorMetadataInterface $mapperGeneratorMetada )); } + /* + * We increase the depth of the context to allow to check the max depth of the mapping + * + * $context = MapperContext::withIncrementedDepth($context); + */ $addedDependenciesStatements[] = new Stmt\Expression(new Expr\Assign( $contextVariable, new Expr\StaticCall(new Name\FullyQualified(MapperContext::class), 'withIncrementedDepth', [ @@ -154,6 +213,23 @@ public function generate(MapperGeneratorMetadataInterface $mapperGeneratorMetada $duplicatedStatements = []; $setterStatements = []; foreach ($propertiesMapping as $propertyMapping) { + /* + * This is the main loop to map the properties from the source to the target, there is 3 main steps in order to generated this code : + * + * * Generate code on how to read the value from the source, which returns statements and an output expression + * * Generate code on how to transform the value, which use the output expression, add some statements and return a new output expression + * * Generate code on how to write this transformed value to the target, which use the output expression and add some statements + * + * As an example this could generate the following code : + * + * * Extract value from a private property : $this->extractCallbacks['propertyName']($source) + * * Transform the value, which is an object in this example, with another mapper : $this->mappers['SOURCE_TO_TARGET_MAPPER']->map(..., $context); + * * Write the value to a private property : $this->hydrateCallbacks['propertyName']($target, ...) + * + * Since it use expression that may not create variable this would produce the following code + * + * $this->hydrateCallbacks['propertyName']($target, $this->mappers['SOURCE_TO_TARGET_MAPPER']->map($this->extractCallbacks['propertyName']($source), $context)); + */ if ($propertyMapping->shouldIgnoreProperty($mapperGeneratorMetadata->shouldMapPrivateProperties())) { continue; } @@ -161,13 +237,20 @@ public function generate(MapperGeneratorMetadataInterface $mapperGeneratorMetada $transformer = $propertyMapping->transformer; $fieldValueVariable = new Expr\Variable($uniqueVariableScope->getUniqueName('fieldValue')); + /** Create expression on how to read the value from the source */ $sourcePropertyAccessor = new Expr\Assign($fieldValueVariable, $propertyMapping->readAccessor->getExpression($sourceInput)); + /* Create expression to transform the readed value into the wanted writed value, depending on the transform it may add new statements to get the correct value */ [$output, $propStatements] = $transformer->transform($fieldValueVariable, $result, $propertyMapping, $uniqueVariableScope); $extractCallback = $propertyMapping->readAccessor->getExtractCallback($mapperGeneratorMetadata->getSource()); if (null !== $extractCallback) { + /* + * Add read callback to the constructor of the generated mapper + * + * $this->extractCallbacks['propertyName'] = $extractCallback; + */ $constructStatements[] = new Stmt\Expression(new Expr\Assign( new Expr\ArrayDimFetch(new Expr\PropertyFetch(new Expr\Variable('this'), 'extractCallbacks'), new Scalar\String_($propertyMapping->property)), $extractCallback @@ -179,6 +262,7 @@ public function generate(MapperGeneratorMetadataInterface $mapperGeneratorMetada } if ($propertyMapping->writeMutator->type !== WriteMutator::TYPE_ADDER_AND_REMOVER) { + /** Create expression to write the transformed value to the target only if not add / remove mutator, as it's already called by the transformer in this case */ $writeExpression = $propertyMapping->writeMutator->getExpression($result, $output, $transformer instanceof AssignedByReferenceTransformerInterface ? $transformer->assignByRef() : false); if (null === $writeExpression) { continue; @@ -190,16 +274,26 @@ public function generate(MapperGeneratorMetadataInterface $mapperGeneratorMetada $hydrateCallback = $propertyMapping->writeMutator->getHydrateCallback($mapperGeneratorMetadata->getTarget()); if (null !== $hydrateCallback) { + /* + * Add hydrate callback to the constructor of the generated mapper + * + * $this->hydrateCallback['propertyName'] = $hydrateCallback; + */ $constructStatements[] = new Stmt\Expression(new Expr\Assign( new Expr\ArrayDimFetch(new Expr\PropertyFetch(new Expr\Variable('this'), 'hydrateCallbacks'), new Scalar\String_($propertyMapping->property)), $hydrateCallback )); } + /** We generate a list of conditions that will allow the field to be mapped to the target */ $conditions = []; if ($propertyMapping->checkExists) { if (\stdClass::class === $mapperGeneratorMetadata->getSource()) { + /* + * In case of source is an \stdClass we ensure that the property exists + * property_exists($source, 'propertyName') + */ $conditions[] = new Expr\FuncCall(new Name('property_exists'), [ new Arg($sourceInput), new Arg(new Scalar\String_($propertyMapping->property)), @@ -207,6 +301,10 @@ public function generate(MapperGeneratorMetadataInterface $mapperGeneratorMetada } if ('array' === $mapperGeneratorMetadata->getSource()) { + /* + * In case of source is an array we ensure that the key exists + * array_key_exists('propertyName', $source) + */ $conditions[] = new Expr\FuncCall(new Name('array_key_exists'), [ new Arg(new Scalar\String_($propertyMapping->property)), new Arg($sourceInput), @@ -215,6 +313,10 @@ public function generate(MapperGeneratorMetadataInterface $mapperGeneratorMetada } if ($mapperGeneratorMetadata->shouldCheckAttributes()) { + /* + * In case of supporting attributes checking, we check if the property is allowed to be mapped + * MapperContext::isAllowedAttribute($context, 'propertyName', $source) + */ $conditions[] = new Expr\StaticCall(new Name\FullyQualified(MapperContext::class), 'isAllowedAttribute', [ new Arg($contextVariable), new Arg(new Scalar\String_($propertyMapping->property)), @@ -223,6 +325,11 @@ public function generate(MapperGeneratorMetadataInterface $mapperGeneratorMetada } if (null !== $propertyMapping->sourceGroups) { + /* + * When there is groups associated to the source property we check if the context has the same groups + * + * (null !== $context[MapperContext::GROUPS] ?? null && array_intersect($context[MapperContext::GROUPS] ?? [], ['group1', 'group2'])) + */ $conditions[] = new Expr\BinaryOp\BooleanAnd( new Expr\BinaryOp\NotIdentical( new Expr\ConstFetch(new Name('null')), @@ -244,6 +351,11 @@ public function generate(MapperGeneratorMetadataInterface $mapperGeneratorMetada } if (null !== $propertyMapping->targetGroups) { + /* + * When there is groups associated to the target property we check if the context has the same groups + * + * (null !== $context[MapperContext::GROUPS] ?? null && array_intersect($context[MapperContext::GROUPS] ?? [], ['group1', 'group2'])) + */ $conditions[] = new Expr\BinaryOp\BooleanAnd( new Expr\BinaryOp\NotIdentical( new Expr\ConstFetch(new Name('null')), @@ -265,6 +377,11 @@ public function generate(MapperGeneratorMetadataInterface $mapperGeneratorMetada } if (null !== $propertyMapping->maxDepth) { + /* + * When there is a max depth for this property we check if the context has a depth lower or equal to the max depth + * + * ($context[MapperContext::DEPTH] ?? 0) <= $maxDepth + */ $conditions[] = new Expr\BinaryOp\SmallerOrEqual( new Expr\BinaryOp\Coalesce( new Expr\ArrayDimFetch($contextVariable, new Scalar\String_(MapperContext::DEPTH)), @@ -275,6 +392,13 @@ public function generate(MapperGeneratorMetadataInterface $mapperGeneratorMetada } if ($conditions) { + /** + * If there is any conditions generated we encapsulate the mapping into it. + * + * if (condition1 && condition2 && ...) { + * ... // mapping statements + * } + */ $condition = array_shift($conditions); while ($conditions) { @@ -286,6 +410,11 @@ public function generate(MapperGeneratorMetadataInterface $mapperGeneratorMetada ])]; } + /** + * Here we dispatch those statements into two categories + * * Statements that need to be executed before the constructor, if the property need to be write in the constructor + * * Statements that need to be executed after the constructor. + */ $propInConstructor = \in_array($propertyMapping->property, $inConstructor, true); foreach ($propStatements as $propStatement) { if ($propInConstructor) { @@ -297,6 +426,16 @@ public function generate(MapperGeneratorMetadataInterface $mapperGeneratorMetada } if (\count($duplicatedStatements) > 0 && \count($inConstructor)) { + /* + * Generate else statements when the result is already an object, which means it has already been created, so we need to execute the statements that need to be executed before the constructor since the constructor has already been called + * if (null !== $result { + * .. // create object statements + * } else { + * // remap property from the constructor in case object already exists so we do not loose information + * $source->propertyName = $this->extractCallbacks['propertyName']($source); + * ... + * } + */ $statements[] = new Stmt\Else_(array_merge($addedDependenciesStatements, $duplicatedStatements)); } else { foreach ($addedDependenciesStatements as $statement) { @@ -304,12 +443,21 @@ public function generate(MapperGeneratorMetadataInterface $mapperGeneratorMetada } } + /* Add the rest of statements to handle the mapping */ foreach ($setterStatements as $propStatement) { $statements[] = $propStatement; } + /* return $result; */ $statements[] = new Stmt\Return_($result); + /** + * Create the map method for this mapper. + * + * public function map($source, array $context = []) { + * ... // statements + * } + */ $mapMethod = new Stmt\ClassMethod('map', [ 'flags' => Stmt\Class_::MODIFIER_PUBLIC, 'params' => [ @@ -321,6 +469,17 @@ public function generate(MapperGeneratorMetadataInterface $mapperGeneratorMetada 'returnType' => \PHP_VERSION_ID >= 80000 ? 'mixed' : null, ]); + /** + * Create the constructor for this mapper. + * + * public function __construct() { + * // construct statements + * $this->extractCallbacks['propertyName'] = \Closure::bind(function ($object) { + * return $object->propertyName; + * }; + * ... + * } + */ $constructMethod = new Stmt\ClassMethod('__construct', [ 'flags' => Stmt\Class_::MODIFIER_PUBLIC, 'stmts' => $constructStatements, @@ -329,6 +488,17 @@ public function generate(MapperGeneratorMetadataInterface $mapperGeneratorMetada $classStmts = [$constructMethod, $mapMethod]; if (\count($injectMapperStatements) > 0) { + /* + * Create the injectMapper methods for this mapper + * + * This is not done into the constructor in order to avoid circular dependency between mappers + * + * public function injectMappers(AutoMapperRegistryInterface $autoMapperRegistry) { + * // inject mapper statements + * $this->mappers['SOURCE_TO_TARGET_MAPPER'] = $autoMapperRegistry->getMapper($source, $target); + * ... + * } + */ $classStmts[] = new Stmt\ClassMethod('injectMappers', [ 'flags' => Stmt\Class_::MODIFIER_PUBLIC, 'params' => [ @@ -339,6 +509,13 @@ public function generate(MapperGeneratorMetadataInterface $mapperGeneratorMetada ]); } + /* + * Create the class for this mapper + * + * final class SourceToTargetMapper extends GeneratedMapper { + * ... // class methods + * } + */ return new Stmt\Class_(new Name($mapperGeneratorMetadata->getMapperClassName()), [ 'flags' => Stmt\Class_::MODIFIER_FINAL, 'extends' => new Name\FullyQualified(GeneratedMapper::class), @@ -352,12 +529,26 @@ private function getCreateObjectStatements(MapperGeneratorMetadataInterface $map $source = $mapperMetadata->getSource(); if ('array' === $target) { + /* + * If the target is an array, we just create an empty array. + * $result = []; + */ return [[new Stmt\Expression(new Expr\Assign($result, new Expr\Array_()))], [], [], []]; } if (\stdClass::class === $target && \stdClass::class === $source) { + /* + * If the target and source is a stdClass, we just clone the object using serialization + * $result = unserialize(serialize($source)); + */ return [[new Stmt\Expression(new Expr\Assign($result, new Expr\FuncCall(new Name('unserialize'), [new Arg(new Expr\FuncCall(new Name('serialize'), [new Arg($sourceInput)]))])))], [], [], []]; - } elseif (\stdClass::class === $target) { + } + + if (\stdClass::class === $target) { + /* + * If the target is a stdClass, we create a new stdClass + * $result = \new stdClass(); + */ return [[new Stmt\Expression(new Expr\Assign($result, new Expr\New_(new Name(\stdClass::class))))], [], [], []]; } @@ -370,11 +561,19 @@ private function getCreateObjectStatements(MapperGeneratorMetadataInterface $map $classDiscriminatorMapping = 'array' !== $target && null !== $this->classDiscriminator ? $this->classDiscriminator->getMappingForClass($target) : null; if (null !== $classDiscriminatorMapping && null !== ($propertyMapping = $mapperMetadata->getPropertyMapping($classDiscriminatorMapping->getTypeProperty()))) { + /* Here we generated the code that allow to put the type into the output variable so we are able to determine which mapper to use */ [$output, $createObjectStatements] = $propertyMapping->transformer->transform($propertyMapping->readAccessor->getExpression($sourceInput), $result, $propertyMapping, $uniqueVariableScope); foreach ($classDiscriminatorMapping->getTypesMapping() as $typeValue => $typeTarget) { $mapperName = 'Discriminator_Mapper_' . $source . '_' . $typeTarget; + /* + * We inject dependencies for all the discriminator variant + * + * $this->mappers['Discriminator_Mapper_VariantA'] = $autoMapperRegistry->getMapper($source, VariantA::class); + * $this->mappers['Discriminator_Mapper_VariantB'] = $autoMapperRegistry->getMapper($source, VariantB::class); + * ... + */ $injectMapperStatements[] = new Stmt\Expression(new Expr\Assign( new Expr\ArrayDimFetch(new Expr\PropertyFetch(new Expr\Variable('this'), 'mappers'), new Scalar\String_($mapperName)), new Expr\MethodCall(new Expr\Variable('autoMapperRegistry'), 'getMapper', [ @@ -382,6 +581,14 @@ private function getCreateObjectStatements(MapperGeneratorMetadataInterface $map new Arg(new Scalar\String_($typeTarget)), ]) )); + + /* + * We return the object created with the correct mapper depending on the variant, this will skip the next mapping phase in this situation + * + * if ('VariantA' === $output) { + * return $this->mappers['Discriminator_Mapper_VariantA']->map($source, $context); + * } + */ $createObjectStatements[] = new Stmt\If_(new Expr\BinaryOp\Identical( new Scalar\String_($typeValue), $output @@ -405,15 +612,41 @@ private function getCreateObjectStatements(MapperGeneratorMetadataInterface $map $constructArguments = []; foreach ($propertiesMapping as $propertyMapping) { + /* + * This is the main loop to map the properties from the source to the target in the constructor, there is 2 main steps in order to generated this code : + * + * * Generate code on how to read the value from the source, which returns statements and an output expression + * * Generate code on how to transform the value, which use the output expression, add some statements and return a new output expression + * + * As an example this could generate the following code : + * + * * Extract value from a private property : $this->extractCallbacks['propertyName']($source) + * * Transform the value, which is an object in this example, with another mapper : $this->mappers['SOURCE_TO_TARGET_MAPPER']->map(..., $context); + * + * The output expression of the transform will then be used as argument for the object constructor + * + * $constructArg1 = $this->mappers['SOURCE_TO_TARGET_MAPPER']->map($this->extractCallbacks['propertyName']($source), $context); + * $result = new Foo($constructArg1); + */ if (null === $propertyMapping->writeMutatorConstructor || null === ($parameter = $propertyMapping->writeMutatorConstructor->parameter)) { continue; } $constructVar = new Expr\Variable($uniqueVariableScope->getUniqueName('constructArg')); + /* Get extract and transform statements for this property */ [$output, $propStatements] = $propertyMapping->transformer->transform($propertyMapping->readAccessor->getExpression($sourceInput), $constructVar, $propertyMapping, $uniqueVariableScope); $constructArguments[$parameter->getPosition()] = new Arg($constructVar); + /* + * Check if there is a constructor argument in the context, otherwise we use the transformed value + * + * if (MapperContext::hasConstructorArgument($context, $target, 'propertyName')) { + * $constructArg1 = MapperContext::getConstructorArgument($context, $target, 'propertyName'); + * } else { + * $constructArg1 = $source->propertyName; + * } + */ $propStatements[] = new Stmt\Expression(new Expr\Assign($constructVar, $output)); $createObjectStatements[] = new Stmt\If_(new Expr\StaticCall(new Name\FullyQualified(MapperContext::class), 'hasConstructorArgument', [ new Arg($contextVariable), @@ -433,10 +666,20 @@ private function getCreateObjectStatements(MapperGeneratorMetadataInterface $map $inConstructor[] = $propertyMapping->property; } + /* We loop to get constructor arguments that were not present in the source */ foreach ($targetConstructor->getParameters() as $constructorParameter) { if (!\array_key_exists($constructorParameter->getPosition(), $constructArguments) && $constructorParameter->isDefaultValueAvailable()) { $constructVar = new Expr\Variable($uniqueVariableScope->getUniqueName('constructArg')); + /* + * Check if there is a constructor argument in the context, otherwise we use the default value + * + * if (MapperContext::hasConstructorArgument($context, $target, 'propertyName')) { + * $constructArg2 = MapperContext::getConstructorArgument($context, $target, 'propertyName'); + * } else { + * $constructArg2 = 'default value'; + * } + */ $createObjectStatements[] = new Stmt\If_(new Expr\StaticCall(new Name\FullyQualified(MapperContext::class), 'hasConstructorArgument', [ new Arg($contextVariable), new Arg(new Scalar\String_($target)), @@ -460,8 +703,22 @@ private function getCreateObjectStatements(MapperGeneratorMetadataInterface $map ksort($constructArguments); + /* + * Create object with the constructor arguments + * + * $result = new Foo($constructArg1, $constructArg2, ...); + */ $createObjectStatements[] = new Stmt\Expression(new Expr\Assign($result, new Expr\New_(new Name\FullyQualified($target), $constructArguments))); } elseif (null !== $targetConstructor && $mapperMetadata->isTargetCloneable()) { + /* + * When the target does not have a constructor but is cloneable, we clone a cached version of the target created with reflection to improve performance + * + * // constructor of mapper + * $this->cachedTarget = (new \ReflectionClass(Foo:class))->newInstanceWithoutConstructor(); + * + * // map method + * $result = clone $this->cachedTarget; + */ $constructStatements[] = new Stmt\Expression(new Expr\Assign( new Expr\PropertyFetch(new Expr\Variable('this'), 'cachedTarget'), new Expr\MethodCall(new Expr\New_(new Name\FullyQualified(\ReflectionClass::class), [ @@ -470,6 +727,15 @@ private function getCreateObjectStatements(MapperGeneratorMetadataInterface $map )); $createObjectStatements[] = new Stmt\Expression(new Expr\Assign($result, new Expr\Clone_(new Expr\PropertyFetch(new Expr\Variable('this'), 'cachedTarget')))); } elseif (null !== $targetConstructor) { + /* + * When the target does not have a constructor and is not cloneable, we cache the reflection class to improve performance + * + * // constructor of mapper + * $this->cachedTarget = (new \ReflectionClass(Foo:class)); + * + * // map method + * $result = $this->cachedTarget->newInstanceWithoutConstructor(); + */ $constructStatements[] = new Stmt\Expression(new Expr\Assign( new Expr\PropertyFetch(new Expr\Variable('this'), 'cachedTarget'), new Expr\New_(new Name\FullyQualified(\ReflectionClass::class), [ @@ -481,6 +747,11 @@ private function getCreateObjectStatements(MapperGeneratorMetadataInterface $map 'newInstanceWithoutConstructor' ))); } else { + /* + * Create object with constructor (which have no arguments) + * + * $result = new Foo(); + */ $createObjectStatements[] = new Stmt\Expression(new Expr\Assign($result, new Expr\New_(new Name\FullyQualified($target)))); } diff --git a/src/Transformer/AbstractArrayTransformer.php b/src/Transformer/AbstractArrayTransformer.php index c09a4e16..226d00f5 100644 --- a/src/Transformer/AbstractArrayTransformer.php +++ b/src/Transformer/AbstractArrayTransformer.php @@ -25,6 +25,9 @@ abstract protected function getAssignExpr(Expr $valuesVar, Expr $outputVar, Expr public function transform(Expr $input, Expr $target, PropertyMapping $propertyMapping, UniqueVariableScope $uniqueVariableScope): array { + /** + * $values = [];. + */ $valuesVar = new Expr\Variable($uniqueVariableScope->getUniqueName('values')); $statements = [ new Stmt\Expression(new Expr\Assign($valuesVar, new Expr\Array_())), @@ -35,9 +38,15 @@ public function transform(Expr $input, Expr $target, PropertyMapping $propertyMa $assignByRef = $this->itemTransformer instanceof AssignedByReferenceTransformerInterface && $this->itemTransformer->assignByRef(); + /* Get the transform statements for the source property */ [$output, $itemStatements] = $this->itemTransformer->transform($loopValueVar, $target, $propertyMapping, $uniqueVariableScope); if ($propertyMapping->writeMutator && $propertyMapping->writeMutator->type === WriteMutator::TYPE_ADDER_AND_REMOVER) { + /** + * Use add and remove methods. + * + * $target->add($output); + */ $mappedValueVar = new Expr\Variable($uniqueVariableScope->getUniqueName('mappedValue')); $itemStatements[] = new Stmt\Expression(new Expr\Assign($mappedValueVar, $output)); $itemStatements[] = new Stmt\If_(new Expr\BinaryOp\NotIdentical(new Expr\ConstFetch(new Name('null')), $mappedValueVar), [ @@ -46,6 +55,13 @@ public function transform(Expr $input, Expr $target, PropertyMapping $propertyMa ], ]); } else { + /* + * Assign the value to the array. + * + * $values[] = $output; + * or + * $values[$key] = $output; + */ $itemStatements[] = new Stmt\Expression($this->getAssignExpr($valuesVar, $output, $loopKeyVar, $assignByRef)); } diff --git a/src/Transformer/ArrayTransformer.php b/src/Transformer/ArrayTransformer.php index d4189845..42b1cf47 100644 --- a/src/Transformer/ArrayTransformer.php +++ b/src/Transformer/ArrayTransformer.php @@ -13,6 +13,11 @@ */ final readonly class ArrayTransformer extends AbstractArrayTransformer { + /** + * Assign the value by pushing it to the array. + * + * $values[] = $output; + */ protected function getAssignExpr(Expr $valuesVar, Expr $outputVar, Expr $loopKeyVar, bool $assignByRef): Expr { if ($assignByRef) { diff --git a/src/Transformer/BuiltinTransformer.php b/src/Transformer/BuiltinTransformer.php index 974b8e4d..0dc6b8bc 100644 --- a/src/Transformer/BuiltinTransformer.php +++ b/src/Transformer/BuiltinTransformer.php @@ -69,22 +69,32 @@ public function transform(Expr $input, Expr $target, PropertyMapping $propertyMa return $type->getBuiltinType(); }, $this->targetTypes); - // Source type is in target => no cast if (\in_array($this->sourceType->getBuiltinType(), $targetTypes, true)) { + /* Output type is the same as input type so we simply return the same expression */ return [$input, []]; } - // Cast needed foreach (self::CAST_MAPPING[$this->sourceType->getBuiltinType()] as $castType => $castMethod) { if (\in_array($castType, $targetTypes, true)) { if (method_exists($this, $castMethod)) { + /* + * Use specific cast expression if callback exist in this class + * + * $array = [$source->property]; + */ return [$this->$castMethod($input), []]; } + /* + * Use the cast expression find in the cast matrix + * + * $bool = (bool) $source->int; + */ return [new $castMethod($input), []]; } } + /* When there is no possibility to cast we assume that the mutator will be able to handle the value */ return [$input, []]; } diff --git a/src/Transformer/CallbackTransformer.php b/src/Transformer/CallbackTransformer.php index c01685b8..f9472110 100644 --- a/src/Transformer/CallbackTransformer.php +++ b/src/Transformer/CallbackTransformer.php @@ -24,17 +24,14 @@ public function __construct( public function transform(Expr $input, Expr $target, PropertyMapping $propertyMapping, UniqueVariableScope $uniqueVariableScope): array { - /* - * $output = $this->callbacks[$callbackName]($input); - */ - $arguments = [ new Arg($input), + new Arg($target), ]; - if ($target instanceof Expr) { - $arguments[] = new Arg($target); - } + /* + * $this->callbacks['callbackName']($input, $target); + */ return [new Expr\FuncCall( new Expr\ArrayDimFetch(new Expr\PropertyFetch(new Expr\Variable('this'), 'callbacks'), new Scalar\String_($this->callbackName)), $arguments), [], diff --git a/src/Transformer/CopyEnumTransformer.php b/src/Transformer/CopyEnumTransformer.php index 2877189a..371016c3 100644 --- a/src/Transformer/CopyEnumTransformer.php +++ b/src/Transformer/CopyEnumTransformer.php @@ -17,6 +17,7 @@ final class CopyEnumTransformer implements TransformerInterface { public function transform(Expr $input, Expr $target, PropertyMapping $propertyMapping, UniqueVariableScope $uniqueVariableScope): array { + /* No transform here it's the same value and it's a copy so we do not need to clone */ return [$input, []]; } } diff --git a/src/Transformer/CopyTransformer.php b/src/Transformer/CopyTransformer.php index f57dfcfd..c6421e64 100644 --- a/src/Transformer/CopyTransformer.php +++ b/src/Transformer/CopyTransformer.php @@ -17,6 +17,7 @@ final class CopyTransformer implements TransformerInterface { public function transform(Expr $input, Expr $target, PropertyMapping $propertyMapping, UniqueVariableScope $uniqueVariableScope): array { + /* No transform here it's the same value and it's a copy so we do not need to clone */ return [$input, []]; } } diff --git a/src/Transformer/DateTimeImmutableToMutableTransformer.php b/src/Transformer/DateTimeImmutableToMutableTransformer.php index 8cd79a95..ca040df3 100644 --- a/src/Transformer/DateTimeImmutableToMutableTransformer.php +++ b/src/Transformer/DateTimeImmutableToMutableTransformer.php @@ -20,6 +20,11 @@ final class DateTimeImmutableToMutableTransformer implements TransformerInterfac { public function transform(Expr $input, Expr $target, PropertyMapping $propertyMapping, UniqueVariableScope $uniqueVariableScope): array { + /* + * In case of immutable source we clone the value by using format into a new mutable DateTime. + * + * \DateTime::createFromFormat(\DateTime::RFC3339, $input->format(\DateTime::RFC3339)); + */ return [ new Expr\StaticCall(new Name\FullyQualified(\DateTime::class), 'createFromFormat', [ new Arg(new String_(\DateTime::RFC3339)), diff --git a/src/Transformer/DateTimeMutableToImmutableTransformer.php b/src/Transformer/DateTimeMutableToImmutableTransformer.php index e0c68f0d..c5850036 100644 --- a/src/Transformer/DateTimeMutableToImmutableTransformer.php +++ b/src/Transformer/DateTimeMutableToImmutableTransformer.php @@ -19,6 +19,11 @@ final class DateTimeMutableToImmutableTransformer implements TransformerInterfac { public function transform(Expr $input, Expr $target, PropertyMapping $propertyMapping, UniqueVariableScope $uniqueVariableScope): array { + /* + * In case of mutable source we create the immutable value by using createFromMutable. + * + * \DateTimeImmutable::createFromMutable($input); + */ return [ new Expr\StaticCall(new Name\FullyQualified(\DateTimeImmutable::class), 'createFromMutable', [ new Arg($input), diff --git a/src/Transformer/DateTimeToStringTransformer.php b/src/Transformer/DateTimeToStringTransformer.php index 491bed31..407a3504 100644 --- a/src/Transformer/DateTimeToStringTransformer.php +++ b/src/Transformer/DateTimeToStringTransformer.php @@ -25,6 +25,11 @@ public function __construct( public function transform(Expr $input, Expr $target, PropertyMapping $propertyMapping, UniqueVariableScope $uniqueVariableScope): array { + /* + * Format the date time object to a string. + * + * $input->format($context[MapperContext::DATETIME_FORMAT] ?? \DateTimeInterface::RFC3339); + */ return [new Expr\MethodCall($input, 'format', [ new Arg( new Expr\BinaryOp\Coalesce( diff --git a/src/Transformer/DictionaryTransformer.php b/src/Transformer/DictionaryTransformer.php index 6d3358fd..d62785de 100644 --- a/src/Transformer/DictionaryTransformer.php +++ b/src/Transformer/DictionaryTransformer.php @@ -13,6 +13,11 @@ */ final readonly class DictionaryTransformer extends AbstractArrayTransformer { + /** + * Assign the value by using the key as the array key. + * + * $values[$key] = $output; + */ protected function getAssignExpr(Expr $valuesVar, Expr $outputVar, Expr $loopKeyVar, bool $assignByRef): Expr { if ($assignByRef) { diff --git a/src/Transformer/MultipleTransformer.php b/src/Transformer/MultipleTransformer.php index 35b93692..c9a5b59e 100644 --- a/src/Transformer/MultipleTransformer.php +++ b/src/Transformer/MultipleTransformer.php @@ -50,6 +50,18 @@ public function transform(Expr $input, Expr $target, PropertyMapping $propertyMa new Stmt\Expression(new Expr\Assign($output, $input)), ]; + /* + * In case of the source type can be mixed we need to check the type before doing the transformation. + * + * if (is_bool($input)) { + * $output = $input; + * } + * + * if (is_int($input)) { + * $output = (bool) $input; + * } + * + */ foreach ($this->transformers as $transformerData) { $transformer = $transformerData['transformer']; $type = $transformerData['type']; diff --git a/src/Transformer/NullableTransformer.php b/src/Transformer/NullableTransformer.php index 0f82854e..c52fbc98 100644 --- a/src/Transformer/NullableTransformer.php +++ b/src/Transformer/NullableTransformer.php @@ -32,6 +32,18 @@ public function transform(Expr $input, Expr $target, PropertyMapping $propertyMa $assignClass = ($this->itemTransformer instanceof AssignedByReferenceTransformerInterface && $this->itemTransformer->assignByRef()) ? Expr\AssignRef::class : Expr\Assign::class; if ($this->isTargetNullable) { + /** + * If target is nullable we set the default value to null, if not nullable there will no default value. + * + * $value = null; + * + * if ($input !== null) { + * ... // item statements + * $value = $output; + * } + * + * // mutator statements + */ $newOutput = new Expr\Variable($uniqueVariableScope->getUniqueName('value')); $statements[] = new Stmt\Expression(new Expr\Assign($newOutput, new Expr\ConstFetch(new Name('null')))); $itemStatements[] = new Stmt\Expression(new $assignClass($newOutput, $output)); diff --git a/src/Transformer/ObjectTransformer.php b/src/Transformer/ObjectTransformer.php index 188fd539..06791903 100644 --- a/src/Transformer/ObjectTransformer.php +++ b/src/Transformer/ObjectTransformer.php @@ -30,6 +30,11 @@ public function transform(Expr $input, Expr $target, PropertyMapping $propertyMa { $mapperName = $this->getDependencyName(); + /* + * Use a sub mapper to map the property + * + * $this->mappers['Mapper_SourceType_TargetType']->map($input, MapperContext::withNewContext($context, $propertyMapping->property)); + */ return [new Expr\MethodCall(new Expr\ArrayDimFetch( new Expr\PropertyFetch(new Expr\Variable('this'), 'mappers'), new Scalar\String_($mapperName) diff --git a/src/Transformer/SourceEnumTransformer.php b/src/Transformer/SourceEnumTransformer.php index c032e9f0..69828824 100644 --- a/src/Transformer/SourceEnumTransformer.php +++ b/src/Transformer/SourceEnumTransformer.php @@ -17,6 +17,7 @@ final class SourceEnumTransformer implements TransformerInterface { public function transform(Expr $input, Expr $target, PropertyMapping $propertyMapping, UniqueVariableScope $uniqueVariableScope): array { + /* $input->value */ return [new Expr\PropertyFetch($input, 'value'), []]; } } diff --git a/src/Transformer/StringToDateTimeTransformer.php b/src/Transformer/StringToDateTimeTransformer.php index 75700a3e..da27fdff 100644 --- a/src/Transformer/StringToDateTimeTransformer.php +++ b/src/Transformer/StringToDateTimeTransformer.php @@ -29,6 +29,11 @@ public function transform(Expr $input, Expr $target, PropertyMapping $propertyMa { $className = \DateTimeInterface::class === $this->className ? \DateTimeImmutable::class : $this->className; + /* + * Create a \DateTime[Immutable] object from a string. + * + * \DateTimeImmutable::createFromFormat($context[MapperContext::DATETIME_FORMAT] ?? \DateTimeInterface::RFC3339, $input); + */ return [new Expr\StaticCall(new Name\FullyQualified($className), 'createFromFormat', [ new Arg( new Expr\BinaryOp\Coalesce( diff --git a/src/Transformer/StringToSymfonyUidTransformer.php b/src/Transformer/StringToSymfonyUidTransformer.php index c3384b84..2e72e0a0 100644 --- a/src/Transformer/StringToSymfonyUidTransformer.php +++ b/src/Transformer/StringToSymfonyUidTransformer.php @@ -24,6 +24,11 @@ public function __construct( public function transform(Expr $input, Expr $target, PropertyMapping $propertyMapping, UniqueVariableScope $uniqueVariableScope): array { + /* + * Create a Symfony Uid object from a string. + * + * new \Symfony\Component\Uid\Uuid($input); + */ return [ new Expr\New_(new Name($this->className), [new Arg($input)]), [], diff --git a/src/Transformer/SymfonyUidCopyTransformer.php b/src/Transformer/SymfonyUidCopyTransformer.php index 370a36b7..39b0a9a9 100644 --- a/src/Transformer/SymfonyUidCopyTransformer.php +++ b/src/Transformer/SymfonyUidCopyTransformer.php @@ -21,6 +21,11 @@ final class SymfonyUidCopyTransformer implements TransformerInterface { public function transform(Expr $input, Expr $target, PropertyMapping $propertyMapping, UniqueVariableScope $uniqueVariableScope): array { + /* + * Create a Symfony Uid object from another Symfony Uid object. + * + * $input instanceof \Symfony\Component\Uid\Ulid ? new \Symfony\Component\Uid\Ulid($input->toBase32()) : new \Symfony\Component\Uid\Uuid($input->toRfc4122()); + */ return [ new Expr\Ternary( new Expr\Instanceof_($input, new Name(Ulid::class)), diff --git a/src/Transformer/SymfonyUidToStringTransformer.php b/src/Transformer/SymfonyUidToStringTransformer.php index de252693..70a0f30e 100644 --- a/src/Transformer/SymfonyUidToStringTransformer.php +++ b/src/Transformer/SymfonyUidToStringTransformer.php @@ -22,6 +22,11 @@ public function __construct( public function transform(Expr $input, Expr $target, PropertyMapping $propertyMapping, UniqueVariableScope $uniqueVariableScope): array { + /* + * Create a string from a Symfony Uid object. + * + * $input->toBase32() or $input->toRfc4122(); + */ if ($this->isUlid) { return [ // ulid diff --git a/src/Transformer/TargetEnumTransformer.php b/src/Transformer/TargetEnumTransformer.php index c8fbd748..d3be03ed 100644 --- a/src/Transformer/TargetEnumTransformer.php +++ b/src/Transformer/TargetEnumTransformer.php @@ -24,6 +24,11 @@ public function __construct( public function transform(Expr $input, Expr $target, PropertyMapping $propertyMapping, UniqueVariableScope $uniqueVariableScope): array { + /* + * Transform a string into a BackendEnum. + * + * \Backend\Enum\TargetEnum::from($input); + */ return [new Expr\StaticCall(new Name\FullyQualified($this->targetClassName), 'from', [ new Arg($input), ]), []];