From 82f110d0d0ddf9f4b8163993a6f7726785d601f5 Mon Sep 17 00:00:00 2001 From: Neur0toxine Date: Mon, 11 Mar 2024 18:09:10 +0300 Subject: [PATCH 01/12] update liip/serializer --- composer.json | 4 +- src/Command/GenerateModelsCommand.php | 11 +- src/Component/ModelsGenerator.php | 3 +- .../Generator/DeserializerGenerator.php | 310 ++++++-------- .../Serializer/Generator/Recursion.php | 57 +++ .../Generator/SerializerGenerator.php | 340 +++++----------- .../Serializer/Parser/BaseJMSParser.php | 197 --------- .../Parser/JMSCore/Exception/Exception.php | 14 + .../JMSCore/Type/Exception/Exception.php | 11 + .../JMSCore/Type/Exception/InvalidNode.php | 9 + .../JMSCore/Type/Exception/SyntaxError.php | 9 + .../{JMSLexer.php => JMSCore/Type/Lexer.php} | 31 +- .../Serializer/Parser/JMSCore/Type/Parser.php | 162 ++++++++ .../Parser/JMSCore/Type/ParserInterface.php | 10 + src/Component/Serializer/Parser/JMSParser.php | 385 +++++------------- .../Serializer/Parser/JMSTypeParser.php | 98 ++--- 16 files changed, 643 insertions(+), 1008 deletions(-) create mode 100644 src/Component/Serializer/Generator/Recursion.php delete mode 100644 src/Component/Serializer/Parser/BaseJMSParser.php create mode 100644 src/Component/Serializer/Parser/JMSCore/Exception/Exception.php create mode 100644 src/Component/Serializer/Parser/JMSCore/Type/Exception/Exception.php create mode 100644 src/Component/Serializer/Parser/JMSCore/Type/Exception/InvalidNode.php create mode 100644 src/Component/Serializer/Parser/JMSCore/Type/Exception/SyntaxError.php rename src/Component/Serializer/Parser/{JMSLexer.php => JMSCore/Type/Lexer.php} (83%) create mode 100644 src/Component/Serializer/Parser/JMSCore/Type/Parser.php create mode 100644 src/Component/Serializer/Parser/JMSCore/Type/ParserInterface.php 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; + } + } } From 4de6a3b798428142e78527dbf4a5df9b2b3f46a6 Mon Sep 17 00:00:00 2001 From: Neur0toxine Date: Mon, 11 Mar 2024 18:18:01 +0300 Subject: [PATCH 02/12] fix contracts support --- .../Serializer/ArraySupportDecorator.php | 7 +++---- tests/bootstrap.php | 5 ++++- tests/src/ResourceGroup/CustomersTest.php | 17 +++++++++-------- 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/src/Component/Serializer/ArraySupportDecorator.php b/src/Component/Serializer/ArraySupportDecorator.php index 0e97d0e..e94de20 100644 --- a/src/Component/Serializer/ArraySupportDecorator.php +++ b/src/Component/Serializer/ArraySupportDecorator.php @@ -24,8 +24,7 @@ use Pnz\JsonException\Json; */ class ArraySupportDecorator implements SerializerInterface { - /** @var \Liip\Serializer\SerializerInterface */ - private $serializer; + private SerializerInterface $serializer; /** * ArraySupportDecorator constructor. @@ -69,7 +68,7 @@ class ArraySupportDecorator implements SerializerInterface /** * @inheritDoc */ - public function deserialize(string $data, string $type, string $format, ?Context $context = null) + public function deserialize(string $data, string $type, string $format, ?Context $context = null): mixed { if ('json' !== $format) { throw new UnsupportedFormatException('Liip serializer only supports JSON for now'); @@ -109,7 +108,7 @@ class ArraySupportDecorator implements SerializerInterface * * @return array|object */ - public function fromArray(array $data, string $type, ?Context $context = null) + public function fromArray(array $data, string $type, ?Context $context = null): mixed { if (static::isArrayType($type)) { return $this->decodeArray($data, $type, $context); diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 771a104..e2fe6ef 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -18,7 +18,10 @@ if (!is_file($autoloadFile = __DIR__ . '/../vendor/autoload.php')) { $loader = require $autoloadFile; $loader->add('RetailCrm\\TestUtils', __DIR__ . '/tests/utils'); $loader->add('RetailCrm\\Tests', __DIR__ . '/src'); -AnnotationRegistry::registerLoader('class_exists'); + +if (method_exists(AnnotationRegistry::class, 'registerLoader')) { + AnnotationRegistry::registerLoader('class_exists'); +} if (file_exists(__DIR__ . '/../.env')) { $dotenv = Dotenv\Dotenv::createImmutable(__DIR__ . '/..'); diff --git a/tests/src/ResourceGroup/CustomersTest.php b/tests/src/ResourceGroup/CustomersTest.php index 3b99ed9..bf1691e 100644 --- a/tests/src/ResourceGroup/CustomersTest.php +++ b/tests/src/ResourceGroup/CustomersTest.php @@ -2698,14 +2698,15 @@ EOF; $response = $client->customers->get(4770, $request); self::assertModelsCallback($json, $response, static function ($expected, $actual) { - $actualTags = $actual['customer']['tags']; - $actual['customer']['tags'] = array_filter( - array_map(static function ($tag) use ($actualTags) { - if (in_array($tag['name'], $actualTags, true)) { - return $tag; - } - }, $expected['customer']['tags']) - ); + // TODO: Check if it's really necessary to do tag conversion here. Maybe underlying serializer is broken. +// $actualTags = $actual['customer']['tags']; +// $actual['customer']['tags'] = array_filter( +// array_map(static function ($tag) use ($actualTags) { +// if (in_array($tag['name'], $actualTags, true)) { +// return $tag; +// } +// }, $expected['customer']['tags']) +// ); $expected['customer']['marginSumm'] = (float)$expected['customer']['marginSumm']; $expected['customer']['totalSumm'] = (float)$expected['customer']['totalSumm']; From 232c22a55712ffc23ad78ec2f23977860e4a0e1c Mon Sep 17 00:00:00 2001 From: Neur0toxine Date: Mon, 11 Mar 2024 18:24:44 +0300 Subject: [PATCH 03/12] fix customer tags support --- .../Generator/SerializerGenerator.php | 16 ++++++++++++++++ tests/src/ResourceGroup/CustomersTest.php | 17 ++++++++--------- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/src/Component/Serializer/Generator/SerializerGenerator.php b/src/Component/Serializer/Generator/SerializerGenerator.php index 9288246..b4a903d 100644 --- a/src/Component/Serializer/Generator/SerializerGenerator.php +++ b/src/Component/Serializer/Generator/SerializerGenerator.php @@ -23,6 +23,7 @@ use RetailCrm\Api\Component\Serializer\Template\CustomSerialization; use RetailCrm\Api\Component\Serializer\Type\PropertyTypeMixed; use RetailCrm\Api\Interfaces\Orders\CustomerInterface; use RetailCrm\Api\Model\Entity\Customers\Customer; +use RetailCrm\Api\Model\Entity\Customers\CustomerTag; use RetailCrm\Api\Model\Entity\CustomersCorporate\CustomerCorporate; use RetailCrm\Api\Model\Entity\CustomersCorporate\SerializedRelationAbstractCustomer; use RetailCrm\Api\Model\Entity\Orders\SerializedRelationCustomer; @@ -152,6 +153,10 @@ final class SerializerGenerator ); } + if ($classMetadata->getClassName() === CustomerTag::class) { + return $this->generateForCustomerTag($arrayPath, $modelPath); + } + $stack[$classMetadata->getClassName()] = ($stack[$classMetadata->getClassName()] ?? 0) + 1; $code = ''; @@ -242,6 +247,17 @@ final 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 list $serializerGroups * @param array $stack diff --git a/tests/src/ResourceGroup/CustomersTest.php b/tests/src/ResourceGroup/CustomersTest.php index bf1691e..3b99ed9 100644 --- a/tests/src/ResourceGroup/CustomersTest.php +++ b/tests/src/ResourceGroup/CustomersTest.php @@ -2698,15 +2698,14 @@ EOF; $response = $client->customers->get(4770, $request); self::assertModelsCallback($json, $response, static function ($expected, $actual) { - // TODO: Check if it's really necessary to do tag conversion here. Maybe underlying serializer is broken. -// $actualTags = $actual['customer']['tags']; -// $actual['customer']['tags'] = array_filter( -// array_map(static function ($tag) use ($actualTags) { -// if (in_array($tag['name'], $actualTags, true)) { -// return $tag; -// } -// }, $expected['customer']['tags']) -// ); + $actualTags = $actual['customer']['tags']; + $actual['customer']['tags'] = array_filter( + array_map(static function ($tag) use ($actualTags) { + if (in_array($tag['name'], $actualTags, true)) { + return $tag; + } + }, $expected['customer']['tags']) + ); $expected['customer']['marginSumm'] = (float)$expected['customer']['marginSumm']; $expected['customer']['totalSumm'] = (float)$expected['customer']['totalSumm']; From 0d747fa1e7a12393f3869e074ee1af8fe6f54d14 Mon Sep 17 00:00:00 2001 From: Neur0toxine Date: Mon, 11 Mar 2024 18:37:34 +0300 Subject: [PATCH 04/12] cq tools fixes --- phpcs.xml.dist | 3 + phpmd.xml | 2 + phpstan-baseline-serializer.neon | 191 +++++++++++++++++++++++ phpstan-baseline.neon | 95 ----------- phpstan.neon | 1 + src/Model/Entity/References/Currency.php | 2 + 6 files changed, 199 insertions(+), 95 deletions(-) create mode 100644 phpstan-baseline-serializer.neon diff --git a/phpcs.xml.dist b/phpcs.xml.dist index d12440d..5001cfd 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -10,4 +10,7 @@ src/ tests/ + + src/Component/Serializer/Generator/* + src/Component/Serializer/Parser/* diff --git a/phpmd.xml b/phpmd.xml index 3cd0206..0e11758 100644 --- a/phpmd.xml +++ b/phpmd.xml @@ -44,4 +44,6 @@ tests/* + src/Component/Serializer/Generator/* + src/Component/Serializer/Parser/* diff --git a/phpstan-baseline-serializer.neon b/phpstan-baseline-serializer.neon new file mode 100644 index 0000000..46a7126 --- /dev/null +++ b/phpstan-baseline-serializer.neon @@ -0,0 +1,191 @@ +parameters: + ignoreErrors: + - + message: "#^Parameter \\#1 \\$config of static method Liip\\\\Serializer\\\\Configuration\\\\GeneratorConfiguration\\:\\:createFomArray\\(\\) expects array\\{default_group_combinations\\?\\: array\\\\>\\|null, default_versions\\?\\: array\\\\|null, classes\\?\\: array\\\\>\\|null, options\\?\\: array\\\\}, array\\{default_group_combinations\\: array\\{\\}, default_versions\\: array\\{\\}, classes\\: non\\-empty\\-array\\\\} given\\.$#" + count: 1 + path: src/Component/ModelsGenerator.php + + - + message: "#^Parameter \\#3 \\$classesToGenerate of class RetailCrm\\\\Api\\\\Component\\\\Serializer\\\\Generator\\\\DeserializerGenerator constructor expects array\\, array\\ given\\.$#" + count: 1 + path: src/Component/ModelsGenerator.php + + - + message: "#^Parameter \\#2 \\$method of method Liip\\\\Serializer\\\\Template\\\\Deserialization\\:\\:renderSetter\\(\\) expects string, string\\|null given\\.$#" + count: 1 + path: src/Component/Serializer/Generator/DeserializerGenerator.php + + - + message: "#^Parameter \\#4 \\$stack of method RetailCrm\\\\Api\\\\Component\\\\Serializer\\\\Generator\\\\DeserializerGenerator\\:\\:generateCodeForClass\\(\\) expects array\\\\>, array given\\.$#" + count: 2 + path: src/Component/Serializer/Generator/DeserializerGenerator.php + + - + message: "#^Method RetailCrm\\\\Api\\\\Component\\\\Serializer\\\\Generator\\\\SerializerGenerator\\:\\:buildSerializerFunctionName\\(\\) should return string but returns string\\|null\\.$#" + count: 1 + path: src/Component/Serializer/Generator/SerializerGenerator.php + + - + message: "#^Parameter \\#2 \\$method of method Liip\\\\Serializer\\\\Template\\\\Serialization\\:\\:renderGetter\\(\\) expects string, string\\|null given\\.$#" + count: 1 + path: src/Component/Serializer/Generator/SerializerGenerator.php + + - + message: "#^Parameter \\#3 \\$serializerGroups of method RetailCrm\\\\Api\\\\Component\\\\Serializer\\\\Generator\\\\SerializerGenerator\\:\\:generateCodeForClass\\(\\) expects array\\, array given\\.$#" + count: 4 + path: src/Component/Serializer/Generator/SerializerGenerator.php + + - + message: "#^Class RetailCrm\\\\Api\\\\Component\\\\Serializer\\\\Parser\\\\JMSCore\\\\Type\\\\Lexer extends generic class Doctrine\\\\Common\\\\Lexer\\\\AbstractLexer but does not specify its types\\: T, V$#" + count: 1 + path: src/Component/Serializer/Parser/JMSCore/Type/Lexer.php + + - + message: "#^Method RetailCrm\\\\Api\\\\Component\\\\Serializer\\\\Parser\\\\JMSCore\\\\Type\\\\Lexer\\:\\:getType\\(\\) has parameter \\$value with no type specified\\.$#" + count: 1 + path: src/Component/Serializer/Parser/JMSCore/Type/Lexer.php + + - + message: "#^Method RetailCrm\\\\Api\\\\Component\\\\Serializer\\\\Parser\\\\JMSCore\\\\Type\\\\Lexer\\:\\:parse\\(\\) has no return type specified\\.$#" + count: 1 + path: src/Component/Serializer/Parser/JMSCore/Type/Lexer.php + + - + message: "#^Parameter \\#1 \\$haystack of function stripos expects string, float\\|int\\|string given\\.$#" + count: 1 + path: src/Component/Serializer/Parser/JMSCore/Type/Lexer.php + + - + message: "#^Parameter \\#1 \\$haystack of function strpos expects string, float\\|int\\|string given\\.$#" + count: 1 + path: src/Component/Serializer/Parser/JMSCore/Type/Lexer.php + + - + message: "#^Cannot access property \\$value on Doctrine\\\\Common\\\\Lexer\\\\Token\\\\|null\\.$#" + count: 2 + path: src/Component/Serializer/Parser/JMSCore/Type/Parser.php + + - + message: "#^Method RetailCrm\\\\Api\\\\Component\\\\Serializer\\\\Parser\\\\JMSCore\\\\Type\\\\Parser\\:\\:getConstant\\(\\) should return string but returns string\\|false\\.$#" + count: 1 + path: src/Component/Serializer/Parser/JMSCore/Type/Parser.php + + - + message: "#^Method RetailCrm\\\\Api\\\\Component\\\\Serializer\\\\Parser\\\\JMSCore\\\\Type\\\\Parser\\:\\:parse\\(\\) return type has no value type specified in iterable type array\\.$#" + count: 1 + path: src/Component/Serializer/Parser/JMSCore/Type/Parser.php + + - + message: "#^Method RetailCrm\\\\Api\\\\Component\\\\Serializer\\\\Parser\\\\JMSCore\\\\Type\\\\Parser\\:\\:parse\\(\\) should return array but returns mixed\\.$#" + count: 1 + path: src/Component/Serializer/Parser/JMSCore/Type/Parser.php + + - + message: "#^Method RetailCrm\\\\Api\\\\Component\\\\Serializer\\\\Parser\\\\JMSCore\\\\Type\\\\Parser\\:\\:visitArrayType\\(\\) return type has no value type specified in iterable type array\\.$#" + count: 1 + path: src/Component/Serializer/Parser/JMSCore/Type/Parser.php + + - + message: "#^Method RetailCrm\\\\Api\\\\Component\\\\Serializer\\\\Parser\\\\JMSCore\\\\Type\\\\Parser\\:\\:visitCompoundType\\(\\) return type has no value type specified in iterable type array\\.$#" + count: 1 + path: src/Component/Serializer/Parser/JMSCore/Type/Parser.php + + - + message: "#^Method RetailCrm\\\\Api\\\\Component\\\\Serializer\\\\Parser\\\\JMSCore\\\\Type\\\\Parser\\:\\:visitSimpleType\\(\\) never returns string so it can be removed from the return type\\.$#" + count: 1 + path: src/Component/Serializer/Parser/JMSCore/Type/Parser.php + + - + message: "#^Parameter \\#1 \\$value of method RetailCrm\\\\Api\\\\Component\\\\Serializer\\\\Parser\\\\JMSCore\\\\Type\\\\Parser\\:\\:getConstant\\(\\) expects int, int\\\\|int\\<4, 8\\>\\|int\\<11, max\\>\\|string\\|UnitEnum\\|null given\\.$#" + count: 1 + path: src/Component/Serializer/Parser/JMSCore/Type/Parser.php + + - + message: "#^Parameter \\#1 \\$value of method RetailCrm\\\\Api\\\\Component\\\\Serializer\\\\Parser\\\\JMSCore\\\\Type\\\\Parser\\:\\:getConstant\\(\\) expects int, int\\|string\\|UnitEnum\\|null given\\.$#" + count: 1 + path: src/Component/Serializer/Parser/JMSCore/Type/Parser.php + + - + message: "#^Method RetailCrm\\\\Api\\\\Component\\\\Serializer\\\\Parser\\\\JMSCore\\\\Type\\\\ParserInterface\\:\\:parse\\(\\) return type has no value type specified in iterable type array\\.$#" + count: 1 + path: src/Component/Serializer/Parser/JMSCore/Type/ParserInterface.php + + - + message: "#^Method RetailCrm\\\\Api\\\\Component\\\\Serializer\\\\Parser\\\\JMSParser\\:\\:gatherClassAnnotations\\(\\) has parameter \\$reflectionClass with generic class ReflectionClass but does not specify its types\\: T$#" + count: 1 + path: src/Component/Serializer/Parser/JMSParser.php + + - + message: "#^Method RetailCrm\\\\Api\\\\Component\\\\Serializer\\\\Parser\\\\JMSParser\\:\\:getMethodName\\(\\) has parameter \\$annotations with no value type specified in iterable type array\\.$#" + count: 1 + path: src/Component/Serializer/Parser/JMSParser.php + + - + message: "#^Method RetailCrm\\\\Api\\\\Component\\\\Serializer\\\\Parser\\\\JMSParser\\:\\:getProperty\\(\\) has parameter \\$annotations with no value type specified in iterable type array\\.$#" + count: 1 + path: src/Component/Serializer/Parser/JMSParser.php + + - + message: "#^Method RetailCrm\\\\Api\\\\Component\\\\Serializer\\\\Parser\\\\JMSParser\\:\\:getReturnType\\(\\) has parameter \\$reflClass with generic class ReflectionClass but does not specify its types\\: T$#" + count: 1 + path: src/Component/Serializer/Parser/JMSParser.php + + - + message: "#^Method RetailCrm\\\\Api\\\\Component\\\\Serializer\\\\Parser\\\\JMSParser\\:\\:getSerializedName\\(\\) has parameter \\$annotations with no value type specified in iterable type array\\.$#" + count: 1 + path: src/Component/Serializer/Parser/JMSParser.php + + - + message: "#^Method RetailCrm\\\\Api\\\\Component\\\\Serializer\\\\Parser\\\\JMSParser\\:\\:isPostDeserializeMethod\\(\\) has parameter \\$annotations with no value type specified in iterable type array\\.$#" + count: 1 + path: src/Component/Serializer/Parser/JMSParser.php + + - + message: "#^Method RetailCrm\\\\Api\\\\Component\\\\Serializer\\\\Parser\\\\JMSParser\\:\\:isVirtualProperty\\(\\) has parameter \\$annotations with no value type specified in iterable type array\\.$#" + count: 1 + path: src/Component/Serializer/Parser/JMSParser.php + + - + message: "#^Method RetailCrm\\\\Api\\\\Component\\\\Serializer\\\\Parser\\\\JMSParser\\:\\:parseClass\\(\\) has parameter \\$reflClass with generic class ReflectionClass but does not specify its types\\: T$#" + count: 1 + path: src/Component/Serializer/Parser/JMSParser.php + + - + message: "#^Method RetailCrm\\\\Api\\\\Component\\\\Serializer\\\\Parser\\\\JMSParser\\:\\:parseMethods\\(\\) has parameter \\$reflClass with generic class ReflectionClass but does not specify its types\\: T$#" + count: 1 + path: src/Component/Serializer/Parser/JMSParser.php + + - + message: "#^Method RetailCrm\\\\Api\\\\Component\\\\Serializer\\\\Parser\\\\JMSParser\\:\\:parseProperties\\(\\) has parameter \\$reflClass with generic class ReflectionClass but does not specify its types\\: T$#" + count: 1 + path: src/Component/Serializer/Parser/JMSParser.php + + - + message: "#^Method RetailCrm\\\\Api\\\\Component\\\\Serializer\\\\Parser\\\\JMSParser\\:\\:parsePropertyAnnotations\\(\\) has parameter \\$annotations with no value type specified in iterable type array\\.$#" + count: 1 + path: src/Component/Serializer/Parser/JMSParser.php + + - + message: "#^Parameter \\#1 \\$objectOrClass of class ReflectionClass constructor expects class\\-string\\\\|T of object, string given\\.$#" + count: 1 + path: src/Component/Serializer/Parser/JMSParser.php + + - + message: "#^Class Doctrine\\\\Common\\\\Collections\\\\ArrayCollection not found\\.$#" + count: 1 + path: src/Component/Serializer/Parser/JMSTypeParser.php + + - + message: "#^Class Doctrine\\\\Common\\\\Collections\\\\Collection not found\\.$#" + count: 1 + path: src/Component/Serializer/Parser/JMSTypeParser.php + + - + message: "#^Method RetailCrm\\\\Api\\\\Component\\\\Serializer\\\\Parser\\\\JMSTypeParser\\:\\:parseType\\(\\) has parameter \\$typeInfo with no value type specified in iterable type array\\.$#" + count: 1 + path: src/Component/Serializer/Parser/JMSTypeParser.php + + - + message: "#^Parameter \\#4 \\$traversableClass of class Liip\\\\MetadataParser\\\\Metadata\\\\PropertyTypeIterable constructor expects class\\-string\\\\|null, string\\|null given\\.$#" + count: 2 + path: src/Component/Serializer/Parser/JMSTypeParser.php diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 971ac59..47a94db 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -210,36 +210,6 @@ parameters: count: 2 path: src/Component/Serializer/ArraySupportDecorator.php - - - message: "#^Unsafe access to private constant RetailCrm\\\\Api\\\\Component\\\\Serializer\\\\Generator\\\\DeserializerGenerator\\:\\:FILENAME_PREFIX through static\\:\\:\\.$#" - count: 1 - path: src/Component/Serializer/Generator/DeserializerGenerator.php - - - - message: "#^Argument of an invalid type mixed supplied for foreach, only iterables are supported\\.$#" - count: 1 - path: src/Component/Serializer/Generator/SerializerGenerator.php - - - - message: "#^Cannot call method getClassName\\(\\) on mixed\\.$#" - count: 1 - path: src/Component/Serializer/Generator/SerializerGenerator.php - - - - message: "#^Cannot call method getGroups\\(\\) on mixed\\.$#" - count: 3 - path: src/Component/Serializer/Generator/SerializerGenerator.php - - - - message: "#^Cannot call method getVersions\\(\\) on mixed\\.$#" - count: 1 - path: src/Component/Serializer/Generator/SerializerGenerator.php - - - - message: "#^Unsafe access to private constant RetailCrm\\\\Api\\\\Component\\\\Serializer\\\\Generator\\\\SerializerGenerator\\:\\:FILENAME_PREFIX through static\\:\\:\\.$#" - count: 1 - path: src/Component/Serializer/Generator/SerializerGenerator.php - - message: "#^Method RetailCrm\\\\Api\\\\Component\\\\Serializer\\\\ModelsChecksumGenerator\\:\\:getStoredChecksums\\(\\) should return array\\ but returns mixed\\.$#" count: 1 @@ -250,41 +220,6 @@ parameters: count: 4 path: src/Component/Serializer/ModelsChecksumGenerator.php - - - message: "#^Cannot cast mixed to float\\.$#" - count: 1 - path: src/Component/Serializer/Parser/BaseJMSParser.php - - - - message: "#^Cannot cast mixed to int\\.$#" - count: 1 - path: src/Component/Serializer/Parser/BaseJMSParser.php - - - - message: "#^Method RetailCrm\\\\Api\\\\Component\\\\Serializer\\\\Parser\\\\BaseJMSParser\\:\\:parse\\(\\) should return array but returns mixed\\.$#" - count: 1 - path: src/Component/Serializer/Parser/BaseJMSParser.php - - - - message: "#^Parameter \\#2 \\.\\.\\.\\$values of function sprintf expects bool\\|float\\|int\\|string\\|null, mixed given\\.$#" - count: 2 - path: src/Component/Serializer/Parser/BaseJMSParser.php - - - - message: "#^Class RetailCrm\\\\Api\\\\Component\\\\Serializer\\\\Parser\\\\JMSLexer extends generic class Doctrine\\\\Common\\\\Lexer\\\\AbstractLexer but does not specify its types\\: T, V$#" - count: 1 - path: src/Component/Serializer/Parser/JMSLexer.php - - - - message: "#^Method RetailCrm\\\\Api\\\\Component\\\\Serializer\\\\Parser\\\\JMSLexer\\:\\:parse\\(\\) should return int\\|string\\|null but returns int\\|string\\|UnitEnum\\|null\\.$#" - count: 1 - path: src/Component/Serializer/Parser/JMSLexer.php - - - - message: "#^Parameter \\#1 \\$object of function get_class expects object, mixed given\\.$#" - count: 2 - path: src/Component/Serializer/Parser/JMSParser.php - - message: "#^Parameter \\#2 \\$string2 of function strncmp expects string, class\\-string\\|false given\\.$#" count: 1 @@ -295,36 +230,6 @@ parameters: count: 1 path: src/Component/Serializer/Parser/JMSParser.php - - - message: "#^Parameter \\#1 \\$className of class Liip\\\\MetadataParser\\\\Metadata\\\\PropertyTypeClass constructor expects string, mixed given\\.$#" - count: 1 - path: src/Component/Serializer/Parser/JMSTypeParser.php - - - - message: "#^Parameter \\#1 \\$className of static method Liip\\\\MetadataParser\\\\Metadata\\\\PropertyTypeDateTime\\:\\:fromDateTimeClass\\(\\) expects string, mixed given\\.$#" - count: 2 - path: src/Component/Serializer/Parser/JMSTypeParser.php - - - - message: "#^Parameter \\#1 \\$typeName of class Liip\\\\MetadataParser\\\\Metadata\\\\PropertyTypePrimitive constructor expects string, mixed given\\.$#" - count: 1 - path: src/Component/Serializer/Parser/JMSTypeParser.php - - - - message: "#^Parameter \\#1 \\$typeName of static method Liip\\\\MetadataParser\\\\Metadata\\\\PropertyTypeDateTime\\:\\:isTypeDateTime\\(\\) expects string, mixed given\\.$#" - count: 2 - path: src/Component/Serializer/Parser/JMSTypeParser.php - - - - message: "#^Parameter \\#1 \\$typeName of static method Liip\\\\MetadataParser\\\\Metadata\\\\PropertyTypePrimitive\\:\\:isTypePrimitive\\(\\) expects string, mixed given\\.$#" - count: 1 - path: src/Component/Serializer/Parser/JMSTypeParser.php - - - - message: "#^Parameter \\#1 \\$value of function count expects array\\|Countable, mixed given\\.$#" - count: 1 - path: src/Component/Serializer/Parser/JMSTypeParser.php - - message: "#^Unsafe call to private method RetailCrm\\\\Api\\\\Component\\\\Transformer\\\\DateTimeTransformer\\:\\:createFromFormat\\(\\) through static\\:\\:\\.$#" count: 3 diff --git a/phpstan.neon b/phpstan.neon index d0feec5..ade0673 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,4 +1,5 @@ includes: + - phpstan-baseline-serializer.neon - phpstan-baseline.neon # TODO: This should be removed eventually. parameters: diff --git a/src/Model/Entity/References/Currency.php b/src/Model/Entity/References/Currency.php index 96abbe2..596852e 100644 --- a/src/Model/Entity/References/Currency.php +++ b/src/Model/Entity/References/Currency.php @@ -16,6 +16,8 @@ use RetailCrm\Api\Component\Serializer\Annotation as JMS; * * @category Currency * @package RetailCrm\Api\Model\Entity\References + * + * @SuppressWarnings(PHPMD.LongVariable) */ class Currency { From 49844e50d6edc9dc1e77ef5e33c5a960c977376c Mon Sep 17 00:00:00 2001 From: Neur0toxine Date: Mon, 11 Mar 2024 18:39:25 +0300 Subject: [PATCH 05/12] update php test matrix --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ba3378b..af5d23d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - php-version: ['7.3', '7.4', '8.0', '8.1', '8.2'] + php-version: ['7.4', '8.0', '8.1', '8.2', '8.3'] steps: - name: Check out code into the workspace uses: actions/checkout@v2 From 8ced4dc8a1fb9b2fc097e8fc91bd473c04772cc8 Mon Sep 17 00:00:00 2001 From: Neur0toxine Date: Mon, 11 Mar 2024 18:39:46 +0300 Subject: [PATCH 06/12] update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4835979..87b2396 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ You can find more info in the [documentation](doc/index.md). ## Requirements -* PHP 7.3 and above +* PHP 7.4 and above * PHP's cURL support * PHP's JSON support * Any HTTP client compatible with PSR-18 (covered by the installation instructions). From f53f545c222139c2f94bd71cda3eceba1376681a Mon Sep 17 00:00:00 2001 From: Neur0toxine Date: Mon, 11 Mar 2024 20:31:56 +0300 Subject: [PATCH 07/12] WIP: php 7.4 support --- composer.json | 2 +- .../Generator/DeserializerGenerator.php | 21 ++++- .../Serializer/Parser/JMSCore/Type/Parser.php | 81 ++++++++++++++----- .../Serializer/Parser/JMSCore/Type/Token.php | 64 +++++++++++++++ .../Serializer/Parser/JMSTypeParser.php | 27 ++++++- 5 files changed, 169 insertions(+), 26 deletions(-) create mode 100644 src/Component/Serializer/Parser/JMSCore/Type/Token.php diff --git a/composer.json b/composer.json index 1af738a..cc9a36e 100644 --- a/composer.json +++ b/composer.json @@ -26,7 +26,7 @@ "php-http/message-factory": "^1.0", "php-http/discovery": "^1.13", "doctrine/annotations": "^1.13|^2.0", - "liip/serializer": "2.6.*", + "liip/serializer": "2.2.* || 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/Component/Serializer/Generator/DeserializerGenerator.php b/src/Component/Serializer/Generator/DeserializerGenerator.php index 4bd7c28..f424f96 100644 --- a/src/Component/Serializer/Generator/DeserializerGenerator.php +++ b/src/Component/Serializer/Generator/DeserializerGenerator.php @@ -253,6 +253,15 @@ final class DeserializerGenerator ); } + private function isArrayTraversable(PropertyTypeArray $array): bool + { + if (method_exists($array, 'isCollection')) { + return $array->isCollection(); + } + + return $array->isTraversable(); + } + /** * @param array $stack */ @@ -266,13 +275,23 @@ final class DeserializerGenerator switch ($type) { case $type instanceof PropertyTypeArray: - if ($type->isTraversable()) { + if ($this->isArrayTraversable($type)) { return $this->generateCodeForArrayCollection($propertyMetadata, $type, $arrayPath, $modelPropertyPath, $stack); } return $this->generateCodeForArray($type, $arrayPath, $modelPropertyPath, $stack); case $type instanceof PropertyTypeDateTime: + if (method_exists($type, 'getDeserializeFormat')) { + $format = $type->getDeserializeFormat(); + + if (null !== $format) { + return $this->templating->renderAssignDateTimeFromFormat($type->isImmutable(), (string) $modelPropertyPath, (string) $arrayPath, $format, $type->getZone()); + } + + return $this->templating->renderAssignDateTimeToField($type->isImmutable(), (string) $modelPropertyPath, (string) $arrayPath); + } + $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()); diff --git a/src/Component/Serializer/Parser/JMSCore/Type/Parser.php b/src/Component/Serializer/Parser/JMSCore/Type/Parser.php index b6250df..d77b5f9 100644 --- a/src/Component/Serializer/Parser/JMSCore/Type/Parser.php +++ b/src/Component/Serializer/Parser/JMSCore/Type/Parser.php @@ -16,15 +16,21 @@ final class Parser implements ParserInterface */ private Lexer $lexer; + private ?Token $token = null; + + private string $input; + /** * @var bool */ private bool $root = true; - public function parse(string $string): array + public function parse(string $type): array { + $this->input = $type; + $this->lexer = new Lexer(); - $this->lexer->setInput($string); + $this->lexer->setInput($type); $this->lexer->moveNext(); return $this->visit(); @@ -33,7 +39,7 @@ final class Parser implements ParserInterface /** * @return mixed */ - private function visit() + private function visit(bool $fetchingParam = false) { $this->lexer->moveNext(); @@ -43,15 +49,44 @@ final class Parser implements ParserInterface ); } - 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) { + if (is_array($this->lexer->token)) { + $this->token = Token::fromArray($this->lexer->token); + } else { + $this->token = Token::fromObject($this->lexer->token); + } + + if ("" === $this->token->value && $fetchingParam) { + $len = 0; + $this->lexer->moveNext(); + + while (true) { + if (is_array($this->lexer->token)) { + $this->token = Token::fromArray($this->lexer->token); + } else { + $this->token = Token::fromObject($this->lexer->token); + } + + if ("" === $this->token->value) { + $len++; + break; + } + + $len += strlen($this->token->value); + $this->lexer->moveNext(); + } + + return substr($this->input, 9, $len + substr_count($this->input, ' ')); + } + + if (Lexer::T_FLOAT === $this->token->type) { + return floatval($this->token->value); + } elseif (Lexer::T_INTEGER === $this->token->type) { + return intval($this->token->value); + } elseif (Lexer::T_NULL === $this->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) { + } elseif (Lexer::T_STRING === $this->token->type) { + return $this->token->value; + } elseif (Lexer::T_IDENTIFIER === $this->token->type) { if ($this->lexer->isNextToken(Lexer::T_TYPE_START)) { return $this->visitCompoundType(); } elseif ($this->lexer->isNextToken(Lexer::T_ARRAY_START)) { @@ -59,14 +94,14 @@ final class Parser implements ParserInterface } return $this->visitSimpleType(); - } elseif (!$this->root && Lexer::T_ARRAY_START === $this->lexer->token->type) { + } elseif (!$this->root && Lexer::T_ARRAY_START === $this->token->type) { return $this->visitArrayType(); } throw new SyntaxError(sprintf( 'Syntax error, unexpected "%s" (%s)', - $this->lexer->token->value, - $this->getConstant($this->lexer->token->type), + $this->token->value, + $this->getConstant($this->token->type), )); } @@ -75,7 +110,7 @@ final class Parser implements ParserInterface */ private function visitSimpleType() { - $value = $this->lexer->token->value; + $value = $this->token->value; return ['name' => $value, 'params' => []]; } @@ -83,13 +118,13 @@ final class Parser implements ParserInterface private function visitCompoundType(): array { $this->root = false; - $name = $this->lexer->token->value; + $name = $this->token->value; $this->match(Lexer::T_TYPE_START); $params = []; if (!$this->lexer->isNextToken(Lexer::T_TYPE_END)) { while (true) { - $params[] = $this->visit(); + $params[] = $this->visit(true); if ($this->lexer->isNextToken(Lexer::T_TYPE_END)) { break; @@ -139,7 +174,13 @@ final class Parser implements ParserInterface ); } - if ($this->lexer->lookahead->type === $token) { + if (is_array($this->lexer->lookahead)) { + $lookahead = Token::fromArray($this->lexer->lookahead); + } else { + $lookahead = Token::fromObject($this->lexer->lookahead); + } + + if ($lookahead->type === $token) { $this->lexer->moveNext(); return; @@ -147,8 +188,8 @@ final class Parser implements ParserInterface throw new SyntaxError(sprintf( 'Syntax error, unexpected "%s" (%s), expected was %s', - $this->lexer->lookahead->value, - $this->getConstant($this->lexer->lookahead->type), + $lookahead->value, + $this->getConstant($lookahead->type), $this->getConstant($token), )); } diff --git a/src/Component/Serializer/Parser/JMSCore/Type/Token.php b/src/Component/Serializer/Parser/JMSCore/Type/Token.php new file mode 100644 index 0000000..f576840 --- /dev/null +++ b/src/Component/Serializer/Parser/JMSCore/Type/Token.php @@ -0,0 +1,64 @@ +value, $source->type, $source->position); + } + + /** + * The string value of the token in the input string + * + * @readonly + * @var string|int + */ + public $value; + + /** + * The type of the token (identifier, numeric, string, input parameter, none) + * + * @readonly + * @var T|null + */ + public $type; + + /** + * The position of the token in the input string + * + * @readonly + */ + public int $position; + + /** + * @param string|int $value + * @param string|int $type + */ + public function __construct($value, $type, int $position) + { + $this->value = $value; + $this->type = $type; + $this->position = $position; + } + + /** @param T ...$types */ + public function isA(...$types): bool + { + return in_array($this->type, $types, true); + } +} diff --git a/src/Component/Serializer/Parser/JMSTypeParser.php b/src/Component/Serializer/Parser/JMSTypeParser.php index 8a70080..1379290 100644 --- a/src/Component/Serializer/Parser/JMSTypeParser.php +++ b/src/Component/Serializer/Parser/JMSTypeParser.php @@ -7,11 +7,13 @@ namespace RetailCrm\Api\Component\Serializer\Parser; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Liip\MetadataParser\Exception\InvalidTypeException; +use Liip\MetadataParser\Metadata\AbstractPropertyType; use Liip\MetadataParser\Metadata\DateTimeOptions; use Liip\MetadataParser\Metadata\PropertyType; use Liip\MetadataParser\Metadata\PropertyTypeClass; use Liip\MetadataParser\Metadata\PropertyTypeDateTime; use Liip\MetadataParser\Metadata\PropertyTypeIterable; +use Liip\MetadataParser\Metadata\PropertyTypeArray; use Liip\MetadataParser\Metadata\PropertyTypePrimitive; use Liip\MetadataParser\Metadata\PropertyTypeUnknown; use RetailCrm\Api\Component\Serializer\Parser\JMSCore\Type\Parser; @@ -28,9 +30,13 @@ final class JMSTypeParser */ private Parser $jmsTypeParser; + private $useArrayDateFormat = true; + public function __construct() { $this->jmsTypeParser = new Parser(); + $this->useArrayDateFormat = null === (new \ReflectionClass(DateTimeOptions::class)) + ->getConstructor()->getParameters()[2]->getType(); } public function parse(string $rawType): PropertyType @@ -57,7 +63,7 @@ final class JMSTypeParser if (0 === \count($typeInfo['params'])) { if (self::TYPE_ARRAY === $typeInfo['name']) { - return new PropertyTypeIterable(new PropertyTypeUnknown(false), false, $nullable); + return self::iterableType(new PropertyTypeUnknown(false), false, $nullable); } if (PropertyTypePrimitive::isTypePrimitive($typeInfo['name'])) { @@ -77,10 +83,10 @@ final class JMSTypeParser $collectionClass = $this->getCollectionClass($typeInfo['name']); if (self::TYPE_ARRAY === $typeInfo['name'] || $collectionClass) { if (1 === \count($typeInfo['params'])) { - return new PropertyTypeIterable($this->parseType($typeInfo['params'][0], true), false, $nullable, $collectionClass); + return self::iterableType($this->parseType($typeInfo['params'][0], true), false, $nullable, $collectionClass); } if (2 === \count($typeInfo['params'])) { - return new PropertyTypeIterable($this->parseType($typeInfo['params'][1], true), true, $nullable, $collectionClass); + return self::iterableType($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))); @@ -102,7 +108,7 @@ final class JMSTypeParser new DateTimeOptions( $serializeFormat, ($typeInfo['params'][1] ?? null) ?: null, - $deserializeFormats, + $this->useArrayDateFormat ? $deserializeFormats : $deserializeFormats[0], ) ); } @@ -119,4 +125,17 @@ final class JMSTypeParser return is_a($name, Collection::class, true) ? $name : null; } } + + private static function iterableType( + PropertyType $subType, + bool $hashmap, + bool $nullable, + ?string $collectionClass = null + ): AbstractPropertyType { + if (class_exists('Liip\MetadataParser\Metadata\PropertyTypeIterable')) { + return new PropertyTypeIterable($subType, $hashmap, $nullable, $collectionClass); + } + + return new PropertyTypeArray($subType, $hashmap, $nullable, $collectionClass !== null); + } } From 7a4d755f8310b473221cd24594f99efcc27b6cf8 Mon Sep 17 00:00:00 2001 From: Neur0toxine Date: Mon, 11 Mar 2024 20:36:12 +0300 Subject: [PATCH 08/12] update phpstan baseline --- phpstan-baseline-serializer.neon | 87 ++++++++++++++++++++++++++------ 1 file changed, 71 insertions(+), 16 deletions(-) diff --git a/phpstan-baseline-serializer.neon b/phpstan-baseline-serializer.neon index 46a7126..eaf39fe 100644 --- a/phpstan-baseline-serializer.neon +++ b/phpstan-baseline-serializer.neon @@ -60,11 +60,6 @@ parameters: count: 1 path: src/Component/Serializer/Parser/JMSCore/Type/Lexer.php - - - message: "#^Cannot access property \\$value on Doctrine\\\\Common\\\\Lexer\\\\Token\\\\|null\\.$#" - count: 2 - path: src/Component/Serializer/Parser/JMSCore/Type/Parser.php - - message: "#^Method RetailCrm\\\\Api\\\\Component\\\\Serializer\\\\Parser\\\\JMSCore\\\\Type\\\\Parser\\:\\:getConstant\\(\\) should return string but returns string\\|false\\.$#" count: 1 @@ -95,16 +90,6 @@ parameters: count: 1 path: src/Component/Serializer/Parser/JMSCore/Type/Parser.php - - - message: "#^Parameter \\#1 \\$value of method RetailCrm\\\\Api\\\\Component\\\\Serializer\\\\Parser\\\\JMSCore\\\\Type\\\\Parser\\:\\:getConstant\\(\\) expects int, int\\\\|int\\<4, 8\\>\\|int\\<11, max\\>\\|string\\|UnitEnum\\|null given\\.$#" - count: 1 - path: src/Component/Serializer/Parser/JMSCore/Type/Parser.php - - - - message: "#^Parameter \\#1 \\$value of method RetailCrm\\\\Api\\\\Component\\\\Serializer\\\\Parser\\\\JMSCore\\\\Type\\\\Parser\\:\\:getConstant\\(\\) expects int, int\\|string\\|UnitEnum\\|null given\\.$#" - count: 1 - path: src/Component/Serializer/Parser/JMSCore/Type/Parser.php - - message: "#^Method RetailCrm\\\\Api\\\\Component\\\\Serializer\\\\Parser\\\\JMSCore\\\\Type\\\\ParserInterface\\:\\:parse\\(\\) return type has no value type specified in iterable type array\\.$#" count: 1 @@ -187,5 +172,75 @@ parameters: - message: "#^Parameter \\#4 \\$traversableClass of class Liip\\\\MetadataParser\\\\Metadata\\\\PropertyTypeIterable constructor expects class\\-string\\\\|null, string\\|null given\\.$#" - count: 2 + count: 1 + path: src/Component/Serializer/Parser/JMSTypeParser.php + + - + message: "#^Cannot access property \\$value on RetailCrm\\\\Api\\\\Component\\\\Serializer\\\\Parser\\\\JMSCore\\\\Type\\\\Token\\|null\\.$#" + count: 2 + path: src/Component/Serializer/Parser/JMSCore/Type/Parser.php + + - + message: "#^Parameter \\#1 \\$string of function strlen expects string, int\\|string given\\.$#" + count: 1 + path: src/Component/Serializer/Parser/JMSCore/Type/Parser.php + + - + message: "#^Parameter \\#1 \\$value of method RetailCrm\\\\Api\\\\Component\\\\Serializer\\\\Parser\\\\JMSCore\\\\Type\\\\Parser\\:\\:getConstant\\(\\) expects int, int\\\\|int\\<4, 8\\>\\|int\\<11, max\\>\\|string\\|null given\\.$#" + count: 1 + path: src/Component/Serializer/Parser/JMSCore/Type/Parser.php + + - + message: "#^Parameter \\#1 \\$value of method RetailCrm\\\\Api\\\\Component\\\\Serializer\\\\Parser\\\\JMSCore\\\\Type\\\\Parser\\:\\:getConstant\\(\\) expects int, int\\|string\\|null given\\.$#" + count: 1 + path: src/Component/Serializer/Parser/JMSCore/Type/Parser.php + + - + message: "#^Property RetailCrm\\\\Api\\\\Component\\\\Serializer\\\\Parser\\\\JMSCore\\\\Type\\\\Parser\\:\\:\\$token with generic class RetailCrm\\\\Api\\\\Component\\\\Serializer\\\\Parser\\\\JMSCore\\\\Type\\\\Token does not specify its types\\: T, V$#" + count: 1 + path: src/Component/Serializer/Parser/JMSCore/Type/Parser.php + + - + message: "#^Access to an undefined property object\\:\\:\\$position\\.$#" + count: 1 + path: src/Component/Serializer/Parser/JMSCore/Type/Token.php + + - + message: "#^Access to an undefined property object\\:\\:\\$type\\.$#" + count: 1 + path: src/Component/Serializer/Parser/JMSCore/Type/Token.php + + - + message: "#^Access to an undefined property object\\:\\:\\$value\\.$#" + count: 1 + path: src/Component/Serializer/Parser/JMSCore/Type/Token.php + + - + message: "#^Method RetailCrm\\\\Api\\\\Component\\\\Serializer\\\\Parser\\\\JMSCore\\\\Type\\\\Token\\:\\:fromArray\\(\\) has parameter \\$source with no value type specified in iterable type array\\.$#" + count: 1 + path: src/Component/Serializer/Parser/JMSCore/Type/Token.php + + - + message: "#^Method RetailCrm\\\\Api\\\\Component\\\\Serializer\\\\Parser\\\\JMSCore\\\\Type\\\\Token\\:\\:fromArray\\(\\) return type with generic class RetailCrm\\\\Api\\\\Component\\\\Serializer\\\\Parser\\\\JMSCore\\\\Type\\\\Token does not specify its types\\: T, V$#" + count: 1 + path: src/Component/Serializer/Parser/JMSCore/Type/Token.php + + - + message: "#^Method RetailCrm\\\\Api\\\\Component\\\\Serializer\\\\Parser\\\\JMSCore\\\\Type\\\\Token\\:\\:fromObject\\(\\) return type with generic class RetailCrm\\\\Api\\\\Component\\\\Serializer\\\\Parser\\\\JMSCore\\\\Type\\\\Token does not specify its types\\: T, V$#" + count: 1 + path: src/Component/Serializer/Parser/JMSCore/Type/Token.php + + - + message: "#^Property RetailCrm\\\\Api\\\\Component\\\\Serializer\\\\Parser\\\\JMSCore\\\\Type\\\\Token\\\\:\\:\\$type \\(\\(T of int\\|string\\)\\|null\\) does not accept int\\|string\\.$#" + count: 1 + path: src/Component/Serializer/Parser/JMSCore/Type/Token.php + + - + message: "#^Cannot call method getParameters\\(\\) on ReflectionMethod\\|null\\.$#" + count: 1 + path: src/Component/Serializer/Parser/JMSTypeParser.php + + - + message: "#^Property RetailCrm\\\\Api\\\\Component\\\\Serializer\\\\Parser\\\\JMSTypeParser\\:\\:\\$useArrayDateFormat has no type specified\\.$#" + count: 1 path: src/Component/Serializer/Parser/JMSTypeParser.php From 312dfeffd71848ff7ebe6cbba953d47ce2298b0c Mon Sep 17 00:00:00 2001 From: Neur0toxine Date: Mon, 11 Mar 2024 20:45:58 +0300 Subject: [PATCH 09/12] better php 7.4 support --- phpcs.xml.dist | 1 + phpstan-baseline.neon | 2 + .../Serializer/ArraySupportDecorator.php | 567 ++++++++++++------ 3 files changed, 392 insertions(+), 178 deletions(-) diff --git a/phpcs.xml.dist b/phpcs.xml.dist index 5001cfd..a10b535 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -13,4 +13,5 @@ src/Component/Serializer/Generator/* src/Component/Serializer/Parser/* + src/Component/Serializer/ArraySupportDecorator.php diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 47a94db..d4b5787 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1,4 +1,6 @@ parameters: + excludePaths: + - src/Component/Serializer/ArraySupportDecorator.php ignoreErrors: - message: "#^Unsafe call to private method RetailCrm\\\\Api\\\\Builder\\\\ClientBuilder\\:\\:buildHandlersChain\\(\\) through static\\:\\:\\.$#" diff --git a/src/Component/Serializer/ArraySupportDecorator.php b/src/Component/Serializer/ArraySupportDecorator.php index e94de20..4d9c980 100644 --- a/src/Component/Serializer/ArraySupportDecorator.php +++ b/src/Component/Serializer/ArraySupportDecorator.php @@ -16,211 +16,422 @@ use Liip\Serializer\Exception\UnsupportedFormatException; use Liip\Serializer\SerializerInterface; use Pnz\JsonException\Json; -/** - * Class ArraySupportDecorator - * - * @category ArraySupportDecorator - * @package RetailCrm\Api\Component\Serializer - */ -class ArraySupportDecorator implements SerializerInterface -{ - private SerializerInterface $serializer; - +if (PHP_VERSION_ID >= 80000) { /** - * ArraySupportDecorator constructor. + * Class ArraySupportDecorator * - * @param \Liip\Serializer\SerializerInterface $serializer + * @category ArraySupportDecorator + * @package RetailCrm\Api\Component\Serializer */ - public function __construct(SerializerInterface $serializer) + class ArraySupportDecorator implements SerializerInterface { - $this->serializer = $serializer; - } + private SerializerInterface $serializer; - /** - * @inheritDoc - * @throws \JsonException - */ - public function serialize($data, string $format, ?Context $context = null): string - { - if ('json' !== $format) { - throw new UnsupportedFormatException('Liip serializer only supports JSON for now'); + /** + * ArraySupportDecorator constructor. + * + * @param \Liip\Serializer\SerializerInterface $serializer + */ + public function __construct(SerializerInterface $serializer) + { + $this->serializer = $serializer; } - if (is_array($data)) { - try { - return Json::encode($this->encodeArray($data, $context), JSON_UNESCAPED_SLASHES); - } catch (JsonException $exception) { - throw new Exception( - sprintf( - 'Failed to JSON encode data for %s. This is not supposed to happen.', - // @phpstan-ignore-next-line - is_object($data) ? get_class($data) : gettype($data) - ), - 0, - $exception - ); - } - } - - return $this->serializer->serialize($data, $format, $context); - } - - /** - * @inheritDoc - */ - public function deserialize(string $data, string $type, string $format, ?Context $context = null): mixed - { - if ('json' !== $format) { - throw new UnsupportedFormatException('Liip serializer only supports JSON for now'); - } - - if (static::isArrayType($type)) { - try { - $array = Json::decode($data, true); - } catch (JsonException $exception) { - throw new Exception('Failed to JSON decode data. This is not supposed to happen.', 0, $exception); + /** + * @inheritDoc + * @throws \JsonException + */ + public function serialize($data, string $format, ?Context $context = null): string + { + if ('json' !== $format) { + throw new UnsupportedFormatException('Liip serializer only supports JSON for now'); } - return $this->serializer->fromArray($this->decodeArray($array, $type, $context), $type, $context); - } - - return $this->serializer->deserialize($data, $type, $format, $context); - } - - /** - * @inheritDoc - * - * @return array - */ - public function toArray($data, ?Context $context = null): array - { - if (is_array($data)) { - return $this->encodeArray($data, $context); - } - - return $this->serializer->toArray($data, $context); - } - - /** - * @inheritDoc - * - * @param array $data - * - * @return array|object - */ - public function fromArray(array $data, string $type, ?Context $context = null): mixed - { - if (static::isArrayType($type)) { - return $this->decodeArray($data, $type, $context); - } - - return $this->serializer->fromArray($data, $type, $context); - } - - /** - * Encodes array of objects into simple multidimensional array. - * - * @param mixed[] $data - * @param \Liip\Serializer\Context|null $context - * - * @return mixed[] - * @throws \Liip\Serializer\Exception\Exception - * @throws \Liip\Serializer\Exception\UnsupportedTypeException - */ - private function encodeArray(array $data, ?Context $context = null): array - { - $result = []; - - foreach ($data as $key => $value) { - switch (gettype($value)) { - case 'array': - $result[$key] = $this->encodeArray($value, $context); - break; - case 'object': - $result[$key] = $this->serializer->toArray($value, $context); - break; - default: - $result[$key] = $value; - break; + if (is_array($data)) { + try { + return Json::encode($this->encodeArray($data, $context), JSON_UNESCAPED_SLASHES); + } catch (JsonException $exception) { + throw new Exception( + sprintf( + 'Failed to JSON encode data for %s. This is not supposed to happen.', + // @phpstan-ignore-next-line + is_object($data) ? get_class($data) : gettype($data) + ), + 0, + $exception + ); + } } + + return $this->serializer->serialize($data, $format, $context); } - return $result; - } + /** + * @inheritDoc + */ + public function deserialize(string $data, string $type, string $format, ?Context $context = null): mixed + { + if ('json' !== $format) { + throw new UnsupportedFormatException('Liip serializer only supports JSON for now'); + } - /** - * Decodes array of arrays to array of objects. - * - * @param mixed[] $data - * @param string $type - * @param \Liip\Serializer\Context|null $context - * - * @return array - * @throws \Liip\Serializer\Exception\Exception - * @throws \Liip\Serializer\Exception\UnsupportedTypeException - */ - private function decodeArray(array $data, string $type, ?Context $context = null): array - { - $result = []; - $subtype = static::getArrayValueType($type); - - if (class_exists($subtype)) { - foreach ($data as $key => $item) { - if (is_array($item)) { - $result[$key] = $this->decodeArray($item, $subtype, $context); - continue; + if (static::isArrayType($type)) { + try { + $array = Json::decode($data, true); + } catch (JsonException $exception) { + throw new Exception('Failed to JSON decode data. This is not supposed to happen.', 0, $exception); } - $result[$key] = $item; + return $this->serializer->fromArray($this->decodeArray($array, $type, $context), $type, $context); + } + + return $this->serializer->deserialize($data, $type, $format, $context); + } + + /** + * @inheritDoc + * + * @return array + */ + public function toArray($data, ?Context $context = null): array + { + if (is_array($data)) { + return $this->encodeArray($data, $context); + } + + return $this->serializer->toArray($data, $context); + } + + /** + * @inheritDoc + * + * @param array $data + * + * @return array|object + */ + public function fromArray(array $data, string $type, ?Context $context = null): mixed + { + if (static::isArrayType($type)) { + return $this->decodeArray($data, $type, $context); + } + + return $this->serializer->fromArray($data, $type, $context); + } + + /** + * Encodes array of objects into simple multidimensional array. + * + * @param mixed[] $data + * @param \Liip\Serializer\Context|null $context + * + * @return mixed[] + * @throws \Liip\Serializer\Exception\Exception + * @throws \Liip\Serializer\Exception\UnsupportedTypeException + */ + private function encodeArray(array $data, ?Context $context = null): array + { + $result = []; + + foreach ($data as $key => $value) { + switch (gettype($value)) { + case 'array': + $result[$key] = $this->encodeArray($value, $context); + break; + case 'object': + $result[$key] = $this->serializer->toArray($value, $context); + break; + default: + $result[$key] = $value; + break; + } } return $result; } - return $data; - } + /** + * Decodes array of arrays to array of objects. + * + * @param mixed[] $data + * @param string $type + * @param \Liip\Serializer\Context|null $context + * + * @return array + * @throws \Liip\Serializer\Exception\Exception + * @throws \Liip\Serializer\Exception\UnsupportedTypeException + */ + private function decodeArray(array $data, string $type, ?Context $context = null): array + { + $result = []; + $subtype = static::getArrayValueType($type); - /** - * Returns true if provided type is an array. - * - * @param string $type - * - * @return bool - */ - private static function isArrayType(string $type): bool - { - return false !== strpos($type, 'array'); - } + if (class_exists($subtype)) { + foreach ($data as $key => $item) { + if (is_array($item)) { + $result[$key] = $this->decodeArray($item, $subtype, $context); + continue; + } - /** - * Returns array value type from types like 'array' or 'array'. - * - * @param string $type - * - * @return string - */ - private static function getArrayValueType(string $type): string - { - $matches = []; + $result[$key] = $item; + } - preg_match_all( - '/array(\s+)?\<([\w\|\\\\]+)\s+\,\s+([\w\|\\\\]+)\>/m', - $type, - $matches, - PREG_SET_ORDER, - 0 - ); + return $result; + } - if (count($matches) > 0) { - return $matches[count($matches) - 1]; + return $data; } - preg_match_all('/array(\s+)?\<([\w\|\\\\]+)\>/m', $type, $matches, PREG_SET_ORDER, 0); - - if (count($matches) > 0) { - return $matches[count($matches) - 1]; + /** + * Returns true if provided type is an array. + * + * @param string $type + * + * @return bool + */ + private static function isArrayType(string $type): bool + { + return false !== strpos($type, 'array'); } - return 'mixed'; + /** + * Returns array value type from types like 'array' or 'array'. + * + * @param string $type + * + * @return string + */ + private static function getArrayValueType(string $type): string + { + $matches = []; + + preg_match_all( + '/array(\s+)?\<([\w\|\\\\]+)\s+\,\s+([\w\|\\\\]+)\>/m', + $type, + $matches, + PREG_SET_ORDER, + 0 + ); + + if (count($matches) > 0) { + return $matches[count($matches) - 1]; + } + + preg_match_all('/array(\s+)?\<([\w\|\\\\]+)\>/m', $type, $matches, PREG_SET_ORDER, 0); + + if (count($matches) > 0) { + return $matches[count($matches) - 1]; + } + + return 'mixed'; + } + } +} else { + /** + * Class ArraySupportDecorator + * + * @category ArraySupportDecorator + * @package RetailCrm\Api\Component\Serializer + */ + class ArraySupportDecorator implements SerializerInterface + { + private SerializerInterface $serializer; + + /** + * ArraySupportDecorator constructor. + * + * @param \Liip\Serializer\SerializerInterface $serializer + */ + public function __construct(SerializerInterface $serializer) + { + $this->serializer = $serializer; + } + + /** + * @inheritDoc + * @throws \JsonException + */ + public function serialize($data, string $format, ?Context $context = null): string + { + if ('json' !== $format) { + throw new UnsupportedFormatException('Liip serializer only supports JSON for now'); + } + + if (is_array($data)) { + try { + return Json::encode($this->encodeArray($data, $context), JSON_UNESCAPED_SLASHES); + } catch (JsonException $exception) { + throw new Exception( + sprintf( + 'Failed to JSON encode data for %s. This is not supposed to happen.', + // @phpstan-ignore-next-line + is_object($data) ? get_class($data) : gettype($data) + ), + 0, + $exception + ); + } + } + + return $this->serializer->serialize($data, $format, $context); + } + + /** + * @inheritDoc + */ + public function deserialize(string $data, string $type, string $format, ?Context $context = null) + { + if ('json' !== $format) { + throw new UnsupportedFormatException('Liip serializer only supports JSON for now'); + } + + if (static::isArrayType($type)) { + try { + $array = Json::decode($data, true); + } catch (JsonException $exception) { + throw new Exception('Failed to JSON decode data. This is not supposed to happen.', 0, $exception); + } + + return $this->serializer->fromArray($this->decodeArray($array, $type, $context), $type, $context); + } + + return $this->serializer->deserialize($data, $type, $format, $context); + } + + /** + * @inheritDoc + * + * @return array + */ + public function toArray($data, ?Context $context = null): array + { + if (is_array($data)) { + return $this->encodeArray($data, $context); + } + + return $this->serializer->toArray($data, $context); + } + + /** + * @inheritDoc + * + * @param array $data + * + * @return array|object + */ + public function fromArray(array $data, string $type, ?Context $context = null) + { + if (static::isArrayType($type)) { + return $this->decodeArray($data, $type, $context); + } + + return $this->serializer->fromArray($data, $type, $context); + } + + /** + * Encodes array of objects into simple multidimensional array. + * + * @param mixed[] $data + * @param \Liip\Serializer\Context|null $context + * + * @return mixed[] + * @throws \Liip\Serializer\Exception\Exception + * @throws \Liip\Serializer\Exception\UnsupportedTypeException + */ + private function encodeArray(array $data, ?Context $context = null): array + { + $result = []; + + foreach ($data as $key => $value) { + switch (gettype($value)) { + case 'array': + $result[$key] = $this->encodeArray($value, $context); + break; + case 'object': + $result[$key] = $this->serializer->toArray($value, $context); + break; + default: + $result[$key] = $value; + break; + } + } + + return $result; + } + + /** + * Decodes array of arrays to array of objects. + * + * @param mixed[] $data + * @param string $type + * @param \Liip\Serializer\Context|null $context + * + * @return array + * @throws \Liip\Serializer\Exception\Exception + * @throws \Liip\Serializer\Exception\UnsupportedTypeException + */ + private function decodeArray(array $data, string $type, ?Context $context = null): array + { + $result = []; + $subtype = static::getArrayValueType($type); + + if (class_exists($subtype)) { + foreach ($data as $key => $item) { + if (is_array($item)) { + $result[$key] = $this->decodeArray($item, $subtype, $context); + continue; + } + + $result[$key] = $item; + } + + return $result; + } + + return $data; + } + + /** + * Returns true if provided type is an array. + * + * @param string $type + * + * @return bool + */ + private static function isArrayType(string $type): bool + { + return false !== strpos($type, 'array'); + } + + /** + * Returns array value type from types like 'array' or 'array'. + * + * @param string $type + * + * @return string + */ + private static function getArrayValueType(string $type): string + { + $matches = []; + + preg_match_all( + '/array(\s+)?\<([\w\|\\\\]+)\s+\,\s+([\w\|\\\\]+)\>/m', + $type, + $matches, + PREG_SET_ORDER, + 0 + ); + + if (count($matches) > 0) { + return $matches[count($matches) - 1]; + } + + preg_match_all('/array(\s+)?\<([\w\|\\\\]+)\>/m', $type, $matches, PREG_SET_ORDER, 0); + + if (count($matches) > 0) { + return $matches[count($matches) - 1]; + } + + return 'mixed'; + } } } From 18f5b8c1969fc1a899cbc6b906ab0e50074a21dd Mon Sep 17 00:00:00 2001 From: Neur0toxine Date: Tue, 12 Mar 2024 09:30:21 +0300 Subject: [PATCH 10/12] update phpcs command & composer-compile-plugin --- composer.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index cc9a36e..09ed2ad 100644 --- a/composer.json +++ b/composer.json @@ -28,7 +28,7 @@ "doctrine/annotations": "^1.13|^2.0", "liip/serializer": "2.2.* || 2.6.*", "php-http/httplug": "^2.2", - "civicrm/composer-compile-plugin": "^0.18.0", + "civicrm/composer-compile-plugin": "^0.20", "symfony/console": "^4.0|^5.0|^6.0", "psr/event-dispatcher": "^1.0", "neur0toxine/psr.http-client-implementation.php-http-curl": "*", @@ -67,7 +67,7 @@ "phpunit": "./vendor/bin/phpunit -c phpunit.xml.dist --coverage-text", "phpunit-ci": "@php -dpcov.enabled=1 -dpcov.directory=. -dpcov.exclude=\"~vendor~\" ./vendor/bin/phpunit --teamcity -c phpunit.xml.dist", "phpmd": "./vendor/bin/phpmd src text ./phpmd.xml", - "phpcs": "./vendor/bin/phpcs -p src --runtime-set testVersion 7.3-8.2 && ./vendor/bin/phpcs -p tests --runtime-set testVersion 7.3-8.2 --warning-severity=0", + "phpcs": "./vendor/bin/phpcs -p src --runtime-set testVersion 7.4-8.3 && ./vendor/bin/phpcs -p tests --runtime-set testVersion 7.4-8.3 --warning-severity=0", "phpstan": "./vendor/bin/phpstan analyse -c phpstan.neon src --memory-limit=-1", "phpstan-dockerized-ci": "docker run --rm -it -w=/app -v ${PWD}:/app oskarstark/phpstan-ga:1.0.1 analyse src -c phpstan.neon --memory-limit=1G --no-progress", "lint:fix": "./vendor/bin/phpcbf src", From 759c1ee5a8a07f21494a152504c32e30d18e5d8d Mon Sep 17 00:00:00 2001 From: Neur0toxine Date: Tue, 12 Mar 2024 09:59:53 +0300 Subject: [PATCH 11/12] restore php 7.3 support --- .github/workflows/ci.yml | 2 +- README.md | 2 +- composer.json | 4 ++-- .../Serializer/ArraySupportDecorator.php | 6 ++++-- .../Generator/DeserializerGenerator.php | 18 +++++++++++------ .../Generator/SerializerGenerator.php | 20 +++++++++++++------ .../Serializer/Parser/JMSCore/Type/Parser.php | 18 ++++++++--------- .../Serializer/Parser/JMSCore/Type/Token.php | 4 +++- src/Component/Serializer/Parser/JMSParser.php | 9 ++++++--- .../Serializer/Parser/JMSTypeParser.php | 8 +++----- 10 files changed, 54 insertions(+), 37 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index af5d23d..99a7fb9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - php-version: ['7.4', '8.0', '8.1', '8.2', '8.3'] + php-version: ['7.3', '7.4', '8.0', '8.1', '8.2', '8.3'] steps: - name: Check out code into the workspace uses: actions/checkout@v2 diff --git a/README.md b/README.md index 87b2396..4835979 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ You can find more info in the [documentation](doc/index.md). ## Requirements -* PHP 7.4 and above +* PHP 7.3 and above * PHP's cURL support * PHP's JSON support * Any HTTP client compatible with PSR-18 (covered by the installation instructions). diff --git a/composer.json b/composer.json index 09ed2ad..d9e2be7 100644 --- a/composer.json +++ b/composer.json @@ -16,7 +16,7 @@ } ], "require": { - "php": ">=7.4", + "php": ">=7.3", "ext-json": "*", "psr/log": "^1|^2|^3", "psr/http-client": "^1.0", @@ -67,7 +67,7 @@ "phpunit": "./vendor/bin/phpunit -c phpunit.xml.dist --coverage-text", "phpunit-ci": "@php -dpcov.enabled=1 -dpcov.directory=. -dpcov.exclude=\"~vendor~\" ./vendor/bin/phpunit --teamcity -c phpunit.xml.dist", "phpmd": "./vendor/bin/phpmd src text ./phpmd.xml", - "phpcs": "./vendor/bin/phpcs -p src --runtime-set testVersion 7.4-8.3 && ./vendor/bin/phpcs -p tests --runtime-set testVersion 7.4-8.3 --warning-severity=0", + "phpcs": "./vendor/bin/phpcs -p src --runtime-set testVersion 7.3-8.3 && ./vendor/bin/phpcs -p tests --runtime-set testVersion 7.3-8.3 --warning-severity=0", "phpstan": "./vendor/bin/phpstan analyse -c phpstan.neon src --memory-limit=-1", "phpstan-dockerized-ci": "docker run --rm -it -w=/app -v ${PWD}:/app oskarstark/phpstan-ga:1.0.1 analyse src -c phpstan.neon --memory-limit=1G --no-progress", "lint:fix": "./vendor/bin/phpcbf src", diff --git a/src/Component/Serializer/ArraySupportDecorator.php b/src/Component/Serializer/ArraySupportDecorator.php index 4d9c980..c3c33b5 100644 --- a/src/Component/Serializer/ArraySupportDecorator.php +++ b/src/Component/Serializer/ArraySupportDecorator.php @@ -25,7 +25,8 @@ if (PHP_VERSION_ID >= 80000) { */ class ArraySupportDecorator implements SerializerInterface { - private SerializerInterface $serializer; + /** @var \Liip\Serializer\SerializerInterface */ + private $serializer; /** * ArraySupportDecorator constructor. @@ -234,7 +235,8 @@ if (PHP_VERSION_ID >= 80000) { */ class ArraySupportDecorator implements SerializerInterface { - private SerializerInterface $serializer; + /** @var \Liip\Serializer\SerializerInterface */ + private $serializer; /** * ArraySupportDecorator constructor. diff --git a/src/Component/Serializer/Generator/DeserializerGenerator.php b/src/Component/Serializer/Generator/DeserializerGenerator.php index f424f96..7eca46a 100644 --- a/src/Component/Serializer/Generator/DeserializerGenerator.php +++ b/src/Component/Serializer/Generator/DeserializerGenerator.php @@ -29,17 +29,23 @@ final class DeserializerGenerator { private const FILENAME_PREFIX = 'deserialize'; - private Filesystem $filesystem; + /** @var \Symfony\Component\Filesystem\Filesystem */ + private $filesystem; - private GeneratorConfiguration $configuration; + /** @var \Liip\Serializer\Configuration\GeneratorConfiguration */ + private $configuration; - private Deserialization $templating; + /** @var \Liip\Serializer\Template\Deserialization */ + private $templating; - private CustomDeserialization $customTemplating; + /** @var \RetailCrm\Api\Component\Serializer\Template\CustomDeserialization */ + private $customTemplating; - private string $cacheDirectory; + /** @var string */ + private $cacheDirectory; - private Builder $metadataBuilder; + /** @var \Liip\MetadataParser\Builder */ + private $metadataBuilder; /** * @param list $classesToGenerate This is a list of FQCN classnames diff --git a/src/Component/Serializer/Generator/SerializerGenerator.php b/src/Component/Serializer/Generator/SerializerGenerator.php index b4a903d..dd1b5f5 100644 --- a/src/Component/Serializer/Generator/SerializerGenerator.php +++ b/src/Component/Serializer/Generator/SerializerGenerator.php @@ -33,15 +33,23 @@ final class SerializerGenerator { private const FILENAME_PREFIX = 'serialize'; - private Filesystem $filesystem; + /** @var \Symfony\Component\Filesystem\Filesystem */ + private $filesystem; - private Serialization $templating; - private GeneratorConfiguration $configuration; - private string $cacheDirectory; + /** @var \Liip\Serializer\Template\Serialization */ + private $templating; - private CustomSerialization $customTemplating; + /** @var \Liip\Serializer\Configuration\GeneratorConfiguration */ + private $configuration; - private Builder $metadataBuilder; + /** @var string */ + private $cacheDirectory; + + /** @var \RetailCrm\Api\Component\Serializer\Template\CustomSerialization */ + private $customTemplating; + + /** @var \Liip\MetadataParser\Builder */ + private $metadataBuilder; public function __construct( Serialization $templating, diff --git a/src/Component/Serializer/Parser/JMSCore/Type/Parser.php b/src/Component/Serializer/Parser/JMSCore/Type/Parser.php index d77b5f9..d2fb74a 100644 --- a/src/Component/Serializer/Parser/JMSCore/Type/Parser.php +++ b/src/Component/Serializer/Parser/JMSCore/Type/Parser.php @@ -11,19 +11,17 @@ use RetailCrm\Api\Component\Serializer\Parser\JMSCore\Type\Exception\SyntaxError */ final class Parser implements ParserInterface { - /** - * @var Lexer - */ - private Lexer $lexer; + /** @var \RetailCrm\Api\Component\Serializer\Parser\JMSCore\Type\Lexer */ + private $lexer; - private ?Token $token = null; + /** @var \RetailCrm\Api\Component\Serializer\Parser\JMSCore\Type\Token|null */ + private $token = null; - private string $input; + /** @var string */ + private $input; - /** - * @var bool - */ - private bool $root = true; + /** @var bool */ + private $root = true; public function parse(string $type): array { diff --git a/src/Component/Serializer/Parser/JMSCore/Type/Token.php b/src/Component/Serializer/Parser/JMSCore/Type/Token.php index f576840..eaf999b 100644 --- a/src/Component/Serializer/Parser/JMSCore/Type/Token.php +++ b/src/Component/Serializer/Parser/JMSCore/Type/Token.php @@ -41,9 +41,11 @@ final class Token /** * The position of the token in the input string * + * @var int + * * @readonly */ - public int $position; + public $position; /** * @param string|int $value diff --git a/src/Component/Serializer/Parser/JMSParser.php b/src/Component/Serializer/Parser/JMSParser.php index 4d6ddd4..cb2e0a8 100644 --- a/src/Component/Serializer/Parser/JMSParser.php +++ b/src/Component/Serializer/Parser/JMSParser.php @@ -41,11 +41,14 @@ class JMSParser implements ModelParserInterface { private const ACCESS_ORDER_CUSTOM = 'custom'; - private Reader $annotationsReader; + /** @var \Doctrine\Common\Annotations\Reader */ + private $annotationsReader; - private PhpTypeParser $phpTypeParser; + /** @var \Liip\MetadataParser\TypeParser\PhpTypeParser */ + private $phpTypeParser; - protected JMSTypeParser $jmsTypeParser; + /** @var \RetailCrm\Api\Component\Serializer\Parser\JMSTypeParser */ + protected $jmsTypeParser; public function __construct(Reader $annotationsReader) { diff --git a/src/Component/Serializer/Parser/JMSTypeParser.php b/src/Component/Serializer/Parser/JMSTypeParser.php index 1379290..1b8681b 100644 --- a/src/Component/Serializer/Parser/JMSTypeParser.php +++ b/src/Component/Serializer/Parser/JMSTypeParser.php @@ -25,12 +25,10 @@ final class JMSTypeParser private const TYPE_ARRAY_COLLECTION = 'ArrayCollection'; private const TYPE_DATETIME_INTERFACE = 'DateTimeInterface'; - /** - * @var Parser - */ - private Parser $jmsTypeParser; + /** @var \RetailCrm\Api\Component\Serializer\Parser\JMSCore\Type\Parser */ + private $jmsTypeParser; - private $useArrayDateFormat = true; + private $useArrayDateFormat; public function __construct() { From 5bb006588b113c15bb8ee8d59b5287a82eceaf80 Mon Sep 17 00:00:00 2001 From: Neur0toxine Date: Tue, 12 Mar 2024 13:05:57 +0300 Subject: [PATCH 12/12] add explanation for DateTime lexer behavior backport --- .../Serializer/Parser/JMSCore/Type/Parser.php | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/Component/Serializer/Parser/JMSCore/Type/Parser.php b/src/Component/Serializer/Parser/JMSCore/Type/Parser.php index d2fb74a..4ad29f1 100644 --- a/src/Component/Serializer/Parser/JMSCore/Type/Parser.php +++ b/src/Component/Serializer/Parser/JMSCore/Type/Parser.php @@ -53,6 +53,25 @@ final class Parser implements ParserInterface $this->token = Token::fromObject($this->lexer->token); } + /* + * There is a difference in the behavior of the tokenizer depending on the version of the lexer. This affects + * how the lexer interprets date formats for compound date types. In our case it has a huge impact because + * we're using `DateTime<'Y-m-d H:i:s'>` in the API. Newer lexer versions will treat the whole format + * (`Y-m-d H:i:s`) as a token of string type. Older lexer versions will treat each symbol of the format as + * a single token, and this will inevitably break type parsing during model compilation (it will say something + * about unexpected token). + * + * The condition below is used to work around this, albeit in a very hacky way. The token it tries to detect is + * actually a single quote ('). After detecting this token and determining that this time parser has been + * called recursively for a compound type, it will try to determine the parameter length and extract it as + * a single token. In other words, we're trying to mimic newer lexer behavior while using an older version. + * + * This implementation is very limited, as it may fail abruptly with more complex compound types, also it + * may fail to extract the type correctly if it has different whitespace symbols / multiple whitespace symbols. + * It was tested only with the compound date type mentioned above. + * + * A special fallback parser would be a better solution, but it would take much more time and effort to create. + */ if ("" === $this->token->value && $fetchingParam) { $len = 0; $this->lexer->moveNext();