diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ba3378b..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.3', '7.4', '8.0', '8.1', '8.2'] + 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/composer.json b/composer.json index c198e0c..d9e2be7 100644 --- a/composer.json +++ b/composer.json @@ -16,7 +16,7 @@ } ], "require": { - "php": ">=7.3.0", + "php": ">=7.3", "ext-json": "*", "psr/log": "^1|^2|^3", "psr/http-client": "^1.0", @@ -26,9 +26,9 @@ "php-http/message-factory": "^1.0", "php-http/discovery": "^1.13", "doctrine/annotations": "^1.13|^2.0", - "liip/serializer": "2.2.*", + "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.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/phpcs.xml.dist b/phpcs.xml.dist index d12440d..a10b535 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -10,4 +10,8 @@ src/ tests/ + + src/Component/Serializer/Generator/* + src/Component/Serializer/Parser/* + src/Component/Serializer/ArraySupportDecorator.php 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..eaf39fe --- /dev/null +++ b/phpstan-baseline-serializer.neon @@ -0,0 +1,246 @@ +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: "#^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: "#^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: 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 diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 971ac59..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\\:\\:\\.$#" @@ -210,36 +212,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 +222,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 +232,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/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/ArraySupportDecorator.php b/src/Component/Serializer/ArraySupportDecorator.php index 0e97d0e..c3c33b5 100644 --- a/src/Component/Serializer/ArraySupportDecorator.php +++ b/src/Component/Serializer/ArraySupportDecorator.php @@ -16,212 +16,424 @@ 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 -{ - /** @var \Liip\Serializer\SerializerInterface */ - private $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; - } + /** @var \Liip\Serializer\SerializerInterface */ + private $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) - { - 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) - { - 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 + { + /** @var \Liip\Serializer\SerializerInterface */ + private $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'; + } } } diff --git a/src/Component/Serializer/Generator/DeserializerGenerator.php b/src/Component/Serializer/Generator/DeserializerGenerator.php index eb46c7d..7eca46a 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; - - /** - * @var \RetailCrm\Api\Component\Serializer\Template\CustomDeserialization - */ - private $customTemplating; - - /** - * @var Filesystem - */ + /** @var \Symfony\Component\Filesystem\Filesystem */ private $filesystem; - /** - * @var Builder - */ + /** @var \Liip\Serializer\Configuration\GeneratorConfiguration */ + private $configuration; + + /** @var \Liip\Serializer\Template\Deserialization */ + private $templating; + + /** @var \RetailCrm\Api\Component\Serializer\Template\CustomDeserialization */ + private $customTemplating; + + /** @var string */ + private $cacheDirectory; + + /** @var \Liip\MetadataParser\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 +105,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 +116,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 +126,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 +146,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 +168,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 +211,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 +223,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 +245,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, @@ -342,14 +259,17 @@ class DeserializerGenerator ); } + private function isArrayTraversable(PropertyTypeArray $array): bool + { + if (method_exists($array, 'isCollection')) { + return $array->isCollection(); + } + + return $array->isTraversable(); + } + /** - * @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 +279,50 @@ 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 ($this->isArrayTraversable($type)) { + 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'); + 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); } - $format = $type->getDeserializeFormat() ?: $type->getFormat(); - - if (null !== $format) { - return $this->templating->renderAssignDateTimeFromFormat( - $type->isImmutable(), - (string) $modelPropertyPath, - (string) $arrayPath, - $format - ); + $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()); } - return $this->templating->renderAssignDateTimeToField( - $type->isImmutable(), - (string) $modelPropertyPath, - (string) $arrayPath - ); + 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 +330,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 +344,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 +365,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..dd1b5f5 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; - - /** - * @var \RetailCrm\Api\Component\Serializer\Template\CustomSerialization - */ - private $customTemplating; - - /** - * @var Builder - */ - private $metadataBuilder; - - /** - * @var GeneratorConfiguration - */ - private $configuration; - - /** - * @var string - */ - private $cacheDirectory; - - /** - * @var Filesystem - */ + /** @var \Symfony\Component\Filesystem\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 - */ + /** @var \Liip\Serializer\Template\Serialization */ + private $templating; + + /** @var \Liip\Serializer\Configuration\GeneratorConfiguration */ + private $configuration; + + /** @var string */ + private $cacheDirectory; + + /** @var \RetailCrm\Api\Component\Serializer\Template\CustomSerialization */ + private $customTemplating; + + /** @var \Liip\MetadataParser\Builder */ + private $metadataBuilder; + 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 +119,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 +127,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 +139,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, @@ -226,17 +166,10 @@ class SerializerGenerator } $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); @@ -334,15 +267,8 @@ class SerializerGenerator } /** - * @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 +278,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 +315,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 +331,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 +353,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 +388,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..4ad29f1 --- /dev/null +++ b/src/Component/Serializer/Parser/JMSCore/Type/Parser.php @@ -0,0 +1,220 @@ +input = $type; + + $this->lexer = new Lexer(); + $this->lexer->setInput($type); + $this->lexer->moveNext(); + + return $this->visit(); + } + + /** + * @return mixed + */ + private function visit(bool $fetchingParam = false) + { + $this->lexer->moveNext(); + + if (!$this->lexer->token) { + throw new SyntaxError( + 'Syntax error, unexpected end of stream', + ); + } + + if (is_array($this->lexer->token)) { + $this->token = Token::fromArray($this->lexer->token); + } else { + $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(); + + 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->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)) { + return $this->visitArrayType(); + } + + return $this->visitSimpleType(); + } elseif (!$this->root && Lexer::T_ARRAY_START === $this->token->type) { + return $this->visitArrayType(); + } + + throw new SyntaxError(sprintf( + 'Syntax error, unexpected "%s" (%s)', + $this->token->value, + $this->getConstant($this->token->type), + )); + } + + /** + * @return string|mixed[] + */ + private function visitSimpleType() + { + $value = $this->token->value; + + return ['name' => $value, 'params' => []]; + } + + private function visitCompoundType(): array + { + $this->root = false; + $name = $this->token->value; + $this->match(Lexer::T_TYPE_START); + + $params = []; + if (!$this->lexer->isNextToken(Lexer::T_TYPE_END)) { + while (true) { + $params[] = $this->visit(true); + + 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 (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; + } + + throw new SyntaxError(sprintf( + 'Syntax error, unexpected "%s" (%s), expected was %s', + $lookahead->value, + $this->getConstant($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 @@ +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 + * + * @var int + * + * @readonly + */ + public $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/JMSParser.php b/src/Component/Serializer/Parser/JMSParser.php index 9034218..cb2e0a8 100644 --- a/src/Component/Serializer/Parser/JMSParser.php +++ b/src/Component/Serializer/Parser/JMSParser.php @@ -1,21 +1,18 @@ - * @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 - */ + /** @var \Doctrine\Common\Annotations\Reader */ private $annotationsReader; - /** - * @var PhpTypeParser - */ + /** @var \Liip\MetadataParser\TypeParser\PhpTypeParser */ private $phpTypeParser; - /** - * @var JMSTypeParser - */ - private $jmsTypeParser; + /** @var \RetailCrm\Api\Component\Serializer\Parser\JMSTypeParser */ + protected $jmsTypeParser; - /** - * JMSParser constructor. - * - * @param \Doctrine\Common\Annotations\Reader $annotationsReader - */ public function __construct(Reader $annotationsReader) { $this->annotationsReader = $annotationsReader; @@ -82,124 +57,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 +158,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 +178,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 +187,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 +224,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 +277,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 +317,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 +349,6 @@ class JMSParser implements ModelParserInterface return null; } - /** - * @param array $annotations - * - * @return bool - */ private function isVirtualProperty(array $annotations): bool { foreach ($annotations as $annotation) { @@ -518,11 +360,6 @@ class JMSParser implements ModelParserInterface return false; } - /** - * @param array $annotations - * - * @return bool - */ private function isPostDeserializeMethod(array $annotations): bool { foreach ($annotations as $annotation) { @@ -534,16 +371,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..1b8681b 100644 --- a/src/Component/Serializer/Parser/JMSTypeParser.php +++ b/src/Component/Serializer/Parser/JMSTypeParser.php @@ -1,59 +1,42 @@ - * @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 \RetailCrm\Api\Component\Serializer\Parser\JMSCore\Type\Parser */ private $jmsTypeParser; - /** - * JMSTypeParser constructor. - */ + private $useArrayDateFormat; + public function __construct() { - $this->jmsTypeParser = new BaseJMSParser(); + $this->jmsTypeParser = new Parser(); + $this->useArrayDateFormat = null === (new \ReflectionClass(DateTimeOptions::class)) + ->getConstructor()->getParameters()[2]->getType(); } - /** - * @param string $rawType - * - * @return \Liip\MetadataParser\Metadata\PropertyType - */ public function parse(string $rawType): PropertyType { if ('' === $rawType) { @@ -63,12 +46,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 +61,12 @@ class JMSTypeParser if (0 === \count($typeInfo['params'])) { if (self::TYPE_ARRAY === $typeInfo['name']) { - return new PropertyTypeArray(new PropertyTypeUnknown(false), false, $nullable); + return self::iterableType(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 +78,62 @@ 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 self::iterableType($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 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) - )); + 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 + $this->useArrayDateFormat ? $deserializeFormats : $deserializeFormats[0], ) ); } 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; + } + } + + 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); + } } 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 { 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__ . '/..');