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 new file mode 100644 index 0000000..611de74 --- /dev/null +++ b/Mapping/Factory/ClassMetadataFactory.php @@ -0,0 +1,68 @@ +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); + } +} diff --git a/Mapping/Factory/KeyResolverTrait.php b/Mapping/Factory/KeyResolverTrait.php new file mode 100644 index 0000000..ce16ee5 --- /dev/null +++ b/Mapping/Factory/KeyResolverTrait.php @@ -0,0 +1,24 @@ +typeGuesser = $typeGuesser; + $this->propertyAccessExtractor = $propertyAccessExtractor; + $this->nameConverter = $nameConverter; + $this->accessorGuesser = $accessorGuesser; + } + + /** + * {@inheritdoc} + */ + public function getMetadataFor($value, string $propertyName, array $context = []): PropertyMetadataInterface + { + $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()) + ->setAccessor(new Metadata($accessor->getName(), $accessor->getOptions())); + + 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]; + } + + /** + * @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 new file mode 100644 index 0000000..218877b --- /dev/null +++ b/Mapping/Factory/PropertyMetadataFactoryInterface.php @@ -0,0 +1,40 @@ +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 @@ +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..8ae4d70 --- /dev/null +++ b/Mapping/Guess/GuessInterface.php @@ -0,0 +1,51 @@ +guesser = $guesser; + } + + /** + * {@inheritdoc} + */ + public function guessType(string $class, string $property, array $context = []): ?TypeGuessInterface + { + $result = null; + $maxConfidence = -1; + + foreach ($this->guesser as $item) { + $guess = $item->guessType($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/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 54% rename from Guess/GuessInterface.php rename to Mapping/Guess/TypeGuessInterface.php index 40898d4..bcd4daa 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 extends GuessInterface { + /** + * @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 @@ +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; } + /** + * @param bool $nullable + * + * @return PropertyMetadata + */ + public function setNullable(bool $nullable): self + { + $this->nullable = $nullable; + + return $this; + } + /** * @param MetadataInterface $visibility * 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; diff --git a/composer.json b/composer.json index 7925538..a41dbd2 100644 --- a/composer.json +++ b/composer.json @@ -22,6 +22,7 @@ "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": {