From 550230cb36147085c4520b12f54c918d5320ecff Mon Sep 17 00:00:00 2001 From: andrey1s Date: Mon, 29 Jan 2018 10:11:20 +0300 Subject: [PATCH 1/3] add guess type by property info --- Mapping/Guess/Guess.php | 97 +++++++++++++++++++ Mapping/Guess/GuessInterface.php | 51 ++++++++++ Mapping/Guess/Type/ChainTypeGuesser.php | 48 +++++++++ Mapping/Guess/Type/PropertyTypeGuesser.php | 89 +++++++++++++++++ Mapping/Guess/Type/TypeGuess.php | 43 ++++++++ .../Guess/TypeGuessInterface.php | 8 +- Mapping/Guess/TypeGuesserInterface.php | 24 +++++ composer.json | 4 +- 8 files changed, 361 insertions(+), 3 deletions(-) create mode 100644 Mapping/Guess/Guess.php create mode 100644 Mapping/Guess/GuessInterface.php create mode 100644 Mapping/Guess/Type/ChainTypeGuesser.php create mode 100644 Mapping/Guess/Type/PropertyTypeGuesser.php create mode 100644 Mapping/Guess/Type/TypeGuess.php rename Guess/GuessInterface.php => Mapping/Guess/TypeGuessInterface.php (58%) create mode 100644 Mapping/Guess/TypeGuesserInterface.php diff --git a/Mapping/Guess/Guess.php b/Mapping/Guess/Guess.php new file mode 100644 index 0000000..7b903a3 --- /dev/null +++ b/Mapping/Guess/Guess.php @@ -0,0 +1,97 @@ +confidence = $confidence; + $this->options = $options; + $this->name = $name; + } + + /** + * {@inheritdoc} + */ + public function getName(): string + { + return $this->name; + } + + /** + * {@inheritdoc} + */ + public function getOptions(): array + { + return $this->options; + } + + /** + * Returns the confidence that the guessed value is correct. + * + * @return int One of the constants VERY_HIGH_CONFIDENCE, HIGH_CONFIDENCE, + * MEDIUM_CONFIDENCE and LOW_CONFIDENCE + */ + public function getConfidence(): int + { + return $this->confidence; + } + + /** + * Returns the guess most likely to be correct from a list of guesses. + * + * If there are multiple guesses with the same, highest confidence, the + * returned guess is any of them. + * + * @param GuessInterface[]|TypeGuessInterface[] $guesses An array of guesses + * + * @return GuessInterface|TypeGuessInterface|null + */ + public static function getBestGuess(array $guesses) + { + $result = null; + $maxConfidence = -1; + + foreach ($guesses as $guess) { + if ($maxConfidence < $confidence = $guess->getConfidence()) { + $maxConfidence = $confidence; + $result = $guess; + } + } + + return $result; + } +} diff --git a/Mapping/Guess/GuessInterface.php b/Mapping/Guess/GuessInterface.php new file mode 100644 index 0000000..ec4ad3e --- /dev/null +++ b/Mapping/Guess/GuessInterface.php @@ -0,0 +1,51 @@ +guesser = $guesser; + } + + /** + * {@inheritdoc} + */ + public function guessType(string $class, string $property, array $context = []): ?TypeGuessInterface + { + $guesses = []; + foreach ($this->guesser as $item) { + $guess = $item->guessType($class, $property, $context); + if (null !== $guess) { + $guesses[] = $guess; + } + } + + return Guess::getBestGuess($guesses); + } +} diff --git a/Mapping/Guess/Type/PropertyTypeGuesser.php b/Mapping/Guess/Type/PropertyTypeGuesser.php new file mode 100644 index 0000000..975a568 --- /dev/null +++ b/Mapping/Guess/Type/PropertyTypeGuesser.php @@ -0,0 +1,89 @@ + BooleanType::class, + Type::BUILTIN_TYPE_FLOAT => FloatType::class, + Type::BUILTIN_TYPE_INT => IntegerType::class, + Type::BUILTIN_TYPE_STRING => StringType::class, + ]; + + /** + * PropertyTypeGuesser constructor. + * + * @param PropertyTypeExtractorInterface $extractor + * @param array $map + */ + public function __construct(PropertyTypeExtractorInterface $extractor, array $map = []) + { + $this->extractor = $extractor; + if (!empty($map)) { + $this->map = $map; + } + } + + /** + * {@inheritdoc} + */ + public function guessType(string $class, string $property, array $context = []): ?TypeGuessInterface + { + $types = $this->extractor->getTypes($class, $property, $context); + $guess = []; + if (null !== $types) { + foreach ($types as $type) { + $builtinType = $type->getBuiltinType(); + if (isset($this->map[$builtinType])) { + $guess[] = new TypeGuess($this->map[$builtinType], [], $type->isNullable(), GuessInterface::HIGH_CONFIDENCE); + } elseif (Type::BUILTIN_TYPE_OBJECT === $builtinType) { + $guess[] = new TypeGuess(ObjectType::class, [ + 'data_class' => $type->getClassName(), + ], $type->isNullable(), GuessInterface::HIGH_CONFIDENCE); + } elseif ($type->isCollection() && null !== ($collectionValueType = $type->getCollectionValueType())) { + $collectionType = $collectionValueType->getBuiltinType(); + if (isset($this->map[$collectionType])) { + $guess[] = new TypeGuess(ArrayType::class, [ + 'type' => $this->map[$collectionType], + ], $type->isNullable(), GuessInterface::HIGH_CONFIDENCE); + } elseif (Type::BUILTIN_TYPE_OBJECT === $collectionType) { + $guess[] = new TypeGuess(CollectionType::class, [ + 'data_class' => $collectionValueType->getClassName(), + ], $type->isNullable(), GuessInterface::HIGH_CONFIDENCE); + } + } + } + } + + return Guess::getBestGuess($guess); + } +} diff --git a/Mapping/Guess/Type/TypeGuess.php b/Mapping/Guess/Type/TypeGuess.php new file mode 100644 index 0000000..4501f7f --- /dev/null +++ b/Mapping/Guess/Type/TypeGuess.php @@ -0,0 +1,43 @@ +nullable = $nullable; + } + + /** + * @return bool + */ + public function isNullable(): bool + { + return $this->nullable; + } +} diff --git a/Guess/GuessInterface.php b/Mapping/Guess/TypeGuessInterface.php similarity index 58% rename from Guess/GuessInterface.php rename to Mapping/Guess/TypeGuessInterface.php index 40898d4..98665c2 100644 --- a/Guess/GuessInterface.php +++ b/Mapping/Guess/TypeGuessInterface.php @@ -7,8 +7,12 @@ * file that was distributed with this source code. */ -namespace FDevs\Serializer\Guess; +namespace FDevs\Serializer\Mapping\Guess; -class GuessInterface +interface TypeGuessInterface { + /** + * @return bool + */ + public function isNullable(): bool; } diff --git a/Mapping/Guess/TypeGuesserInterface.php b/Mapping/Guess/TypeGuesserInterface.php new file mode 100644 index 0000000..3a815db --- /dev/null +++ b/Mapping/Guess/TypeGuesserInterface.php @@ -0,0 +1,24 @@ + Date: Tue, 30 Jan 2018 10:26:37 +0300 Subject: [PATCH 2/3] add ClassMetadataFactory --- Mapping/Factory/ClassMetadataFactory.php | 61 ++++++++++++++ Mapping/Factory/KeyResolverTrait.php | 16 ++++ Mapping/Factory/MetadataFactory.php | 7 ++ Mapping/Factory/PropertyMetadataFactory.php | 82 +++++++++++++++++++ .../PropertyMetadataFactoryInterface.php | 34 ++++++++ Mapping/Guess/GuessInterface.php | 8 +- Mapping/Guess/Type/ChainTypeGuesser.php | 15 +++- Mapping/Guess/TypeGuessInterface.php | 2 +- Mapping/PropertyMetadata.php | 10 +++ composer.json | 5 +- 10 files changed, 228 insertions(+), 12 deletions(-) create mode 100644 Mapping/Factory/ClassMetadataFactory.php create mode 100644 Mapping/Factory/KeyResolverTrait.php create mode 100644 Mapping/Factory/PropertyMetadataFactory.php create mode 100644 Mapping/Factory/PropertyMetadataFactoryInterface.php diff --git a/Mapping/Factory/ClassMetadataFactory.php b/Mapping/Factory/ClassMetadataFactory.php new file mode 100644 index 0000000..d8316b2 --- /dev/null +++ b/Mapping/Factory/ClassMetadataFactory.php @@ -0,0 +1,61 @@ +getKeyPrefix($value, $context); + if (empty($this->metas[$key])) { + $class = $this->getClass($value); + $meta = new ClassMetadata($class); + $properties = $this->getProperties($value, $context); + foreach ($properties as $property) { + if ($this->propertyMetadataFactory->hasMetadataFor($value, $property, $context)) { + $meta->addPropertyMetadata($this->propertyMetadataFactory->getMetadataFor($value, $property, $context)); + } + } + $this->metas[$key] = $meta; + } + + return $this->metas[$key]; + } + + /** + * {@inheritdoc} + */ + public function hasMetadataFor($value, array $context = []): bool + { + return (is_object($value) || is_string($value)) && null !== $this->getProperties($value, $context); + } + + private function getProperties($value, array $context = []): ?array + { + return $this->propertyListExtractor->getProperties($this->getClass($value), $context); + } +} \ No newline at end of file diff --git a/Mapping/Factory/KeyResolverTrait.php b/Mapping/Factory/KeyResolverTrait.php new file mode 100644 index 0000000..3ecc392 --- /dev/null +++ b/Mapping/Factory/KeyResolverTrait.php @@ -0,0 +1,16 @@ +typeGuesser = $typeGuesser; + $this->propertyAccessExtractor = $propertyAccessExtractor; + } + + /** + * @inheritDoc + */ + public function getMetadataFor($value, string $propertyName, array $context = []): PropertyMetadataInterface + { + $meta = new PropertyMetadata($propertyName); + $type = $this->guessType($value, $propertyName, $context); + $meta + ->setType(new Metadata($type->getName(), $type->getOptions())) + ->setNullable($type->isNullable());; + + return $meta; + } + + /** + * @inheritDoc + */ + public function hasMetadataFor($value, string $propertyName, array $context = []): bool + { + return (is_object($value) || is_string($value)) + && $this->propertyAccessExtractor->isReadable($this->getClass($value), $propertyName, $context) + && null !== $this->guessType($value, $propertyName, $context); + } + + /** + * @param object|string $value + * @param string $propertyName + * @param array $context + * @return TypeGuessInterface|null + */ + private function guessType($value, string $propertyName, array $context) + { + $key = $this->getKeyPrefix($value, $context) . '_' . $propertyName; + if (empty($this->types[$key])) { + $this->types[$key] = $this->typeGuesser->guessType($this->getClass($value), $propertyName, $context); + } + + return $this->types[$key]; + } +} \ No newline at end of file diff --git a/Mapping/Factory/PropertyMetadataFactoryInterface.php b/Mapping/Factory/PropertyMetadataFactoryInterface.php new file mode 100644 index 0000000..2a1d4cc --- /dev/null +++ b/Mapping/Factory/PropertyMetadataFactoryInterface.php @@ -0,0 +1,34 @@ +guesser as $item) { $guess = $item->guessType($class, $property, $context); - if (null !== $guess) { - $guesses[] = $guess; + if (null !== $guess && $maxConfidence < $confidence = $guess->getConfidence()) { + $maxConfidence = $confidence; + $result = $guess; + if ($confidence >= GuessInterface::VERY_HIGH_CONFIDENCE) { + break; + } } } - return Guess::getBestGuess($guesses); + return $result; } } diff --git a/Mapping/Guess/TypeGuessInterface.php b/Mapping/Guess/TypeGuessInterface.php index 98665c2..bcd4daa 100644 --- a/Mapping/Guess/TypeGuessInterface.php +++ b/Mapping/Guess/TypeGuessInterface.php @@ -9,7 +9,7 @@ namespace FDevs\Serializer\Mapping\Guess; -interface TypeGuessInterface +interface TypeGuessInterface extends GuessInterface { /** * @return bool diff --git a/Mapping/PropertyMetadata.php b/Mapping/PropertyMetadata.php index 7f9a2d9..91c82a1 100644 --- a/Mapping/PropertyMetadata.php +++ b/Mapping/PropertyMetadata.php @@ -85,6 +85,16 @@ public function isNullable(): bool return $this->nullable; } + /** + * @param bool $nullable + * @return PropertyMetadata + */ + public function setNullable(bool $nullable): self + { + $this->nullable = $nullable; + return $this; + } + /** * @param MetadataInterface $visibility * diff --git a/composer.json b/composer.json index fd5c834..a41dbd2 100644 --- a/composer.json +++ b/composer.json @@ -22,17 +22,16 @@ "symfony/config": "~4.0", "symfony/options-resolver": "^4.0", "symfony/property-access": "~4.0", + "symfony/property-info": "^4.0", "symfony/serializer": "~4.0" }, "require-dev": { "composer/semver": "~1.4", - "symfony/property-info": "^4.0", "friendsofphp/php-cs-fixer": "^2.10", "phpunit/phpunit": "^6.5" }, "suggest": { - "composer/semver": "use with visible versions", - "symfony/property-info": "use guess property info" + "composer/semver": "use with visible versions" }, "config":{ "bin-dir": "bin", From 07dcf77ed5db9f267d7e1b4dbd04d121a2032e5d Mon Sep 17 00:00:00 2001 From: andrey1s Date: Wed, 31 Jan 2018 10:18:06 +0300 Subject: [PATCH 3/3] add guesser accessor and name converter --- Exception/NoSuchAccessorException.php | 59 +++++++++++++ Mapping/Factory/ClassMetadataFactory.php | 9 +- Mapping/Factory/KeyResolverTrait.php | 12 ++- Mapping/Factory/MetadataFactory.php | 4 +- Mapping/Factory/PropertyMetadataFactory.php | 82 ++++++++++++++++--- .../PropertyMetadataFactoryInterface.php | 18 ++-- .../Guess/Accessor/ChainAccessorGuesser.php | 53 ++++++++++++ Mapping/Guess/AccessorGuesserInterface.php | 24 ++++++ Mapping/Guess/Type/ChainTypeGuesser.php | 1 - Mapping/NameConverter/ChainConverter.php | 42 ++++++++++ Mapping/NameConverterInterface.php | 23 ++++++ Mapping/PropertyMetadata.php | 2 + Mapping/PropertyMetadataInterface.php | 2 + 13 files changed, 308 insertions(+), 23 deletions(-) create mode 100644 Exception/NoSuchAccessorException.php create mode 100644 Mapping/Guess/Accessor/ChainAccessorGuesser.php create mode 100644 Mapping/Guess/AccessorGuesserInterface.php create mode 100644 Mapping/NameConverter/ChainConverter.php create mode 100644 Mapping/NameConverterInterface.php diff --git a/Exception/NoSuchAccessorException.php b/Exception/NoSuchAccessorException.php new file mode 100644 index 0000000..1df2d3f --- /dev/null +++ b/Exception/NoSuchAccessorException.php @@ -0,0 +1,59 @@ +className; + } + + /** + * @return string + */ + public function getPropertyName(): string + { + return $this->propertyName; + } + + /** + * @return array + */ + public function getContext(): array + { + return $this->context; + } +} diff --git a/Mapping/Factory/ClassMetadataFactory.php b/Mapping/Factory/ClassMetadataFactory.php index d8316b2..611de74 100644 --- a/Mapping/Factory/ClassMetadataFactory.php +++ b/Mapping/Factory/ClassMetadataFactory.php @@ -1,5 +1,12 @@ propertyListExtractor->getProperties($this->getClass($value), $context); } -} \ No newline at end of file +} diff --git a/Mapping/Factory/KeyResolverTrait.php b/Mapping/Factory/KeyResolverTrait.php index 3ecc392..ce16ee5 100644 --- a/Mapping/Factory/KeyResolverTrait.php +++ b/Mapping/Factory/KeyResolverTrait.php @@ -1,16 +1,24 @@ typeGuesser = $typeGuesser; $this->propertyAccessExtractor = $propertyAccessExtractor; + $this->nameConverter = $nameConverter; + $this->accessorGuesser = $accessorGuesser; } /** - * @inheritDoc + * {@inheritdoc} */ public function getMetadataFor($value, string $propertyName, array $context = []): PropertyMetadataInterface { - $meta = new PropertyMetadata($propertyName); + $meta = new PropertyMetadata($this->nameConverter->convert($value, $propertyName, $context)); $type = $this->guessType($value, $propertyName, $context); + $accessor = $this->guessAccessor($value, $propertyName, $context); $meta ->setType(new Metadata($type->getName(), $type->getOptions())) - ->setNullable($type->isNullable());; + ->setNullable($type->isNullable()) + ->setAccessor(new Metadata($accessor->getName(), $accessor->getOptions())); return $meta; } /** - * @inheritDoc + * {@inheritdoc} */ public function hasMetadataFor($value, string $propertyName, array $context = []): bool { @@ -66,17 +102,41 @@ public function hasMetadataFor($value, string $propertyName, array $context = [] /** * @param object|string $value - * @param string $propertyName - * @param array $context + * @param string $propertyName + * @param array $context + * * @return TypeGuessInterface|null */ private function guessType($value, string $propertyName, array $context) { - $key = $this->getKeyPrefix($value, $context) . '_' . $propertyName; + $key = $this->getKeyPrefix($value, $context).'_'.$propertyName; if (empty($this->types[$key])) { $this->types[$key] = $this->typeGuesser->guessType($this->getClass($value), $propertyName, $context); } return $this->types[$key]; } -} \ No newline at end of file + + /** + * @param object|string $value + * @param string $propertyName + * @param array $context + * + * @throws NoSuchAccessorException + * + * @return GuessInterface + */ + private function guessAccessor($value, string $propertyName, array $context) + { + $key = $this->getKeyPrefix($value, $context).'_'.$propertyName; + if (empty($this->accessors[$key])) { + $className = $this->getClass($value); + $this->accessors[$key] = $this->accessorGuesser->guessAccessor($className, $propertyName, $context); + if (null === $this->accessors[$key]) { + throw new NoSuchAccessorException($className, $propertyName, $context); + } + } + + return $this->accessors[$key]; + } +} diff --git a/Mapping/Factory/PropertyMetadataFactoryInterface.php b/Mapping/Factory/PropertyMetadataFactoryInterface.php index 2a1d4cc..218877b 100644 --- a/Mapping/Factory/PropertyMetadataFactoryInterface.php +++ b/Mapping/Factory/PropertyMetadataFactoryInterface.php @@ -1,5 +1,12 @@ accessors = $accessors; + } + + /** + * {@inheritdoc} + */ + public function guessAccessor(string $class, string $property, array $context = []): ?GuessInterface + { + $result = null; + $maxConfidence = -1; + + foreach ($this->accessors as $accessor) { + $guess = $accessor->guessAccessor($class, $property, $context); + if (null !== $guess && $maxConfidence < $confidence = $guess->getConfidence()) { + $maxConfidence = $confidence; + $result = $guess; + if ($confidence >= GuessInterface::VERY_HIGH_CONFIDENCE) { + break; + } + } + } + + return $result; + } +} diff --git a/Mapping/Guess/AccessorGuesserInterface.php b/Mapping/Guess/AccessorGuesserInterface.php new file mode 100644 index 0000000..d9ecf00 --- /dev/null +++ b/Mapping/Guess/AccessorGuesserInterface.php @@ -0,0 +1,24 @@ +converters = $converters; + } + + /** + * {@inheritdoc} + */ + public function convert($value, string $propertyName, array $context = []): string + { + foreach ($this->converters as $converter) { + $propertyName = $converter->convert($value, $propertyName, $context); + } + + return $propertyName; + } +} diff --git a/Mapping/NameConverterInterface.php b/Mapping/NameConverterInterface.php new file mode 100644 index 0000000..2153ad4 --- /dev/null +++ b/Mapping/NameConverterInterface.php @@ -0,0 +1,23 @@ +nullable = $nullable; + return $this; } diff --git a/Mapping/PropertyMetadataInterface.php b/Mapping/PropertyMetadataInterface.php index f49dc36..93da67b 100644 --- a/Mapping/PropertyMetadataInterface.php +++ b/Mapping/PropertyMetadataInterface.php @@ -33,6 +33,8 @@ public function getAdvancedVisibility(): \iterable; /** * @return MetadataInterface[]|\iterable + * + * @deprecated */ public function getNameConverter(): \iterable;