diff --git a/composer.json b/composer.json index c198e0c..1af738a 100644 --- a/composer.json +++ b/composer.json @@ -16,7 +16,7 @@ } ], "require": { - "php": ">=7.3.0", + "php": ">=7.4", "ext-json": "*", "psr/log": "^1|^2|^3", "psr/http-client": "^1.0", @@ -26,7 +26,7 @@ "php-http/message-factory": "^1.0", "php-http/discovery": "^1.13", "doctrine/annotations": "^1.13|^2.0", - "liip/serializer": "2.2.*", + "liip/serializer": "2.6.*", "php-http/httplug": "^2.2", "civicrm/composer-compile-plugin": "^0.18.0", "symfony/console": "^4.0|^5.0|^6.0", diff --git a/src/Command/GenerateModelsCommand.php b/src/Command/GenerateModelsCommand.php index 64d70c5..18b9c1b 100644 --- a/src/Command/GenerateModelsCommand.php +++ b/src/Command/GenerateModelsCommand.php @@ -13,6 +13,7 @@ use RetailCrm\Api\Component\ModelsGenerator; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Style\SymfonyStyle; /** * Class GenerateModelsCommand @@ -82,7 +83,15 @@ class GenerateModelsCommand extends AbstractModelsProcessorCommand $output->writeln(''); } - $generator->generate(); + try { + $generator->generate(); + } catch (\Throwable $throwable) { + $styled = new SymfonyStyle($input, $output); + $styled->error($throwable->getMessage()); + $styled->writeln($throwable->getTraceAsString()); + + return -1; + } $output->writeln(sprintf( ' ✓ Done, generated code for %d models.', diff --git a/src/Component/ModelsGenerator.php b/src/Component/ModelsGenerator.php index 7e76185..8235326 100644 --- a/src/Component/ModelsGenerator.php +++ b/src/Component/ModelsGenerator.php @@ -11,12 +11,12 @@ namespace RetailCrm\Api\Component; use Doctrine\Common\Annotations\AnnotationReader; use Liip\MetadataParser\Builder; +use Liip\MetadataParser\ModelParser\RawMetadata\PropertyCollection; use Liip\MetadataParser\Parser; use Liip\MetadataParser\RecursionChecker; use Liip\Serializer\Configuration\GeneratorConfiguration; use Liip\Serializer\Template\Deserialization; use Liip\Serializer\Template\Serialization; -use RetailCrm\Api\Component\Utils; use RetailCrm\Api\Component\Serializer\Generator\DeserializerGenerator; use RetailCrm\Api\Component\Serializer\Generator\SerializerGenerator; use RetailCrm\Api\Component\Serializer\ModelsChecksumGenerator; @@ -177,6 +177,7 @@ class ModelsGenerator $configurationArray['classes'][$class] = []; } + PropertyCollection::useIdenticalNamingStrategy(); $configuration = GeneratorConfiguration::createFomArray($configurationArray); $parsers = [new JMSParser(new AnnotationReader())]; $builder = new Builder(new Parser($parsers), new RecursionChecker(null, [])); diff --git a/src/Component/Serializer/Generator/DeserializerGenerator.php b/src/Component/Serializer/Generator/DeserializerGenerator.php index eb46c7d..4bd7c28 100644 --- a/src/Component/Serializer/Generator/DeserializerGenerator.php +++ b/src/Component/Serializer/Generator/DeserializerGenerator.php @@ -1,11 +1,6 @@ - * @author Pavel Kovalenko - * @see https://github.com/liip/serializer - * @internal - * - * @SuppressWarnings(PHPMD) - */ -class DeserializerGenerator +final class DeserializerGenerator { private const FILENAME_PREFIX = 'deserialize'; - /** - * @var Deserialization - */ - private $templating; + private Filesystem $filesystem; + + private GeneratorConfiguration $configuration; + + private Deserialization $templating; + + private CustomDeserialization $customTemplating; + + private string $cacheDirectory; + + private Builder $metadataBuilder; /** - * @var \RetailCrm\Api\Component\Serializer\Template\CustomDeserialization - */ - private $customTemplating; - - /** - * @var Filesystem - */ - private $filesystem; - - /** - * @var Builder - */ - private $metadataBuilder; - - /** - * This is a list of fqn classnames - * - * I.e. - * - * [ - * Product::class, - * ]; - * - * @var string[] - */ - private $classesToGenerate; - - /** - * @var string - */ - private $cacheDirectory; - - /** - * @param \Liip\Serializer\Template\Deserialization $templating - * @param \RetailCrm\Api\Component\Serializer\Template\CustomDeserialization $customTemplating - * @param string[] $classesToGenerate - * @param string $cacheDirectory + * @param list $classesToGenerate This is a list of FQCN classnames */ public function __construct( Deserialization $templating, CustomDeserialization $customTemplating, array $classesToGenerate, - string $cacheDirectory + string $cacheDirectory, + GeneratorConfiguration $configuration = null ) { + $this->cacheDirectory = $cacheDirectory; $this->templating = $templating; $this->customTemplating = $customTemplating; - $this->classesToGenerate = $classesToGenerate; - $this->cacheDirectory = $cacheDirectory; $this->filesystem = new Filesystem(); + $this->configuration = $this->createGeneratorConfiguration($configuration, $classesToGenerate); } - /** - * @param string $className - * - * @return string - */ public static function buildDeserializerFunctionName(string $className): string { - return static::FILENAME_PREFIX . '_' . str_replace('\\', '_', $className); + return self::FILENAME_PREFIX.'_'.str_replace('\\', '_', $className); } - /** - * @param \Liip\MetadataParser\Builder $metadataBuilder - * - * @throws \Exception - */ public function generate(Builder $metadataBuilder): void { $this->metadataBuilder = $metadataBuilder; - $this->filesystem->mkdir($this->cacheDirectory); - foreach ($this->classesToGenerate as $className) { + /** @var ClassToGenerate $classToGenerate */ + foreach ($this->configuration as $classToGenerate) { // we do not use the oldest version reducer here and hope for the best // otherwise we end up with generated property names for accessor methods - $classMetadata = $metadataBuilder->build($className, [ + $classMetadata = $metadataBuilder->build($classToGenerate->getClassName(), [ new TakeBestReducer(), ]); $this->writeFile($classMetadata); } } - /** - * @param \Liip\MetadataParser\Metadata\ClassMetadata $classMetadata - * - * @throws \Exception - */ private function writeFile(ClassMetadata $classMetadata): void { - if (count($classMetadata->getConstructorParameters())) { - throw new RuntimeException(sprintf( - 'We currently do not support deserializing when the root class has a non-empty constructor. Class %s', - $classMetadata->getClassName() - )); + if (\count($classMetadata->getConstructorParameters())) { + throw new \Exception(sprintf('We currently do not support deserializing when the root class has a non-empty constructor. Class %s', $classMetadata->getClassName())); } - $functionName = static::buildDeserializerFunctionName($classMetadata->getClassName()); + $functionName = self::buildDeserializerFunctionName($classMetadata->getClassName()); $arrayPath = new ArrayPath('jsonData'); $code = $this->templating->renderFunction( @@ -162,13 +99,7 @@ class DeserializerGenerator } /** - * @param \Liip\MetadataParser\Metadata\ClassMetadata $classMetadata - * @param \Liip\Serializer\Path\ArrayPath $arrayPath - * @param \Liip\Serializer\Path\ModelPath $modelPath - * @param mixed[] $stack - * - * @return string - * @throws \Exception + * @param array $stack */ private function generateCodeForClass( ClassMetadata $classMetadata, @@ -179,9 +110,9 @@ class DeserializerGenerator $stack[$classMetadata->getClassName()] = ($stack[$classMetadata->getClassName()] ?? 0) + 1; $constructorArgumentNames = []; + $overwrittenNames = []; $initCode = ''; $code = ''; - foreach ($classMetadata->getProperties() as $propertyMetadata) { $propertyArrayPath = $arrayPath->withFieldName($propertyMetadata->getSerializedName()); @@ -189,6 +120,9 @@ class DeserializerGenerator $argument = $classMetadata->getConstructorParameter($propertyMetadata->getName()); $default = var_export($argument->isRequired() ? null : $argument->getDefaultValue(), true); $tempVariable = ModelPath::tempVariable([(string) $modelPath, $propertyMetadata->getName()]); + if (\array_key_exists($propertyMetadata->getName(), $constructorArgumentNames)) { + $overwrittenNames[$propertyMetadata->getName()] = true; + } $constructorArgumentNames[$propertyMetadata->getName()] = (string) $tempVariable; $initCode .= $this->templating->renderArgument( @@ -206,26 +140,21 @@ class DeserializerGenerator } $constructorArguments = []; - foreach ($classMetadata->getConstructorParameters() as $definition) { - if (array_key_exists($definition->getName(), $constructorArgumentNames)) { + if (\array_key_exists($definition->getName(), $constructorArgumentNames)) { $constructorArguments[] = $constructorArgumentNames[$definition->getName()]; continue; } - if ($definition->isRequired()) { - throw new RuntimeException(sprintf( - 'Unknown constructor argument "%s" in "%s(%s)"', - $definition->getName(), - $classMetadata->getClassName(), - implode(', ', array_keys($constructorArgumentNames)) - )); + $msg = sprintf('Unknown constructor argument "%s". Class %s only has properties that tell how to handle %s.', $definition->getName(), $classMetadata->getClassName(), implode(', ', array_keys($constructorArgumentNames))); + if ($overwrittenNames) { + $msg .= sprintf(' Multiple definitions for fields %s seen - the last one overwrites previous ones.', implode(', ', array_keys($overwrittenNames))); + } + throw new \Exception($msg); } - $constructorArguments[] = var_export($definition->getDefaultValue(), true); } - - if (count($constructorArgumentNames) > 0) { + if (\count($constructorArgumentNames) > 0) { $code .= $this->templating->renderUnset(array_values($constructorArgumentNames)); } @@ -233,13 +162,7 @@ class DeserializerGenerator return $this->generateCustomerInterface($classMetadata, $arrayPath, $modelPath, $initCode, $stack); } - return $this->templating->renderClass( - (string) $modelPath, - $classMetadata->getClassName(), - $constructorArguments, - $code, - $initCode - ); + return $this->templating->renderClass((string) $modelPath, $classMetadata->getClassName(), $constructorArguments, $code, $initCode); } /** @@ -282,13 +205,7 @@ class DeserializerGenerator } /** - * @param \Liip\MetadataParser\Metadata\PropertyMetadata $propertyMetadata - * @param \Liip\Serializer\Path\ArrayPath $arrayPath - * @param \Liip\Serializer\Path\ModelPath $modelPath - * @param mixed[] $stack - * - * @return string - * @throws \Exception + * @param array $stack */ private function generateCodeForProperty( PropertyMetadata $propertyMetadata, @@ -300,16 +217,16 @@ class DeserializerGenerator return ''; } + if (Recursion::hasMaxDepthReached($propertyMetadata, $stack)) { + return ''; + } + if ($propertyMetadata->getAccessor()->hasSetterMethod()) { $tempVariable = ModelPath::tempVariable([(string) $modelPath, $propertyMetadata->getName()]); $code = $this->generateCodeForField($propertyMetadata, $arrayPath, $tempVariable, $stack); $code .= $this->templating->renderConditional( (string) $tempVariable, - $this->templating->renderSetter( - (string) $modelPath, - (string) $propertyMetadata->getAccessor()->getSetterMethod(), - (string) $tempVariable - ) + $this->templating->renderSetter((string) $modelPath, $propertyMetadata->getAccessor()->getSetterMethod(), (string) $tempVariable) ); $code .= $this->templating->renderUnset([(string) $tempVariable]); @@ -322,13 +239,7 @@ class DeserializerGenerator } /** - * @param \Liip\MetadataParser\Metadata\PropertyMetadata $propertyMetadata - * @param \Liip\Serializer\Path\ArrayPath $arrayPath - * @param \Liip\Serializer\Path\ModelPath $modelPath - * @param mixed[] $stack - * - * @return string - * @throws \Exception + * @param array $stack */ private function generateCodeForField( PropertyMetadata $propertyMetadata, @@ -343,13 +254,7 @@ class DeserializerGenerator } /** - * @param \Liip\MetadataParser\Metadata\PropertyMetadata $propertyMetadata - * @param \Liip\Serializer\Path\ArrayPath $arrayPath - * @param \Liip\Serializer\Path\ModelPath $modelPropertyPath - * @param mixed[] $stack - * - * @return string - * @throws \Exception + * @param array $stack */ private function generateInnerCodeForFieldType( PropertyMetadata $propertyMetadata, @@ -359,64 +264,40 @@ class DeserializerGenerator ): string { $type = $propertyMetadata->getType(); - if ($type instanceof PropertyTypeArray) { - if ($type->getSubType() instanceof PropertyTypePrimitive) { - // for arrays of scalars, copy the field even when its an empty array - return $this->templating->renderAssignJsonDataToField((string) $modelPropertyPath, (string) $arrayPath); - } - - // either array or hashmap with second param the type of values - // the index works the same whether its numeric or hashmap - return $this->generateCodeForArray($type, $arrayPath, $modelPropertyPath, $stack); - } - switch ($type) { + case $type instanceof PropertyTypeArray: + if ($type->isTraversable()) { + return $this->generateCodeForArrayCollection($propertyMetadata, $type, $arrayPath, $modelPropertyPath, $stack); + } + + return $this->generateCodeForArray($type, $arrayPath, $modelPropertyPath, $stack); + case $type instanceof PropertyTypeDateTime: - if (null !== $type->getZone()) { - throw new RuntimeException('Timezone support is not implemented'); + $formats = $type->getDeserializeFormats() ?: (\is_string($type->getFormat()) ? [$type->getFormat()] : $type->getFormat()); + if (null !== $formats) { + return $this->templating->renderAssignDateTimeFromFormat($type->isImmutable(), (string) $modelPropertyPath, (string) $arrayPath, $formats, $type->getZone()); } - $format = $type->getDeserializeFormat() ?: $type->getFormat(); + return $this->templating->renderAssignDateTimeToField($type->isImmutable(), (string) $modelPropertyPath, (string) $arrayPath); - if (null !== $format) { - return $this->templating->renderAssignDateTimeFromFormat( - $type->isImmutable(), - (string) $modelPropertyPath, - (string) $arrayPath, - $format - ); - } - - return $this->templating->renderAssignDateTimeToField( - $type->isImmutable(), - (string) $modelPropertyPath, - (string) $arrayPath - ); case $type instanceof PropertyTypePrimitive && 'float' === $type->getTypeName(): - return $this->templating->renderAssignJsonDataToFieldWithCasting( - (string) $modelPropertyPath, - (string) $arrayPath, - 'float' - ); + return $this->templating->renderAssignJsonDataToFieldWithCasting((string) $modelPropertyPath, (string) $arrayPath, 'float'); + case $type instanceof PropertyTypePrimitive: case $type instanceof PropertyTypeUnknown: case $type instanceof PropertyTypeMixed: return $this->templating->renderAssignJsonDataToField((string) $modelPropertyPath, (string) $arrayPath); + case $type instanceof PropertyTypeClass: return $this->generateCodeForClass($type->getClassMetadata(), $arrayPath, $modelPropertyPath, $stack); + default: - throw new RuntimeException('Unexpected type ' . get_class($type) . ' at ' . $modelPropertyPath); + throw new \Exception('Unexpected type '. get_class($type) .' at '.$modelPropertyPath); } } /** - * @param \Liip\MetadataParser\Metadata\PropertyTypeArray $type - * @param \Liip\Serializer\Path\ArrayPath $arrayPath - * @param \Liip\Serializer\Path\ModelPath $modelPath - * @param mixed[] $stack - * - * @return string - * @throws \Exception + * @param array $stack */ private function generateCodeForArray( PropertyTypeArray $type, @@ -424,6 +305,11 @@ class DeserializerGenerator ModelPath $modelPath, array $stack ): string { + if ($type->getSubType() instanceof PropertyTypePrimitive) { + // for arrays of scalars, copy the field even when its an empty array + return $this->templating->renderAssignJsonDataToField((string) $modelPath, (string) $arrayPath); + } + $index = ModelPath::indexVariable((string) $arrayPath); $arrayPropertyPath = $arrayPath->withVariable((string) $index); $modelPropertyPath = $modelPath->withArray((string) $index); @@ -433,22 +319,16 @@ class DeserializerGenerator case $subType instanceof PropertyTypeArray: $innerCode = $this->generateCodeForArray($subType, $arrayPropertyPath, $modelPropertyPath, $stack); break; + case $subType instanceof PropertyTypeClass: - $innerCode = $this->generateCodeForClass( - $subType->getClassMetadata(), - $arrayPropertyPath, - $modelPropertyPath, - $stack - ); + $innerCode = $this->generateCodeForClass($subType->getClassMetadata(), $arrayPropertyPath, $modelPropertyPath, $stack); break; + case $subType instanceof PropertyTypeUnknown: - $innerCode = $this->templating->renderAssignJsonDataToField( - $modelPropertyPath, - $arrayPropertyPath - ); - break; + return $this->templating->renderAssignJsonDataToField((string) $modelPath, (string) $arrayPath); + default: - throw new RuntimeException('Unexpected array subtype ' . get_class($subType)); + throw new \Exception('Unexpected array subtype '. get_class($subType)); } if ('' === $innerCode) { @@ -460,4 +340,42 @@ class DeserializerGenerator return $code; } + + /** + * @param array $stack + */ + private function generateCodeForArrayCollection( + PropertyMetadata $propertyMetadata, + PropertyTypeArray $type, + ArrayPath $arrayPath, + ModelPath $modelPath, + array $stack + ): string { + $tmpVariable = ModelPath::tempVariable([(string) $modelPath, $propertyMetadata->getName()]); + $innerCode = $this->generateCodeForArray($type, $arrayPath, $tmpVariable, $stack); + + if ('' === $innerCode) { + return ''; + } + + return $innerCode.$this->templating->renderArrayCollection((string) $modelPath, (string) $tmpVariable); + } + + /** + * @param list $classesToGenerate + */ + private function createGeneratorConfiguration( + ?GeneratorConfiguration $configuration, + array $classesToGenerate + ): GeneratorConfiguration { + if (null === $configuration) { + $configuration = new GeneratorConfiguration([], []); + } + + foreach ($classesToGenerate as $className) { + $configuration->addClassToGenerate(new ClassToGenerate($configuration, $className)); + } + + return $configuration; + } } diff --git a/src/Component/Serializer/Generator/Recursion.php b/src/Component/Serializer/Generator/Recursion.php new file mode 100644 index 0000000..a54e3a4 --- /dev/null +++ b/src/Component/Serializer/Generator/Recursion.php @@ -0,0 +1,57 @@ + $stack + */ + public static function check(string $className, array $stack, string $modelPath): bool + { + if (\array_key_exists($className, $stack) && $stack[$className] > 1) { + throw new \Exception(sprintf('recursion for %s at %s', key($stack), $modelPath)); + } + + return false; + } + + /** + * @param array $stack + */ + public static function hasMaxDepthReached(PropertyMetadata $propertyMetadata, array $stack): bool + { + if (null === $propertyMetadata->getMaxDepth()) { + return false; + } + + $className = self::getClassNameFromProperty($propertyMetadata); + if (null === $className) { + return false; + } + + $classStackCount = $stack[$className] ?? 0; + + return $classStackCount > $propertyMetadata->getMaxDepth(); + } + + private static function getClassNameFromProperty(PropertyMetadata $propertyMetadata): ?string + { + $type = $propertyMetadata->getType(); + if ($type instanceof PropertyTypeArray) { + $type = $type->getLeafType(); + } + + if (!($type instanceof PropertyTypeClass)) { + return null; + } + + return $type->getClassName(); + } +} diff --git a/src/Component/Serializer/Generator/SerializerGenerator.php b/src/Component/Serializer/Generator/SerializerGenerator.php index d83b55e..9288246 100644 --- a/src/Component/Serializer/Generator/SerializerGenerator.php +++ b/src/Component/Serializer/Generator/SerializerGenerator.php @@ -1,17 +1,9 @@ - * @author Pavel Kovalenko - * @see https://github.com/liip/serializer - * @internal - * - * @SuppressWarnings(PHPMD) - */ -class SerializerGenerator +final class SerializerGenerator { private const FILENAME_PREFIX = 'serialize'; - /** - * @var Serialization - */ - private $templating; + private Filesystem $filesystem; - /** - * @var \RetailCrm\Api\Component\Serializer\Template\CustomSerialization - */ - private $customTemplating; + private Serialization $templating; + private GeneratorConfiguration $configuration; + private string $cacheDirectory; - /** - * @var Builder - */ - private $metadataBuilder; + private CustomSerialization $customTemplating; - /** - * @var GeneratorConfiguration - */ - private $configuration; + private Builder $metadataBuilder; - /** - * @var string - */ - private $cacheDirectory; - - /** - * @var Filesystem - */ - private $filesystem; - - /** - * SerializerGenerator constructor. - * - * @param \Liip\Serializer\Template\Serialization $templating - * @param \RetailCrm\Api\Component\Serializer\Template\CustomSerialization $customTemplating - * @param \Liip\Serializer\Configuration\GeneratorConfiguration $configuration - * @param string $cacheDirectory - */ public function __construct( Serialization $templating, CustomSerialization $customTemplating, GeneratorConfiguration $configuration, string $cacheDirectory ) { - $this->templating = $templating; + $this->cacheDirectory = $cacheDirectory; + $this->configuration = $configuration; + $this->templating = $templating; $this->customTemplating = $customTemplating; - $this->configuration = $configuration; - $this->cacheDirectory = $cacheDirectory; - $this->filesystem = new Filesystem(); } /** - * @param string $className - * @param string|null $apiVersion - * @param array $serializerGroups - * - * @return string + * @param list $serializerGroups */ - public static function buildSerializerFunctionName( - string $className, - ?string $apiVersion, - array $serializerGroups - ): string { - $functionName = static::FILENAME_PREFIX . '_' . $className; - - if (count($serializerGroups)) { - $functionName .= '_' . implode('_', $serializerGroups); + public static function buildSerializerFunctionName(string $className, ?string $apiVersion, array $serializerGroups): string + { + $functionName = self::FILENAME_PREFIX.'_'.$className; + if (\count($serializerGroups)) { + $functionName .= '_'.implode('_', $serializerGroups); } - if (null !== $apiVersion) { - $functionName .= '_' . $apiVersion; + $functionName .= '_'.$apiVersion; } - return (string) preg_replace('/[^a-zA-Z0-9_]/', '_', $functionName); + return preg_replace('/[^a-zA-Z0-9_]/', '_', $functionName); } - /** - * @param \Liip\MetadataParser\Builder $metadataBuilder - */ public function generate(Builder $metadataBuilder): void { $this->metadataBuilder = $metadataBuilder; - $this->filesystem->mkdir($this->cacheDirectory); foreach ($this->configuration as $classToGenerate) { foreach ($classToGenerate as $groupCombination) { $className = $classToGenerate->getClassName(); - foreach ($groupCombination->getVersions() as $version) { + $groups = $groupCombination->getGroups(); if ('' === $version) { - $metadata = $metadataBuilder->build($className, [ - new PreferredReducer(), - new TakeBestReducer(), - ]); - - $this->writeFile($className, null, $groupCombination->getGroups(), $metadata); + if ([] === $groups) { + $metadata = $metadataBuilder->build($className, [ + new PreferredReducer(), + new TakeBestReducer(), + ]); + $this->writeFile($className, null, [], $metadata); + } else { + $metadata = $metadataBuilder->build($className, [ + new GroupReducer($groups), + new PreferredReducer(), + new TakeBestReducer(), + ]); + $this->writeFile($className, null, $groups, $metadata); + } } else { $metadata = $metadataBuilder->build($className, [ new VersionReducer($version), - new GroupReducer($groupCombination->getGroups()), + new GroupReducer($groups), new TakeBestReducer(), ]); - - $this->writeFile($className, $version, $groupCombination->getGroups(), $metadata); + $this->writeFile($className, $version, $groups, $metadata); } } } @@ -168,10 +110,7 @@ class SerializerGenerator } /** - * @param string $className - * @param string|null $apiVersion - * @param array $serializerGroups - * @param \Liip\MetadataParser\Metadata\ClassMetadata $classMetadata + * @param list $serializerGroups */ private function writeFile( string $className, @@ -179,8 +118,7 @@ class SerializerGenerator array $serializerGroups, ClassMetadata $classMetadata ): void { - sort($serializerGroups); - $functionName = static::buildSerializerFunctionName($className, $apiVersion, $serializerGroups); + $functionName = self::buildSerializerFunctionName($className, $apiVersion, $serializerGroups); $code = $this->templating->renderFunction( $functionName, @@ -192,15 +130,8 @@ class SerializerGenerator } /** - * @param \Liip\MetadataParser\Metadata\ClassMetadata $classMetadata - * @param string|null $apiVersion - * @param array $serializerGroups - * @param string $arrayPath - * @param string $modelPath - * @param array $stack - * - * @return string - * @throws \Exception + * @param list $serializerGroups + * @param array $stack */ private function generateCodeForClass( ClassMetadata $classMetadata, @@ -221,22 +152,11 @@ class SerializerGenerator ); } - if ($classMetadata->getClassName() === CustomerTag::class) { - return $this->generateForCustomerTag($arrayPath, $modelPath); - } - $stack[$classMetadata->getClassName()] = ($stack[$classMetadata->getClassName()] ?? 0) + 1; - $code = ''; + $code = ''; foreach ($classMetadata->getProperties() as $propertyMetadata) { - $code .= $this->generateCodeForField( - $propertyMetadata, - $apiVersion, - $serializerGroups, - $arrayPath, - $modelPath, - $stack - ); + $code .= $this->generateCodeForField($propertyMetadata, $apiVersion, $serializerGroups, $arrayPath, $modelPath, $stack); } return $this->templating->renderClass($arrayPath, $code); @@ -323,26 +243,8 @@ class SerializerGenerator } /** - * @param string $arrayPath - * @param string $modelPath - * - * @return string - */ - private function generateForCustomerTag(string $arrayPath, string $modelPath): string - { - return $this->templating->renderAssign($arrayPath, $modelPath . '->name'); - } - - /** - * @param \Liip\MetadataParser\Metadata\PropertyMetadata $propertyMetadata - * @param string|null $apiVersion - * @param array $serializerGroups - * @param string $arrayPath - * @param string $modelPath - * @param array $stack - * - * @return string - * @throws \Exception + * @param list $serializerGroups + * @param array $stack */ private function generateCodeForField( PropertyMetadata $propertyMetadata, @@ -352,61 +254,34 @@ class SerializerGenerator string $modelPath, array $stack ): string { - $modelPropertyPath = $modelPath . '->' . $propertyMetadata->getName(); - $fieldPath = $arrayPath . '["' . $propertyMetadata->getSerializedName() . '"]'; + if (Recursion::hasMaxDepthReached($propertyMetadata, $stack)) { + return ''; + } + + $modelPropertyPath = $modelPath.'->'.$propertyMetadata->getName(); + $fieldPath = $arrayPath.'["'.$propertyMetadata->getSerializedName().'"]'; if ($propertyMetadata->getAccessor()->hasGetterMethod()) { - $tempVariable = str_replace(['->', '[', ']', '$'], '', $modelPath) . ucfirst($propertyMetadata->getName()); + $tempVariable = str_replace(['->', '[', ']', '$'], '', $modelPath).ucfirst($propertyMetadata->getName()); return $this->templating->renderConditional( - $this->templating->renderTempVariable( - $tempVariable, - $this->templating->renderGetter( - $modelPath, - (string) $propertyMetadata->getAccessor()->getGetterMethod() - ) - ), - $this->generateCodeForFieldType( - $propertyMetadata->getType(), - $apiVersion, - $serializerGroups, - $fieldPath, - '$' . $tempVariable, - $stack - ) + $this->templating->renderTempVariable($tempVariable, $this->templating->renderGetter($modelPath, $propertyMetadata->getAccessor()->getGetterMethod())), + $this->generateCodeForFieldType($propertyMetadata->getType(), $apiVersion, $serializerGroups, $fieldPath, '$'.$tempVariable, $stack) ); } if (!$propertyMetadata->isPublic()) { - throw new RuntimeException(sprintf( - 'Property %s is not public and no getter has been defined. Stack %s', - $modelPropertyPath, - var_export($stack, true) - )); + throw new \Exception(sprintf('Property %s is not public and no getter has been defined. Stack %s', $modelPropertyPath, var_export($stack, true))); } return $this->templating->renderConditional( $modelPropertyPath, - $this->generateCodeForFieldType( - $propertyMetadata->getType(), - $apiVersion, - $serializerGroups, - $fieldPath, - $modelPropertyPath, - $stack - ) + $this->generateCodeForFieldType($propertyMetadata->getType(), $apiVersion, $serializerGroups, $fieldPath, $modelPropertyPath, $stack) ); } /** - * @param \Liip\MetadataParser\Metadata\PropertyType $type - * @param string|null $apiVersion - * @param array $serializerGroups - * @param string $fieldPath - * @param string $modelPropertyPath - * @param array $stack - * - * @return string - * @throws \Exception + * @param list $serializerGroups + * @param array $stack */ private function generateCodeForFieldType( PropertyType $type, @@ -416,31 +291,9 @@ class SerializerGenerator string $modelPropertyPath, array $stack ): string { - if ($type instanceof PropertyTypeArray) { - if ($type->getSubType() instanceof PropertyTypePrimitive) { - // for arrays of scalars, copy the field even when its an empty array - return $this->templating->renderAssign($fieldPath, $modelPropertyPath); - } - - // either array or hashmap with second param the type of values - // the index works the same whether its numeric or hashmap - return $this->generateCodeForArray( - $type, - $apiVersion, - $serializerGroups, - $fieldPath, - $modelPropertyPath, - $stack - ); - } - switch ($type) { case $type instanceof PropertyTypeDateTime: - if (null !== $type->getZone()) { - throw new \RuntimeException('Timezone support is not implemented'); - } - - $dateFormat = $type->getFormat() ?: DateTime::ATOM; + $dateFormat = $type->getFormat() ?: \DateTimeInterface::ISO8601; return $this->templating->renderAssign( $fieldPath, @@ -454,30 +307,19 @@ class SerializerGenerator return $this->templating->renderAssign($fieldPath, $modelPropertyPath); case $type instanceof PropertyTypeClass: - return $this->generateCodeForClass( - $type->getClassMetadata(), - $apiVersion, - $serializerGroups, - $fieldPath, - $modelPropertyPath, - $stack - ); + return $this->generateCodeForClass($type->getClassMetadata(), $apiVersion, $serializerGroups, $fieldPath, $modelPropertyPath, $stack); + + case $type instanceof PropertyTypeArray: + return $this->generateCodeForArray($type, $apiVersion, $serializerGroups, $fieldPath, $modelPropertyPath, $stack); default: - throw new RuntimeException('Unexpected type ' . \get_class($type) . ' at ' . $modelPropertyPath); + throw new \Exception('Unexpected type '. get_class($type) .' at '.$modelPropertyPath); } } /** - * @param \Liip\MetadataParser\Metadata\PropertyTypeArray $type - * @param string|null $apiVersion - * @param array $serializerGroups - * @param string $arrayPath - * @param string $modelPath - * @param array $stack - * - * @return string - * @throws \Exception + * @param list $serializerGroups + * @param array $stack */ private function generateCodeForArray( PropertyTypeArray $type, @@ -487,35 +329,25 @@ class SerializerGenerator string $modelPath, array $stack ): string { - $index = '$index' . \mb_strlen($arrayPath); + $index = '$index'.mb_strlen($arrayPath); $subType = $type->getSubType(); switch ($subType) { - case $subType instanceof PropertyTypeArray: - $innerCode = $this->generateCodeForArray( - $subType, - $apiVersion, - $serializerGroups, - $arrayPath . '[' . $index . ']', - $modelPath . '[' . $index . ']', - $stack - ); - break; - case $subType instanceof PropertyTypeClass: - $innerCode = $this->generateCodeForClass( - $subType->getClassMetadata(), - $apiVersion, - $serializerGroups, - $arrayPath . '[' . $index . ']', - $modelPath . '[' . $index . ']', - $stack - ); - break; + case $subType instanceof PropertyTypePrimitive: + case $subType instanceof PropertyTypeArray && self::isArrayForPrimitive($subType): case $subType instanceof PropertyTypeUnknown: - $innerCode = $this->templating->renderAssign($arrayPath, $modelPath); + return $this->templating->renderArrayAssign($arrayPath, $modelPath); + + case $subType instanceof PropertyTypeArray: + $innerCode = $this->generateCodeForArray($subType, $apiVersion, $serializerGroups, $arrayPath.'['.$index.']', $modelPath.'['.$index.']', $stack); break; + + case $subType instanceof PropertyTypeClass: + $innerCode = $this->generateCodeForClass($subType->getClassMetadata(), $apiVersion, $serializerGroups, $arrayPath.'['.$index.']', $modelPath.'['.$index.']', $stack); + break; + default: - throw new RuntimeException('Unexpected array subtype ' . get_class($subType)); + throw new \Exception('Unexpected array subtype '. get_class($subType)); } if ('' === $innerCode) { @@ -532,4 +364,16 @@ class SerializerGenerator return $this->templating->renderLoopArray($arrayPath, $modelPath, $index, $innerCode); } + + private static function isArrayForPrimitive(PropertyTypeArray $type): bool + { + do { + $type = $type->getSubType(); + if ($type instanceof PropertyTypePrimitive) { + return true; + } + } while ($type instanceof PropertyTypeArray); + + return false; + } } diff --git a/src/Component/Serializer/Parser/BaseJMSParser.php b/src/Component/Serializer/Parser/BaseJMSParser.php deleted file mode 100644 index 8f484d8..0000000 --- a/src/Component/Serializer/Parser/BaseJMSParser.php +++ /dev/null @@ -1,197 +0,0 @@ - - * @internal - */ -final class BaseJMSParser -{ - /** - * @var \RetailCrm\Api\Component\Serializer\Parser\JMSLexer - */ - private $lexer; - - /** - * @var bool - */ - private $root = true; - - /** - * @param string $string - * - * @return array|mixed[] - */ - public function parse(string $string): array - { - $this->lexer = new JMSLexer(); - $this->lexer->setInput($string); - $this->lexer->moveNext(); - - return $this->visit(); - } - - /** - * @return mixed - * - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - */ - private function visit() - { - $this->lexer->moveNext(); - - if (!$this->lexer->token) { - throw new SyntaxError( - 'Syntax error, unexpected end of stream' - ); - } - - if (JMSLexer::T_FLOAT === $this->lexer->token['type']) { - return (float)$this->lexer->token['value']; - } elseif (JMSLexer::T_INTEGER === $this->lexer->token['type']) { - return (int)$this->lexer->token['value']; - } elseif (JMSLexer::T_NULL === $this->lexer->token['type']) { - return null; - } elseif (JMSLexer::T_STRING === $this->lexer->token['type']) { - return $this->lexer->token['value']; - } elseif (JMSLexer::T_IDENTIFIER === $this->lexer->token['type']) { - if ($this->lexer->isNextToken(JMSLexer::T_TYPE_START)) { - return $this->visitCompoundType(); - } elseif ($this->lexer->isNextToken(JMSLexer::T_ARRAY_START)) { - return $this->visitArrayType(); - } - - return $this->visitSimpleType(); - } elseif (!$this->root && JMSLexer::T_ARRAY_START === $this->lexer->token['type']) { - return $this->visitArrayType(); - } - - throw new SyntaxError(sprintf( - 'Syntax error, unexpected "%s" (%s)', - $this->lexer->token['value'], - // @phpstan-ignore-next-line - $this->getConstant($this->lexer->token['type']) - )); - } - - /** - * @return mixed[] - */ - private function visitSimpleType(): array - { - $value = $this->lexer->token['value']; // @phpstan-ignore-line - - return ['name' => $value, 'params' => []]; - } - - /** - * @return array - */ - private function visitCompoundType(): array - { - $this->root = false; - $name = $this->lexer->token['value']; // @phpstan-ignore-line - $this->match(JMSLexer::T_TYPE_START); - - $params = []; - if (!$this->lexer->isNextToken(JMSLexer::T_TYPE_END)) { - while (true) { - $params[] = $this->visit(); - - if ($this->lexer->isNextToken(JMSLexer::T_TYPE_END)) { - break; - } - - $this->match(JMSLexer::T_COMMA); - } - } - - $this->match(JMSLexer::T_TYPE_END); - - return [ - 'name' => $name, - 'params' => $params, - ]; - } - - /** - * @return array - */ - private function visitArrayType(): array - { - /* - * Here we should call $this->match(JMSLexer::T_ARRAY_START); to make it clean - * but the token has already been consumed by moveNext() in visit() - */ - $params = []; - - if (!$this->lexer->isNextToken(JMSLexer::T_ARRAY_END)) { - while (true) { - $params[] = $this->visit(); - - if ($this->lexer->isNextToken(JMSLexer::T_ARRAY_END)) { - break; - } - - $this->match(JMSLexer::T_COMMA); - } - } - - $this->match(JMSLexer::T_ARRAY_END); - - return $params; - } - - /** - * @param int $token - */ - private function match(int $token): void - { - if (!$this->lexer->lookahead) { - throw new SyntaxError( - sprintf('Syntax error, unexpected end of stream, expected %s', $this->getConstant($token)) - ); - } - - if ($this->lexer->lookahead['type'] === $token) { - $this->lexer->moveNext(); - - return; - } - - throw new SyntaxError(sprintf( - 'Syntax error, unexpected "%s" (%s), expected was %s', - $this->lexer->lookahead['value'], - $this->getConstant($this->lexer->lookahead['type']), // @phpstan-ignore-line - $this->getConstant($token) - )); - } - - /** - * @param int $value - * - * @return string - */ - private function getConstant(int $value): string - { - $oClass = new ReflectionClass(JMSLexer::class); - - return (string) array_search($value, $oClass->getConstants()); - } -} diff --git a/src/Component/Serializer/Parser/JMSCore/Exception/Exception.php b/src/Component/Serializer/Parser/JMSCore/Exception/Exception.php new file mode 100644 index 0000000..11f7919 --- /dev/null +++ b/src/Component/Serializer/Parser/JMSCore/Exception/Exception.php @@ -0,0 +1,14 @@ + + */ +interface Exception extends \Throwable +{ +} diff --git a/src/Component/Serializer/Parser/JMSCore/Type/Exception/Exception.php b/src/Component/Serializer/Parser/JMSCore/Type/Exception/Exception.php new file mode 100644 index 0000000..236ebf0 --- /dev/null +++ b/src/Component/Serializer/Parser/JMSCore/Type/Exception/Exception.php @@ -0,0 +1,11 @@ +getType($type); - } catch (Throwable $e) { + } catch (\Throwable $e) { throw new SyntaxError($e->getMessage(), 0, $e); } } - /** - * @return string[] - */ protected function getCatchablePatterns(): array { return [ @@ -63,18 +47,15 @@ final class JMSLexer extends AbstractLexer ]; } - /** - * @return string[] - */ protected function getNonCatchablePatterns(): array { return ['\s+']; } /** - * {{@inheritDoc}} + * {@inheritDoc} * - * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @return int|string|null */ protected function getType(&$value) { diff --git a/src/Component/Serializer/Parser/JMSCore/Type/Parser.php b/src/Component/Serializer/Parser/JMSCore/Type/Parser.php new file mode 100644 index 0000000..b6250df --- /dev/null +++ b/src/Component/Serializer/Parser/JMSCore/Type/Parser.php @@ -0,0 +1,162 @@ +lexer = new Lexer(); + $this->lexer->setInput($string); + $this->lexer->moveNext(); + + return $this->visit(); + } + + /** + * @return mixed + */ + private function visit() + { + $this->lexer->moveNext(); + + if (!$this->lexer->token) { + throw new SyntaxError( + 'Syntax error, unexpected end of stream', + ); + } + + if (Lexer::T_FLOAT === $this->lexer->token->type) { + return floatval($this->lexer->token->value); + } elseif (Lexer::T_INTEGER === $this->lexer->token->type) { + return intval($this->lexer->token->value); + } elseif (Lexer::T_NULL === $this->lexer->token->type) { + return null; + } elseif (Lexer::T_STRING === $this->lexer->token->type) { + return $this->lexer->token->value; + } elseif (Lexer::T_IDENTIFIER === $this->lexer->token->type) { + if ($this->lexer->isNextToken(Lexer::T_TYPE_START)) { + return $this->visitCompoundType(); + } elseif ($this->lexer->isNextToken(Lexer::T_ARRAY_START)) { + return $this->visitArrayType(); + } + + return $this->visitSimpleType(); + } elseif (!$this->root && Lexer::T_ARRAY_START === $this->lexer->token->type) { + return $this->visitArrayType(); + } + + throw new SyntaxError(sprintf( + 'Syntax error, unexpected "%s" (%s)', + $this->lexer->token->value, + $this->getConstant($this->lexer->token->type), + )); + } + + /** + * @return string|mixed[] + */ + private function visitSimpleType() + { + $value = $this->lexer->token->value; + + return ['name' => $value, 'params' => []]; + } + + private function visitCompoundType(): array + { + $this->root = false; + $name = $this->lexer->token->value; + $this->match(Lexer::T_TYPE_START); + + $params = []; + if (!$this->lexer->isNextToken(Lexer::T_TYPE_END)) { + while (true) { + $params[] = $this->visit(); + + if ($this->lexer->isNextToken(Lexer::T_TYPE_END)) { + break; + } + + $this->match(Lexer::T_COMMA); + } + } + + $this->match(Lexer::T_TYPE_END); + + return [ + 'name' => $name, + 'params' => $params, + ]; + } + + private function visitArrayType(): array + { + /* + * Here we should call $this->match(Lexer::T_ARRAY_START); to make it clean + * but the token has already been consumed by moveNext() in visit() + */ + + $params = []; + if (!$this->lexer->isNextToken(Lexer::T_ARRAY_END)) { + while (true) { + $params[] = $this->visit(); + if ($this->lexer->isNextToken(Lexer::T_ARRAY_END)) { + break; + } + + $this->match(Lexer::T_COMMA); + } + } + + $this->match(Lexer::T_ARRAY_END); + + return $params; + } + + private function match(int $token): void + { + if (!$this->lexer->lookahead) { + throw new SyntaxError( + sprintf('Syntax error, unexpected end of stream, expected %s', $this->getConstant($token)), + ); + } + + if ($this->lexer->lookahead->type === $token) { + $this->lexer->moveNext(); + + return; + } + + throw new SyntaxError(sprintf( + 'Syntax error, unexpected "%s" (%s), expected was %s', + $this->lexer->lookahead->value, + $this->getConstant($this->lexer->lookahead->type), + $this->getConstant($token), + )); + } + + private function getConstant(int $value): string + { + $oClass = new \ReflectionClass(Lexer::class); + + return array_search($value, $oClass->getConstants()); + } +} diff --git a/src/Component/Serializer/Parser/JMSCore/Type/ParserInterface.php b/src/Component/Serializer/Parser/JMSCore/Type/ParserInterface.php new file mode 100644 index 0000000..32e56ab --- /dev/null +++ b/src/Component/Serializer/Parser/JMSCore/Type/ParserInterface.php @@ -0,0 +1,10 @@ + - * @author Pavel Kovalenko - * @see https://github.com/liip/metadata-parser * @internal - * - * @SuppressWarnings(PHPMD) */ class JMSParser implements ModelParserInterface { private const ACCESS_ORDER_CUSTOM = 'custom'; - /** - * @var Reader - */ - private $annotationsReader; + private Reader $annotationsReader; - /** - * @var PhpTypeParser - */ - private $phpTypeParser; + private PhpTypeParser $phpTypeParser; - /** - * @var JMSTypeParser - */ - private $jmsTypeParser; + protected JMSTypeParser $jmsTypeParser; - /** - * JMSParser constructor. - * - * @param \Doctrine\Common\Annotations\Reader $annotationsReader - */ public function __construct(Reader $annotationsReader) { $this->annotationsReader = $annotationsReader; @@ -82,124 +54,99 @@ class JMSParser implements ModelParserInterface $this->jmsTypeParser = new JMSTypeParser(); } - /** - * @param \Liip\MetadataParser\ModelParser\RawMetadata\RawClassMetadata $classMetadata - */ public function parse(RawClassMetadata $classMetadata): void { try { - $refClass = new ReflectionClass($classMetadata->getClassName()); // @phpstan-ignore-line - } catch (ReflectionException $exception) { - throw ParseException::classNotFound($classMetadata->getClassName(), $exception); + $reflClass = new \ReflectionClass($classMetadata->getClassName()); + } catch (\ReflectionException $e) { + throw ParseException::classNotFound($classMetadata->getClassName(), $e); } - $this->parseProperties($refClass, $classMetadata); - $this->parseMethods($refClass, $classMetadata); - $this->parseClass($refClass, $classMetadata); + try { + $this->parseProperties($reflClass, $classMetadata); + $this->parseMethods($reflClass, $classMetadata); + $this->parseClass($reflClass, $classMetadata); + } catch (SyntaxError $exception) { + throw new ParseException($exception->getMessage(), $exception->getCode(), $exception); + } } - /** - * @param \ReflectionClass $refClass - * @param \Liip\MetadataParser\ModelParser\RawMetadata\RawClassMetadata $classMetadata - * - * @phpstan-ignore-next-line - */ - private function parseProperties(ReflectionClass $refClass, RawClassMetadata $classMetadata): void + private function parseProperties(\ReflectionClass $reflClass, RawClassMetadata $classMetadata): void { - if ($refParentClass = $refClass->getParentClass()) { - $this->parseProperties($refParentClass, $classMetadata); + if ($reflParentClass = $reflClass->getParentClass()) { + $this->parseProperties($reflParentClass, $classMetadata); } - foreach ($refClass->getProperties() as $refProperty) { + foreach ($reflClass->getProperties() as $reflProperty) { try { - $annotations = $this->annotationsReader->getPropertyAnnotations($refProperty); - } catch (AnnotationException $exception) { - throw ParseException::propertyError((string) $classMetadata, $refProperty->getName(), $exception); + $annotations = $this->annotationsReader->getPropertyAnnotations($reflProperty); + } catch (AnnotationException $e) { + throw ParseException::propertyError((string) $classMetadata, $reflProperty->getName(), $e); } - $property = $this->getProperty($classMetadata, $refProperty, $annotations); + $property = $this->getProperty($classMetadata, $reflProperty, $annotations); $this->parsePropertyAnnotations($classMetadata, $property, $annotations); } } - /** - * @param \ReflectionClass $refClass - * @param \Liip\MetadataParser\ModelParser\RawMetadata\RawClassMetadata $classMetadata - * - * @phpstan-ignore-next-line - */ - private function parseMethods(ReflectionClass $refClass, RawClassMetadata $classMetadata): void + private function parseMethods(\ReflectionClass $reflClass, RawClassMetadata $classMetadata): void { - if ($refParentClass = $refClass->getParentClass()) { - $this->parseMethods($refParentClass, $classMetadata); + if ($reflParentClass = $reflClass->getParentClass()) { + $this->parseMethods($reflParentClass, $classMetadata); } - foreach ($refClass->getMethods() as $refMethod) { - if (false === $refMethod->getDocComment()) { - continue; - } - + foreach ($reflClass->getMethods() as $reflMethod) { try { - $annotations = $this->annotationsReader->getMethodAnnotations($refMethod); - } catch (AnnotationException $exception) { - throw ParseException::propertyError((string) $classMetadata, $refMethod->getName(), $exception); + $annotations = $this->annotationsReader->getMethodAnnotations($reflMethod); + } catch (AnnotationException $e) { + throw ParseException::propertyError((string) $classMetadata, $reflMethod->getName(), $e); } if ($this->isVirtualProperty($annotations)) { - if (!$refMethod->isPublic()) { - throw ParseException::nonPublicMethod((string) $classMetadata, $refMethod->getName()); + if (!$reflMethod->isPublic()) { + throw ParseException::nonPublicMethod((string) $classMetadata, $reflMethod->getName()); } - $methodName = $this->getMethodName($annotations, $refMethod); + $methodName = $this->getMethodName($annotations, $reflMethod); $name = $this->getSerializedName($annotations) ?: $methodName; $property = new PropertyVariationMetadata($methodName, true, true); $classMetadata->addPropertyVariation($name, $property); - $property->setType($this->getReturnType($property, $refMethod, $refClass)); - $property->setAccessor(new PropertyAccessor($refMethod->getName(), null)); + $property->setType($this->getReturnType($property, $reflMethod, $reflClass)); + $property->setAccessor(new PropertyAccessor($reflMethod->getName(), null)); $this->parsePropertyAnnotations($classMetadata, $property, $annotations); } if ($this->isPostDeserializeMethod($annotations)) { - if (!$refMethod->isPublic()) { - throw ParseException::nonPublicMethod((string) $classMetadata, $refMethod->getName()); + if (!$reflMethod->isPublic()) { + throw ParseException::nonPublicMethod((string) $classMetadata, $reflMethod->getName()); } - $classMetadata->addPostDeserializeMethod($refMethod->getName()); + $classMetadata->addPostDeserializeMethod($reflMethod->getName()); } } } - /** - * @param \ReflectionClass $refClass - * @param \Liip\MetadataParser\ModelParser\RawMetadata\RawClassMetadata $classMetadata - * - * @phpstan-ignore-next-line - */ - private function parseClass(ReflectionClass $refClass, RawClassMetadata $classMetadata): void + private function parseClass(\ReflectionClass $reflClass, RawClassMetadata $classMetadata): void { try { - $annotations = $this->gatherClassAnnotations($refClass); + $annotations = $this->gatherClassAnnotations($reflClass); } catch (AnnotationException $e) { - throw ParseException::classError($refClass->getName(), $e); + throw ParseException::classError($reflClass->getName(), $e); } foreach ($annotations as $annotation) { switch (true) { case $annotation instanceof AccessorOrder: if (self::ACCESS_ORDER_CUSTOM !== $annotation->order) { - throw ParseException::unsupportedClassAnnotation( - (string) $classMetadata, - 'AccessorOrder::' . $annotation->order - ); + throw ParseException::unsupportedClassAnnotation((string) $classMetadata, 'AccessorOrder::'.$annotation->order); } - // usort is not stable for the same result. we want to preserve order of - // the fields that are not explicitly mentioned + // usort is not stable for the same result. we want to preserve order of the fields that are not explicitly mentioned $order = []; - $init = count($annotation->custom); + $init = \count($annotation->custom); foreach ($classMetadata->getPropertyCollections() as $property) { $position = $property->getPosition($annotation->custom); if (null === $position) { @@ -208,21 +155,17 @@ class JMSParser implements ModelParserInterface $order[$property->getSerializedName()] = $position; } - $classMetadata->sortProperties(static function ( - PropertyCollection $propA, - PropertyCollection $propB - ) use ($order): int { + $classMetadata->sortProperties(static function (PropertyCollection $propA, PropertyCollection $propB) use ($order): int { return $order[$propA->getSerializedName()] <=> $order[$propB->getSerializedName()]; }); break; + case $annotation instanceof ExclusionPolicy: if (ExclusionPolicy::NONE !== $annotation->policy) { - throw ParseException::unsupportedClassAnnotation( - (string) $classMetadata, - 'ExclusionPolicy::' . $annotation->policy - ); + throw ParseException::unsupportedClassAnnotation((string) $classMetadata, 'ExclusionPolicy::'.$annotation->policy); } break; + default: if ( 0 === strncmp( @@ -232,10 +175,7 @@ class JMSParser implements ModelParserInterface ) ) { // if there are annotations we can safely ignore, we need to explicitly ignore them - throw ParseException::unsupportedClassAnnotation( - (string) $classMetadata, - get_class($annotation) - ); + throw ParseException::unsupportedClassAnnotation((string) $classMetadata, \get_class($annotation)); } } } @@ -244,51 +184,36 @@ class JMSParser implements ModelParserInterface /** * Find the annotations we care about by looking through all ancestors of $reflectionClass. * - * @param \ReflectionClass $reflectionClass - * * @return object[] Hashmap of annotation class => annotation object - * @throws \Doctrine\Common\Annotations\AnnotationException * - * @phpstan-ignore-next-line + * @throws AnnotationException */ - private function gatherClassAnnotations(ReflectionClass $reflectionClass): array + private function gatherClassAnnotations(\ReflectionClass $reflectionClass): array { $map = []; - if ($parent = $reflectionClass->getParentClass()) { $map = $this->gatherClassAnnotations($parent); } - $annotations = $this->annotationsReader->getClassAnnotations($reflectionClass); - foreach ($annotations as $annotation) { - $map[get_class($annotation)] = $annotation; + $map[\get_class($annotation)] = $annotation; } return $map; } - /** - * @param \Liip\MetadataParser\ModelParser\RawMetadata\RawClassMetadata $classMetadata - * @param \Liip\MetadataParser\ModelParser\RawMetadata\PropertyVariationMetadata $property - * @param array $annotations - */ - private function parsePropertyAnnotations( - RawClassMetadata $classMetadata, - PropertyVariationMetadata $property, - array $annotations - ): void { + private function parsePropertyAnnotations(RawClassMetadata $classMetadata, PropertyVariationMetadata $property, array $annotations): void + { foreach ($annotations as $annotation) { switch (true) { case $annotation instanceof Type: + if (null === $annotation->name) { + throw ParseException::propertyTypeNameNull((string) $classMetadata, (string) $property); + } try { $type = $this->jmsTypeParser->parse($annotation->name); - } catch (InvalidTypeException $exception) { - throw ParseException::propertyTypeError( - (string) $classMetadata, - (string) $property, - $exception - ); + } catch (InvalidTypeException $e) { + throw ParseException::propertyTypeError((string) $classMetadata, (string) $property, $e); } if ($property->getType() instanceof PropertyTypeUnknown) { @@ -296,52 +221,49 @@ class JMSParser implements ModelParserInterface } else { try { $property->setType($property->getType()->merge($type)); - } catch (UnexpectedValueException $exception) { - throw ParseException::propertyTypeConflict( - (string) $classMetadata, - (string) $property, - (string) $property->getType(), - (string) $type, - $exception - ); + } catch (\UnexpectedValueException $e) { + throw ParseException::propertyTypeConflict((string) $classMetadata, (string) $property, (string) $property->getType(), (string) $type, $e); } } break; + case $annotation instanceof Exclude: if (null !== $annotation->if) { - throw ParseException::unsupportedPropertyAnnotation( - (string) $classMetadata, - (string) $property, - 'Exclude::if' - ); + throw ParseException::unsupportedPropertyAnnotation((string) $classMetadata, (string) $property, 'Exclude::if'); } $classMetadata->removePropertyVariation((string) $property); break; + case $annotation instanceof Groups: $property->setGroups($annotation->groups); break; + case $annotation instanceof Accessor: $property->setAccessor(new PropertyAccessor($annotation->getter, $annotation->setter)); break; + case $annotation instanceof Since: $property->setVersionRange($property->getVersionRange()->withSince($annotation->version)); break; + case $annotation instanceof Until: $property->setVersionRange($property->getVersionRange()->withUntil($annotation->version)); break; - case $annotation instanceof SerializedName: - // we handle this separately + + case $annotation instanceof MaxDepth: + $property->setMaxDepth($annotation->depth); + break; + case $annotation instanceof VirtualProperty: // we handle this separately + case $annotation instanceof SerializedName: + // we handle this separately break; + default: - if (0 === strncmp('JMS\Serializer\\', get_class($annotation), mb_strlen('JMS\Serializer\\'))) { + if (0 === strncmp('JMS\Serializer\\', \get_class($annotation), mb_strlen('JMS\Serializer\\'))) { // if there are annotations we can safely ignore, we need to explicitly ignore them - throw ParseException::unsupportedPropertyAnnotation( - (string) $classMetadata, - (string) $property, - get_class($annotation) - ); + throw ParseException::unsupportedPropertyAnnotation((string) $classMetadata, (string) $property, \get_class($annotation)); } break; } @@ -352,96 +274,38 @@ class JMSParser implements ModelParserInterface * Returns the property metadata for the specified property. * * If the property already exists on the class metadata this is returned. - * If the property has a serialized name that overrides the name of an existing property, - * it will be renamed and merged. - * - * @param \Liip\MetadataParser\ModelParser\RawMetadata\RawClassMetadata $classMetadata - * @param \ReflectionProperty $refProperty - * @param array $annotations - * - * @return \Liip\MetadataParser\ModelParser\RawMetadata\PropertyVariationMetadata - * @throws \ReflectionException + * If the property has a serialized name that overrides the name of an existing property, it will be renamed and merged. */ - private function getProperty( - RawClassMetadata $classMetadata, - ReflectionProperty $refProperty, - array $annotations - ): PropertyVariationMetadata { - $defaultName = PropertyCollection::serializedName($refProperty->getName()); + private function getProperty(RawClassMetadata $classMetadata, \ReflectionProperty $reflProperty, array $annotations): PropertyVariationMetadata + { + $defaultName = PropertyCollection::serializedName($reflProperty->getName()); $name = $this->getSerializedName($annotations) ?: $defaultName; - - if ($classMetadata->hasPropertyVariation($refProperty->getName())) { - $property = $classMetadata->getPropertyVariation($refProperty->getName()); - + if ($classMetadata->hasPropertyVariation($reflProperty->getName())) { + $property = $classMetadata->getPropertyVariation($reflProperty->getName()); if ($defaultName !== $name && $classMetadata->hasPropertyCollection($defaultName)) { - $classMetadata->removePropertyVariation($defaultName); - $this->addPropertyVariation($defaultName, $name, $property, $classMetadata); + $classMetadata->renameProperty($defaultName, $name); } } else { - $property = PropertyVariationMetadata::fromReflection($refProperty); - $this->addPropertyVariation($defaultName, $name, $property, $classMetadata); + $property = PropertyVariationMetadata::fromReflection($reflProperty); + $classMetadata->addPropertyVariation($name, $property); } return $property; } - /** - * This workaround helps to avoid unnecessary camelCase to snake_case conversion while - * using default property metadata classes. This allows us to produce code we expect - * without rewriting the whole metadata parsing library. - * - * @param string $defaultName - * @param string $name - * @param \Liip\MetadataParser\ModelParser\RawMetadata\PropertyVariationMetadata $property - * @param \Liip\MetadataParser\ModelParser\RawMetadata\RawClassMetadata $classMetadata - * - * @throws \ReflectionException - */ - private function addPropertyVariation( - string $defaultName, - string $name, - PropertyVariationMetadata $property, - RawClassMetadata $classMetadata - ): void { - if ($classMetadata->hasPropertyCollection($defaultName)) { - $prop = $classMetadata->getPropertyCollection($defaultName); - } else { - $prop = new PropertyCollection($name); - $classMetadata->addPropertyCollection($prop); - } - - $propName = new ReflectionProperty(get_class($prop), 'serializedName'); - $propName->setAccessible(true); - $propName->setValue($prop, $name); - - $prop->addVariation($property); - } - - /** - * @param \Liip\MetadataParser\ModelParser\RawMetadata\PropertyVariationMetadata $property - * @param \ReflectionMethod $refMethod - * @param \ReflectionClass $refClass - * - * @return \Liip\MetadataParser\Metadata\PropertyType - * - * @phpstan-ignore-next-line - */ - private function getReturnType( - PropertyVariationMetadata $property, - ReflectionMethod $refMethod, - ReflectionClass $refClass - ): PropertyType { + private function getReturnType(PropertyVariationMetadata $property, \ReflectionMethod $reflMethod, \ReflectionClass $reflClass): PropertyType + { $type = new PropertyTypeUnknown(true); - $refType = $refMethod->getReturnType(); - if (null !== $refType) { - $type = $this->phpTypeParser->parseReflectionType($refType); + $reflType = $reflMethod->getReturnType(); + if (null !== $reflType) { + $type = $this->phpTypeParser->parseReflectionType($reflType); } try { - $docBlockType = $this->getReturnTypeOfMethod($refMethod, $refClass); - } catch (InvalidTypeException $exception) { - throw ParseException::propertyTypeError($refClass->getName(), (string) $property, $exception); + $docBlockType = $this->getReturnTypeOfMethod($reflMethod); + } catch (InvalidTypeException $e) { + throw ParseException::propertyTypeError($reflClass->getName(), (string) $property, $e); } if (null === $docBlockType) { @@ -450,47 +314,27 @@ class JMSParser implements ModelParserInterface try { return $type->merge($docBlockType); - } catch (UnexpectedValueException $exception) { - throw ParseException::propertyTypeConflict( - $refClass->getName(), - (string) $property, - (string) $type, - (string) $docBlockType, - $exception - ); + } catch (\UnexpectedValueException $e) { + throw ParseException::propertyTypeConflict($reflClass->getName(), (string) $property, (string) $type, (string) $docBlockType, $e); } } - /** - * @param \ReflectionMethod $refMethod - * @param \ReflectionClass $refClass - * - * @return \Liip\MetadataParser\Metadata\PropertyType|null - * - * @phpstan-ignore-next-line - */ - private function getReturnTypeOfMethod(ReflectionMethod $refMethod, ReflectionClass $refClass): ?PropertyType + private function getReturnTypeOfMethod(\ReflectionMethod $reflMethod): ?PropertyType { - $docComment = $refMethod->getDocComment(); - + $docComment = $reflMethod->getDocComment(); if (false === $docComment) { return null; } foreach (explode("\n", $docComment) as $line) { if (1 === preg_match('/@return ([^ ]+)/', $line, $matches)) { - return $this->phpTypeParser->parseAnnotationType($matches[1], $refClass); + return $this->phpTypeParser->parseAnnotationType($matches[1], $reflMethod->getDeclaringClass()); } } return null; } - /** - * @param array $annotations - * - * @return string|null - */ private function getSerializedName(array $annotations): ?string { foreach ($annotations as $annotation) { @@ -502,11 +346,6 @@ class JMSParser implements ModelParserInterface return null; } - /** - * @param array $annotations - * - * @return bool - */ private function isVirtualProperty(array $annotations): bool { foreach ($annotations as $annotation) { @@ -518,11 +357,6 @@ class JMSParser implements ModelParserInterface return false; } - /** - * @param array $annotations - * - * @return bool - */ private function isPostDeserializeMethod(array $annotations): bool { foreach ($annotations as $annotation) { @@ -534,16 +368,9 @@ class JMSParser implements ModelParserInterface return false; } - /** - * @param array $annotations - * @param \ReflectionMethod $refMethod - * - * @return string - */ - private function getMethodName(array $annotations, ReflectionMethod $refMethod): string + private function getMethodName(array $annotations, \ReflectionMethod $reflMethod): string { - $name = $refMethod->getName(); - + $name = $reflMethod->getName(); foreach ($annotations as $annotation) { if ($annotation instanceof VirtualProperty && null !== $annotation->name) { $name = $annotation->name; diff --git a/src/Component/Serializer/Parser/JMSTypeParser.php b/src/Component/Serializer/Parser/JMSTypeParser.php index ecb86f9..8a70080 100644 --- a/src/Component/Serializer/Parser/JMSTypeParser.php +++ b/src/Component/Serializer/Parser/JMSTypeParser.php @@ -1,59 +1,38 @@ - * @author Pavel Kovalenko - * @see https://github.com/liip/metadata-parser - * @internal - * - * @SuppressWarnings(PHPMD) - */ -class JMSTypeParser +final class JMSTypeParser { private const TYPE_ARRAY = 'array'; + private const TYPE_ARRAY_COLLECTION = 'ArrayCollection'; + private const TYPE_DATETIME_INTERFACE = 'DateTimeInterface'; /** - * @var \RetailCrm\Api\Component\Serializer\Parser\BaseJMSParser + * @var Parser */ - private $jmsTypeParser; + private Parser $jmsTypeParser; - /** - * JMSTypeParser constructor. - */ public function __construct() { - $this->jmsTypeParser = new BaseJMSParser(); + $this->jmsTypeParser = new Parser(); } - /** - * @param string $rawType - * - * @return \Liip\MetadataParser\Metadata\PropertyType - */ public function parse(string $rawType): PropertyType { if ('' === $rawType) { @@ -63,12 +42,6 @@ class JMSTypeParser return $this->parseType($this->jmsTypeParser->parse($rawType)); } - /** - * @param array $typeInfo - * @param bool $isSubType - * - * @return \Liip\MetadataParser\Metadata\PropertyType - */ private function parseType(array $typeInfo, bool $isSubType = false): PropertyType { $typeInfo = array_merge( @@ -84,13 +57,12 @@ class JMSTypeParser if (0 === \count($typeInfo['params'])) { if (self::TYPE_ARRAY === $typeInfo['name']) { - return new PropertyTypeArray(new PropertyTypeUnknown(false), false, $nullable); + return new PropertyTypeIterable(new PropertyTypeUnknown(false), false, $nullable); } if (PropertyTypePrimitive::isTypePrimitive($typeInfo['name'])) { return new PropertyTypePrimitive($typeInfo['name'], $nullable); } - if (PropertyTypeDateTime::isTypeDateTime($typeInfo['name'])) { return PropertyTypeDateTime::fromDateTimeClass($typeInfo['name'], $nullable); } @@ -102,41 +74,49 @@ class JMSTypeParser return new PropertyTypeClass($typeInfo['name'], $nullable); } - if (self::TYPE_ARRAY === $typeInfo['name']) { + $collectionClass = $this->getCollectionClass($typeInfo['name']); + if (self::TYPE_ARRAY === $typeInfo['name'] || $collectionClass) { if (1 === \count($typeInfo['params'])) { - return new PropertyTypeArray( - $this->parseType($typeInfo['params'][0], true), - false, - $nullable - ); + return new PropertyTypeIterable($this->parseType($typeInfo['params'][0], true), false, $nullable, $collectionClass); } if (2 === \count($typeInfo['params'])) { - return new PropertyTypeArray( - $this->parseType($typeInfo['params'][1], true), - true, - $nullable - ); + return new PropertyTypeIterable($this->parseType($typeInfo['params'][1], true), true, $nullable, $collectionClass); } - throw new InvalidTypeException(sprintf( - 'JMS property type array can\'t have more than 2 parameters (%s)', - var_export($typeInfo, true) - )); + throw new InvalidTypeException(sprintf('JMS property type array can\'t have more than 2 parameters (%s)', var_export($typeInfo, true))); } - if (PropertyTypeDateTime::isTypeDateTime($typeInfo['name'])) { + if (PropertyTypeDateTime::isTypeDateTime($typeInfo['name']) || (self::TYPE_DATETIME_INTERFACE === $typeInfo['name'])) { // the case of datetime without params is already handled above, we know we have params + $serializeFormat = $typeInfo['params'][0] ?: null; + // {@link \JMS\Serializer\Handler\DateHandler} of jms/serializer defaults to using the serialization format as a deserialization format if none was supplied... + $deserializeFormats = ($typeInfo['params'][2] ?? null) ?: $serializeFormat; + // ... and always converts single strings to arrays + $deserializeFormats = \is_string($deserializeFormats) ? [$deserializeFormats] : $deserializeFormats; + // Jms defaults to DateTime when given DateTimeInterface despite the documentation saying DateTimeImmutable, {@see \JMS\Serializer\Handler\DateHandler} in jms/serializer + $className = (self::TYPE_DATETIME_INTERFACE === $typeInfo['name']) ? \DateTime::class : $typeInfo['name']; + return PropertyTypeDateTime::fromDateTimeClass( - $typeInfo['name'], + $className, $nullable, new DateTimeOptions( - $typeInfo['params'][0] ?: null, + $serializeFormat, ($typeInfo['params'][1] ?? null) ?: null, - ($typeInfo['params'][2] ?? null) ?: null + $deserializeFormats, ) ); } throw new InvalidTypeException(sprintf('Unknown JMS property found (%s)', var_export($typeInfo, true))); } + + private function getCollectionClass(string $name): ?string + { + switch ($name) { + case self::TYPE_ARRAY_COLLECTION: + return ArrayCollection::class; + default: + return is_a($name, Collection::class, true) ? $name : null; + } + } }