1
0
mirror of synced 2024-11-21 12:56:08 +03:00

update liip/serializer

This commit is contained in:
Pavel 2024-03-11 18:09:10 +03:00
parent 7cf7cbf467
commit 82f110d0d0
16 changed files with 643 additions and 1008 deletions

View File

@ -16,7 +16,7 @@
} }
], ],
"require": { "require": {
"php": ">=7.3.0", "php": ">=7.4",
"ext-json": "*", "ext-json": "*",
"psr/log": "^1|^2|^3", "psr/log": "^1|^2|^3",
"psr/http-client": "^1.0", "psr/http-client": "^1.0",
@ -26,7 +26,7 @@
"php-http/message-factory": "^1.0", "php-http/message-factory": "^1.0",
"php-http/discovery": "^1.13", "php-http/discovery": "^1.13",
"doctrine/annotations": "^1.13|^2.0", "doctrine/annotations": "^1.13|^2.0",
"liip/serializer": "2.2.*", "liip/serializer": "2.6.*",
"php-http/httplug": "^2.2", "php-http/httplug": "^2.2",
"civicrm/composer-compile-plugin": "^0.18.0", "civicrm/composer-compile-plugin": "^0.18.0",
"symfony/console": "^4.0|^5.0|^6.0", "symfony/console": "^4.0|^5.0|^6.0",

View File

@ -13,6 +13,7 @@ use RetailCrm\Api\Component\ModelsGenerator;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
/** /**
* Class GenerateModelsCommand * Class GenerateModelsCommand
@ -82,7 +83,15 @@ class GenerateModelsCommand extends AbstractModelsProcessorCommand
$output->writeln(''); $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( $output->writeln(sprintf(
'<fg=black;bg=green> ✓ Done, generated code for %d models.</>', '<fg=black;bg=green> ✓ Done, generated code for %d models.</>',

View File

@ -11,12 +11,12 @@ namespace RetailCrm\Api\Component;
use Doctrine\Common\Annotations\AnnotationReader; use Doctrine\Common\Annotations\AnnotationReader;
use Liip\MetadataParser\Builder; use Liip\MetadataParser\Builder;
use Liip\MetadataParser\ModelParser\RawMetadata\PropertyCollection;
use Liip\MetadataParser\Parser; use Liip\MetadataParser\Parser;
use Liip\MetadataParser\RecursionChecker; use Liip\MetadataParser\RecursionChecker;
use Liip\Serializer\Configuration\GeneratorConfiguration; use Liip\Serializer\Configuration\GeneratorConfiguration;
use Liip\Serializer\Template\Deserialization; use Liip\Serializer\Template\Deserialization;
use Liip\Serializer\Template\Serialization; use Liip\Serializer\Template\Serialization;
use RetailCrm\Api\Component\Utils;
use RetailCrm\Api\Component\Serializer\Generator\DeserializerGenerator; use RetailCrm\Api\Component\Serializer\Generator\DeserializerGenerator;
use RetailCrm\Api\Component\Serializer\Generator\SerializerGenerator; use RetailCrm\Api\Component\Serializer\Generator\SerializerGenerator;
use RetailCrm\Api\Component\Serializer\ModelsChecksumGenerator; use RetailCrm\Api\Component\Serializer\ModelsChecksumGenerator;
@ -177,6 +177,7 @@ class ModelsGenerator
$configurationArray['classes'][$class] = []; $configurationArray['classes'][$class] = [];
} }
PropertyCollection::useIdenticalNamingStrategy();
$configuration = GeneratorConfiguration::createFomArray($configurationArray); $configuration = GeneratorConfiguration::createFomArray($configurationArray);
$parsers = [new JMSParser(new AnnotationReader())]; $parsers = [new JMSParser(new AnnotationReader())];
$builder = new Builder(new Parser($parsers), new RecursionChecker(null, [])); $builder = new Builder(new Parser($parsers), new RecursionChecker(null, []));

View File

@ -1,11 +1,6 @@
<?php <?php
/** declare(strict_types=1);
* PHP version 7.3
*
* @category DeserializerGenerator
* @package RetailCrm\Api\Component\Serializer\Generator
*/
namespace RetailCrm\Api\Component\Serializer\Generator; namespace RetailCrm\Api\Component\Serializer\Generator;
@ -18,6 +13,8 @@ use Liip\MetadataParser\Metadata\PropertyTypeDateTime;
use Liip\MetadataParser\Metadata\PropertyTypePrimitive; use Liip\MetadataParser\Metadata\PropertyTypePrimitive;
use Liip\MetadataParser\Metadata\PropertyTypeUnknown; use Liip\MetadataParser\Metadata\PropertyTypeUnknown;
use Liip\MetadataParser\Reducer\TakeBestReducer; use Liip\MetadataParser\Reducer\TakeBestReducer;
use Liip\Serializer\Configuration\ClassToGenerate;
use Liip\Serializer\Configuration\GeneratorConfiguration;
use Liip\Serializer\Path\ArrayPath; use Liip\Serializer\Path\ArrayPath;
use Liip\Serializer\Path\ModelPath; use Liip\Serializer\Path\ModelPath;
use Liip\Serializer\Template\Deserialization; use Liip\Serializer\Template\Deserialization;
@ -26,129 +23,69 @@ use RetailCrm\Api\Component\Serializer\Type\PropertyTypeMixed;
use RetailCrm\Api\Interfaces\Orders\CustomerInterface; use RetailCrm\Api\Interfaces\Orders\CustomerInterface;
use RetailCrm\Api\Model\Entity\Customers\Customer; use RetailCrm\Api\Model\Entity\Customers\Customer;
use RetailCrm\Api\Model\Entity\CustomersCorporate\CustomerCorporate; use RetailCrm\Api\Model\Entity\CustomersCorporate\CustomerCorporate;
use RuntimeException;
use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\Filesystem\Filesystem;
/** final class DeserializerGenerator
* Class DeserializerGenerator
*
* @category DeserializerGenerator
* @package RetailCrm\Api\Component\Serializer\Generator
* @license https://github.com/liip/serializer/blob/master/LICENSE MIT License
* @author Liip <https://github.com/liip>
* @author Pavel Kovalenko
* @see https://github.com/liip/serializer
* @internal
*
* @SuppressWarnings(PHPMD)
*/
class DeserializerGenerator
{ {
private const FILENAME_PREFIX = 'deserialize'; private const FILENAME_PREFIX = 'deserialize';
/** private Filesystem $filesystem;
* @var Deserialization
*/ private GeneratorConfiguration $configuration;
private $templating;
private Deserialization $templating;
private CustomDeserialization $customTemplating;
private string $cacheDirectory;
private Builder $metadataBuilder;
/** /**
* @var \RetailCrm\Api\Component\Serializer\Template\CustomDeserialization * @param list<class-string> $classesToGenerate This is a list of FQCN classnames
*/
private $customTemplating;
/**
* @var Filesystem
*/
private $filesystem;
/**
* @var Builder
*/
private $metadataBuilder;
/**
* This is a list of fqn classnames
*
* I.e.
*
* [
* Product::class,
* ];
*
* @var string[]
*/
private $classesToGenerate;
/**
* @var string
*/
private $cacheDirectory;
/**
* @param \Liip\Serializer\Template\Deserialization $templating
* @param \RetailCrm\Api\Component\Serializer\Template\CustomDeserialization $customTemplating
* @param string[] $classesToGenerate
* @param string $cacheDirectory
*/ */
public function __construct( public function __construct(
Deserialization $templating, Deserialization $templating,
CustomDeserialization $customTemplating, CustomDeserialization $customTemplating,
array $classesToGenerate, array $classesToGenerate,
string $cacheDirectory string $cacheDirectory,
GeneratorConfiguration $configuration = null
) { ) {
$this->cacheDirectory = $cacheDirectory;
$this->templating = $templating; $this->templating = $templating;
$this->customTemplating = $customTemplating; $this->customTemplating = $customTemplating;
$this->classesToGenerate = $classesToGenerate;
$this->cacheDirectory = $cacheDirectory;
$this->filesystem = new Filesystem(); $this->filesystem = new Filesystem();
$this->configuration = $this->createGeneratorConfiguration($configuration, $classesToGenerate);
} }
/**
* @param string $className
*
* @return string
*/
public static function buildDeserializerFunctionName(string $className): 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 public function generate(Builder $metadataBuilder): void
{ {
$this->metadataBuilder = $metadataBuilder; $this->metadataBuilder = $metadataBuilder;
$this->filesystem->mkdir($this->cacheDirectory); $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 // 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 // otherwise we end up with generated property names for accessor methods
$classMetadata = $metadataBuilder->build($className, [ $classMetadata = $metadataBuilder->build($classToGenerate->getClassName(), [
new TakeBestReducer(), new TakeBestReducer(),
]); ]);
$this->writeFile($classMetadata); $this->writeFile($classMetadata);
} }
} }
/**
* @param \Liip\MetadataParser\Metadata\ClassMetadata $classMetadata
*
* @throws \Exception
*/
private function writeFile(ClassMetadata $classMetadata): void private function writeFile(ClassMetadata $classMetadata): void
{ {
if (count($classMetadata->getConstructorParameters())) { if (\count($classMetadata->getConstructorParameters())) {
throw new RuntimeException(sprintf( throw new \Exception(sprintf('We currently do not support deserializing when the root class has a non-empty constructor. Class %s', $classMetadata->getClassName()));
'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'); $arrayPath = new ArrayPath('jsonData');
$code = $this->templating->renderFunction( $code = $this->templating->renderFunction(
@ -162,13 +99,7 @@ class DeserializerGenerator
} }
/** /**
* @param \Liip\MetadataParser\Metadata\ClassMetadata $classMetadata * @param array<string, positive-int> $stack
* @param \Liip\Serializer\Path\ArrayPath $arrayPath
* @param \Liip\Serializer\Path\ModelPath $modelPath
* @param mixed[] $stack
*
* @return string
* @throws \Exception
*/ */
private function generateCodeForClass( private function generateCodeForClass(
ClassMetadata $classMetadata, ClassMetadata $classMetadata,
@ -179,9 +110,9 @@ class DeserializerGenerator
$stack[$classMetadata->getClassName()] = ($stack[$classMetadata->getClassName()] ?? 0) + 1; $stack[$classMetadata->getClassName()] = ($stack[$classMetadata->getClassName()] ?? 0) + 1;
$constructorArgumentNames = []; $constructorArgumentNames = [];
$overwrittenNames = [];
$initCode = ''; $initCode = '';
$code = ''; $code = '';
foreach ($classMetadata->getProperties() as $propertyMetadata) { foreach ($classMetadata->getProperties() as $propertyMetadata) {
$propertyArrayPath = $arrayPath->withFieldName($propertyMetadata->getSerializedName()); $propertyArrayPath = $arrayPath->withFieldName($propertyMetadata->getSerializedName());
@ -189,6 +120,9 @@ class DeserializerGenerator
$argument = $classMetadata->getConstructorParameter($propertyMetadata->getName()); $argument = $classMetadata->getConstructorParameter($propertyMetadata->getName());
$default = var_export($argument->isRequired() ? null : $argument->getDefaultValue(), true); $default = var_export($argument->isRequired() ? null : $argument->getDefaultValue(), true);
$tempVariable = ModelPath::tempVariable([(string) $modelPath, $propertyMetadata->getName()]); $tempVariable = ModelPath::tempVariable([(string) $modelPath, $propertyMetadata->getName()]);
if (\array_key_exists($propertyMetadata->getName(), $constructorArgumentNames)) {
$overwrittenNames[$propertyMetadata->getName()] = true;
}
$constructorArgumentNames[$propertyMetadata->getName()] = (string) $tempVariable; $constructorArgumentNames[$propertyMetadata->getName()] = (string) $tempVariable;
$initCode .= $this->templating->renderArgument( $initCode .= $this->templating->renderArgument(
@ -206,26 +140,21 @@ class DeserializerGenerator
} }
$constructorArguments = []; $constructorArguments = [];
foreach ($classMetadata->getConstructorParameters() as $definition) { foreach ($classMetadata->getConstructorParameters() as $definition) {
if (array_key_exists($definition->getName(), $constructorArgumentNames)) { if (\array_key_exists($definition->getName(), $constructorArgumentNames)) {
$constructorArguments[] = $constructorArgumentNames[$definition->getName()]; $constructorArguments[] = $constructorArgumentNames[$definition->getName()];
continue; continue;
} }
if ($definition->isRequired()) { if ($definition->isRequired()) {
throw new RuntimeException(sprintf( $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)));
'Unknown constructor argument "%s" in "%s(%s)"', if ($overwrittenNames) {
$definition->getName(), $msg .= sprintf(' Multiple definitions for fields %s seen - the last one overwrites previous ones.', implode(', ', array_keys($overwrittenNames)));
$classMetadata->getClassName(), }
implode(', ', array_keys($constructorArgumentNames)) throw new \Exception($msg);
));
} }
$constructorArguments[] = var_export($definition->getDefaultValue(), true); $constructorArguments[] = var_export($definition->getDefaultValue(), true);
} }
if (\count($constructorArgumentNames) > 0) {
if (count($constructorArgumentNames) > 0) {
$code .= $this->templating->renderUnset(array_values($constructorArgumentNames)); $code .= $this->templating->renderUnset(array_values($constructorArgumentNames));
} }
@ -233,13 +162,7 @@ class DeserializerGenerator
return $this->generateCustomerInterface($classMetadata, $arrayPath, $modelPath, $initCode, $stack); return $this->generateCustomerInterface($classMetadata, $arrayPath, $modelPath, $initCode, $stack);
} }
return $this->templating->renderClass( return $this->templating->renderClass((string) $modelPath, $classMetadata->getClassName(), $constructorArguments, $code, $initCode);
(string) $modelPath,
$classMetadata->getClassName(),
$constructorArguments,
$code,
$initCode
);
} }
/** /**
@ -282,13 +205,7 @@ class DeserializerGenerator
} }
/** /**
* @param \Liip\MetadataParser\Metadata\PropertyMetadata $propertyMetadata * @param array<string, positive-int> $stack
* @param \Liip\Serializer\Path\ArrayPath $arrayPath
* @param \Liip\Serializer\Path\ModelPath $modelPath
* @param mixed[] $stack
*
* @return string
* @throws \Exception
*/ */
private function generateCodeForProperty( private function generateCodeForProperty(
PropertyMetadata $propertyMetadata, PropertyMetadata $propertyMetadata,
@ -300,16 +217,16 @@ class DeserializerGenerator
return ''; return '';
} }
if (Recursion::hasMaxDepthReached($propertyMetadata, $stack)) {
return '';
}
if ($propertyMetadata->getAccessor()->hasSetterMethod()) { if ($propertyMetadata->getAccessor()->hasSetterMethod()) {
$tempVariable = ModelPath::tempVariable([(string) $modelPath, $propertyMetadata->getName()]); $tempVariable = ModelPath::tempVariable([(string) $modelPath, $propertyMetadata->getName()]);
$code = $this->generateCodeForField($propertyMetadata, $arrayPath, $tempVariable, $stack); $code = $this->generateCodeForField($propertyMetadata, $arrayPath, $tempVariable, $stack);
$code .= $this->templating->renderConditional( $code .= $this->templating->renderConditional(
(string) $tempVariable, (string) $tempVariable,
$this->templating->renderSetter( $this->templating->renderSetter((string) $modelPath, $propertyMetadata->getAccessor()->getSetterMethod(), (string) $tempVariable)
(string) $modelPath,
(string) $propertyMetadata->getAccessor()->getSetterMethod(),
(string) $tempVariable
)
); );
$code .= $this->templating->renderUnset([(string) $tempVariable]); $code .= $this->templating->renderUnset([(string) $tempVariable]);
@ -322,13 +239,7 @@ class DeserializerGenerator
} }
/** /**
* @param \Liip\MetadataParser\Metadata\PropertyMetadata $propertyMetadata * @param array<string, positive-int> $stack
* @param \Liip\Serializer\Path\ArrayPath $arrayPath
* @param \Liip\Serializer\Path\ModelPath $modelPath
* @param mixed[] $stack
*
* @return string
* @throws \Exception
*/ */
private function generateCodeForField( private function generateCodeForField(
PropertyMetadata $propertyMetadata, PropertyMetadata $propertyMetadata,
@ -343,13 +254,7 @@ class DeserializerGenerator
} }
/** /**
* @param \Liip\MetadataParser\Metadata\PropertyMetadata $propertyMetadata * @param array<string, positive-int> $stack
* @param \Liip\Serializer\Path\ArrayPath $arrayPath
* @param \Liip\Serializer\Path\ModelPath $modelPropertyPath
* @param mixed[] $stack
*
* @return string
* @throws \Exception
*/ */
private function generateInnerCodeForFieldType( private function generateInnerCodeForFieldType(
PropertyMetadata $propertyMetadata, PropertyMetadata $propertyMetadata,
@ -359,64 +264,40 @@ class DeserializerGenerator
): string { ): string {
$type = $propertyMetadata->getType(); $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) { switch ($type) {
case $type instanceof PropertyTypeArray:
if ($type->isTraversable()) {
return $this->generateCodeForArrayCollection($propertyMetadata, $type, $arrayPath, $modelPropertyPath, $stack);
}
return $this->generateCodeForArray($type, $arrayPath, $modelPropertyPath, $stack);
case $type instanceof PropertyTypeDateTime: case $type instanceof PropertyTypeDateTime:
if (null !== $type->getZone()) { $formats = $type->getDeserializeFormats() ?: (\is_string($type->getFormat()) ? [$type->getFormat()] : $type->getFormat());
throw new RuntimeException('Timezone support is not implemented'); if (null !== $formats) {
return $this->templating->renderAssignDateTimeFromFormat($type->isImmutable(), (string) $modelPropertyPath, (string) $arrayPath, $formats, $type->getZone());
} }
$format = $type->getDeserializeFormat() ?: $type->getFormat(); return $this->templating->renderAssignDateTimeToField($type->isImmutable(), (string) $modelPropertyPath, (string) $arrayPath);
if (null !== $format) {
return $this->templating->renderAssignDateTimeFromFormat(
$type->isImmutable(),
(string) $modelPropertyPath,
(string) $arrayPath,
$format
);
}
return $this->templating->renderAssignDateTimeToField(
$type->isImmutable(),
(string) $modelPropertyPath,
(string) $arrayPath
);
case $type instanceof PropertyTypePrimitive && 'float' === $type->getTypeName(): case $type instanceof PropertyTypePrimitive && 'float' === $type->getTypeName():
return $this->templating->renderAssignJsonDataToFieldWithCasting( return $this->templating->renderAssignJsonDataToFieldWithCasting((string) $modelPropertyPath, (string) $arrayPath, 'float');
(string) $modelPropertyPath,
(string) $arrayPath,
'float'
);
case $type instanceof PropertyTypePrimitive: case $type instanceof PropertyTypePrimitive:
case $type instanceof PropertyTypeUnknown: case $type instanceof PropertyTypeUnknown:
case $type instanceof PropertyTypeMixed: case $type instanceof PropertyTypeMixed:
return $this->templating->renderAssignJsonDataToField((string) $modelPropertyPath, (string) $arrayPath); return $this->templating->renderAssignJsonDataToField((string) $modelPropertyPath, (string) $arrayPath);
case $type instanceof PropertyTypeClass: case $type instanceof PropertyTypeClass:
return $this->generateCodeForClass($type->getClassMetadata(), $arrayPath, $modelPropertyPath, $stack); return $this->generateCodeForClass($type->getClassMetadata(), $arrayPath, $modelPropertyPath, $stack);
default: 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 array<string, positive-int> $stack
* @param \Liip\Serializer\Path\ArrayPath $arrayPath
* @param \Liip\Serializer\Path\ModelPath $modelPath
* @param mixed[] $stack
*
* @return string
* @throws \Exception
*/ */
private function generateCodeForArray( private function generateCodeForArray(
PropertyTypeArray $type, PropertyTypeArray $type,
@ -424,6 +305,11 @@ class DeserializerGenerator
ModelPath $modelPath, ModelPath $modelPath,
array $stack array $stack
): string { ): 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); $index = ModelPath::indexVariable((string) $arrayPath);
$arrayPropertyPath = $arrayPath->withVariable((string) $index); $arrayPropertyPath = $arrayPath->withVariable((string) $index);
$modelPropertyPath = $modelPath->withArray((string) $index); $modelPropertyPath = $modelPath->withArray((string) $index);
@ -433,22 +319,16 @@ class DeserializerGenerator
case $subType instanceof PropertyTypeArray: case $subType instanceof PropertyTypeArray:
$innerCode = $this->generateCodeForArray($subType, $arrayPropertyPath, $modelPropertyPath, $stack); $innerCode = $this->generateCodeForArray($subType, $arrayPropertyPath, $modelPropertyPath, $stack);
break; break;
case $subType instanceof PropertyTypeClass: case $subType instanceof PropertyTypeClass:
$innerCode = $this->generateCodeForClass( $innerCode = $this->generateCodeForClass($subType->getClassMetadata(), $arrayPropertyPath, $modelPropertyPath, $stack);
$subType->getClassMetadata(),
$arrayPropertyPath,
$modelPropertyPath,
$stack
);
break; break;
case $subType instanceof PropertyTypeUnknown: case $subType instanceof PropertyTypeUnknown:
$innerCode = $this->templating->renderAssignJsonDataToField( return $this->templating->renderAssignJsonDataToField((string) $modelPath, (string) $arrayPath);
$modelPropertyPath,
$arrayPropertyPath
);
break;
default: default:
throw new RuntimeException('Unexpected array subtype ' . get_class($subType)); throw new \Exception('Unexpected array subtype '. get_class($subType));
} }
if ('' === $innerCode) { if ('' === $innerCode) {
@ -460,4 +340,42 @@ class DeserializerGenerator
return $code; return $code;
} }
/**
* @param array<string, positive-int> $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<class-string> $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;
}
} }

View File

@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
namespace RetailCrm\Api\Component\Serializer\Generator;
use Liip\MetadataParser\Metadata\PropertyMetadata;
use Liip\MetadataParser\Metadata\PropertyTypeArray;
use Liip\MetadataParser\Metadata\PropertyTypeClass;
abstract class Recursion
{
/**
* @param array<string, positive-int> $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<string, positive-int> $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();
}
}

View File

@ -1,17 +1,9 @@
<?php <?php
/**
* PHP version 7.3
*
* @category SerializerGenerator
* @package RetailCrm\Api\Component\Serializer\Generator
*/
declare(strict_types=1); declare(strict_types=1);
namespace RetailCrm\Api\Component\Serializer\Generator; namespace RetailCrm\Api\Component\Serializer\Generator;
use DateTime;
use Liip\MetadataParser\Builder; use Liip\MetadataParser\Builder;
use Liip\MetadataParser\Metadata\ClassMetadata; use Liip\MetadataParser\Metadata\ClassMetadata;
use Liip\MetadataParser\Metadata\PropertyMetadata; use Liip\MetadataParser\Metadata\PropertyMetadata;
@ -27,140 +19,90 @@ use Liip\MetadataParser\Reducer\TakeBestReducer;
use Liip\MetadataParser\Reducer\VersionReducer; use Liip\MetadataParser\Reducer\VersionReducer;
use Liip\Serializer\Configuration\GeneratorConfiguration; use Liip\Serializer\Configuration\GeneratorConfiguration;
use Liip\Serializer\Template\Serialization; use Liip\Serializer\Template\Serialization;
use RetailCrm\Api\Interfaces\Orders\CustomerInterface;
use RetailCrm\Api\Model\Entity\Customers\Customer;
use RetailCrm\Api\Model\Entity\Customers\CustomerTag;
use RetailCrm\Api\Model\Entity\CustomersCorporate\CustomerCorporate;
use RetailCrm\Api\Component\Serializer\Template\CustomSerialization; use RetailCrm\Api\Component\Serializer\Template\CustomSerialization;
use RetailCrm\Api\Component\Serializer\Type\PropertyTypeMixed; use RetailCrm\Api\Component\Serializer\Type\PropertyTypeMixed;
use RetailCrm\Api\Interfaces\Orders\CustomerInterface;
use RetailCrm\Api\Model\Entity\Customers\Customer;
use RetailCrm\Api\Model\Entity\CustomersCorporate\CustomerCorporate;
use RetailCrm\Api\Model\Entity\CustomersCorporate\SerializedRelationAbstractCustomer; use RetailCrm\Api\Model\Entity\CustomersCorporate\SerializedRelationAbstractCustomer;
use RetailCrm\Api\Model\Entity\Orders\SerializedRelationCustomer; use RetailCrm\Api\Model\Entity\Orders\SerializedRelationCustomer;
use RuntimeException;
use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\Filesystem\Filesystem;
/** final class SerializerGenerator
* Class SerializerGenerator
*
* @category SerializerGenerator
* @package RetailCrm\Api\Component\Serializer\Generator
* @license https://github.com/liip/serializer/blob/master/LICENSE MIT License
* @author Liip <https://github.com/liip>
* @author Pavel Kovalenko
* @see https://github.com/liip/serializer
* @internal
*
* @SuppressWarnings(PHPMD)
*/
class SerializerGenerator
{ {
private const FILENAME_PREFIX = 'serialize'; private const FILENAME_PREFIX = 'serialize';
/** private Filesystem $filesystem;
* @var Serialization
*/
private $templating;
/** private Serialization $templating;
* @var \RetailCrm\Api\Component\Serializer\Template\CustomSerialization private GeneratorConfiguration $configuration;
*/ private string $cacheDirectory;
private $customTemplating;
/** private CustomSerialization $customTemplating;
* @var Builder
*/
private $metadataBuilder;
/** private Builder $metadataBuilder;
* @var GeneratorConfiguration<mixed>
*/
private $configuration;
/**
* @var string
*/
private $cacheDirectory;
/**
* @var Filesystem
*/
private $filesystem;
/**
* SerializerGenerator constructor.
*
* @param \Liip\Serializer\Template\Serialization $templating
* @param \RetailCrm\Api\Component\Serializer\Template\CustomSerialization $customTemplating
* @param \Liip\Serializer\Configuration\GeneratorConfiguration<mixed> $configuration
* @param string $cacheDirectory
*/
public function __construct( public function __construct(
Serialization $templating, Serialization $templating,
CustomSerialization $customTemplating, CustomSerialization $customTemplating,
GeneratorConfiguration $configuration, GeneratorConfiguration $configuration,
string $cacheDirectory string $cacheDirectory
) { ) {
$this->templating = $templating; $this->cacheDirectory = $cacheDirectory;
$this->configuration = $configuration;
$this->templating = $templating;
$this->customTemplating = $customTemplating; $this->customTemplating = $customTemplating;
$this->configuration = $configuration;
$this->cacheDirectory = $cacheDirectory;
$this->filesystem = new Filesystem(); $this->filesystem = new Filesystem();
} }
/** /**
* @param string $className * @param list<string> $serializerGroups
* @param string|null $apiVersion
* @param array<mixed> $serializerGroups
*
* @return string
*/ */
public static function buildSerializerFunctionName( public static function buildSerializerFunctionName(string $className, ?string $apiVersion, array $serializerGroups): string
string $className, {
?string $apiVersion, $functionName = self::FILENAME_PREFIX.'_'.$className;
array $serializerGroups if (\count($serializerGroups)) {
): string { $functionName .= '_'.implode('_', $serializerGroups);
$functionName = static::FILENAME_PREFIX . '_' . $className;
if (count($serializerGroups)) {
$functionName .= '_' . implode('_', $serializerGroups);
} }
if (null !== $apiVersion) { 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 public function generate(Builder $metadataBuilder): void
{ {
$this->metadataBuilder = $metadataBuilder; $this->metadataBuilder = $metadataBuilder;
$this->filesystem->mkdir($this->cacheDirectory); $this->filesystem->mkdir($this->cacheDirectory);
foreach ($this->configuration as $classToGenerate) { foreach ($this->configuration as $classToGenerate) {
foreach ($classToGenerate as $groupCombination) { foreach ($classToGenerate as $groupCombination) {
$className = $classToGenerate->getClassName(); $className = $classToGenerate->getClassName();
foreach ($groupCombination->getVersions() as $version) { foreach ($groupCombination->getVersions() as $version) {
$groups = $groupCombination->getGroups();
if ('' === $version) { if ('' === $version) {
$metadata = $metadataBuilder->build($className, [ if ([] === $groups) {
new PreferredReducer(), $metadata = $metadataBuilder->build($className, [
new TakeBestReducer(), new PreferredReducer(),
]); new TakeBestReducer(),
]);
$this->writeFile($className, null, $groupCombination->getGroups(), $metadata); $this->writeFile($className, null, [], $metadata);
} else {
$metadata = $metadataBuilder->build($className, [
new GroupReducer($groups),
new PreferredReducer(),
new TakeBestReducer(),
]);
$this->writeFile($className, null, $groups, $metadata);
}
} else { } else {
$metadata = $metadataBuilder->build($className, [ $metadata = $metadataBuilder->build($className, [
new VersionReducer($version), new VersionReducer($version),
new GroupReducer($groupCombination->getGroups()), new GroupReducer($groups),
new TakeBestReducer(), new TakeBestReducer(),
]); ]);
$this->writeFile($className, $version, $groups, $metadata);
$this->writeFile($className, $version, $groupCombination->getGroups(), $metadata);
} }
} }
} }
@ -168,10 +110,7 @@ class SerializerGenerator
} }
/** /**
* @param string $className * @param list<string> $serializerGroups
* @param string|null $apiVersion
* @param array<mixed> $serializerGroups
* @param \Liip\MetadataParser\Metadata\ClassMetadata $classMetadata
*/ */
private function writeFile( private function writeFile(
string $className, string $className,
@ -179,8 +118,7 @@ class SerializerGenerator
array $serializerGroups, array $serializerGroups,
ClassMetadata $classMetadata ClassMetadata $classMetadata
): void { ): void {
sort($serializerGroups); $functionName = self::buildSerializerFunctionName($className, $apiVersion, $serializerGroups);
$functionName = static::buildSerializerFunctionName($className, $apiVersion, $serializerGroups);
$code = $this->templating->renderFunction( $code = $this->templating->renderFunction(
$functionName, $functionName,
@ -192,15 +130,8 @@ class SerializerGenerator
} }
/** /**
* @param \Liip\MetadataParser\Metadata\ClassMetadata $classMetadata * @param list<string> $serializerGroups
* @param string|null $apiVersion * @param array<string, positive-int> $stack
* @param array<mixed> $serializerGroups
* @param string $arrayPath
* @param string $modelPath
* @param array<mixed> $stack
*
* @return string
* @throws \Exception
*/ */
private function generateCodeForClass( private function generateCodeForClass(
ClassMetadata $classMetadata, ClassMetadata $classMetadata,
@ -221,22 +152,11 @@ class SerializerGenerator
); );
} }
if ($classMetadata->getClassName() === CustomerTag::class) {
return $this->generateForCustomerTag($arrayPath, $modelPath);
}
$stack[$classMetadata->getClassName()] = ($stack[$classMetadata->getClassName()] ?? 0) + 1; $stack[$classMetadata->getClassName()] = ($stack[$classMetadata->getClassName()] ?? 0) + 1;
$code = '';
$code = '';
foreach ($classMetadata->getProperties() as $propertyMetadata) { foreach ($classMetadata->getProperties() as $propertyMetadata) {
$code .= $this->generateCodeForField( $code .= $this->generateCodeForField($propertyMetadata, $apiVersion, $serializerGroups, $arrayPath, $modelPath, $stack);
$propertyMetadata,
$apiVersion,
$serializerGroups,
$arrayPath,
$modelPath,
$stack
);
} }
return $this->templating->renderClass($arrayPath, $code); return $this->templating->renderClass($arrayPath, $code);
@ -323,26 +243,8 @@ class SerializerGenerator
} }
/** /**
* @param string $arrayPath * @param list<string> $serializerGroups
* @param string $modelPath * @param array<string, positive-int> $stack
*
* @return string
*/
private function generateForCustomerTag(string $arrayPath, string $modelPath): string
{
return $this->templating->renderAssign($arrayPath, $modelPath . '->name');
}
/**
* @param \Liip\MetadataParser\Metadata\PropertyMetadata $propertyMetadata
* @param string|null $apiVersion
* @param array<mixed> $serializerGroups
* @param string $arrayPath
* @param string $modelPath
* @param array<mixed> $stack
*
* @return string
* @throws \Exception
*/ */
private function generateCodeForField( private function generateCodeForField(
PropertyMetadata $propertyMetadata, PropertyMetadata $propertyMetadata,
@ -352,61 +254,34 @@ class SerializerGenerator
string $modelPath, string $modelPath,
array $stack array $stack
): string { ): string {
$modelPropertyPath = $modelPath . '->' . $propertyMetadata->getName(); if (Recursion::hasMaxDepthReached($propertyMetadata, $stack)) {
$fieldPath = $arrayPath . '["' . $propertyMetadata->getSerializedName() . '"]'; return '';
}
$modelPropertyPath = $modelPath.'->'.$propertyMetadata->getName();
$fieldPath = $arrayPath.'["'.$propertyMetadata->getSerializedName().'"]';
if ($propertyMetadata->getAccessor()->hasGetterMethod()) { if ($propertyMetadata->getAccessor()->hasGetterMethod()) {
$tempVariable = str_replace(['->', '[', ']', '$'], '', $modelPath) . ucfirst($propertyMetadata->getName()); $tempVariable = str_replace(['->', '[', ']', '$'], '', $modelPath).ucfirst($propertyMetadata->getName());
return $this->templating->renderConditional( return $this->templating->renderConditional(
$this->templating->renderTempVariable( $this->templating->renderTempVariable($tempVariable, $this->templating->renderGetter($modelPath, $propertyMetadata->getAccessor()->getGetterMethod())),
$tempVariable, $this->generateCodeForFieldType($propertyMetadata->getType(), $apiVersion, $serializerGroups, $fieldPath, '$'.$tempVariable, $stack)
$this->templating->renderGetter(
$modelPath,
(string) $propertyMetadata->getAccessor()->getGetterMethod()
)
),
$this->generateCodeForFieldType(
$propertyMetadata->getType(),
$apiVersion,
$serializerGroups,
$fieldPath,
'$' . $tempVariable,
$stack
)
); );
} }
if (!$propertyMetadata->isPublic()) { if (!$propertyMetadata->isPublic()) {
throw new RuntimeException(sprintf( throw new \Exception(sprintf('Property %s is not public and no getter has been defined. Stack %s', $modelPropertyPath, var_export($stack, true)));
'Property %s is not public and no getter has been defined. Stack %s',
$modelPropertyPath,
var_export($stack, true)
));
} }
return $this->templating->renderConditional( return $this->templating->renderConditional(
$modelPropertyPath, $modelPropertyPath,
$this->generateCodeForFieldType( $this->generateCodeForFieldType($propertyMetadata->getType(), $apiVersion, $serializerGroups, $fieldPath, $modelPropertyPath, $stack)
$propertyMetadata->getType(),
$apiVersion,
$serializerGroups,
$fieldPath,
$modelPropertyPath,
$stack
)
); );
} }
/** /**
* @param \Liip\MetadataParser\Metadata\PropertyType $type * @param list<string> $serializerGroups
* @param string|null $apiVersion * @param array<string, positive-int> $stack
* @param array<mixed> $serializerGroups
* @param string $fieldPath
* @param string $modelPropertyPath
* @param array<mixed> $stack
*
* @return string
* @throws \Exception
*/ */
private function generateCodeForFieldType( private function generateCodeForFieldType(
PropertyType $type, PropertyType $type,
@ -416,31 +291,9 @@ class SerializerGenerator
string $modelPropertyPath, string $modelPropertyPath,
array $stack array $stack
): string { ): 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) { switch ($type) {
case $type instanceof PropertyTypeDateTime: case $type instanceof PropertyTypeDateTime:
if (null !== $type->getZone()) { $dateFormat = $type->getFormat() ?: \DateTimeInterface::ISO8601;
throw new \RuntimeException('Timezone support is not implemented');
}
$dateFormat = $type->getFormat() ?: DateTime::ATOM;
return $this->templating->renderAssign( return $this->templating->renderAssign(
$fieldPath, $fieldPath,
@ -454,30 +307,19 @@ class SerializerGenerator
return $this->templating->renderAssign($fieldPath, $modelPropertyPath); return $this->templating->renderAssign($fieldPath, $modelPropertyPath);
case $type instanceof PropertyTypeClass: case $type instanceof PropertyTypeClass:
return $this->generateCodeForClass( return $this->generateCodeForClass($type->getClassMetadata(), $apiVersion, $serializerGroups, $fieldPath, $modelPropertyPath, $stack);
$type->getClassMetadata(),
$apiVersion, case $type instanceof PropertyTypeArray:
$serializerGroups, return $this->generateCodeForArray($type, $apiVersion, $serializerGroups, $fieldPath, $modelPropertyPath, $stack);
$fieldPath,
$modelPropertyPath,
$stack
);
default: 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 list<string> $serializerGroups
* @param string|null $apiVersion * @param array<string, positive-int> $stack
* @param array<mixed> $serializerGroups
* @param string $arrayPath
* @param string $modelPath
* @param array<mixed> $stack
*
* @return string
* @throws \Exception
*/ */
private function generateCodeForArray( private function generateCodeForArray(
PropertyTypeArray $type, PropertyTypeArray $type,
@ -487,35 +329,25 @@ class SerializerGenerator
string $modelPath, string $modelPath,
array $stack array $stack
): string { ): string {
$index = '$index' . \mb_strlen($arrayPath); $index = '$index'.mb_strlen($arrayPath);
$subType = $type->getSubType(); $subType = $type->getSubType();
switch ($subType) { switch ($subType) {
case $subType instanceof PropertyTypeArray: case $subType instanceof PropertyTypePrimitive:
$innerCode = $this->generateCodeForArray( case $subType instanceof PropertyTypeArray && self::isArrayForPrimitive($subType):
$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 PropertyTypeUnknown: 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; break;
case $subType instanceof PropertyTypeClass:
$innerCode = $this->generateCodeForClass($subType->getClassMetadata(), $apiVersion, $serializerGroups, $arrayPath.'['.$index.']', $modelPath.'['.$index.']', $stack);
break;
default: default:
throw new RuntimeException('Unexpected array subtype ' . get_class($subType)); throw new \Exception('Unexpected array subtype '. get_class($subType));
} }
if ('' === $innerCode) { if ('' === $innerCode) {
@ -532,4 +364,16 @@ class SerializerGenerator
return $this->templating->renderLoopArray($arrayPath, $modelPath, $index, $innerCode); 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;
}
} }

View File

@ -1,197 +0,0 @@
<?php
/**
* PHP version 7.3
*
* @category BaseJMSParser
* @package RetailCrm\Api\Component\Serializer\Parser
*/
declare(strict_types=1);
namespace RetailCrm\Api\Component\Serializer\Parser;
use ReflectionClass;
use RetailCrm\Api\Component\Serializer\Exception\SyntaxError;
/**
* Class Parser
*
* @package RetailCrm\Api\Component\Serializer\Parser
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
* @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<string, mixed>
*/
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<int, mixed>
*/
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());
}
}

View File

@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace RetailCrm\Api\Component\Serializer\Parser\JMSCore\Exception;
/**
* Base exception for the Serializer.
*
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
*/
interface Exception extends \Throwable
{
}

View File

@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace RetailCrm\Api\Component\Serializer\Parser\JMSCore\Type\Exception;
use RetailCrm\Api\Component\Serializer\Parser\JMSCore\Exception\Exception as BaseException;
interface Exception extends BaseException
{
}

View File

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace RetailCrm\Api\Component\Serializer\Parser\JMSCore\Type\Exception;
final class InvalidNode extends \LogicException implements Exception
{
}

View File

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace RetailCrm\Api\Component\Serializer\Parser\JMSCore\Type\Exception;
final class SyntaxError extends \RuntimeException implements Exception
{
}

View File

@ -1,24 +1,16 @@
<?php <?php
/**
* PHP version 7.3
*
* @category BaseJMSParser
* @package RetailCrm\Api\Component\Serializer\Parser
*/
declare(strict_types=1); declare(strict_types=1);
namespace RetailCrm\Api\Component\Serializer\Parser; namespace RetailCrm\Api\Component\Serializer\Parser\JMSCore\Type;
use Doctrine\Common\Lexer\AbstractLexer; use Doctrine\Common\Lexer\AbstractLexer;
use RetailCrm\Api\Component\Serializer\Exception\SyntaxError; use RetailCrm\Api\Component\Serializer\Parser\JMSCore\Type\Exception\SyntaxError;
use Throwable;
/** /**
* @internal * @internal
*/ */
final class JMSLexer extends AbstractLexer final class Lexer extends AbstractLexer
{ {
public const T_UNKNOWN = 0; public const T_UNKNOWN = 0;
public const T_INTEGER = 1; public const T_INTEGER = 1;
@ -32,23 +24,15 @@ final class JMSLexer extends AbstractLexer
public const T_IDENTIFIER = 9; public const T_IDENTIFIER = 9;
public const T_NULL = 10; public const T_NULL = 10;
/**
* @param string $type
*
* @return int|string|null
*/
public function parse(string $type) public function parse(string $type)
{ {
try { try {
return $this->getType($type); return $this->getType($type);
} catch (Throwable $e) { } catch (\Throwable $e) {
throw new SyntaxError($e->getMessage(), 0, $e); throw new SyntaxError($e->getMessage(), 0, $e);
} }
} }
/**
* @return string[]
*/
protected function getCatchablePatterns(): array protected function getCatchablePatterns(): array
{ {
return [ return [
@ -63,18 +47,15 @@ final class JMSLexer extends AbstractLexer
]; ];
} }
/**
* @return string[]
*/
protected function getNonCatchablePatterns(): array protected function getNonCatchablePatterns(): array
{ {
return ['\s+']; return ['\s+'];
} }
/** /**
* {{@inheritDoc}} * {@inheritDoc}
* *
* @SuppressWarnings(PHPMD.CyclomaticComplexity) * @return int|string|null
*/ */
protected function getType(&$value) protected function getType(&$value)
{ {

View File

@ -0,0 +1,162 @@
<?php
declare(strict_types=1);
namespace RetailCrm\Api\Component\Serializer\Parser\JMSCore\Type;
use RetailCrm\Api\Component\Serializer\Parser\JMSCore\Type\Exception\SyntaxError;
/**
* @internal
*/
final class Parser implements ParserInterface
{
/**
* @var Lexer
*/
private Lexer $lexer;
/**
* @var bool
*/
private bool $root = true;
public function parse(string $string): array
{
$this->lexer = new Lexer();
$this->lexer->setInput($string);
$this->lexer->moveNext();
return $this->visit();
}
/**
* @return mixed
*/
private function visit()
{
$this->lexer->moveNext();
if (!$this->lexer->token) {
throw new SyntaxError(
'Syntax error, unexpected end of stream',
);
}
if (Lexer::T_FLOAT === $this->lexer->token->type) {
return floatval($this->lexer->token->value);
} elseif (Lexer::T_INTEGER === $this->lexer->token->type) {
return intval($this->lexer->token->value);
} elseif (Lexer::T_NULL === $this->lexer->token->type) {
return null;
} elseif (Lexer::T_STRING === $this->lexer->token->type) {
return $this->lexer->token->value;
} elseif (Lexer::T_IDENTIFIER === $this->lexer->token->type) {
if ($this->lexer->isNextToken(Lexer::T_TYPE_START)) {
return $this->visitCompoundType();
} elseif ($this->lexer->isNextToken(Lexer::T_ARRAY_START)) {
return $this->visitArrayType();
}
return $this->visitSimpleType();
} elseif (!$this->root && Lexer::T_ARRAY_START === $this->lexer->token->type) {
return $this->visitArrayType();
}
throw new SyntaxError(sprintf(
'Syntax error, unexpected "%s" (%s)',
$this->lexer->token->value,
$this->getConstant($this->lexer->token->type),
));
}
/**
* @return string|mixed[]
*/
private function visitSimpleType()
{
$value = $this->lexer->token->value;
return ['name' => $value, 'params' => []];
}
private function visitCompoundType(): array
{
$this->root = false;
$name = $this->lexer->token->value;
$this->match(Lexer::T_TYPE_START);
$params = [];
if (!$this->lexer->isNextToken(Lexer::T_TYPE_END)) {
while (true) {
$params[] = $this->visit();
if ($this->lexer->isNextToken(Lexer::T_TYPE_END)) {
break;
}
$this->match(Lexer::T_COMMA);
}
}
$this->match(Lexer::T_TYPE_END);
return [
'name' => $name,
'params' => $params,
];
}
private function visitArrayType(): array
{
/*
* Here we should call $this->match(Lexer::T_ARRAY_START); to make it clean
* but the token has already been consumed by moveNext() in visit()
*/
$params = [];
if (!$this->lexer->isNextToken(Lexer::T_ARRAY_END)) {
while (true) {
$params[] = $this->visit();
if ($this->lexer->isNextToken(Lexer::T_ARRAY_END)) {
break;
}
$this->match(Lexer::T_COMMA);
}
}
$this->match(Lexer::T_ARRAY_END);
return $params;
}
private function match(int $token): void
{
if (!$this->lexer->lookahead) {
throw new SyntaxError(
sprintf('Syntax error, unexpected end of stream, expected %s', $this->getConstant($token)),
);
}
if ($this->lexer->lookahead->type === $token) {
$this->lexer->moveNext();
return;
}
throw new SyntaxError(sprintf(
'Syntax error, unexpected "%s" (%s), expected was %s',
$this->lexer->lookahead->value,
$this->getConstant($this->lexer->lookahead->type),
$this->getConstant($token),
));
}
private function getConstant(int $value): string
{
$oClass = new \ReflectionClass(Lexer::class);
return array_search($value, $oClass->getConstants());
}
}

View File

@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace RetailCrm\Api\Component\Serializer\Parser\JMSCore\Type;
interface ParserInterface
{
public function parse(string $type): array;
}

View File

@ -1,21 +1,18 @@
<?php <?php
/** declare(strict_types=1);
* PHP version 7.3
*
* @category JMSParser
* @package RetailCrm\Api\Component\Serializer\Parser
*/
namespace RetailCrm\Api\Component\Serializer\Parser; namespace RetailCrm\Api\Component\Serializer\Parser;
use Doctrine\Common\Annotations\AnnotationException; use Doctrine\Common\Annotations\AnnotationException;
use Doctrine\Common\Annotations\Reader; use Doctrine\Common\Annotations\Reader;
use Liip\MetadataParser\ModelParser\ModelParserInterface;
use RetailCrm\Api\Component\Serializer\Annotation\Accessor; use RetailCrm\Api\Component\Serializer\Annotation\Accessor;
use RetailCrm\Api\Component\Serializer\Annotation\AccessorOrder; use RetailCrm\Api\Component\Serializer\Annotation\AccessorOrder;
use RetailCrm\Api\Component\Serializer\Annotation\Exclude; use RetailCrm\Api\Component\Serializer\Annotation\Exclude;
use RetailCrm\Api\Component\Serializer\Annotation\ExclusionPolicy; use RetailCrm\Api\Component\Serializer\Annotation\ExclusionPolicy;
use RetailCrm\Api\Component\Serializer\Annotation\Groups; use RetailCrm\Api\Component\Serializer\Annotation\Groups;
use RetailCrm\Api\Component\Serializer\Annotation\MaxDepth;
use RetailCrm\Api\Component\Serializer\Annotation\PostDeserialize; use RetailCrm\Api\Component\Serializer\Annotation\PostDeserialize;
use RetailCrm\Api\Component\Serializer\Annotation\SerializedName; use RetailCrm\Api\Component\Serializer\Annotation\SerializedName;
use RetailCrm\Api\Component\Serializer\Annotation\Since; use RetailCrm\Api\Component\Serializer\Annotation\Since;
@ -27,54 +24,29 @@ use Liip\MetadataParser\Exception\ParseException;
use Liip\MetadataParser\Metadata\PropertyAccessor; use Liip\MetadataParser\Metadata\PropertyAccessor;
use Liip\MetadataParser\Metadata\PropertyType; use Liip\MetadataParser\Metadata\PropertyType;
use Liip\MetadataParser\Metadata\PropertyTypeUnknown; use Liip\MetadataParser\Metadata\PropertyTypeUnknown;
use Liip\MetadataParser\ModelParser\ModelParserInterface;
use Liip\MetadataParser\ModelParser\RawMetadata\PropertyCollection; use Liip\MetadataParser\ModelParser\RawMetadata\PropertyCollection;
use Liip\MetadataParser\ModelParser\RawMetadata\PropertyVariationMetadata; use Liip\MetadataParser\ModelParser\RawMetadata\PropertyVariationMetadata;
use Liip\MetadataParser\ModelParser\RawMetadata\RawClassMetadata; use Liip\MetadataParser\ModelParser\RawMetadata\RawClassMetadata;
use Liip\MetadataParser\TypeParser\PhpTypeParser; use Liip\MetadataParser\TypeParser\PhpTypeParser;
use ReflectionClass; use RetailCrm\Api\Component\Serializer\Parser\JMSCore\Type\Exception\SyntaxError;
use ReflectionException;
use ReflectionMethod;
use ReflectionProperty;
use UnexpectedValueException;
/** /**
* Class JMSParser * Parse JMSSerializer annotations.
*
* Run this parser *after* the PHPDoc parser as JMS annotations are more precise.
* *
* @category JMSParser
* @package RetailCrm\Api\Component\Serializer\Parser
* @license https://github.com/liip/metadata-parser/blob/master/LICENSE MIT License
* @author Liip <https://github.com/liip>
* @author Pavel Kovalenko
* @see https://github.com/liip/metadata-parser
* @internal * @internal
*
* @SuppressWarnings(PHPMD)
*/ */
class JMSParser implements ModelParserInterface class JMSParser implements ModelParserInterface
{ {
private const ACCESS_ORDER_CUSTOM = 'custom'; private const ACCESS_ORDER_CUSTOM = 'custom';
/** private Reader $annotationsReader;
* @var Reader
*/
private $annotationsReader;
/** private PhpTypeParser $phpTypeParser;
* @var PhpTypeParser
*/
private $phpTypeParser;
/** protected JMSTypeParser $jmsTypeParser;
* @var JMSTypeParser
*/
private $jmsTypeParser;
/**
* JMSParser constructor.
*
* @param \Doctrine\Common\Annotations\Reader $annotationsReader
*/
public function __construct(Reader $annotationsReader) public function __construct(Reader $annotationsReader)
{ {
$this->annotationsReader = $annotationsReader; $this->annotationsReader = $annotationsReader;
@ -82,124 +54,99 @@ class JMSParser implements ModelParserInterface
$this->jmsTypeParser = new JMSTypeParser(); $this->jmsTypeParser = new JMSTypeParser();
} }
/**
* @param \Liip\MetadataParser\ModelParser\RawMetadata\RawClassMetadata $classMetadata
*/
public function parse(RawClassMetadata $classMetadata): void public function parse(RawClassMetadata $classMetadata): void
{ {
try { try {
$refClass = new ReflectionClass($classMetadata->getClassName()); // @phpstan-ignore-line $reflClass = new \ReflectionClass($classMetadata->getClassName());
} catch (ReflectionException $exception) { } catch (\ReflectionException $e) {
throw ParseException::classNotFound($classMetadata->getClassName(), $exception); throw ParseException::classNotFound($classMetadata->getClassName(), $e);
} }
$this->parseProperties($refClass, $classMetadata); try {
$this->parseMethods($refClass, $classMetadata); $this->parseProperties($reflClass, $classMetadata);
$this->parseClass($refClass, $classMetadata); $this->parseMethods($reflClass, $classMetadata);
$this->parseClass($reflClass, $classMetadata);
} catch (SyntaxError $exception) {
throw new ParseException($exception->getMessage(), $exception->getCode(), $exception);
}
} }
/** private function parseProperties(\ReflectionClass $reflClass, RawClassMetadata $classMetadata): void
* @param \ReflectionClass $refClass
* @param \Liip\MetadataParser\ModelParser\RawMetadata\RawClassMetadata $classMetadata
*
* @phpstan-ignore-next-line
*/
private function parseProperties(ReflectionClass $refClass, RawClassMetadata $classMetadata): void
{ {
if ($refParentClass = $refClass->getParentClass()) { if ($reflParentClass = $reflClass->getParentClass()) {
$this->parseProperties($refParentClass, $classMetadata); $this->parseProperties($reflParentClass, $classMetadata);
} }
foreach ($refClass->getProperties() as $refProperty) { foreach ($reflClass->getProperties() as $reflProperty) {
try { try {
$annotations = $this->annotationsReader->getPropertyAnnotations($refProperty); $annotations = $this->annotationsReader->getPropertyAnnotations($reflProperty);
} catch (AnnotationException $exception) { } catch (AnnotationException $e) {
throw ParseException::propertyError((string) $classMetadata, $refProperty->getName(), $exception); 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); $this->parsePropertyAnnotations($classMetadata, $property, $annotations);
} }
} }
/** private function parseMethods(\ReflectionClass $reflClass, RawClassMetadata $classMetadata): void
* @param \ReflectionClass $refClass
* @param \Liip\MetadataParser\ModelParser\RawMetadata\RawClassMetadata $classMetadata
*
* @phpstan-ignore-next-line
*/
private function parseMethods(ReflectionClass $refClass, RawClassMetadata $classMetadata): void
{ {
if ($refParentClass = $refClass->getParentClass()) { if ($reflParentClass = $reflClass->getParentClass()) {
$this->parseMethods($refParentClass, $classMetadata); $this->parseMethods($reflParentClass, $classMetadata);
} }
foreach ($refClass->getMethods() as $refMethod) { foreach ($reflClass->getMethods() as $reflMethod) {
if (false === $refMethod->getDocComment()) {
continue;
}
try { try {
$annotations = $this->annotationsReader->getMethodAnnotations($refMethod); $annotations = $this->annotationsReader->getMethodAnnotations($reflMethod);
} catch (AnnotationException $exception) { } catch (AnnotationException $e) {
throw ParseException::propertyError((string) $classMetadata, $refMethod->getName(), $exception); throw ParseException::propertyError((string) $classMetadata, $reflMethod->getName(), $e);
} }
if ($this->isVirtualProperty($annotations)) { if ($this->isVirtualProperty($annotations)) {
if (!$refMethod->isPublic()) { if (!$reflMethod->isPublic()) {
throw ParseException::nonPublicMethod((string) $classMetadata, $refMethod->getName()); throw ParseException::nonPublicMethod((string) $classMetadata, $reflMethod->getName());
} }
$methodName = $this->getMethodName($annotations, $refMethod); $methodName = $this->getMethodName($annotations, $reflMethod);
$name = $this->getSerializedName($annotations) ?: $methodName; $name = $this->getSerializedName($annotations) ?: $methodName;
$property = new PropertyVariationMetadata($methodName, true, true); $property = new PropertyVariationMetadata($methodName, true, true);
$classMetadata->addPropertyVariation($name, $property); $classMetadata->addPropertyVariation($name, $property);
$property->setType($this->getReturnType($property, $refMethod, $refClass)); $property->setType($this->getReturnType($property, $reflMethod, $reflClass));
$property->setAccessor(new PropertyAccessor($refMethod->getName(), null)); $property->setAccessor(new PropertyAccessor($reflMethod->getName(), null));
$this->parsePropertyAnnotations($classMetadata, $property, $annotations); $this->parsePropertyAnnotations($classMetadata, $property, $annotations);
} }
if ($this->isPostDeserializeMethod($annotations)) { if ($this->isPostDeserializeMethod($annotations)) {
if (!$refMethod->isPublic()) { if (!$reflMethod->isPublic()) {
throw ParseException::nonPublicMethod((string) $classMetadata, $refMethod->getName()); throw ParseException::nonPublicMethod((string) $classMetadata, $reflMethod->getName());
} }
$classMetadata->addPostDeserializeMethod($refMethod->getName()); $classMetadata->addPostDeserializeMethod($reflMethod->getName());
} }
} }
} }
/** private function parseClass(\ReflectionClass $reflClass, RawClassMetadata $classMetadata): void
* @param \ReflectionClass $refClass
* @param \Liip\MetadataParser\ModelParser\RawMetadata\RawClassMetadata $classMetadata
*
* @phpstan-ignore-next-line
*/
private function parseClass(ReflectionClass $refClass, RawClassMetadata $classMetadata): void
{ {
try { try {
$annotations = $this->gatherClassAnnotations($refClass); $annotations = $this->gatherClassAnnotations($reflClass);
} catch (AnnotationException $e) { } catch (AnnotationException $e) {
throw ParseException::classError($refClass->getName(), $e); throw ParseException::classError($reflClass->getName(), $e);
} }
foreach ($annotations as $annotation) { foreach ($annotations as $annotation) {
switch (true) { switch (true) {
case $annotation instanceof AccessorOrder: case $annotation instanceof AccessorOrder:
if (self::ACCESS_ORDER_CUSTOM !== $annotation->order) { if (self::ACCESS_ORDER_CUSTOM !== $annotation->order) {
throw ParseException::unsupportedClassAnnotation( throw ParseException::unsupportedClassAnnotation((string) $classMetadata, 'AccessorOrder::'.$annotation->order);
(string) $classMetadata,
'AccessorOrder::' . $annotation->order
);
} }
// usort is not stable for the same result. we want to preserve order of // usort is not stable for the same result. we want to preserve order of the fields that are not explicitly mentioned
// the fields that are not explicitly mentioned
$order = []; $order = [];
$init = count($annotation->custom); $init = \count($annotation->custom);
foreach ($classMetadata->getPropertyCollections() as $property) { foreach ($classMetadata->getPropertyCollections() as $property) {
$position = $property->getPosition($annotation->custom); $position = $property->getPosition($annotation->custom);
if (null === $position) { if (null === $position) {
@ -208,21 +155,17 @@ class JMSParser implements ModelParserInterface
$order[$property->getSerializedName()] = $position; $order[$property->getSerializedName()] = $position;
} }
$classMetadata->sortProperties(static function ( $classMetadata->sortProperties(static function (PropertyCollection $propA, PropertyCollection $propB) use ($order): int {
PropertyCollection $propA,
PropertyCollection $propB
) use ($order): int {
return $order[$propA->getSerializedName()] <=> $order[$propB->getSerializedName()]; return $order[$propA->getSerializedName()] <=> $order[$propB->getSerializedName()];
}); });
break; break;
case $annotation instanceof ExclusionPolicy: case $annotation instanceof ExclusionPolicy:
if (ExclusionPolicy::NONE !== $annotation->policy) { if (ExclusionPolicy::NONE !== $annotation->policy) {
throw ParseException::unsupportedClassAnnotation( throw ParseException::unsupportedClassAnnotation((string) $classMetadata, 'ExclusionPolicy::'.$annotation->policy);
(string) $classMetadata,
'ExclusionPolicy::' . $annotation->policy
);
} }
break; break;
default: default:
if ( if (
0 === strncmp( 0 === strncmp(
@ -232,10 +175,7 @@ class JMSParser implements ModelParserInterface
) )
) { ) {
// if there are annotations we can safely ignore, we need to explicitly ignore them // if there are annotations we can safely ignore, we need to explicitly ignore them
throw ParseException::unsupportedClassAnnotation( throw ParseException::unsupportedClassAnnotation((string) $classMetadata, \get_class($annotation));
(string) $classMetadata,
get_class($annotation)
);
} }
} }
} }
@ -244,51 +184,36 @@ class JMSParser implements ModelParserInterface
/** /**
* Find the annotations we care about by looking through all ancestors of $reflectionClass. * Find the annotations we care about by looking through all ancestors of $reflectionClass.
* *
* @param \ReflectionClass $reflectionClass
*
* @return object[] Hashmap of annotation class => annotation object * @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 = []; $map = [];
if ($parent = $reflectionClass->getParentClass()) { if ($parent = $reflectionClass->getParentClass()) {
$map = $this->gatherClassAnnotations($parent); $map = $this->gatherClassAnnotations($parent);
} }
$annotations = $this->annotationsReader->getClassAnnotations($reflectionClass); $annotations = $this->annotationsReader->getClassAnnotations($reflectionClass);
foreach ($annotations as $annotation) { foreach ($annotations as $annotation) {
$map[get_class($annotation)] = $annotation; $map[\get_class($annotation)] = $annotation;
} }
return $map; return $map;
} }
/** private function parsePropertyAnnotations(RawClassMetadata $classMetadata, PropertyVariationMetadata $property, array $annotations): void
* @param \Liip\MetadataParser\ModelParser\RawMetadata\RawClassMetadata $classMetadata {
* @param \Liip\MetadataParser\ModelParser\RawMetadata\PropertyVariationMetadata $property
* @param array<mixed> $annotations
*/
private function parsePropertyAnnotations(
RawClassMetadata $classMetadata,
PropertyVariationMetadata $property,
array $annotations
): void {
foreach ($annotations as $annotation) { foreach ($annotations as $annotation) {
switch (true) { switch (true) {
case $annotation instanceof Type: case $annotation instanceof Type:
if (null === $annotation->name) {
throw ParseException::propertyTypeNameNull((string) $classMetadata, (string) $property);
}
try { try {
$type = $this->jmsTypeParser->parse($annotation->name); $type = $this->jmsTypeParser->parse($annotation->name);
} catch (InvalidTypeException $exception) { } catch (InvalidTypeException $e) {
throw ParseException::propertyTypeError( throw ParseException::propertyTypeError((string) $classMetadata, (string) $property, $e);
(string) $classMetadata,
(string) $property,
$exception
);
} }
if ($property->getType() instanceof PropertyTypeUnknown) { if ($property->getType() instanceof PropertyTypeUnknown) {
@ -296,52 +221,49 @@ class JMSParser implements ModelParserInterface
} else { } else {
try { try {
$property->setType($property->getType()->merge($type)); $property->setType($property->getType()->merge($type));
} catch (UnexpectedValueException $exception) { } catch (\UnexpectedValueException $e) {
throw ParseException::propertyTypeConflict( throw ParseException::propertyTypeConflict((string) $classMetadata, (string) $property, (string) $property->getType(), (string) $type, $e);
(string) $classMetadata,
(string) $property,
(string) $property->getType(),
(string) $type,
$exception
);
} }
} }
break; break;
case $annotation instanceof Exclude: case $annotation instanceof Exclude:
if (null !== $annotation->if) { if (null !== $annotation->if) {
throw ParseException::unsupportedPropertyAnnotation( throw ParseException::unsupportedPropertyAnnotation((string) $classMetadata, (string) $property, 'Exclude::if');
(string) $classMetadata,
(string) $property,
'Exclude::if'
);
} }
$classMetadata->removePropertyVariation((string) $property); $classMetadata->removePropertyVariation((string) $property);
break; break;
case $annotation instanceof Groups: case $annotation instanceof Groups:
$property->setGroups($annotation->groups); $property->setGroups($annotation->groups);
break; break;
case $annotation instanceof Accessor: case $annotation instanceof Accessor:
$property->setAccessor(new PropertyAccessor($annotation->getter, $annotation->setter)); $property->setAccessor(new PropertyAccessor($annotation->getter, $annotation->setter));
break; break;
case $annotation instanceof Since: case $annotation instanceof Since:
$property->setVersionRange($property->getVersionRange()->withSince($annotation->version)); $property->setVersionRange($property->getVersionRange()->withSince($annotation->version));
break; break;
case $annotation instanceof Until: case $annotation instanceof Until:
$property->setVersionRange($property->getVersionRange()->withUntil($annotation->version)); $property->setVersionRange($property->getVersionRange()->withUntil($annotation->version));
break; break;
case $annotation instanceof SerializedName:
// we handle this separately case $annotation instanceof MaxDepth:
$property->setMaxDepth($annotation->depth);
break;
case $annotation instanceof VirtualProperty: case $annotation instanceof VirtualProperty:
// we handle this separately // we handle this separately
case $annotation instanceof SerializedName:
// we handle this separately
break; break;
default: 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 // if there are annotations we can safely ignore, we need to explicitly ignore them
throw ParseException::unsupportedPropertyAnnotation( throw ParseException::unsupportedPropertyAnnotation((string) $classMetadata, (string) $property, \get_class($annotation));
(string) $classMetadata,
(string) $property,
get_class($annotation)
);
} }
break; break;
} }
@ -352,96 +274,38 @@ class JMSParser implements ModelParserInterface
* Returns the property metadata for the specified property. * Returns the property metadata for the specified property.
* *
* If the property already exists on the class metadata this is returned. * 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, * If the property has a serialized name that overrides the name of an existing property, it will be renamed and merged.
* it will be renamed and merged.
*
* @param \Liip\MetadataParser\ModelParser\RawMetadata\RawClassMetadata $classMetadata
* @param \ReflectionProperty $refProperty
* @param array<mixed> $annotations
*
* @return \Liip\MetadataParser\ModelParser\RawMetadata\PropertyVariationMetadata
* @throws \ReflectionException
*/ */
private function getProperty( private function getProperty(RawClassMetadata $classMetadata, \ReflectionProperty $reflProperty, array $annotations): PropertyVariationMetadata
RawClassMetadata $classMetadata, {
ReflectionProperty $refProperty, $defaultName = PropertyCollection::serializedName($reflProperty->getName());
array $annotations
): PropertyVariationMetadata {
$defaultName = PropertyCollection::serializedName($refProperty->getName());
$name = $this->getSerializedName($annotations) ?: $defaultName; $name = $this->getSerializedName($annotations) ?: $defaultName;
if ($classMetadata->hasPropertyVariation($reflProperty->getName())) {
if ($classMetadata->hasPropertyVariation($refProperty->getName())) { $property = $classMetadata->getPropertyVariation($reflProperty->getName());
$property = $classMetadata->getPropertyVariation($refProperty->getName());
if ($defaultName !== $name && $classMetadata->hasPropertyCollection($defaultName)) { if ($defaultName !== $name && $classMetadata->hasPropertyCollection($defaultName)) {
$classMetadata->removePropertyVariation($defaultName); $classMetadata->renameProperty($defaultName, $name);
$this->addPropertyVariation($defaultName, $name, $property, $classMetadata);
} }
} else { } else {
$property = PropertyVariationMetadata::fromReflection($refProperty); $property = PropertyVariationMetadata::fromReflection($reflProperty);
$this->addPropertyVariation($defaultName, $name, $property, $classMetadata); $classMetadata->addPropertyVariation($name, $property);
} }
return $property; return $property;
} }
/** private function getReturnType(PropertyVariationMetadata $property, \ReflectionMethod $reflMethod, \ReflectionClass $reflClass): PropertyType
* 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 {
$type = new PropertyTypeUnknown(true); $type = new PropertyTypeUnknown(true);
$refType = $refMethod->getReturnType();
if (null !== $refType) { $reflType = $reflMethod->getReturnType();
$type = $this->phpTypeParser->parseReflectionType($refType); if (null !== $reflType) {
$type = $this->phpTypeParser->parseReflectionType($reflType);
} }
try { try {
$docBlockType = $this->getReturnTypeOfMethod($refMethod, $refClass); $docBlockType = $this->getReturnTypeOfMethod($reflMethod);
} catch (InvalidTypeException $exception) { } catch (InvalidTypeException $e) {
throw ParseException::propertyTypeError($refClass->getName(), (string) $property, $exception); throw ParseException::propertyTypeError($reflClass->getName(), (string) $property, $e);
} }
if (null === $docBlockType) { if (null === $docBlockType) {
@ -450,47 +314,27 @@ class JMSParser implements ModelParserInterface
try { try {
return $type->merge($docBlockType); return $type->merge($docBlockType);
} catch (UnexpectedValueException $exception) { } catch (\UnexpectedValueException $e) {
throw ParseException::propertyTypeConflict( throw ParseException::propertyTypeConflict($reflClass->getName(), (string) $property, (string) $type, (string) $docBlockType, $e);
$refClass->getName(),
(string) $property,
(string) $type,
(string) $docBlockType,
$exception
);
} }
} }
/** private function getReturnTypeOfMethod(\ReflectionMethod $reflMethod): ?PropertyType
* @param \ReflectionMethod $refMethod
* @param \ReflectionClass $refClass
*
* @return \Liip\MetadataParser\Metadata\PropertyType|null
*
* @phpstan-ignore-next-line
*/
private function getReturnTypeOfMethod(ReflectionMethod $refMethod, ReflectionClass $refClass): ?PropertyType
{ {
$docComment = $refMethod->getDocComment(); $docComment = $reflMethod->getDocComment();
if (false === $docComment) { if (false === $docComment) {
return null; return null;
} }
foreach (explode("\n", $docComment) as $line) { foreach (explode("\n", $docComment) as $line) {
if (1 === preg_match('/@return ([^ ]+)/', $line, $matches)) { if (1 === preg_match('/@return ([^ ]+)/', $line, $matches)) {
return $this->phpTypeParser->parseAnnotationType($matches[1], $refClass); return $this->phpTypeParser->parseAnnotationType($matches[1], $reflMethod->getDeclaringClass());
} }
} }
return null; return null;
} }
/**
* @param array<mixed> $annotations
*
* @return string|null
*/
private function getSerializedName(array $annotations): ?string private function getSerializedName(array $annotations): ?string
{ {
foreach ($annotations as $annotation) { foreach ($annotations as $annotation) {
@ -502,11 +346,6 @@ class JMSParser implements ModelParserInterface
return null; return null;
} }
/**
* @param array<mixed> $annotations
*
* @return bool
*/
private function isVirtualProperty(array $annotations): bool private function isVirtualProperty(array $annotations): bool
{ {
foreach ($annotations as $annotation) { foreach ($annotations as $annotation) {
@ -518,11 +357,6 @@ class JMSParser implements ModelParserInterface
return false; return false;
} }
/**
* @param array<mixed> $annotations
*
* @return bool
*/
private function isPostDeserializeMethod(array $annotations): bool private function isPostDeserializeMethod(array $annotations): bool
{ {
foreach ($annotations as $annotation) { foreach ($annotations as $annotation) {
@ -534,16 +368,9 @@ class JMSParser implements ModelParserInterface
return false; return false;
} }
/** private function getMethodName(array $annotations, \ReflectionMethod $reflMethod): string
* @param array<mixed> $annotations
* @param \ReflectionMethod $refMethod
*
* @return string
*/
private function getMethodName(array $annotations, ReflectionMethod $refMethod): string
{ {
$name = $refMethod->getName(); $name = $reflMethod->getName();
foreach ($annotations as $annotation) { foreach ($annotations as $annotation) {
if ($annotation instanceof VirtualProperty && null !== $annotation->name) { if ($annotation instanceof VirtualProperty && null !== $annotation->name) {
$name = $annotation->name; $name = $annotation->name;

View File

@ -1,59 +1,38 @@
<?php <?php
/** declare(strict_types=1);
* PHP version 7.3
*
* @category JMSTypeParser
* @package RetailCrm\Api\Component\Serializer\Parser
*/
namespace RetailCrm\Api\Component\Serializer\Parser; namespace RetailCrm\Api\Component\Serializer\Parser;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Liip\MetadataParser\Exception\InvalidTypeException; use Liip\MetadataParser\Exception\InvalidTypeException;
use Liip\MetadataParser\Metadata\DateTimeOptions; use Liip\MetadataParser\Metadata\DateTimeOptions;
use Liip\MetadataParser\Metadata\PropertyType; use Liip\MetadataParser\Metadata\PropertyType;
use Liip\MetadataParser\Metadata\PropertyTypeArray;
use Liip\MetadataParser\Metadata\PropertyTypeClass; use Liip\MetadataParser\Metadata\PropertyTypeClass;
use Liip\MetadataParser\Metadata\PropertyTypeDateTime; use Liip\MetadataParser\Metadata\PropertyTypeDateTime;
use Liip\MetadataParser\Metadata\PropertyTypeIterable;
use Liip\MetadataParser\Metadata\PropertyTypePrimitive; use Liip\MetadataParser\Metadata\PropertyTypePrimitive;
use Liip\MetadataParser\Metadata\PropertyTypeUnknown; use Liip\MetadataParser\Metadata\PropertyTypeUnknown;
use RetailCrm\Api\Component\Serializer\Parser\JMSCore\Type\Parser;
use RetailCrm\Api\Component\Serializer\Type\PropertyTypeMixed; use RetailCrm\Api\Component\Serializer\Type\PropertyTypeMixed;
/** final class JMSTypeParser
* Class JMSTypeParser
*
* @category JMSTypeParser
* @package RetailCrm\Api\Component\Serializer\Parser
* @license https://github.com/liip/metadata-parser/blob/master/LICENSE MIT License
* @author Liip <https://github.com/liip>
* @author Pavel Kovalenko
* @see https://github.com/liip/metadata-parser
* @internal
*
* @SuppressWarnings(PHPMD)
*/
class JMSTypeParser
{ {
private const TYPE_ARRAY = 'array'; private const TYPE_ARRAY = 'array';
private const TYPE_ARRAY_COLLECTION = 'ArrayCollection';
private const TYPE_DATETIME_INTERFACE = 'DateTimeInterface';
/** /**
* @var \RetailCrm\Api\Component\Serializer\Parser\BaseJMSParser * @var Parser
*/ */
private $jmsTypeParser; private Parser $jmsTypeParser;
/**
* JMSTypeParser constructor.
*/
public function __construct() public function __construct()
{ {
$this->jmsTypeParser = new BaseJMSParser(); $this->jmsTypeParser = new Parser();
} }
/**
* @param string $rawType
*
* @return \Liip\MetadataParser\Metadata\PropertyType
*/
public function parse(string $rawType): PropertyType public function parse(string $rawType): PropertyType
{ {
if ('' === $rawType) { if ('' === $rawType) {
@ -63,12 +42,6 @@ class JMSTypeParser
return $this->parseType($this->jmsTypeParser->parse($rawType)); return $this->parseType($this->jmsTypeParser->parse($rawType));
} }
/**
* @param array<mixed> $typeInfo
* @param bool $isSubType
*
* @return \Liip\MetadataParser\Metadata\PropertyType
*/
private function parseType(array $typeInfo, bool $isSubType = false): PropertyType private function parseType(array $typeInfo, bool $isSubType = false): PropertyType
{ {
$typeInfo = array_merge( $typeInfo = array_merge(
@ -84,13 +57,12 @@ class JMSTypeParser
if (0 === \count($typeInfo['params'])) { if (0 === \count($typeInfo['params'])) {
if (self::TYPE_ARRAY === $typeInfo['name']) { if (self::TYPE_ARRAY === $typeInfo['name']) {
return new PropertyTypeArray(new PropertyTypeUnknown(false), false, $nullable); return new PropertyTypeIterable(new PropertyTypeUnknown(false), false, $nullable);
} }
if (PropertyTypePrimitive::isTypePrimitive($typeInfo['name'])) { if (PropertyTypePrimitive::isTypePrimitive($typeInfo['name'])) {
return new PropertyTypePrimitive($typeInfo['name'], $nullable); return new PropertyTypePrimitive($typeInfo['name'], $nullable);
} }
if (PropertyTypeDateTime::isTypeDateTime($typeInfo['name'])) { if (PropertyTypeDateTime::isTypeDateTime($typeInfo['name'])) {
return PropertyTypeDateTime::fromDateTimeClass($typeInfo['name'], $nullable); return PropertyTypeDateTime::fromDateTimeClass($typeInfo['name'], $nullable);
} }
@ -102,41 +74,49 @@ class JMSTypeParser
return new PropertyTypeClass($typeInfo['name'], $nullable); 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'])) { if (1 === \count($typeInfo['params'])) {
return new PropertyTypeArray( return new PropertyTypeIterable($this->parseType($typeInfo['params'][0], true), false, $nullable, $collectionClass);
$this->parseType($typeInfo['params'][0], true),
false,
$nullable
);
} }
if (2 === \count($typeInfo['params'])) { if (2 === \count($typeInfo['params'])) {
return new PropertyTypeArray( return new PropertyTypeIterable($this->parseType($typeInfo['params'][1], true), true, $nullable, $collectionClass);
$this->parseType($typeInfo['params'][1], true),
true,
$nullable
);
} }
throw new InvalidTypeException(sprintf( throw new InvalidTypeException(sprintf('JMS property type array can\'t have more than 2 parameters (%s)', var_export($typeInfo, true)));
'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 // 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( return PropertyTypeDateTime::fromDateTimeClass(
$typeInfo['name'], $className,
$nullable, $nullable,
new DateTimeOptions( new DateTimeOptions(
$typeInfo['params'][0] ?: null, $serializeFormat,
($typeInfo['params'][1] ?? null) ?: null, ($typeInfo['params'][1] ?? null) ?: null,
($typeInfo['params'][2] ?? null) ?: null $deserializeFormats,
) )
); );
} }
throw new InvalidTypeException(sprintf('Unknown JMS property found (%s)', var_export($typeInfo, true))); 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;
}
}
} }