├── Annotation ├── Context.php ├── DiscriminatorMap.php ├── Groups.php ├── Ignore.php ├── MaxDepth.php ├── SerializedName.php └── SerializedPath.php ├── Attribute ├── Context.php ├── DiscriminatorMap.php ├── Groups.php ├── Ignore.php ├── MaxDepth.php ├── SerializedName.php └── SerializedPath.php ├── CHANGELOG.md ├── CacheWarmer └── CompiledClassMetadataCacheWarmer.php ├── Command └── DebugCommand.php ├── Context ├── ContextBuilderInterface.php ├── ContextBuilderTrait.php ├── Encoder │ ├── CsvEncoderContextBuilder.php │ ├── JsonEncoderContextBuilder.php │ ├── XmlEncoderContextBuilder.php │ └── YamlEncoderContextBuilder.php ├── Normalizer │ ├── AbstractNormalizerContextBuilder.php │ ├── AbstractObjectNormalizerContextBuilder.php │ ├── BackedEnumNormalizerContextBuilder.php │ ├── ConstraintViolationListNormalizerContextBuilder.php │ ├── DateIntervalNormalizerContextBuilder.php │ ├── DateTimeNormalizerContextBuilder.php │ ├── FormErrorNormalizerContextBuilder.php │ ├── GetSetMethodNormalizerContextBuilder.php │ ├── JsonSerializableNormalizerContextBuilder.php │ ├── ObjectNormalizerContextBuilder.php │ ├── ProblemNormalizerContextBuilder.php │ ├── PropertyNormalizerContextBuilder.php │ ├── UidNormalizerContextBuilder.php │ └── UnwrappingDenormalizerContextBuilder.php └── SerializerContextBuilder.php ├── DataCollector └── SerializerDataCollector.php ├── Debug ├── TraceableEncoder.php ├── TraceableNormalizer.php └── TraceableSerializer.php ├── DependencyInjection └── SerializerPass.php ├── Encoder ├── ChainDecoder.php ├── ChainEncoder.php ├── ContextAwareDecoderInterface.php ├── ContextAwareEncoderInterface.php ├── CsvEncoder.php ├── DecoderInterface.php ├── EncoderInterface.php ├── JsonDecode.php ├── JsonEncode.php ├── JsonEncoder.php ├── NormalizationAwareInterface.php ├── XmlEncoder.php └── YamlEncoder.php ├── Exception ├── BadMethodCallException.php ├── CircularReferenceException.php ├── ExceptionInterface.php ├── ExtraAttributesException.php ├── InvalidArgumentException.php ├── LogicException.php ├── MappingException.php ├── MissingConstructorArgumentsException.php ├── NotEncodableValueException.php ├── NotNormalizableValueException.php ├── PartialDenormalizationException.php ├── RuntimeException.php ├── UnexpectedPropertyException.php ├── UnexpectedValueException.php ├── UnsupportedException.php └── UnsupportedFormatException.php ├── Extractor ├── ObjectPropertyListExtractor.php └── ObjectPropertyListExtractorInterface.php ├── LICENSE ├── Mapping ├── AttributeMetadata.php ├── AttributeMetadataInterface.php ├── ClassDiscriminatorFromClassMetadata.php ├── ClassDiscriminatorMapping.php ├── ClassDiscriminatorResolverInterface.php ├── ClassMetadata.php ├── ClassMetadataInterface.php ├── Factory │ ├── CacheClassMetadataFactory.php │ ├── ClassMetadataFactory.php │ ├── ClassMetadataFactoryCompiler.php │ ├── ClassMetadataFactoryInterface.php │ ├── ClassResolverTrait.php │ └── CompiledClassMetadataFactory.php └── Loader │ ├── AttributeLoader.php │ ├── FileLoader.php │ ├── LoaderChain.php │ ├── LoaderInterface.php │ ├── XmlFileLoader.php │ ├── YamlFileLoader.php │ └── schema │ └── dic │ └── serializer-mapping │ └── serializer-mapping-1.0.xsd ├── NameConverter ├── AdvancedNameConverterInterface.php ├── CamelCaseToSnakeCaseNameConverter.php ├── MetadataAwareNameConverter.php ├── NameConverterInterface.php └── SnakeCaseToCamelCaseNameConverter.php ├── Normalizer ├── AbstractNormalizer.php ├── AbstractObjectNormalizer.php ├── ArrayDenormalizer.php ├── BackedEnumNormalizer.php ├── ConstraintViolationListNormalizer.php ├── CustomNormalizer.php ├── DataUriNormalizer.php ├── DateIntervalNormalizer.php ├── DateTimeNormalizer.php ├── DateTimeZoneNormalizer.php ├── DenormalizableInterface.php ├── DenormalizerAwareInterface.php ├── DenormalizerAwareTrait.php ├── DenormalizerInterface.php ├── FormErrorNormalizer.php ├── GetSetMethodNormalizer.php ├── JsonSerializableNormalizer.php ├── MimeMessageNormalizer.php ├── NormalizableInterface.php ├── NormalizerAwareInterface.php ├── NormalizerAwareTrait.php ├── NormalizerInterface.php ├── NumberNormalizer.php ├── ObjectNormalizer.php ├── ObjectToPopulateTrait.php ├── ProblemNormalizer.php ├── PropertyNormalizer.php ├── TranslatableNormalizer.php ├── UidNormalizer.php └── UnwrappingDenormalizer.php ├── README.md ├── Serializer.php ├── SerializerAwareInterface.php ├── SerializerAwareTrait.php ├── SerializerInterface.php └── composer.json /Annotation/Context.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\Annotation; 13 | 14 | // do not deprecate in 6.4/7.0, to make it easier for the ecosystem to support 6.4, 7.4 and 8.0 simultaneously 15 | 16 | class_exists(\Symfony\Component\Serializer\Attribute\Context::class); 17 | 18 | if (false) { 19 | #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] 20 | class Context extends \Symfony\Component\Serializer\Attribute\Context 21 | { 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Annotation/DiscriminatorMap.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\Annotation; 13 | 14 | class_exists(\Symfony\Component\Serializer\Attribute\DiscriminatorMap::class); 15 | 16 | if (false) { 17 | #[\Attribute(\Attribute::TARGET_CLASS)] 18 | class DiscriminatorMap extends \Symfony\Component\Serializer\Attribute\DiscriminatorMap 19 | { 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Annotation/Groups.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\Annotation; 13 | 14 | class_exists(\Symfony\Component\Serializer\Attribute\Groups::class); 15 | 16 | if (false) { 17 | #[\Attribute(\Attribute::TARGET_METHOD | \Attribute::TARGET_PROPERTY | \Attribute::TARGET_CLASS)] 18 | class Groups extends \Symfony\Component\Serializer\Attribute\Groups 19 | { 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Annotation/Ignore.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\Annotation; 13 | 14 | class_exists(\Symfony\Component\Serializer\Attribute\Ignore::class); 15 | 16 | if (false) { 17 | #[\Attribute(\Attribute::TARGET_METHOD | \Attribute::TARGET_PROPERTY)] 18 | class Ignore extends \Symfony\Component\Serializer\Attribute\Ignore 19 | { 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Annotation/MaxDepth.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\Annotation; 13 | 14 | class_exists(\Symfony\Component\Serializer\Attribute\MaxDepth::class); 15 | 16 | if (false) { 17 | #[\Attribute(\Attribute::TARGET_METHOD | \Attribute::TARGET_PROPERTY)] 18 | class MaxDepth extends \Symfony\Component\Serializer\Attribute\MaxDepth 19 | { 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Annotation/SerializedName.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\Annotation; 13 | 14 | class_exists(\Symfony\Component\Serializer\Attribute\SerializedName::class); 15 | 16 | if (false) { 17 | #[\Attribute(\Attribute::TARGET_METHOD | \Attribute::TARGET_PROPERTY)] 18 | class SerializedName extends \Symfony\Component\Serializer\Attribute\SerializedName 19 | { 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Annotation/SerializedPath.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\Annotation; 13 | 14 | class_exists(\Symfony\Component\Serializer\Attribute\SerializedPath::class); 15 | 16 | if (false) { 17 | #[\Attribute(\Attribute::TARGET_METHOD | \Attribute::TARGET_PROPERTY)] 18 | class SerializedPath extends \Symfony\Component\Serializer\Attribute\SerializedPath 19 | { 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Attribute/Context.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\Attribute; 13 | 14 | use Symfony\Component\Serializer\Exception\InvalidArgumentException; 15 | 16 | /** 17 | * @author Maxime Steinhausser 18 | */ 19 | #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] 20 | class Context 21 | { 22 | private array $groups; 23 | 24 | /** 25 | * @param array $context The common context to use when serializing or deserializing 26 | * @param array $normalizationContext The context to use when serializing 27 | * @param array $denormalizationContext The context to use when deserializing 28 | * @param string|string[] $groups The groups to use when serializing or deserializing 29 | * 30 | * @throws InvalidArgumentException 31 | */ 32 | public function __construct( 33 | private readonly array $context = [], 34 | private readonly array $normalizationContext = [], 35 | private readonly array $denormalizationContext = [], 36 | string|array $groups = [], 37 | ) { 38 | if (!$context && !$normalizationContext && !$denormalizationContext) { 39 | throw new InvalidArgumentException(\sprintf('At least one of the "context", "normalizationContext", or "denormalizationContext" options must be provided as a non-empty array to "%s".', static::class)); 40 | } 41 | 42 | $this->groups = (array) $groups; 43 | 44 | foreach ($this->groups as $group) { 45 | if (!\is_string($group)) { 46 | throw new InvalidArgumentException(\sprintf('Parameter "groups" given to "%s" must be a string or an array of strings, "%s" given.', static::class, get_debug_type($group))); 47 | } 48 | } 49 | } 50 | 51 | public function getContext(): array 52 | { 53 | return $this->context; 54 | } 55 | 56 | public function getNormalizationContext(): array 57 | { 58 | return $this->normalizationContext; 59 | } 60 | 61 | public function getDenormalizationContext(): array 62 | { 63 | return $this->denormalizationContext; 64 | } 65 | 66 | public function getGroups(): array 67 | { 68 | return $this->groups; 69 | } 70 | } 71 | 72 | if (!class_exists(\Symfony\Component\Serializer\Annotation\Context::class, false)) { 73 | class_alias(Context::class, \Symfony\Component\Serializer\Annotation\Context::class); 74 | } 75 | -------------------------------------------------------------------------------- /Attribute/DiscriminatorMap.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\Attribute; 13 | 14 | use Symfony\Component\Serializer\Exception\InvalidArgumentException; 15 | 16 | /** 17 | * @author Samuel Roze 18 | */ 19 | #[\Attribute(\Attribute::TARGET_CLASS)] 20 | class DiscriminatorMap 21 | { 22 | /** 23 | * @param string $typeProperty The property holding the type discriminator 24 | * @param array $mapping The mapping between types and classes (i.e. ['admin_user' => AdminUser::class]) 25 | * @param ?string $defaultType The fallback value if nothing specified by $typeProperty 26 | * 27 | * @throws InvalidArgumentException 28 | */ 29 | public function __construct( 30 | private readonly string $typeProperty, 31 | private readonly array $mapping, 32 | private readonly ?string $defaultType = null, 33 | ) { 34 | if (!$typeProperty) { 35 | throw new InvalidArgumentException(\sprintf('Parameter "typeProperty" given to "%s" cannot be empty.', static::class)); 36 | } 37 | 38 | if (!$mapping) { 39 | throw new InvalidArgumentException(\sprintf('Parameter "mapping" given to "%s" cannot be empty.', static::class)); 40 | } 41 | 42 | if (null !== $this->defaultType && !\array_key_exists($this->defaultType, $this->mapping)) { 43 | throw new InvalidArgumentException(\sprintf('Default type "%s" given to "%s" must be present in "mapping" types.', $this->defaultType, static::class)); 44 | } 45 | } 46 | 47 | public function getTypeProperty(): string 48 | { 49 | return $this->typeProperty; 50 | } 51 | 52 | public function getMapping(): array 53 | { 54 | return $this->mapping; 55 | } 56 | 57 | public function getDefaultType(): ?string 58 | { 59 | return $this->defaultType; 60 | } 61 | } 62 | 63 | if (!class_exists(\Symfony\Component\Serializer\Annotation\DiscriminatorMap::class, false)) { 64 | class_alias(DiscriminatorMap::class, \Symfony\Component\Serializer\Annotation\DiscriminatorMap::class); 65 | } 66 | -------------------------------------------------------------------------------- /Attribute/Groups.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\Attribute; 13 | 14 | use Symfony\Component\Serializer\Exception\InvalidArgumentException; 15 | 16 | /** 17 | * @author Kévin Dunglas 18 | */ 19 | #[\Attribute(\Attribute::TARGET_METHOD | \Attribute::TARGET_PROPERTY | \Attribute::TARGET_CLASS)] 20 | class Groups 21 | { 22 | /** 23 | * @var string[] 24 | */ 25 | private readonly array $groups; 26 | 27 | /** 28 | * @param string|string[] $groups The groups to define on the attribute target 29 | */ 30 | public function __construct(string|array $groups) 31 | { 32 | $this->groups = (array) $groups; 33 | 34 | if (!$this->groups) { 35 | throw new InvalidArgumentException(\sprintf('Parameter given to "%s" cannot be empty.', static::class)); 36 | } 37 | 38 | foreach ($this->groups as $group) { 39 | if (!\is_string($group) || '' === $group) { 40 | throw new InvalidArgumentException(\sprintf('Parameter given to "%s" must be a string or an array of non-empty strings.', static::class)); 41 | } 42 | } 43 | } 44 | 45 | /** 46 | * @return string[] 47 | */ 48 | public function getGroups(): array 49 | { 50 | return $this->groups; 51 | } 52 | } 53 | 54 | if (!class_exists(\Symfony\Component\Serializer\Annotation\Groups::class, false)) { 55 | class_alias(Groups::class, \Symfony\Component\Serializer\Annotation\Groups::class); 56 | } 57 | -------------------------------------------------------------------------------- /Attribute/Ignore.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\Attribute; 13 | 14 | /** 15 | * @author Kévin Dunglas 16 | */ 17 | #[\Attribute(\Attribute::TARGET_METHOD | \Attribute::TARGET_PROPERTY)] 18 | class Ignore 19 | { 20 | } 21 | 22 | if (!class_exists(\Symfony\Component\Serializer\Annotation\Ignore::class, false)) { 23 | class_alias(Ignore::class, \Symfony\Component\Serializer\Annotation\Ignore::class); 24 | } 25 | -------------------------------------------------------------------------------- /Attribute/MaxDepth.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\Attribute; 13 | 14 | use Symfony\Component\Serializer\Exception\InvalidArgumentException; 15 | 16 | /** 17 | * @author Kévin Dunglas 18 | */ 19 | #[\Attribute(\Attribute::TARGET_METHOD | \Attribute::TARGET_PROPERTY)] 20 | class MaxDepth 21 | { 22 | /** 23 | * @param int $maxDepth The maximum serialization depth 24 | */ 25 | public function __construct(private readonly int $maxDepth) 26 | { 27 | if ($maxDepth <= 0) { 28 | throw new InvalidArgumentException(\sprintf('Parameter given to "%s" must be a positive integer.', static::class)); 29 | } 30 | } 31 | 32 | public function getMaxDepth(): int 33 | { 34 | return $this->maxDepth; 35 | } 36 | } 37 | 38 | if (!class_exists(\Symfony\Component\Serializer\Annotation\MaxDepth::class, false)) { 39 | class_alias(MaxDepth::class, \Symfony\Component\Serializer\Annotation\MaxDepth::class); 40 | } 41 | -------------------------------------------------------------------------------- /Attribute/SerializedName.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\Attribute; 13 | 14 | use Symfony\Component\Serializer\Exception\InvalidArgumentException; 15 | 16 | /** 17 | * @author Fabien Bourigault 18 | */ 19 | #[\Attribute(\Attribute::TARGET_METHOD | \Attribute::TARGET_PROPERTY)] 20 | class SerializedName 21 | { 22 | /** 23 | * @param string $serializedName The name of the property as it will be serialized 24 | */ 25 | public function __construct(private readonly string $serializedName) 26 | { 27 | if ('' === $serializedName) { 28 | throw new InvalidArgumentException(\sprintf('Parameter given to "%s" must be a non-empty string.', self::class)); 29 | } 30 | } 31 | 32 | public function getSerializedName(): string 33 | { 34 | return $this->serializedName; 35 | } 36 | } 37 | 38 | if (!class_exists(\Symfony\Component\Serializer\Annotation\SerializedName::class, false)) { 39 | class_alias(SerializedName::class, \Symfony\Component\Serializer\Annotation\SerializedName::class); 40 | } 41 | -------------------------------------------------------------------------------- /Attribute/SerializedPath.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\Attribute; 13 | 14 | use Symfony\Component\PropertyAccess\Exception\InvalidPropertyPathException; 15 | use Symfony\Component\PropertyAccess\PropertyPath; 16 | use Symfony\Component\Serializer\Exception\InvalidArgumentException; 17 | 18 | /** 19 | * @author Tobias Bönner 20 | */ 21 | #[\Attribute(\Attribute::TARGET_METHOD | \Attribute::TARGET_PROPERTY)] 22 | class SerializedPath 23 | { 24 | private PropertyPath $serializedPath; 25 | 26 | /** 27 | * @param string $serializedPath A path using a valid PropertyAccess syntax where the value is stored in a normalized representation 28 | */ 29 | public function __construct(string $serializedPath) 30 | { 31 | try { 32 | $this->serializedPath = new PropertyPath($serializedPath); 33 | } catch (InvalidPropertyPathException $pathException) { 34 | throw new InvalidArgumentException(\sprintf('Parameter given to "%s" must be a valid property path.', self::class)); 35 | } 36 | } 37 | 38 | public function getSerializedPath(): PropertyPath 39 | { 40 | return $this->serializedPath; 41 | } 42 | } 43 | 44 | if (!class_exists(\Symfony\Component\Serializer\Annotation\SerializedPath::class, false)) { 45 | class_alias(SerializedPath::class, \Symfony\Component\Serializer\Annotation\SerializedPath::class); 46 | } 47 | -------------------------------------------------------------------------------- /CacheWarmer/CompiledClassMetadataCacheWarmer.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\CacheWarmer; 13 | 14 | use Symfony\Component\Filesystem\Filesystem; 15 | use Symfony\Component\HttpKernel\CacheWarmer\CacheWarmerInterface; 16 | use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryCompiler; 17 | use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface; 18 | 19 | trigger_deprecation('symfony/serializer', '7.3', 'The "%s" class is deprecated.', CompiledClassMetadataCacheWarmer::class); 20 | 21 | /** 22 | * @author Fabien Bourigault 23 | * 24 | * @deprecated since Symfony 7.3 25 | */ 26 | final class CompiledClassMetadataCacheWarmer implements CacheWarmerInterface 27 | { 28 | public function __construct( 29 | private readonly array $classesToCompile, 30 | private readonly ClassMetadataFactoryInterface $classMetadataFactory, 31 | private readonly ClassMetadataFactoryCompiler $classMetadataFactoryCompiler, 32 | private readonly Filesystem $filesystem, 33 | ) { 34 | } 35 | 36 | public function warmUp(string $cacheDir, ?string $buildDir = null): array 37 | { 38 | $metadatas = []; 39 | 40 | foreach ($this->classesToCompile as $classToCompile) { 41 | $metadatas[] = $this->classMetadataFactory->getMetadataFor($classToCompile); 42 | } 43 | 44 | $code = $this->classMetadataFactoryCompiler->compile($metadatas); 45 | 46 | $this->filesystem->dumpFile("{$cacheDir}/serializer.class.metadata.php", $code); 47 | 48 | return []; 49 | } 50 | 51 | public function isOptional(): bool 52 | { 53 | return true; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Command/DebugCommand.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\Command; 13 | 14 | use Symfony\Component\Console\Attribute\AsCommand; 15 | use Symfony\Component\Console\Command\Command; 16 | use Symfony\Component\Console\Helper\Dumper; 17 | use Symfony\Component\Console\Helper\Table; 18 | use Symfony\Component\Console\Input\InputArgument; 19 | use Symfony\Component\Console\Input\InputInterface; 20 | use Symfony\Component\Console\Output\OutputInterface; 21 | use Symfony\Component\Console\Style\SymfonyStyle; 22 | use Symfony\Component\Serializer\Mapping\ClassMetadataInterface; 23 | use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface; 24 | 25 | /** 26 | * A console command to debug Serializer information. 27 | * 28 | * @author Loïc Frémont 29 | */ 30 | #[AsCommand(name: 'debug:serializer', description: 'Display serialization information for classes')] 31 | class DebugCommand extends Command 32 | { 33 | public function __construct(private readonly ClassMetadataFactoryInterface $serializer) 34 | { 35 | parent::__construct(); 36 | } 37 | 38 | protected function configure(): void 39 | { 40 | $this 41 | ->addArgument('class', InputArgument::REQUIRED, 'A fully qualified class name') 42 | ->setHelp("The %command.name% 'App\Entity\Dummy' command dumps the serializer groups for the dummy class.") 43 | ; 44 | } 45 | 46 | protected function execute(InputInterface $input, OutputInterface $output): int 47 | { 48 | $class = $input->getArgument('class'); 49 | 50 | if (!class_exists($class)) { 51 | $io = new SymfonyStyle($input, $output); 52 | $io->error(\sprintf('Class "%s" was not found.', $class)); 53 | 54 | return Command::FAILURE; 55 | } 56 | 57 | $this->dumpSerializerDataForClass($input, $output, $class); 58 | 59 | return Command::SUCCESS; 60 | } 61 | 62 | private function dumpSerializerDataForClass(InputInterface $input, OutputInterface $output, string $class): void 63 | { 64 | $io = new SymfonyStyle($input, $output); 65 | $title = \sprintf('%s', $class); 66 | $rows = []; 67 | $dump = new Dumper($output); 68 | 69 | $classMetadata = $this->serializer->getMetadataFor($class); 70 | 71 | foreach ($this->getAttributesData($classMetadata) as $propertyName => $data) { 72 | $rows[] = [ 73 | $propertyName, 74 | $dump($data), 75 | ]; 76 | } 77 | 78 | $io->section($title); 79 | 80 | if (!$rows) { 81 | $io->text('No Serializer data were found for this class.'); 82 | 83 | return; 84 | } 85 | 86 | $table = new Table($output); 87 | $table->setHeaders(['Property', 'Options']); 88 | $table->setRows($rows); 89 | $table->render(); 90 | } 91 | 92 | /** 93 | * @return array> 94 | */ 95 | private function getAttributesData(ClassMetadataInterface $classMetadata): array 96 | { 97 | $data = []; 98 | 99 | $mapping = $classMetadata->getClassDiscriminatorMapping(); 100 | $typeProperty = $mapping?->getTypeProperty(); 101 | 102 | foreach ($classMetadata->getAttributesMetadata() as $attributeMetadata) { 103 | $data[$attributeMetadata->getName()] = [ 104 | 'groups' => $attributeMetadata->getGroups(), 105 | 'maxDepth' => $attributeMetadata->getMaxDepth(), 106 | 'serializedName' => $attributeMetadata->getSerializedName(), 107 | 'serializedPath' => $attributeMetadata->getSerializedPath() ? (string) $attributeMetadata->getSerializedPath() : null, 108 | 'ignore' => $attributeMetadata->isIgnored(), 109 | 'normalizationContexts' => $attributeMetadata->getNormalizationContexts(), 110 | 'denormalizationContexts' => $attributeMetadata->getDenormalizationContexts(), 111 | ]; 112 | 113 | if ($mapping && $typeProperty === $attributeMetadata->getName()) { 114 | $data[$attributeMetadata->getName()]['discriminatorMap'] = $mapping->getTypesMapping(); 115 | } 116 | } 117 | 118 | return $data; 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /Context/ContextBuilderInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\Context; 13 | 14 | /** 15 | * Common interface for context builders. 16 | * 17 | * @author Mathias Arlaud 18 | * @author Robin Chalas 19 | */ 20 | interface ContextBuilderInterface 21 | { 22 | /** 23 | * @param self|array $context 24 | */ 25 | public function withContext(self|array $context): static; 26 | 27 | /** 28 | * @return array 29 | */ 30 | public function toArray(): array; 31 | } 32 | -------------------------------------------------------------------------------- /Context/ContextBuilderTrait.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\Context; 13 | 14 | /** 15 | * @author Mathias Arlaud 16 | */ 17 | trait ContextBuilderTrait 18 | { 19 | /** 20 | * @var array 21 | */ 22 | private array $context = []; 23 | 24 | protected function with(string $key, mixed $value): static 25 | { 26 | $instance = new static(); 27 | $instance->context = array_merge($this->context, [$key => $value]); 28 | 29 | return $instance; 30 | } 31 | 32 | /** 33 | * @param ContextBuilderInterface|array $context 34 | */ 35 | public function withContext(ContextBuilderInterface|array $context): static 36 | { 37 | if ($context instanceof ContextBuilderInterface) { 38 | $context = $context->toArray(); 39 | } 40 | 41 | $instance = new static(); 42 | $instance->context = array_merge($this->context, $context); 43 | 44 | return $instance; 45 | } 46 | 47 | /** 48 | * @return array 49 | */ 50 | public function toArray(): array 51 | { 52 | return $this->context; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Context/Encoder/CsvEncoderContextBuilder.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\Context\Encoder; 13 | 14 | use Symfony\Component\Serializer\Context\ContextBuilderInterface; 15 | use Symfony\Component\Serializer\Context\ContextBuilderTrait; 16 | use Symfony\Component\Serializer\Encoder\CsvEncoder; 17 | use Symfony\Component\Serializer\Exception\InvalidArgumentException; 18 | 19 | /** 20 | * A helper providing autocompletion for available CsvEncoder options. 21 | * 22 | * @author Mathias Arlaud 23 | */ 24 | final class CsvEncoderContextBuilder implements ContextBuilderInterface 25 | { 26 | use ContextBuilderTrait; 27 | 28 | /** 29 | * Configures the column delimiter character. 30 | * 31 | * Must be a single character. 32 | * 33 | * @throws InvalidArgumentException 34 | */ 35 | public function withDelimiter(?string $delimiter): static 36 | { 37 | if (null !== $delimiter && 1 !== \strlen($delimiter)) { 38 | throw new InvalidArgumentException(\sprintf('The "%s" delimiter must be a single character.', $delimiter)); 39 | } 40 | 41 | return $this->with(CsvEncoder::DELIMITER_KEY, $delimiter); 42 | } 43 | 44 | /** 45 | * Configures the field enclosure character. 46 | * 47 | * Must be a single character. 48 | * 49 | * @throws InvalidArgumentException 50 | */ 51 | public function withEnclosure(?string $enclosure): static 52 | { 53 | if (null !== $enclosure && 1 !== \strlen($enclosure)) { 54 | throw new InvalidArgumentException(\sprintf('The "%s" enclosure must be a single character.', $enclosure)); 55 | } 56 | 57 | return $this->with(CsvEncoder::ENCLOSURE_KEY, $enclosure); 58 | } 59 | 60 | /** 61 | * Configures the escape character. 62 | * 63 | * Must be empty or a single character. 64 | * 65 | * @deprecated since Symfony 7.2, to be removed in 8.0 66 | * 67 | * @throws InvalidArgumentException 68 | */ 69 | public function withEscapeChar(?string $escapeChar): static 70 | { 71 | trigger_deprecation('symfony/serializer', '7.2', 'The "%s" method is deprecated. It will be removed in 8.0.', __METHOD__); 72 | 73 | if (null !== $escapeChar && \strlen($escapeChar) > 1) { 74 | throw new InvalidArgumentException(\sprintf('The "%s" escape character must be empty or a single character.', $escapeChar)); 75 | } 76 | 77 | return $this->with(CsvEncoder::ESCAPE_CHAR_KEY, $escapeChar); 78 | } 79 | 80 | /** 81 | * Configures the key separator when (un)flattening arrays. 82 | */ 83 | public function withKeySeparator(?string $keySeparator): static 84 | { 85 | return $this->with(CsvEncoder::KEY_SEPARATOR_KEY, $keySeparator); 86 | } 87 | 88 | /** 89 | * Configures the headers. 90 | * 91 | * @param list|null $headers 92 | */ 93 | public function withHeaders(?array $headers): static 94 | { 95 | return $this->with(CsvEncoder::HEADERS_KEY, $headers); 96 | } 97 | 98 | /** 99 | * Configures whether formulas should be escaped. 100 | */ 101 | public function withEscapedFormulas(?bool $escapedFormulas): static 102 | { 103 | return $this->with(CsvEncoder::ESCAPE_FORMULAS_KEY, $escapedFormulas); 104 | } 105 | 106 | /** 107 | * Configures whether the decoded result should be considered as a collection 108 | * or as a single element. 109 | */ 110 | public function withAsCollection(?bool $asCollection): static 111 | { 112 | return $this->with(CsvEncoder::AS_COLLECTION_KEY, $asCollection); 113 | } 114 | 115 | /** 116 | * Configures whether the input (or output) is containing (or will contain) headers. 117 | */ 118 | public function withNoHeaders(?bool $noHeaders): static 119 | { 120 | return $this->with(CsvEncoder::NO_HEADERS_KEY, $noHeaders); 121 | } 122 | 123 | /** 124 | * Configures the end of line characters. 125 | */ 126 | public function withEndOfLine(?string $endOfLine): static 127 | { 128 | return $this->with(CsvEncoder::END_OF_LINE, $endOfLine); 129 | } 130 | 131 | /** 132 | * Configures whether to add the UTF-8 Byte Order Mark (BOM) 133 | * at the beginning of the encoded result or not. 134 | */ 135 | public function withOutputUtf8Bom(?bool $outputUtf8Bom): static 136 | { 137 | return $this->with(CsvEncoder::OUTPUT_UTF8_BOM_KEY, $outputUtf8Bom); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /Context/Encoder/JsonEncoderContextBuilder.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\Context\Encoder; 13 | 14 | use Symfony\Component\Serializer\Context\ContextBuilderInterface; 15 | use Symfony\Component\Serializer\Context\ContextBuilderTrait; 16 | use Symfony\Component\Serializer\Encoder\JsonDecode; 17 | use Symfony\Component\Serializer\Encoder\JsonEncode; 18 | 19 | /** 20 | * A helper providing autocompletion for available JsonEncoder options. 21 | * 22 | * @author Mathias Arlaud 23 | */ 24 | final class JsonEncoderContextBuilder implements ContextBuilderInterface 25 | { 26 | use ContextBuilderTrait; 27 | 28 | /** 29 | * Configures the json_encode flags bitmask. 30 | * 31 | * @see https://www.php.net/manual/en/json.constants.php 32 | * 33 | * @param positive-int|null $options 34 | */ 35 | public function withEncodeOptions(?int $options): static 36 | { 37 | return $this->with(JsonEncode::OPTIONS, $options); 38 | } 39 | 40 | /** 41 | * Configures the json_decode flags bitmask. 42 | * 43 | * @see https://www.php.net/manual/en/json.constants.php 44 | * 45 | * @param positive-int|null $options 46 | */ 47 | public function withDecodeOptions(?int $options): static 48 | { 49 | return $this->with(JsonDecode::OPTIONS, $options); 50 | } 51 | 52 | /** 53 | * Configures whether decoded objects will be given as 54 | * associative arrays or as nested stdClass. 55 | */ 56 | public function withAssociative(?bool $associative): static 57 | { 58 | return $this->with(JsonDecode::ASSOCIATIVE, $associative); 59 | } 60 | 61 | /** 62 | * Configures the maximum recursion depth. 63 | * 64 | * Must be strictly positive. 65 | * 66 | * @param positive-int|null $recursionDepth 67 | */ 68 | public function withRecursionDepth(?int $recursionDepth): static 69 | { 70 | return $this->with(JsonDecode::RECURSION_DEPTH, $recursionDepth); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Context/Encoder/YamlEncoderContextBuilder.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\Context\Encoder; 13 | 14 | use Symfony\Component\Serializer\Context\ContextBuilderInterface; 15 | use Symfony\Component\Serializer\Context\ContextBuilderTrait; 16 | use Symfony\Component\Serializer\Encoder\YamlEncoder; 17 | 18 | /** 19 | * A helper providing autocompletion for available YamlEncoder options. 20 | * 21 | * Note that the "indentation" setting is not offered in this builder because 22 | * it can only be set during the construction of the YamlEncoder, but not per 23 | * call. 24 | * 25 | * @author Mathias Arlaud 26 | */ 27 | final class YamlEncoderContextBuilder implements ContextBuilderInterface 28 | { 29 | use ContextBuilderTrait; 30 | 31 | /** 32 | * Configures the threshold to switch to inline YAML. 33 | */ 34 | public function withInlineThreshold(?int $inlineThreshold): static 35 | { 36 | return $this->with(YamlEncoder::YAML_INLINE, $inlineThreshold); 37 | } 38 | 39 | /** 40 | * Configures the indentation level. 41 | * 42 | * Must be positive. 43 | * 44 | * @param int<0, max>|null $indentLevel 45 | */ 46 | public function withIndentLevel(?int $indentLevel): static 47 | { 48 | return $this->with(YamlEncoder::YAML_INDENT, $indentLevel); 49 | } 50 | 51 | /** 52 | * Configures \Symfony\Component\Yaml\Dumper::dump flags bitmask. 53 | * 54 | * @see \Symfony\Component\Yaml\Yaml 55 | */ 56 | public function withFlags(?int $flags): static 57 | { 58 | return $this->with(YamlEncoder::YAML_FLAGS, $flags); 59 | } 60 | 61 | /** 62 | * Configures whether to preserve empty objects "{}" or to convert them to null. 63 | */ 64 | public function withPreservedEmptyObjects(?bool $preserveEmptyObjects): static 65 | { 66 | return $this->with(YamlEncoder::PRESERVE_EMPTY_OBJECTS, $preserveEmptyObjects); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Context/Normalizer/BackedEnumNormalizerContextBuilder.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\Context\Normalizer; 13 | 14 | use Symfony\Component\Serializer\Context\ContextBuilderInterface; 15 | use Symfony\Component\Serializer\Context\ContextBuilderTrait; 16 | use Symfony\Component\Serializer\Normalizer\BackedEnumNormalizer; 17 | 18 | /** 19 | * A helper providing autocompletion for available BackedEnumNormalizer options. 20 | * 21 | * @author Nicolas PHILIPPE 22 | */ 23 | final class BackedEnumNormalizerContextBuilder implements ContextBuilderInterface 24 | { 25 | use ContextBuilderTrait; 26 | 27 | /** 28 | * Configures if invalid values are allowed in denormalization. 29 | * They will be denormalized into `null` values. 30 | */ 31 | public function withAllowInvalidValues(bool $allowInvalidValues): static 32 | { 33 | return $this->with(BackedEnumNormalizer::ALLOW_INVALID_VALUES, $allowInvalidValues); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Context/Normalizer/ConstraintViolationListNormalizerContextBuilder.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\Context\Normalizer; 13 | 14 | use Symfony\Component\Serializer\Context\ContextBuilderInterface; 15 | use Symfony\Component\Serializer\Context\ContextBuilderTrait; 16 | use Symfony\Component\Serializer\Normalizer\ConstraintViolationListNormalizer; 17 | 18 | /** 19 | * A helper providing autocompletion for available ConstraintViolationList options. 20 | * 21 | * @author Mathias Arlaud 22 | */ 23 | final class ConstraintViolationListNormalizerContextBuilder implements ContextBuilderInterface 24 | { 25 | use ContextBuilderTrait; 26 | 27 | /** 28 | * Configure the instance field of normalized data. 29 | */ 30 | public function withInstance(mixed $instance): static 31 | { 32 | return $this->with(ConstraintViolationListNormalizer::INSTANCE, $instance); 33 | } 34 | 35 | /** 36 | * Configure the status field of normalized data. 37 | */ 38 | public function withStatus(?int $status): static 39 | { 40 | return $this->with(ConstraintViolationListNormalizer::STATUS, $status); 41 | } 42 | 43 | /** 44 | * Configure the title field of normalized data. 45 | */ 46 | public function withTitle(?string $title): static 47 | { 48 | return $this->with(ConstraintViolationListNormalizer::TITLE, $title); 49 | } 50 | 51 | /** 52 | * Configure the type field of normalized data. 53 | */ 54 | public function withType(?string $type): static 55 | { 56 | return $this->with(ConstraintViolationListNormalizer::TYPE, $type); 57 | } 58 | 59 | /** 60 | * Configures the payload fields which will act as an allowlist 61 | * for the payload field of normalized data. 62 | * 63 | * Eg: ['foo', 'bar'] 64 | * 65 | * @param list|null $payloadFields 66 | */ 67 | public function withPayloadFields(?array $payloadFields): static 68 | { 69 | return $this->with(ConstraintViolationListNormalizer::PAYLOAD_FIELDS, $payloadFields); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Context/Normalizer/DateIntervalNormalizerContextBuilder.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\Context\Normalizer; 13 | 14 | use Symfony\Component\Serializer\Context\ContextBuilderInterface; 15 | use Symfony\Component\Serializer\Context\ContextBuilderTrait; 16 | use Symfony\Component\Serializer\Normalizer\DateIntervalNormalizer; 17 | 18 | /** 19 | * A helper providing autocompletion for available DateIntervalNormalizer options. 20 | * 21 | * @author Mathias Arlaud 22 | */ 23 | final class DateIntervalNormalizerContextBuilder implements ContextBuilderInterface 24 | { 25 | use ContextBuilderTrait; 26 | 27 | /** 28 | * Configures the format of the interval. 29 | * 30 | * @see https://php.net/manual/en/dateinterval.format.php 31 | */ 32 | public function withFormat(?string $format): static 33 | { 34 | return $this->with(DateIntervalNormalizer::FORMAT_KEY, $format); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Context/Normalizer/DateTimeNormalizerContextBuilder.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\Context\Normalizer; 13 | 14 | use Symfony\Component\Serializer\Context\ContextBuilderInterface; 15 | use Symfony\Component\Serializer\Context\ContextBuilderTrait; 16 | use Symfony\Component\Serializer\Exception\InvalidArgumentException; 17 | use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer; 18 | 19 | /** 20 | * A helper providing autocompletion for available DateTimeNormalizer options. 21 | * 22 | * @author Mathias Arlaud 23 | */ 24 | final class DateTimeNormalizerContextBuilder implements ContextBuilderInterface 25 | { 26 | use ContextBuilderTrait; 27 | 28 | /** 29 | * Configures the format of the date. 30 | * 31 | * @see https://secure.php.net/manual/en/datetime.format.php 32 | */ 33 | public function withFormat(?string $format): static 34 | { 35 | return $this->with(DateTimeNormalizer::FORMAT_KEY, $format); 36 | } 37 | 38 | /** 39 | * Configures the timezone of the date. 40 | * 41 | * It could be either a \DateTimeZone or a string 42 | * that will be used to construct the \DateTimeZone 43 | * 44 | * @see https://secure.php.net/manual/en/class.datetimezone.php 45 | * 46 | * @throws InvalidArgumentException 47 | */ 48 | public function withTimezone(\DateTimeZone|string|null $timezone): static 49 | { 50 | if (null === $timezone) { 51 | return $this->with(DateTimeNormalizer::TIMEZONE_KEY, null); 52 | } 53 | 54 | if (\is_string($timezone)) { 55 | try { 56 | $timezone = new \DateTimeZone($timezone); 57 | } catch (\Exception $e) { 58 | throw new InvalidArgumentException(\sprintf('The "%s" timezone is invalid.', $timezone), previous: $e); 59 | } 60 | } 61 | 62 | return $this->with(DateTimeNormalizer::TIMEZONE_KEY, $timezone); 63 | } 64 | 65 | /** 66 | * @param 'int'|'float'|null $cast 67 | */ 68 | public function withCast(?string $cast): static 69 | { 70 | return $this->with(DateTimeNormalizer::CAST_KEY, $cast); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Context/Normalizer/FormErrorNormalizerContextBuilder.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\Context\Normalizer; 13 | 14 | use Symfony\Component\Serializer\Context\ContextBuilderInterface; 15 | use Symfony\Component\Serializer\Context\ContextBuilderTrait; 16 | use Symfony\Component\Serializer\Normalizer\FormErrorNormalizer; 17 | 18 | /** 19 | * A helper providing autocompletion for available FormErrorNormalizer options. 20 | * 21 | * @author Mathias Arlaud 22 | */ 23 | final class FormErrorNormalizerContextBuilder implements ContextBuilderInterface 24 | { 25 | use ContextBuilderTrait; 26 | 27 | /** 28 | * Configures the title of the normalized data. 29 | */ 30 | public function withTitle(?string $title): static 31 | { 32 | return $this->with(FormErrorNormalizer::TITLE, $title); 33 | } 34 | 35 | /** 36 | * Configures the type of the normalized data. 37 | */ 38 | public function withType(?string $type): static 39 | { 40 | return $this->with(FormErrorNormalizer::TYPE, $type); 41 | } 42 | 43 | /** 44 | * Configures the code of the normalized data. 45 | */ 46 | public function withStatusCode(?int $statusCode): static 47 | { 48 | return $this->with(FormErrorNormalizer::CODE, $statusCode); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Context/Normalizer/GetSetMethodNormalizerContextBuilder.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\Context\Normalizer; 13 | 14 | /** 15 | * A helper providing autocompletion for available GetSetMethodNormalizer options. 16 | * 17 | * @author Mathias Arlaud 18 | */ 19 | final class GetSetMethodNormalizerContextBuilder extends AbstractObjectNormalizerContextBuilder 20 | { 21 | } 22 | -------------------------------------------------------------------------------- /Context/Normalizer/JsonSerializableNormalizerContextBuilder.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\Context\Normalizer; 13 | 14 | /** 15 | * A helper providing autocompletion for available JsonSerializableNormalizer options. 16 | * 17 | * @author Mathias Arlaud 18 | */ 19 | final class JsonSerializableNormalizerContextBuilder extends AbstractNormalizerContextBuilder 20 | { 21 | } 22 | -------------------------------------------------------------------------------- /Context/Normalizer/ObjectNormalizerContextBuilder.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\Context\Normalizer; 13 | 14 | /** 15 | * A helper providing autocompletion for available ObjectNormalizer options. 16 | * 17 | * @author Mathias Arlaud 18 | */ 19 | final class ObjectNormalizerContextBuilder extends AbstractObjectNormalizerContextBuilder 20 | { 21 | } 22 | -------------------------------------------------------------------------------- /Context/Normalizer/ProblemNormalizerContextBuilder.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\Context\Normalizer; 13 | 14 | use Symfony\Component\Serializer\Context\ContextBuilderInterface; 15 | use Symfony\Component\Serializer\Context\ContextBuilderTrait; 16 | use Symfony\Component\Serializer\Normalizer\ProblemNormalizer; 17 | 18 | /** 19 | * A helper providing autocompletion for available ProblemNormalizer options. 20 | * 21 | * @author Mathias Arlaud 22 | */ 23 | final class ProblemNormalizerContextBuilder implements ContextBuilderInterface 24 | { 25 | use ContextBuilderTrait; 26 | 27 | /** 28 | * Configure the title field of normalized data. 29 | */ 30 | public function withTitle(?string $title): static 31 | { 32 | return $this->with(ProblemNormalizer::TITLE, $title); 33 | } 34 | 35 | /** 36 | * Configure the type field of normalized data. 37 | */ 38 | public function withType(?string $type): static 39 | { 40 | return $this->with(ProblemNormalizer::TYPE, $type); 41 | } 42 | 43 | /** 44 | * Configure the status field of normalized data. 45 | */ 46 | public function withStatusCode(int|string|null $statusCode): static 47 | { 48 | return $this->with(ProblemNormalizer::STATUS, $statusCode); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Context/Normalizer/PropertyNormalizerContextBuilder.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\Context\Normalizer; 13 | 14 | use Symfony\Component\Serializer\Normalizer\PropertyNormalizer; 15 | 16 | /** 17 | * A helper providing autocompletion for available PropertyNormalizer options. 18 | * 19 | * @author Mathias Arlaud 20 | */ 21 | final class PropertyNormalizerContextBuilder extends AbstractObjectNormalizerContextBuilder 22 | { 23 | /** 24 | * Configures whether fields should be output based on visibility. 25 | */ 26 | public function withNormalizeVisibility(int $normalizeVisibility): static 27 | { 28 | return $this->with(PropertyNormalizer::NORMALIZE_VISIBILITY, $normalizeVisibility); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Context/Normalizer/UidNormalizerContextBuilder.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\Context\Normalizer; 13 | 14 | use Symfony\Component\Serializer\Context\ContextBuilderInterface; 15 | use Symfony\Component\Serializer\Context\ContextBuilderTrait; 16 | use Symfony\Component\Serializer\Exception\InvalidArgumentException; 17 | use Symfony\Component\Serializer\Normalizer\UidNormalizer; 18 | 19 | /** 20 | * A helper providing autocompletion for available UidNormalizer options. 21 | * 22 | * @author Mathias Arlaud 23 | */ 24 | final class UidNormalizerContextBuilder implements ContextBuilderInterface 25 | { 26 | use ContextBuilderTrait; 27 | 28 | /** 29 | * Configures the uuid format for normalization. 30 | * 31 | * @throws InvalidArgumentException 32 | */ 33 | public function withNormalizationFormat(?string $normalizationFormat): static 34 | { 35 | if (null !== $normalizationFormat && !\in_array($normalizationFormat, UidNormalizer::NORMALIZATION_FORMATS, true)) { 36 | throw new InvalidArgumentException(\sprintf('The "%s" normalization format is not valid.', $normalizationFormat)); 37 | } 38 | 39 | return $this->with(UidNormalizer::NORMALIZATION_FORMAT_KEY, $normalizationFormat); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Context/Normalizer/UnwrappingDenormalizerContextBuilder.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\Context\Normalizer; 13 | 14 | use Symfony\Component\PropertyAccess\Exception\InvalidPropertyPathException; 15 | use Symfony\Component\PropertyAccess\PropertyPath; 16 | use Symfony\Component\Serializer\Context\ContextBuilderInterface; 17 | use Symfony\Component\Serializer\Context\ContextBuilderTrait; 18 | use Symfony\Component\Serializer\Exception\InvalidArgumentException; 19 | use Symfony\Component\Serializer\Normalizer\UnwrappingDenormalizer; 20 | 21 | /** 22 | * A helper providing autocompletion for available UnwrappingDenormalizer options. 23 | * 24 | * @author Mathias Arlaud 25 | */ 26 | final class UnwrappingDenormalizerContextBuilder implements ContextBuilderInterface 27 | { 28 | use ContextBuilderTrait; 29 | 30 | /** 31 | * Configures the path of wrapped data during denormalization. 32 | * 33 | * Eg: [foo].bar[bar] 34 | * 35 | * @see https://symfony.com/doc/current/components/property_access.html 36 | * 37 | * @throws InvalidArgumentException 38 | */ 39 | public function withUnwrapPath(?string $unwrapPath): static 40 | { 41 | if (null === $unwrapPath) { 42 | return $this->with(UnwrappingDenormalizer::UNWRAP_PATH, null); 43 | } 44 | 45 | try { 46 | new PropertyPath($unwrapPath); 47 | } catch (InvalidPropertyPathException $e) { 48 | throw new InvalidArgumentException(\sprintf('The "%s" property path is not valid.', $unwrapPath), previous: $e); 49 | } 50 | 51 | return $this->with(UnwrappingDenormalizer::UNWRAP_PATH, $unwrapPath); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Context/SerializerContextBuilder.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\Context; 13 | 14 | use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; 15 | use Symfony\Component\Serializer\Serializer; 16 | 17 | /** 18 | * A helper providing autocompletion for available Serializer options. 19 | * 20 | * @author Mathias Arlaud 21 | */ 22 | final class SerializerContextBuilder implements ContextBuilderInterface 23 | { 24 | use ContextBuilderTrait; 25 | 26 | /** 27 | * Configures whether an empty array should be transformed to an 28 | * object (in JSON: {}) or to a list (in JSON: []). 29 | */ 30 | public function withEmptyArrayAsObject(?bool $emptyArrayAsObject): static 31 | { 32 | return $this->with(Serializer::EMPTY_ARRAY_AS_OBJECT, $emptyArrayAsObject); 33 | } 34 | 35 | public function withCollectDenormalizationErrors(?bool $collectDenormalizationErrors): static 36 | { 37 | return $this->with(DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS, $collectDenormalizationErrors); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Debug/TraceableEncoder.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\Debug; 13 | 14 | use Symfony\Component\Serializer\DataCollector\SerializerDataCollector; 15 | use Symfony\Component\Serializer\Encoder\DecoderInterface; 16 | use Symfony\Component\Serializer\Encoder\EncoderInterface; 17 | use Symfony\Component\Serializer\Encoder\NormalizationAwareInterface; 18 | use Symfony\Component\Serializer\SerializerAwareInterface; 19 | use Symfony\Component\Serializer\SerializerInterface; 20 | 21 | /** 22 | * Collects some data about encoding. 23 | * 24 | * @author Mathias Arlaud 25 | * 26 | * @final 27 | */ 28 | class TraceableEncoder implements EncoderInterface, DecoderInterface, SerializerAwareInterface 29 | { 30 | public function __construct( 31 | private EncoderInterface|DecoderInterface $encoder, 32 | private SerializerDataCollector $dataCollector, 33 | private readonly string $serializerName = 'default', 34 | ) { 35 | } 36 | 37 | public function encode(mixed $data, string $format, array $context = []): string 38 | { 39 | if (!$this->encoder instanceof EncoderInterface) { 40 | throw new \BadMethodCallException(\sprintf('The "%s()" method cannot be called as nested encoder doesn\'t implements "%s".', __METHOD__, EncoderInterface::class)); 41 | } 42 | 43 | $startTime = microtime(true); 44 | $encoded = $this->encoder->encode($data, $format, $context); 45 | $time = microtime(true) - $startTime; 46 | 47 | if ($traceId = ($context[TraceableSerializer::DEBUG_TRACE_ID] ?? null)) { 48 | $this->dataCollector->collectEncoding($traceId, $this->encoder::class, $time, $this->serializerName); 49 | } 50 | 51 | return $encoded; 52 | } 53 | 54 | public function supportsEncoding(string $format, array $context = []): bool 55 | { 56 | if (!$this->encoder instanceof EncoderInterface) { 57 | return false; 58 | } 59 | 60 | return $this->encoder->supportsEncoding($format, $context); 61 | } 62 | 63 | public function decode(string $data, string $format, array $context = []): mixed 64 | { 65 | if (!$this->encoder instanceof DecoderInterface) { 66 | throw new \BadMethodCallException(\sprintf('The "%s()" method cannot be called as nested encoder doesn\'t implements "%s".', __METHOD__, DecoderInterface::class)); 67 | } 68 | 69 | $startTime = microtime(true); 70 | $encoded = $this->encoder->decode($data, $format, $context); 71 | $time = microtime(true) - $startTime; 72 | 73 | if ($traceId = ($context[TraceableSerializer::DEBUG_TRACE_ID] ?? null)) { 74 | $this->dataCollector->collectDecoding($traceId, $this->encoder::class, $time, $this->serializerName); 75 | } 76 | 77 | return $encoded; 78 | } 79 | 80 | public function supportsDecoding(string $format, array $context = []): bool 81 | { 82 | if (!$this->encoder instanceof DecoderInterface) { 83 | return false; 84 | } 85 | 86 | return $this->encoder->supportsDecoding($format, $context); 87 | } 88 | 89 | public function setSerializer(SerializerInterface $serializer): void 90 | { 91 | if (!$this->encoder instanceof SerializerAwareInterface) { 92 | return; 93 | } 94 | 95 | $this->encoder->setSerializer($serializer); 96 | } 97 | 98 | public function needsNormalization(): bool 99 | { 100 | return !$this->encoder instanceof NormalizationAwareInterface; 101 | } 102 | 103 | /** 104 | * Proxies all method calls to the original encoder. 105 | */ 106 | public function __call(string $method, array $arguments): mixed 107 | { 108 | return $this->encoder->{$method}(...$arguments); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /Debug/TraceableNormalizer.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\Debug; 13 | 14 | use Symfony\Component\Serializer\DataCollector\SerializerDataCollector; 15 | use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface; 16 | use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; 17 | use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface; 18 | use Symfony\Component\Serializer\Normalizer\NormalizerInterface; 19 | use Symfony\Component\Serializer\SerializerAwareInterface; 20 | use Symfony\Component\Serializer\SerializerInterface; 21 | 22 | /** 23 | * Collects some data about normalization. 24 | * 25 | * @author Mathias Arlaud 26 | * 27 | * @final 28 | */ 29 | class TraceableNormalizer implements NormalizerInterface, DenormalizerInterface, SerializerAwareInterface, NormalizerAwareInterface, DenormalizerAwareInterface 30 | { 31 | public function __construct( 32 | private NormalizerInterface|DenormalizerInterface $normalizer, 33 | private SerializerDataCollector $dataCollector, 34 | private readonly string $serializerName = 'default', 35 | ) { 36 | } 37 | 38 | public function getSupportedTypes(?string $format): array 39 | { 40 | return $this->normalizer->getSupportedTypes($format); 41 | } 42 | 43 | public function normalize(mixed $data, ?string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null 44 | { 45 | if (!$this->normalizer instanceof NormalizerInterface) { 46 | throw new \BadMethodCallException(\sprintf('The "%s()" method cannot be called as nested normalizer doesn\'t implements "%s".', __METHOD__, NormalizerInterface::class)); 47 | } 48 | 49 | $startTime = microtime(true); 50 | $normalized = $this->normalizer->normalize($data, $format, $context); 51 | $time = microtime(true) - $startTime; 52 | 53 | if ($traceId = ($context[TraceableSerializer::DEBUG_TRACE_ID] ?? null)) { 54 | $this->dataCollector->collectNormalization($traceId, $this->normalizer::class, $time, $this->serializerName); 55 | } 56 | 57 | return $normalized; 58 | } 59 | 60 | public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool 61 | { 62 | if (!$this->normalizer instanceof NormalizerInterface) { 63 | return false; 64 | } 65 | 66 | return $this->normalizer->supportsNormalization($data, $format, $context); 67 | } 68 | 69 | public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): mixed 70 | { 71 | if (!$this->normalizer instanceof DenormalizerInterface) { 72 | throw new \BadMethodCallException(\sprintf('The "%s()" method cannot be called as nested normalizer doesn\'t implements "%s".', __METHOD__, DenormalizerInterface::class)); 73 | } 74 | 75 | $startTime = microtime(true); 76 | $denormalized = $this->normalizer->denormalize($data, $type, $format, $context); 77 | $time = microtime(true) - $startTime; 78 | 79 | if ($traceId = ($context[TraceableSerializer::DEBUG_TRACE_ID] ?? null)) { 80 | $this->dataCollector->collectDenormalization($traceId, $this->normalizer::class, $time, $this->serializerName); 81 | } 82 | 83 | return $denormalized; 84 | } 85 | 86 | public function supportsDenormalization(mixed $data, string $type, ?string $format = null, array $context = []): bool 87 | { 88 | if (!$this->normalizer instanceof DenormalizerInterface) { 89 | return false; 90 | } 91 | 92 | return $this->normalizer->supportsDenormalization($data, $type, $format, $context); 93 | } 94 | 95 | public function setSerializer(SerializerInterface $serializer): void 96 | { 97 | if (!$this->normalizer instanceof SerializerAwareInterface) { 98 | return; 99 | } 100 | 101 | $this->normalizer->setSerializer($serializer); 102 | } 103 | 104 | public function setNormalizer(NormalizerInterface $normalizer): void 105 | { 106 | if (!$this->normalizer instanceof NormalizerAwareInterface) { 107 | return; 108 | } 109 | 110 | $this->normalizer->setNormalizer($normalizer); 111 | } 112 | 113 | public function setDenormalizer(DenormalizerInterface $denormalizer): void 114 | { 115 | if (!$this->normalizer instanceof DenormalizerAwareInterface) { 116 | return; 117 | } 118 | 119 | $this->normalizer->setDenormalizer($denormalizer); 120 | } 121 | 122 | /** 123 | * Proxies all method calls to the original normalizer. 124 | */ 125 | public function __call(string $method, array $arguments): mixed 126 | { 127 | return $this->normalizer->{$method}(...$arguments); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /Encoder/ChainDecoder.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\Encoder; 13 | 14 | use Symfony\Component\Serializer\Exception\RuntimeException; 15 | 16 | /** 17 | * Decoder delegating the decoding to a chain of decoders. 18 | * 19 | * @author Jordi Boggiano 20 | * @author Johannes M. Schmitt 21 | * @author Lukas Kahwe Smith 22 | * 23 | * @final 24 | */ 25 | class ChainDecoder implements ContextAwareDecoderInterface 26 | { 27 | /** 28 | * @var array 29 | */ 30 | private array $decoderByFormat = []; 31 | 32 | /** 33 | * @param array $decoders 34 | */ 35 | public function __construct( 36 | private readonly array $decoders = [], 37 | ) { 38 | } 39 | 40 | final public function decode(string $data, string $format, array $context = []): mixed 41 | { 42 | return $this->getDecoder($format, $context)->decode($data, $format, $context); 43 | } 44 | 45 | public function supportsDecoding(string $format, array $context = []): bool 46 | { 47 | try { 48 | $this->getDecoder($format, $context); 49 | } catch (RuntimeException) { 50 | return false; 51 | } 52 | 53 | return true; 54 | } 55 | 56 | /** 57 | * Gets the decoder supporting the format. 58 | * 59 | * @throws RuntimeException if no decoder is found 60 | */ 61 | private function getDecoder(string $format, array $context): DecoderInterface 62 | { 63 | if (isset($this->decoderByFormat[$format]) 64 | && isset($this->decoders[$this->decoderByFormat[$format]]) 65 | ) { 66 | return $this->decoders[$this->decoderByFormat[$format]]; 67 | } 68 | 69 | $cache = true; 70 | foreach ($this->decoders as $i => $decoder) { 71 | $cache = $cache && !$decoder instanceof ContextAwareDecoderInterface; 72 | if ($decoder->supportsDecoding($format, $context)) { 73 | if ($cache) { 74 | $this->decoderByFormat[$format] = $i; 75 | } 76 | 77 | return $decoder; 78 | } 79 | } 80 | 81 | throw new RuntimeException(\sprintf('No decoder found for format "%s".', $format)); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Encoder/ChainEncoder.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\Encoder; 13 | 14 | use Symfony\Component\Serializer\Debug\TraceableEncoder; 15 | use Symfony\Component\Serializer\Exception\RuntimeException; 16 | 17 | /** 18 | * Encoder delegating the decoding to a chain of encoders. 19 | * 20 | * @author Jordi Boggiano 21 | * @author Johannes M. Schmitt 22 | * @author Lukas Kahwe Smith 23 | * 24 | * @final 25 | */ 26 | class ChainEncoder implements ContextAwareEncoderInterface 27 | { 28 | /** 29 | * @var array 30 | */ 31 | private array $encoderByFormat = []; 32 | 33 | /** 34 | * @param array $encoders 35 | */ 36 | public function __construct( 37 | private readonly array $encoders = [], 38 | ) { 39 | } 40 | 41 | final public function encode(mixed $data, string $format, array $context = []): string 42 | { 43 | return $this->getEncoder($format, $context)->encode($data, $format, $context); 44 | } 45 | 46 | public function supportsEncoding(string $format, array $context = []): bool 47 | { 48 | try { 49 | $this->getEncoder($format, $context); 50 | } catch (RuntimeException) { 51 | return false; 52 | } 53 | 54 | return true; 55 | } 56 | 57 | /** 58 | * Checks whether the normalization is needed for the given format. 59 | */ 60 | public function needsNormalization(string $format, array $context = []): bool 61 | { 62 | $encoder = $this->getEncoder($format, $context); 63 | 64 | if ($encoder instanceof TraceableEncoder) { 65 | return $encoder->needsNormalization(); 66 | } 67 | 68 | if (!$encoder instanceof NormalizationAwareInterface) { 69 | return true; 70 | } 71 | 72 | if ($encoder instanceof self) { 73 | return $encoder->needsNormalization($format, $context); 74 | } 75 | 76 | return false; 77 | } 78 | 79 | /** 80 | * Gets the encoder supporting the format. 81 | * 82 | * @throws RuntimeException if no encoder is found 83 | */ 84 | private function getEncoder(string $format, array $context): EncoderInterface 85 | { 86 | if (isset($this->encoderByFormat[$format]) 87 | && isset($this->encoders[$this->encoderByFormat[$format]]) 88 | ) { 89 | return $this->encoders[$this->encoderByFormat[$format]]; 90 | } 91 | 92 | $cache = true; 93 | foreach ($this->encoders as $i => $encoder) { 94 | $cache = $cache && !$encoder instanceof ContextAwareEncoderInterface; 95 | if ($encoder->supportsEncoding($format, $context)) { 96 | if ($cache) { 97 | $this->encoderByFormat[$format] = $i; 98 | } 99 | 100 | return $encoder; 101 | } 102 | } 103 | 104 | throw new RuntimeException(\sprintf('No encoder found for format "%s".', $format)); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /Encoder/ContextAwareDecoderInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\Encoder; 13 | 14 | /** 15 | * Adds the support of an extra $context parameter for the supportsDecoding method. 16 | * 17 | * @author Kévin Dunglas 18 | */ 19 | interface ContextAwareDecoderInterface extends DecoderInterface 20 | { 21 | /** 22 | * @param array $context options that decoders have access to 23 | */ 24 | public function supportsDecoding(string $format, array $context = []): bool; 25 | } 26 | -------------------------------------------------------------------------------- /Encoder/ContextAwareEncoderInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\Encoder; 13 | 14 | /** 15 | * Adds the support of an extra $context parameter for the supportsEncoding method. 16 | * 17 | * @author Kévin Dunglas 18 | */ 19 | interface ContextAwareEncoderInterface extends EncoderInterface 20 | { 21 | /** 22 | * @param array $context options that encoders have access to 23 | */ 24 | public function supportsEncoding(string $format, array $context = []): bool; 25 | } 26 | -------------------------------------------------------------------------------- /Encoder/DecoderInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\Encoder; 13 | 14 | use Symfony\Component\Serializer\Exception\UnexpectedValueException; 15 | 16 | /** 17 | * @author Jordi Boggiano 18 | */ 19 | interface DecoderInterface 20 | { 21 | /** 22 | * Decodes a string into PHP data. 23 | * 24 | * @param string $data Data to decode 25 | * @param string $format Format name 26 | * @param array $context Options that decoders have access to 27 | * 28 | * The format parameter specifies which format the data is in; valid values 29 | * depend on the specific implementation. Authors implementing this interface 30 | * are encouraged to document which formats they support in a non-inherited 31 | * phpdoc comment. 32 | * 33 | * @throws UnexpectedValueException 34 | */ 35 | public function decode(string $data, string $format, array $context = []): mixed; 36 | 37 | /** 38 | * Checks whether the deserializer can decode from given format. 39 | * 40 | * @param string $format Format name 41 | */ 42 | public function supportsDecoding(string $format): bool; 43 | } 44 | -------------------------------------------------------------------------------- /Encoder/EncoderInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\Encoder; 13 | 14 | use Symfony\Component\Serializer\Exception\UnexpectedValueException; 15 | 16 | /** 17 | * @author Jordi Boggiano 18 | */ 19 | interface EncoderInterface 20 | { 21 | /** 22 | * Encodes data into the given format. 23 | * 24 | * @param mixed $data Data to encode 25 | * @param string $format Format name 26 | * @param array $context Options that normalizers/encoders have access to 27 | * 28 | * @throws UnexpectedValueException 29 | */ 30 | public function encode(mixed $data, string $format, array $context = []): string; 31 | 32 | /** 33 | * Checks whether the serializer can encode to given format. 34 | * 35 | * @param string $format Format name 36 | */ 37 | public function supportsEncoding(string $format): bool; 38 | } 39 | -------------------------------------------------------------------------------- /Encoder/JsonDecode.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\Encoder; 13 | 14 | use Seld\JsonLint\JsonParser; 15 | use Symfony\Component\Serializer\Exception\NotEncodableValueException; 16 | use Symfony\Component\Serializer\Exception\UnsupportedException; 17 | 18 | /** 19 | * Decodes JSON data. 20 | * 21 | * @author Sander Coolen 22 | */ 23 | class JsonDecode implements DecoderInterface 24 | { 25 | /** 26 | * True to return the result as an associative array, false for a nested stdClass hierarchy. 27 | */ 28 | public const ASSOCIATIVE = 'json_decode_associative'; 29 | 30 | /** 31 | * True to enable seld/jsonlint as a source for more specific error messages when json_decode fails. 32 | */ 33 | public const DETAILED_ERROR_MESSAGES = 'json_decode_detailed_errors'; 34 | 35 | public const OPTIONS = 'json_decode_options'; 36 | 37 | /** 38 | * Specifies the recursion depth. 39 | */ 40 | public const RECURSION_DEPTH = 'json_decode_recursion_depth'; 41 | 42 | private array $defaultContext = [ 43 | self::ASSOCIATIVE => false, 44 | self::DETAILED_ERROR_MESSAGES => false, 45 | self::OPTIONS => 0, 46 | self::RECURSION_DEPTH => 512, 47 | ]; 48 | 49 | public function __construct(array $defaultContext = []) 50 | { 51 | $this->defaultContext = array_merge($this->defaultContext, $defaultContext); 52 | } 53 | 54 | /** 55 | * Decodes data. 56 | * 57 | * @param string $data The encoded JSON string to decode 58 | * @param string $format Must be set to JsonEncoder::FORMAT 59 | * @param array $context An optional set of options for the JSON decoder; see below 60 | * 61 | * The $context array is a simple key=>value array, with the following supported keys: 62 | * 63 | * json_decode_associative: boolean 64 | * If true, returns the object as an associative array. 65 | * If false, returns the object as nested stdClass 66 | * If not specified, this method will use the default set in JsonDecode::__construct 67 | * 68 | * json_decode_recursion_depth: integer 69 | * Specifies the maximum recursion depth 70 | * If not specified, this method will use the default set in JsonDecode::__construct 71 | * 72 | * json_decode_options: integer 73 | * Specifies additional options as per documentation for json_decode 74 | * 75 | * json_decode_detailed_errors: bool 76 | * If true, enables seld/jsonlint as a source for more specific error messages when json_decode fails. 77 | * If false or not specified, this method will use default error messages from PHP's json_decode 78 | * 79 | * @throws NotEncodableValueException 80 | * 81 | * @see https://php.net/json_decode 82 | */ 83 | public function decode(string $data, string $format, array $context = []): mixed 84 | { 85 | $associative = $context[self::ASSOCIATIVE] ?? $this->defaultContext[self::ASSOCIATIVE]; 86 | $recursionDepth = $context[self::RECURSION_DEPTH] ?? $this->defaultContext[self::RECURSION_DEPTH]; 87 | $options = $context[self::OPTIONS] ?? $this->defaultContext[self::OPTIONS]; 88 | 89 | try { 90 | $decodedData = json_decode($data, $associative, $recursionDepth, $options); 91 | } catch (\JsonException $e) { 92 | throw new NotEncodableValueException($e->getMessage(), 0, $e); 93 | } 94 | 95 | if (\JSON_THROW_ON_ERROR & $options) { 96 | return $decodedData; 97 | } 98 | 99 | if (\JSON_ERROR_NONE === json_last_error()) { 100 | return $decodedData; 101 | } 102 | $errorMessage = json_last_error_msg(); 103 | 104 | if (!($context[self::DETAILED_ERROR_MESSAGES] ?? $this->defaultContext[self::DETAILED_ERROR_MESSAGES])) { 105 | throw new NotEncodableValueException($errorMessage); 106 | } 107 | 108 | if (!class_exists(JsonParser::class)) { 109 | throw new UnsupportedException(\sprintf('Enabling "%s" serializer option requires seld/jsonlint. Try running "composer require seld/jsonlint".', self::DETAILED_ERROR_MESSAGES)); 110 | } 111 | 112 | throw new NotEncodableValueException((new JsonParser())->lint($data)?->getMessage() ?: $errorMessage); 113 | } 114 | 115 | public function supportsDecoding(string $format): bool 116 | { 117 | return JsonEncoder::FORMAT === $format; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /Encoder/JsonEncode.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\Encoder; 13 | 14 | use Symfony\Component\Serializer\Exception\NotEncodableValueException; 15 | 16 | /** 17 | * Encodes JSON data. 18 | * 19 | * @author Sander Coolen 20 | */ 21 | class JsonEncode implements EncoderInterface 22 | { 23 | /** 24 | * Configure the JSON flags bitmask. 25 | */ 26 | public const OPTIONS = 'json_encode_options'; 27 | 28 | private array $defaultContext = [ 29 | self::OPTIONS => \JSON_PRESERVE_ZERO_FRACTION, 30 | ]; 31 | 32 | public function __construct(array $defaultContext = []) 33 | { 34 | $this->defaultContext = array_merge($this->defaultContext, $defaultContext); 35 | } 36 | 37 | public function encode(mixed $data, string $format, array $context = []): string 38 | { 39 | $options = $context[self::OPTIONS] ?? $this->defaultContext[self::OPTIONS]; 40 | 41 | try { 42 | $encodedJson = json_encode($data, $options); 43 | } catch (\JsonException $e) { 44 | throw new NotEncodableValueException($e->getMessage(), 0, $e); 45 | } 46 | 47 | if (\JSON_THROW_ON_ERROR & $options) { 48 | return $encodedJson; 49 | } 50 | 51 | if (\JSON_ERROR_NONE !== json_last_error() && (false === $encodedJson || !($options & \JSON_PARTIAL_OUTPUT_ON_ERROR))) { 52 | throw new NotEncodableValueException(json_last_error_msg()); 53 | } 54 | 55 | return $encodedJson; 56 | } 57 | 58 | public function supportsEncoding(string $format): bool 59 | { 60 | return JsonEncoder::FORMAT === $format; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Encoder/JsonEncoder.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\Encoder; 13 | 14 | /** 15 | * Encodes JSON data. 16 | * 17 | * @author Jordi Boggiano 18 | */ 19 | class JsonEncoder implements EncoderInterface, DecoderInterface 20 | { 21 | public const FORMAT = 'json'; 22 | 23 | protected JsonEncode $encodingImpl; 24 | protected JsonDecode $decodingImpl; 25 | 26 | private array $defaultContext = [ 27 | JsonDecode::ASSOCIATIVE => true, 28 | ]; 29 | 30 | public function __construct(?JsonEncode $encodingImpl = null, ?JsonDecode $decodingImpl = null, array $defaultContext = []) 31 | { 32 | $this->defaultContext = array_merge($this->defaultContext, $defaultContext); 33 | $this->encodingImpl = $encodingImpl ?? new JsonEncode($this->defaultContext); 34 | $this->decodingImpl = $decodingImpl ?? new JsonDecode($this->defaultContext); 35 | } 36 | 37 | public function encode(mixed $data, string $format, array $context = []): string 38 | { 39 | $context = array_merge($this->defaultContext, $context); 40 | 41 | return $this->encodingImpl->encode($data, self::FORMAT, $context); 42 | } 43 | 44 | public function decode(string $data, string $format, array $context = []): mixed 45 | { 46 | $context = array_merge($this->defaultContext, $context); 47 | 48 | return $this->decodingImpl->decode($data, self::FORMAT, $context); 49 | } 50 | 51 | public function supportsEncoding(string $format): bool 52 | { 53 | return self::FORMAT === $format; 54 | } 55 | 56 | public function supportsDecoding(string $format): bool 57 | { 58 | return self::FORMAT === $format; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Encoder/NormalizationAwareInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\Encoder; 13 | 14 | /** 15 | * Defines the interface of encoders that will normalize data themselves. 16 | * 17 | * Implementing this interface essentially just tells the Serializer that the 18 | * data should not be pre-normalized before being passed to this Encoder. 19 | * 20 | * @author Jordi Boggiano 21 | */ 22 | interface NormalizationAwareInterface 23 | { 24 | } 25 | -------------------------------------------------------------------------------- /Encoder/YamlEncoder.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\Encoder; 13 | 14 | use Symfony\Component\Serializer\Exception\NotEncodableValueException; 15 | use Symfony\Component\Serializer\Exception\RuntimeException; 16 | use Symfony\Component\Yaml\Dumper; 17 | use Symfony\Component\Yaml\Exception\ParseException; 18 | use Symfony\Component\Yaml\Parser; 19 | use Symfony\Component\Yaml\Yaml; 20 | 21 | /** 22 | * Encodes YAML data. 23 | * 24 | * @author Kévin Dunglas 25 | */ 26 | class YamlEncoder implements EncoderInterface, DecoderInterface 27 | { 28 | public const FORMAT = 'yaml'; 29 | private const ALTERNATIVE_FORMAT = 'yml'; 30 | 31 | public const PRESERVE_EMPTY_OBJECTS = 'preserve_empty_objects'; 32 | 33 | /** 34 | * Override the amount of spaces to use for indentation of nested nodes. 35 | * 36 | * This option only works in the constructor, not in calls to `encode`. 37 | */ 38 | public const YAML_INDENTATION = 'yaml_indentation'; 39 | 40 | public const YAML_INLINE = 'yaml_inline'; 41 | /** 42 | * Initial indentation for root element. 43 | */ 44 | public const YAML_INDENT = 'yaml_indent'; 45 | public const YAML_FLAGS = 'yaml_flags'; 46 | 47 | private readonly Dumper $dumper; 48 | private readonly Parser $parser; 49 | private array $defaultContext = [ 50 | self::YAML_INLINE => 0, 51 | self::YAML_INDENT => 0, 52 | self::YAML_FLAGS => 0, 53 | ]; 54 | 55 | public function __construct(?Dumper $dumper = null, ?Parser $parser = null, array $defaultContext = []) 56 | { 57 | if (!class_exists(Dumper::class)) { 58 | throw new RuntimeException('The YamlEncoder class requires the "Yaml" component. Try running "composer require symfony/yaml".'); 59 | } 60 | 61 | if (!$dumper) { 62 | $dumper = \array_key_exists(self::YAML_INDENTATION, $defaultContext) ? new Dumper($defaultContext[self::YAML_INDENTATION]) : new Dumper(); 63 | } 64 | $this->dumper = $dumper; 65 | $this->parser = $parser ?? new Parser(); 66 | unset($defaultContext[self::YAML_INDENTATION]); 67 | $this->defaultContext = array_merge($this->defaultContext, $defaultContext); 68 | } 69 | 70 | public function encode(mixed $data, string $format, array $context = []): string 71 | { 72 | $context = array_merge($this->defaultContext, $context); 73 | 74 | if ($context[self::PRESERVE_EMPTY_OBJECTS] ?? false) { 75 | $context[self::YAML_FLAGS] |= Yaml::DUMP_OBJECT_AS_MAP; 76 | } 77 | 78 | return $this->dumper->dump($data, $context[self::YAML_INLINE], $context[self::YAML_INDENT], $context[self::YAML_FLAGS]); 79 | } 80 | 81 | public function supportsEncoding(string $format): bool 82 | { 83 | return self::FORMAT === $format || self::ALTERNATIVE_FORMAT === $format; 84 | } 85 | 86 | public function decode(string $data, string $format, array $context = []): mixed 87 | { 88 | $context = array_merge($this->defaultContext, $context); 89 | 90 | try { 91 | return $this->parser->parse($data, $context[self::YAML_FLAGS]); 92 | } catch (ParseException $e) { 93 | throw new NotEncodableValueException($e->getMessage(), $e->getCode(), $e); 94 | } 95 | } 96 | 97 | public function supportsDecoding(string $format): bool 98 | { 99 | return self::FORMAT === $format || self::ALTERNATIVE_FORMAT === $format; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /Exception/BadMethodCallException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\Exception; 13 | 14 | class BadMethodCallException extends \BadMethodCallException implements ExceptionInterface 15 | { 16 | } 17 | -------------------------------------------------------------------------------- /Exception/CircularReferenceException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\Exception; 13 | 14 | /** 15 | * CircularReferenceException. 16 | * 17 | * @author Kévin Dunglas 18 | */ 19 | class CircularReferenceException extends RuntimeException 20 | { 21 | } 22 | -------------------------------------------------------------------------------- /Exception/ExceptionInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\Exception; 13 | 14 | /** 15 | * Base exception interface. 16 | * 17 | * @author Johannes M. Schmitt 18 | */ 19 | interface ExceptionInterface extends \Throwable 20 | { 21 | } 22 | -------------------------------------------------------------------------------- /Exception/ExtraAttributesException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\Exception; 13 | 14 | /** 15 | * ExtraAttributesException. 16 | * 17 | * @author Julien DIDIER 18 | */ 19 | class ExtraAttributesException extends RuntimeException 20 | { 21 | public function __construct( 22 | private readonly array $extraAttributes, 23 | ?\Throwable $previous = null, 24 | ) { 25 | $msg = \sprintf('Extra attributes are not allowed ("%s" %s unknown).', implode('", "', $extraAttributes), \count($extraAttributes) > 1 ? 'are' : 'is'); 26 | 27 | parent::__construct($msg, 0, $previous); 28 | } 29 | 30 | /** 31 | * Get the extra attributes that are not allowed. 32 | */ 33 | public function getExtraAttributes(): array 34 | { 35 | return $this->extraAttributes; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Exception/InvalidArgumentException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\Exception; 13 | 14 | /** 15 | * InvalidArgumentException. 16 | * 17 | * @author Johannes M. Schmitt 18 | */ 19 | class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface 20 | { 21 | } 22 | -------------------------------------------------------------------------------- /Exception/LogicException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\Exception; 13 | 14 | /** 15 | * LogicException. 16 | * 17 | * @author Lukas Kahwe Smith 18 | */ 19 | class LogicException extends \LogicException implements ExceptionInterface 20 | { 21 | } 22 | -------------------------------------------------------------------------------- /Exception/MappingException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\Exception; 13 | 14 | /** 15 | * MappingException. 16 | * 17 | * @author Kévin Dunglas 18 | */ 19 | class MappingException extends RuntimeException 20 | { 21 | } 22 | -------------------------------------------------------------------------------- /Exception/MissingConstructorArgumentsException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\Exception; 13 | 14 | /** 15 | * @author Maxime VEBER 16 | */ 17 | class MissingConstructorArgumentsException extends RuntimeException 18 | { 19 | /** 20 | * @param string[] $missingArguments 21 | * @param class-string|null $class 22 | */ 23 | public function __construct( 24 | string $message, 25 | int $code = 0, 26 | ?\Throwable $previous = null, 27 | private array $missingArguments = [], 28 | private ?string $class = null, 29 | ) { 30 | parent::__construct($message, $code, $previous); 31 | } 32 | 33 | /** 34 | * @return string[] 35 | */ 36 | public function getMissingConstructorArguments(): array 37 | { 38 | return $this->missingArguments; 39 | } 40 | 41 | /** 42 | * @return class-string|null 43 | */ 44 | public function getClass(): ?string 45 | { 46 | return $this->class; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Exception/NotEncodableValueException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\Exception; 13 | 14 | /** 15 | * @author Christian Flothmann 16 | */ 17 | class NotEncodableValueException extends UnexpectedValueException 18 | { 19 | } 20 | -------------------------------------------------------------------------------- /Exception/NotNormalizableValueException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\Exception; 13 | 14 | /** 15 | * @author Christian Flothmann 16 | */ 17 | class NotNormalizableValueException extends UnexpectedValueException 18 | { 19 | private ?string $currentType = null; 20 | private ?array $expectedTypes = null; 21 | private ?string $path = null; 22 | private bool $useMessageForUser = false; 23 | 24 | /** 25 | * @param list $expectedTypes 26 | * @param bool $useMessageForUser If the message passed to this exception is something that can be shown 27 | * safely to your user. In other words, avoid catching other exceptions and 28 | * passing their message directly to this class. 29 | */ 30 | public static function createForUnexpectedDataType(string $message, mixed $data, array $expectedTypes, ?string $path = null, bool $useMessageForUser = false, int $code = 0, ?\Throwable $previous = null): self 31 | { 32 | $self = new self($message, $code, $previous); 33 | 34 | $self->currentType = get_debug_type($data); 35 | $self->expectedTypes = array_map(strval(...), $expectedTypes); 36 | $self->path = $path; 37 | $self->useMessageForUser = $useMessageForUser; 38 | 39 | return $self; 40 | } 41 | 42 | public function getCurrentType(): ?string 43 | { 44 | return $this->currentType; 45 | } 46 | 47 | /** 48 | * @return string[]|null 49 | */ 50 | public function getExpectedTypes(): ?array 51 | { 52 | return $this->expectedTypes; 53 | } 54 | 55 | public function getPath(): ?string 56 | { 57 | return $this->path; 58 | } 59 | 60 | public function canUseMessageForUser(): ?bool 61 | { 62 | return $this->useMessageForUser; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Exception/PartialDenormalizationException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\Exception; 13 | 14 | /** 15 | * @author Grégoire Pineau 16 | */ 17 | class PartialDenormalizationException extends UnexpectedValueException 18 | { 19 | /** 20 | * @param NotNormalizableValueException[] $errors 21 | */ 22 | public function __construct( 23 | private mixed $data, 24 | private array $errors, 25 | ) { 26 | } 27 | 28 | public function getData(): mixed 29 | { 30 | return $this->data; 31 | } 32 | 33 | /** 34 | * @return NotNormalizableValueException[] 35 | */ 36 | public function getErrors(): array 37 | { 38 | return $this->errors; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Exception/RuntimeException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\Exception; 13 | 14 | /** 15 | * RuntimeException. 16 | * 17 | * @author Johannes M. Schmitt 18 | */ 19 | class RuntimeException extends \RuntimeException implements ExceptionInterface 20 | { 21 | } 22 | -------------------------------------------------------------------------------- /Exception/UnexpectedPropertyException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\Exception; 13 | 14 | /** 15 | * UnexpectedPropertyException. 16 | * 17 | * @author Aurélien Pillevesse 18 | */ 19 | class UnexpectedPropertyException extends \UnexpectedValueException implements ExceptionInterface 20 | { 21 | public function __construct( 22 | public readonly string $property, 23 | ?\Throwable $previous = null, 24 | ) { 25 | $msg = \sprintf('Property is not allowed ("%s" is unknown).', $this->property); 26 | 27 | parent::__construct($msg, 0, $previous); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Exception/UnexpectedValueException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\Exception; 13 | 14 | /** 15 | * UnexpectedValueException. 16 | * 17 | * @author Lukas Kahwe Smith 18 | */ 19 | class UnexpectedValueException extends \UnexpectedValueException implements ExceptionInterface 20 | { 21 | } 22 | -------------------------------------------------------------------------------- /Exception/UnsupportedException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\Exception; 13 | 14 | /** 15 | * UnsupportedException. 16 | * 17 | * @author Johannes M. Schmitt 18 | */ 19 | class UnsupportedException extends InvalidArgumentException 20 | { 21 | } 22 | -------------------------------------------------------------------------------- /Exception/UnsupportedFormatException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\Exception; 13 | 14 | /** 15 | * @author Konstantin Myakshin 16 | */ 17 | class UnsupportedFormatException extends NotEncodableValueException 18 | { 19 | } 20 | -------------------------------------------------------------------------------- /Extractor/ObjectPropertyListExtractor.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\Extractor; 13 | 14 | use Symfony\Component\PropertyInfo\PropertyListExtractorInterface; 15 | 16 | /** 17 | * @author David Maicher 18 | */ 19 | final class ObjectPropertyListExtractor implements ObjectPropertyListExtractorInterface 20 | { 21 | private \Closure $objectClassResolver; 22 | 23 | public function __construct( 24 | private PropertyListExtractorInterface $propertyListExtractor, 25 | ?callable $objectClassResolver = null, 26 | ) { 27 | $this->objectClassResolver = ($objectClassResolver ?? 'get_class')(...); 28 | } 29 | 30 | public function getProperties(object $object, array $context = []): ?array 31 | { 32 | $class = ($this->objectClassResolver)($object); 33 | 34 | return $this->propertyListExtractor->getProperties($class, $context); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Extractor/ObjectPropertyListExtractorInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\Extractor; 13 | 14 | /** 15 | * @author David Maicher 16 | */ 17 | interface ObjectPropertyListExtractorInterface 18 | { 19 | /** 20 | * Gets the list of properties available for the given object. 21 | * 22 | * @return string[]|null 23 | */ 24 | public function getProperties(object $object, array $context = []): ?array; 25 | } 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2004-present Fabien Potencier 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Mapping/AttributeMetadataInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\Mapping; 13 | 14 | use Symfony\Component\PropertyAccess\PropertyPath; 15 | 16 | /** 17 | * Stores metadata needed for serializing and deserializing attributes. 18 | * 19 | * Primarily, the metadata stores serialization groups. 20 | * 21 | * @internal 22 | * 23 | * @author Kévin Dunglas 24 | */ 25 | interface AttributeMetadataInterface 26 | { 27 | /** 28 | * Gets the attribute name. 29 | */ 30 | public function getName(): string; 31 | 32 | /** 33 | * Adds this attribute to the given group. 34 | */ 35 | public function addGroup(string $group): void; 36 | 37 | /** 38 | * Gets groups of this attribute. 39 | * 40 | * @return string[] 41 | */ 42 | public function getGroups(): array; 43 | 44 | /** 45 | * Sets the serialization max depth for this attribute. 46 | */ 47 | public function setMaxDepth(?int $maxDepth): void; 48 | 49 | /** 50 | * Gets the serialization max depth for this attribute. 51 | */ 52 | public function getMaxDepth(): ?int; 53 | 54 | /** 55 | * Sets the serialization name for this attribute. 56 | */ 57 | public function setSerializedName(?string $serializedName): void; 58 | 59 | /** 60 | * Gets the serialization name for this attribute. 61 | */ 62 | public function getSerializedName(): ?string; 63 | 64 | public function setSerializedPath(?PropertyPath $serializedPath): void; 65 | 66 | public function getSerializedPath(): ?PropertyPath; 67 | 68 | /** 69 | * Sets if this attribute must be ignored or not. 70 | */ 71 | public function setIgnore(bool $ignore): void; 72 | 73 | /** 74 | * Gets if this attribute is ignored or not. 75 | */ 76 | public function isIgnored(): bool; 77 | 78 | /** 79 | * Merges an {@see AttributeMetadataInterface} with in the current one. 80 | */ 81 | public function merge(self $attributeMetadata): void; 82 | 83 | /** 84 | * Gets all the normalization contexts per group ("*" being the base context applied to all groups). 85 | */ 86 | public function getNormalizationContexts(): array; 87 | 88 | /** 89 | * Gets the computed normalization contexts for given groups. 90 | */ 91 | public function getNormalizationContextForGroups(array $groups): array; 92 | 93 | /** 94 | * Sets the normalization context for given groups. 95 | */ 96 | public function setNormalizationContextForGroups(array $context, array $groups = []): void; 97 | 98 | /** 99 | * Gets all the denormalization contexts per group ("*" being the base context applied to all groups). 100 | */ 101 | public function getDenormalizationContexts(): array; 102 | 103 | /** 104 | * Gets the computed denormalization contexts for given groups. 105 | */ 106 | public function getDenormalizationContextForGroups(array $groups): array; 107 | 108 | /** 109 | * Sets the denormalization context for given groups. 110 | */ 111 | public function setDenormalizationContextForGroups(array $context, array $groups = []): void; 112 | } 113 | -------------------------------------------------------------------------------- /Mapping/ClassDiscriminatorFromClassMetadata.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\Mapping; 13 | 14 | use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface; 15 | 16 | /** 17 | * @author Samuel Roze 18 | */ 19 | class ClassDiscriminatorFromClassMetadata implements ClassDiscriminatorResolverInterface 20 | { 21 | private array $mappingForMappedObjectCache = []; 22 | 23 | public function __construct( 24 | private readonly ClassMetadataFactoryInterface $classMetadataFactory, 25 | ) { 26 | } 27 | 28 | public function getMappingForClass(string $class): ?ClassDiscriminatorMapping 29 | { 30 | if ($this->classMetadataFactory->hasMetadataFor($class)) { 31 | return $this->classMetadataFactory->getMetadataFor($class)->getClassDiscriminatorMapping(); 32 | } 33 | 34 | return null; 35 | } 36 | 37 | public function getMappingForMappedObject(object|string $object): ?ClassDiscriminatorMapping 38 | { 39 | if ($this->classMetadataFactory->hasMetadataFor($object)) { 40 | $metadata = $this->classMetadataFactory->getMetadataFor($object); 41 | 42 | if (null !== $metadata->getClassDiscriminatorMapping()) { 43 | return $metadata->getClassDiscriminatorMapping(); 44 | } 45 | } 46 | 47 | $cacheKey = \is_object($object) ? $object::class : $object; 48 | if (!\array_key_exists($cacheKey, $this->mappingForMappedObjectCache)) { 49 | $this->mappingForMappedObjectCache[$cacheKey] = $this->resolveMappingForMappedObject($object); 50 | } 51 | 52 | return $this->mappingForMappedObjectCache[$cacheKey]; 53 | } 54 | 55 | public function getTypeForMappedObject(object|string $object): ?string 56 | { 57 | if (null === $mapping = $this->getMappingForMappedObject($object)) { 58 | return null; 59 | } 60 | 61 | return $mapping->getMappedObjectType($object); 62 | } 63 | 64 | private function resolveMappingForMappedObject(object|string $object): ?ClassDiscriminatorMapping 65 | { 66 | $reflectionClass = new \ReflectionClass($object); 67 | if ($parentClass = $reflectionClass->getParentClass()) { 68 | if (null !== ($parentMapping = $this->getMappingForMappedObject($parentClass->getName()))) { 69 | return $parentMapping; 70 | } 71 | } 72 | 73 | foreach ($reflectionClass->getInterfaceNames() as $interfaceName) { 74 | if (null !== ($interfaceMapping = $this->getMappingForMappedObject($interfaceName))) { 75 | return $interfaceMapping; 76 | } 77 | } 78 | 79 | return null; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Mapping/ClassDiscriminatorMapping.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\Mapping; 13 | 14 | /** 15 | * @author Samuel Roze 16 | */ 17 | class ClassDiscriminatorMapping 18 | { 19 | /** 20 | * @param array $typesMapping 21 | */ 22 | public function __construct( 23 | private readonly string $typeProperty, 24 | private array $typesMapping = [], 25 | private readonly ?string $defaultType = null, 26 | ) { 27 | uasort($this->typesMapping, static function (string $a, string $b): int { 28 | if (is_a($a, $b, true)) { 29 | return -1; 30 | } 31 | 32 | if (is_a($b, $a, true)) { 33 | return 1; 34 | } 35 | 36 | return 0; 37 | }); 38 | } 39 | 40 | public function getTypeProperty(): string 41 | { 42 | return $this->typeProperty; 43 | } 44 | 45 | public function getClassForType(string $type): ?string 46 | { 47 | return $this->typesMapping[$type] ?? null; 48 | } 49 | 50 | public function getMappedObjectType(object|string $object): ?string 51 | { 52 | foreach ($this->typesMapping as $type => $typeClass) { 53 | if (is_a($object, $typeClass, true)) { 54 | return $type; 55 | } 56 | } 57 | 58 | return null; 59 | } 60 | 61 | public function getTypesMapping(): array 62 | { 63 | return $this->typesMapping; 64 | } 65 | 66 | public function getDefaultType(): ?string 67 | { 68 | return $this->defaultType; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Mapping/ClassDiscriminatorResolverInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\Mapping; 13 | 14 | /** 15 | * Knows how to get the class discriminator mapping for classes and objects. 16 | * 17 | * @author Samuel Roze 18 | */ 19 | interface ClassDiscriminatorResolverInterface 20 | { 21 | public function getMappingForClass(string $class): ?ClassDiscriminatorMapping; 22 | 23 | public function getMappingForMappedObject(object|string $object): ?ClassDiscriminatorMapping; 24 | 25 | public function getTypeForMappedObject(object|string $object): ?string; 26 | } 27 | -------------------------------------------------------------------------------- /Mapping/ClassMetadata.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\Mapping; 13 | 14 | /** 15 | * @author Kévin Dunglas 16 | */ 17 | class ClassMetadata implements ClassMetadataInterface 18 | { 19 | /** 20 | * @internal This property is public in order to reduce the size of the 21 | * class' serialized representation. Do not access it. Use 22 | * {@link getName()} instead. 23 | */ 24 | public string $name; 25 | 26 | /** 27 | * @var AttributeMetadataInterface[] 28 | * 29 | * @internal This property is public in order to reduce the size of the 30 | * class' serialized representation. Do not access it. Use 31 | * {@link getAttributesMetadata()} instead. 32 | */ 33 | public array $attributesMetadata = []; 34 | 35 | private ?\ReflectionClass $reflClass = null; 36 | 37 | /** 38 | * @internal This property is public in order to reduce the size of the 39 | * class' serialized representation. Do not access it. Use 40 | * {@link getClassDiscriminatorMapping()} instead. 41 | */ 42 | public ?ClassDiscriminatorMapping $classDiscriminatorMapping = null; 43 | 44 | /** 45 | * Constructs a metadata for the given class. 46 | */ 47 | public function __construct(string $class, ?ClassDiscriminatorMapping $classDiscriminatorMapping = null) 48 | { 49 | $this->name = $class; 50 | $this->classDiscriminatorMapping = $classDiscriminatorMapping; 51 | } 52 | 53 | public function getName(): string 54 | { 55 | return $this->name; 56 | } 57 | 58 | public function addAttributeMetadata(AttributeMetadataInterface $attributeMetadata): void 59 | { 60 | $this->attributesMetadata[$attributeMetadata->getName()] = $attributeMetadata; 61 | } 62 | 63 | public function getAttributesMetadata(): array 64 | { 65 | return $this->attributesMetadata; 66 | } 67 | 68 | public function merge(ClassMetadataInterface $classMetadata): void 69 | { 70 | foreach ($classMetadata->getAttributesMetadata() as $attributeMetadata) { 71 | if (isset($this->attributesMetadata[$attributeMetadata->getName()])) { 72 | $this->attributesMetadata[$attributeMetadata->getName()]->merge($attributeMetadata); 73 | } else { 74 | $this->addAttributeMetadata($attributeMetadata); 75 | } 76 | } 77 | } 78 | 79 | public function getReflectionClass(): \ReflectionClass 80 | { 81 | return $this->reflClass ??= new \ReflectionClass($this->getName()); 82 | } 83 | 84 | public function getClassDiscriminatorMapping(): ?ClassDiscriminatorMapping 85 | { 86 | return $this->classDiscriminatorMapping; 87 | } 88 | 89 | public function setClassDiscriminatorMapping(?ClassDiscriminatorMapping $mapping): void 90 | { 91 | $this->classDiscriminatorMapping = $mapping; 92 | } 93 | 94 | /** 95 | * Returns the names of the properties that should be serialized. 96 | * 97 | * @return string[] 98 | */ 99 | public function __sleep(): array 100 | { 101 | return [ 102 | 'name', 103 | 'attributesMetadata', 104 | 'classDiscriminatorMapping', 105 | ]; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /Mapping/ClassMetadataInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\Mapping; 13 | 14 | /** 15 | * Stores metadata needed for serializing and deserializing objects of specific class. 16 | * 17 | * Primarily, the metadata stores the set of attributes to serialize or deserialize. 18 | * 19 | * There may only exist one metadata for each attribute according to its name. 20 | * 21 | * @internal 22 | * 23 | * @author Kévin Dunglas 24 | */ 25 | interface ClassMetadataInterface 26 | { 27 | /** 28 | * Returns the name of the backing PHP class. 29 | */ 30 | public function getName(): string; 31 | 32 | /** 33 | * Adds an {@link AttributeMetadataInterface}. 34 | */ 35 | public function addAttributeMetadata(AttributeMetadataInterface $attributeMetadata): void; 36 | 37 | /** 38 | * Gets the list of {@link AttributeMetadataInterface}. 39 | * 40 | * @return array 41 | */ 42 | public function getAttributesMetadata(): array; 43 | 44 | /** 45 | * Merges a {@link ClassMetadataInterface} in the current one. 46 | */ 47 | public function merge(self $classMetadata): void; 48 | 49 | /** 50 | * Returns a {@link \ReflectionClass} instance for this class. 51 | */ 52 | public function getReflectionClass(): \ReflectionClass; 53 | 54 | public function getClassDiscriminatorMapping(): ?ClassDiscriminatorMapping; 55 | 56 | public function setClassDiscriminatorMapping(?ClassDiscriminatorMapping $mapping): void; 57 | } 58 | -------------------------------------------------------------------------------- /Mapping/Factory/CacheClassMetadataFactory.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\Mapping\Factory; 13 | 14 | use Psr\Cache\CacheItemPoolInterface; 15 | use Symfony\Component\Serializer\Mapping\ClassMetadataInterface; 16 | 17 | /** 18 | * Caches metadata using a PSR-6 implementation. 19 | * 20 | * @author Kévin Dunglas 21 | */ 22 | class CacheClassMetadataFactory implements ClassMetadataFactoryInterface 23 | { 24 | use ClassResolverTrait; 25 | 26 | /** 27 | * @var array 28 | */ 29 | private array $loadedClasses = []; 30 | 31 | public function __construct( 32 | private readonly ClassMetadataFactoryInterface $decorated, 33 | private readonly CacheItemPoolInterface $cacheItemPool, 34 | ) { 35 | } 36 | 37 | public function getMetadataFor(string|object $value): ClassMetadataInterface 38 | { 39 | $class = $this->getClass($value); 40 | 41 | if (isset($this->loadedClasses[$class])) { 42 | return $this->loadedClasses[$class]; 43 | } 44 | 45 | $key = rawurlencode(strtr($class, '\\', '_')); 46 | 47 | $item = $this->cacheItemPool->getItem($key); 48 | if ($item->isHit()) { 49 | return $this->loadedClasses[$class] = $item->get(); 50 | } 51 | 52 | $metadata = $this->decorated->getMetadataFor($value); 53 | $this->cacheItemPool->save($item->set($metadata)); 54 | 55 | return $this->loadedClasses[$class] = $metadata; 56 | } 57 | 58 | public function hasMetadataFor(mixed $value): bool 59 | { 60 | return $this->decorated->hasMetadataFor($value); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Mapping/Factory/ClassMetadataFactory.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\Mapping\Factory; 13 | 14 | use Symfony\Component\Serializer\Mapping\ClassMetadata; 15 | use Symfony\Component\Serializer\Mapping\ClassMetadataInterface; 16 | use Symfony\Component\Serializer\Mapping\Loader\LoaderInterface; 17 | 18 | /** 19 | * Returns a {@link ClassMetadata}. 20 | * 21 | * @author Kévin Dunglas 22 | */ 23 | class ClassMetadataFactory implements ClassMetadataFactoryInterface 24 | { 25 | use ClassResolverTrait; 26 | 27 | /** 28 | * @var array 29 | */ 30 | private array $loadedClasses; 31 | 32 | public function __construct( 33 | private readonly LoaderInterface $loader, 34 | ) { 35 | } 36 | 37 | public function getMetadataFor(string|object $value): ClassMetadataInterface 38 | { 39 | $class = $this->getClass($value); 40 | 41 | if (isset($this->loadedClasses[$class])) { 42 | return $this->loadedClasses[$class]; 43 | } 44 | 45 | $classMetadata = new ClassMetadata($class); 46 | $this->loader->loadClassMetadata($classMetadata); 47 | 48 | $reflectionClass = $classMetadata->getReflectionClass(); 49 | 50 | // Include metadata from the parent class 51 | if ($parent = $reflectionClass->getParentClass()) { 52 | $classMetadata->merge($this->getMetadataFor($parent->name)); 53 | } 54 | 55 | // Include metadata from all implemented interfaces 56 | foreach ($reflectionClass->getInterfaces() as $interface) { 57 | $classMetadata->merge($this->getMetadataFor($interface->name)); 58 | } 59 | 60 | return $this->loadedClasses[$class] = $classMetadata; 61 | } 62 | 63 | public function hasMetadataFor(mixed $value): bool 64 | { 65 | return \is_object($value) || (\is_string($value) && (class_exists($value) || interface_exists($value, false))); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Mapping/Factory/ClassMetadataFactoryCompiler.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\Mapping\Factory; 13 | 14 | use Symfony\Component\Serializer\Mapping\ClassMetadataInterface; 15 | use Symfony\Component\VarExporter\VarExporter; 16 | 17 | /** 18 | * @author Fabien Bourigault 19 | */ 20 | final class ClassMetadataFactoryCompiler 21 | { 22 | /** 23 | * @param ClassMetadataInterface[] $classMetadatas 24 | */ 25 | public function compile(array $classMetadatas): string 26 | { 27 | return <<generateDeclaredClassMetadata($classMetadatas)} 33 | ]; 34 | EOF; 35 | } 36 | 37 | /** 38 | * @param ClassMetadataInterface[] $classMetadatas 39 | */ 40 | private function generateDeclaredClassMetadata(array $classMetadatas): string 41 | { 42 | $compiled = ''; 43 | 44 | foreach ($classMetadatas as $classMetadata) { 45 | $attributesMetadata = []; 46 | foreach ($classMetadata->getAttributesMetadata() as $attributeMetadata) { 47 | $attributesMetadata[$attributeMetadata->getName()] = [ 48 | $attributeMetadata->getGroups(), 49 | $attributeMetadata->getMaxDepth(), 50 | $attributeMetadata->getSerializedName(), 51 | $attributeMetadata->getSerializedPath(), 52 | ]; 53 | } 54 | 55 | $classDiscriminatorMapping = $classMetadata->getClassDiscriminatorMapping() ? [ 56 | $classMetadata->getClassDiscriminatorMapping()->getTypeProperty(), 57 | $classMetadata->getClassDiscriminatorMapping()->getTypesMapping(), 58 | $classMetadata->getClassDiscriminatorMapping()->getDefaultType(), 59 | ] : null; 60 | 61 | $compiled .= \sprintf("\n'%s' => %s,", $classMetadata->getName(), VarExporter::export([ 62 | $attributesMetadata, 63 | $classDiscriminatorMapping, 64 | ])); 65 | } 66 | 67 | return $compiled; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Mapping/Factory/ClassMetadataFactoryInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\Mapping\Factory; 13 | 14 | use Symfony\Component\Serializer\Exception\InvalidArgumentException; 15 | use Symfony\Component\Serializer\Mapping\ClassMetadataInterface; 16 | 17 | /** 18 | * Returns a {@see ClassMetadataInterface}. 19 | * 20 | * @author Kévin Dunglas 21 | */ 22 | interface ClassMetadataFactoryInterface 23 | { 24 | /** 25 | * If the method was called with the same class name (or an object of that 26 | * class) before, the same metadata instance is returned. 27 | * 28 | * If the factory was configured with a cache, this method will first look 29 | * for an existing metadata instance in the cache. If an existing instance 30 | * is found, it will be returned without further ado. 31 | * 32 | * Otherwise, a new metadata instance is created. If the factory was 33 | * configured with a loader, the metadata is passed to the 34 | * {@link \Symfony\Component\Serializer\Mapping\Loader\LoaderInterface::loadClassMetadata()} method for further 35 | * configuration. At last, the new object is returned. 36 | * 37 | * @throws InvalidArgumentException 38 | */ 39 | public function getMetadataFor(string|object $value): ClassMetadataInterface; 40 | 41 | /** 42 | * Checks if class has metadata. 43 | */ 44 | public function hasMetadataFor(mixed $value): bool; 45 | } 46 | -------------------------------------------------------------------------------- /Mapping/Factory/ClassResolverTrait.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\Mapping\Factory; 13 | 14 | use Symfony\Component\Serializer\Exception\InvalidArgumentException; 15 | 16 | /** 17 | * Resolves a class name. 18 | * 19 | * @internal 20 | * 21 | * @author Kévin Dunglas 22 | */ 23 | trait ClassResolverTrait 24 | { 25 | /** 26 | * Gets a class name for a given class or instance. 27 | * 28 | * @throws InvalidArgumentException If the class does not exist 29 | */ 30 | private function getClass(object|string $value): string 31 | { 32 | if (\is_string($value)) { 33 | if (!class_exists($value) && !interface_exists($value, false)) { 34 | throw new InvalidArgumentException(\sprintf('The class or interface "%s" does not exist.', $value)); 35 | } 36 | 37 | return ltrim($value, '\\'); 38 | } 39 | 40 | return $value::class; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Mapping/Factory/CompiledClassMetadataFactory.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\Mapping\Factory; 13 | 14 | use Symfony\Component\Serializer\Mapping\AttributeMetadata; 15 | use Symfony\Component\Serializer\Mapping\ClassDiscriminatorMapping; 16 | use Symfony\Component\Serializer\Mapping\ClassMetadata; 17 | use Symfony\Component\Serializer\Mapping\ClassMetadataInterface; 18 | 19 | trigger_deprecation('symfony/serializer', '7.3', 'The "%s" class is deprecated.', CompiledClassMetadataFactory::class); 20 | 21 | /** 22 | * @author Fabien Bourigault 23 | * 24 | * @deprecated since Symfony 7.3 25 | */ 26 | final class CompiledClassMetadataFactory implements ClassMetadataFactoryInterface 27 | { 28 | private array $compiledClassMetadata = []; 29 | 30 | private array $loadedClasses = []; 31 | 32 | public function __construct( 33 | string $compiledClassMetadataFile, 34 | private readonly ClassMetadataFactoryInterface $classMetadataFactory, 35 | ) { 36 | if (!file_exists($compiledClassMetadataFile)) { 37 | throw new \RuntimeException("File \"{$compiledClassMetadataFile}\" could not be found."); 38 | } 39 | 40 | $compiledClassMetadata = require $compiledClassMetadataFile; 41 | if (!\is_array($compiledClassMetadata)) { 42 | throw new \RuntimeException(\sprintf('Compiled metadata must be of the type array, %s given.', \gettype($compiledClassMetadata))); 43 | } 44 | 45 | $this->compiledClassMetadata = $compiledClassMetadata; 46 | } 47 | 48 | public function getMetadataFor(string|object $value): ClassMetadataInterface 49 | { 50 | $className = \is_object($value) ? $value::class : $value; 51 | 52 | if (!isset($this->compiledClassMetadata[$className])) { 53 | return $this->classMetadataFactory->getMetadataFor($value); 54 | } 55 | 56 | if (!isset($this->loadedClasses[$className])) { 57 | $classMetadata = new ClassMetadata($className); 58 | foreach ($this->compiledClassMetadata[$className][0] as $name => $compiledAttributesMetadata) { 59 | $classMetadata->attributesMetadata[$name] = $attributeMetadata = new AttributeMetadata($name); 60 | [$attributeMetadata->groups, $attributeMetadata->maxDepth, $attributeMetadata->serializedName] = $compiledAttributesMetadata; 61 | } 62 | $classMetadata->classDiscriminatorMapping = $this->compiledClassMetadata[$className][1] 63 | ? new ClassDiscriminatorMapping(...$this->compiledClassMetadata[$className][1]) 64 | : null 65 | ; 66 | 67 | $this->loadedClasses[$className] = $classMetadata; 68 | } 69 | 70 | return $this->loadedClasses[$className]; 71 | } 72 | 73 | public function hasMetadataFor(mixed $value): bool 74 | { 75 | $className = \is_object($value) ? $value::class : $value; 76 | 77 | return isset($this->compiledClassMetadata[$className]) || $this->classMetadataFactory->hasMetadataFor($value); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Mapping/Loader/FileLoader.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\Mapping\Loader; 13 | 14 | use Symfony\Component\Serializer\Exception\MappingException; 15 | 16 | /** 17 | * Base class for all file based loaders. 18 | * 19 | * @author Kévin Dunglas 20 | */ 21 | abstract class FileLoader implements LoaderInterface 22 | { 23 | /** 24 | * @param string $file The mapping file to load 25 | * 26 | * @throws MappingException if the mapping file does not exist or is not readable 27 | */ 28 | public function __construct( 29 | protected string $file, 30 | ) { 31 | if (!is_file($file)) { 32 | throw new MappingException(\sprintf('The mapping file "%s" does not exist.', $file)); 33 | } 34 | 35 | if (!is_readable($file)) { 36 | throw new MappingException(\sprintf('The mapping file "%s" is not readable.', $file)); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Mapping/Loader/LoaderChain.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\Mapping\Loader; 13 | 14 | use Symfony\Component\Serializer\Exception\MappingException; 15 | use Symfony\Component\Serializer\Mapping\ClassMetadataInterface; 16 | 17 | /** 18 | * Calls multiple {@link LoaderInterface} instances in a chain. 19 | * 20 | * This class accepts multiple instances of LoaderInterface to be passed to the 21 | * constructor. When {@link loadClassMetadata()} is called, the same method is called 22 | * in all of these loaders, regardless of whether any of them was 23 | * successful or not. 24 | * 25 | * @author Bernhard Schussek 26 | * @author Kévin Dunglas 27 | */ 28 | class LoaderChain implements LoaderInterface 29 | { 30 | /** 31 | * Accepts a list of LoaderInterface instances. 32 | * 33 | * @param LoaderInterface[] $loaders An array of LoaderInterface instances 34 | * 35 | * @throws MappingException If any of the loaders does not implement LoaderInterface 36 | */ 37 | public function __construct(private readonly array $loaders) 38 | { 39 | foreach ($loaders as $loader) { 40 | if (!$loader instanceof LoaderInterface) { 41 | throw new MappingException(\sprintf('Class "%s" is expected to implement LoaderInterface.', get_debug_type($loader))); 42 | } 43 | } 44 | } 45 | 46 | public function loadClassMetadata(ClassMetadataInterface $metadata): bool 47 | { 48 | $success = false; 49 | 50 | foreach ($this->loaders as $loader) { 51 | $success = $loader->loadClassMetadata($metadata) || $success; 52 | } 53 | 54 | return $success; 55 | } 56 | 57 | /** 58 | * @return LoaderInterface[] 59 | */ 60 | public function getLoaders(): array 61 | { 62 | return $this->loaders; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Mapping/Loader/LoaderInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\Mapping\Loader; 13 | 14 | use Symfony\Component\Serializer\Mapping\ClassMetadataInterface; 15 | 16 | /** 17 | * Loads {@link ClassMetadataInterface}. 18 | * 19 | * @author Kévin Dunglas 20 | */ 21 | interface LoaderInterface 22 | { 23 | public function loadClassMetadata(ClassMetadataInterface $classMetadata): bool; 24 | } 25 | -------------------------------------------------------------------------------- /Mapping/Loader/schema/dic/serializer-mapping/serializer-mapping-1.0.xsd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | -------------------------------------------------------------------------------- /NameConverter/AdvancedNameConverterInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\NameConverter; 13 | 14 | /** 15 | * Gives access to the class, the format and the context in the property name converters. 16 | * 17 | * @author Kévin Dunglas 18 | * 19 | * @deprecated since Symfony 7.2, use NameConverterInterface instead 20 | */ 21 | interface AdvancedNameConverterInterface extends NameConverterInterface 22 | { 23 | public function normalize(string $propertyName, ?string $class = null, ?string $format = null, array $context = []): string; 24 | 25 | public function denormalize(string $propertyName, ?string $class = null, ?string $format = null, array $context = []): string; 26 | } 27 | -------------------------------------------------------------------------------- /NameConverter/CamelCaseToSnakeCaseNameConverter.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\NameConverter; 13 | 14 | use Symfony\Component\Serializer\Exception\UnexpectedPropertyException; 15 | 16 | /** 17 | * CamelCase to Underscore name converter. 18 | * 19 | * @author Kévin Dunglas 20 | * @author Aurélien Pillevesse 21 | */ 22 | class CamelCaseToSnakeCaseNameConverter implements NameConverterInterface 23 | { 24 | /** 25 | * Require all properties to be written in snake_case. 26 | */ 27 | public const REQUIRE_SNAKE_CASE_PROPERTIES = 'require_snake_case_properties'; 28 | 29 | /** 30 | * @param string[]|null $attributes The list of attributes to rename or null for all attributes 31 | * @param bool $lowerCamelCase Use lowerCamelCase style 32 | */ 33 | public function __construct( 34 | private ?array $attributes = null, 35 | private bool $lowerCamelCase = true, 36 | ) { 37 | } 38 | 39 | /** 40 | * @param class-string|null $class 41 | * @param string|null $format 42 | * @param array $context 43 | */ 44 | public function normalize(string $propertyName/* , ?string $class = null, ?string $format = null, array $context = [] */): string 45 | { 46 | if (null === $this->attributes || \in_array($propertyName, $this->attributes, true)) { 47 | return strtolower(preg_replace('/[A-Z]/', '_\\0', lcfirst($propertyName))); 48 | } 49 | 50 | return $propertyName; 51 | } 52 | 53 | /** 54 | * @param class-string|null $class 55 | * @param string|null $format 56 | * @param array $context 57 | */ 58 | public function denormalize(string $propertyName/* , ?string $class = null, ?string $format = null, array $context = [] */): string 59 | { 60 | $class = 1 < \func_num_args() ? func_get_arg(1) : null; 61 | $format = 2 < \func_num_args() ? func_get_arg(2) : null; 62 | $context = 3 < \func_num_args() ? func_get_arg(3) : []; 63 | 64 | if (($context[self::REQUIRE_SNAKE_CASE_PROPERTIES] ?? false) && $propertyName !== $this->normalize($propertyName, $class, $format, $context)) { 65 | throw new UnexpectedPropertyException($propertyName); 66 | } 67 | 68 | $camelCasedName = preg_replace_callback('/(^|_|\.)+(.)/', fn ($match) => ('.' === $match[1] ? '_' : '').strtoupper($match[2]), $propertyName); 69 | 70 | if ($this->lowerCamelCase) { 71 | $camelCasedName = lcfirst($camelCasedName); 72 | } 73 | 74 | if (null === $this->attributes || \in_array($camelCasedName, $this->attributes, true)) { 75 | return $camelCasedName; 76 | } 77 | 78 | return $propertyName; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /NameConverter/NameConverterInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\NameConverter; 13 | 14 | /** 15 | * Defines the interface for property name converters. 16 | * 17 | * @author Kévin Dunglas 18 | */ 19 | interface NameConverterInterface 20 | { 21 | /** 22 | * Converts a property name to its normalized value. 23 | * 24 | * @param class-string|null $class 25 | * @param string|null $format 26 | * @param array $context 27 | */ 28 | public function normalize(string $propertyName/* , ?string $class = null, ?string $format = null, array $context = [] */): string; 29 | 30 | /** 31 | * Converts a property name to its denormalized value. 32 | * 33 | * @param class-string|null $class 34 | * @param string|null $format 35 | * @param array $context 36 | */ 37 | public function denormalize(string $propertyName/* , ?string $class = null, ?string $format = null, array $context = [] */): string; 38 | } 39 | -------------------------------------------------------------------------------- /NameConverter/SnakeCaseToCamelCaseNameConverter.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\NameConverter; 13 | 14 | use Symfony\Component\Serializer\Exception\UnexpectedPropertyException; 15 | 16 | /** 17 | * Underscore to camelCase name converter. 18 | * 19 | * @author Kévin Dunglas 20 | */ 21 | final readonly class SnakeCaseToCamelCaseNameConverter implements NameConverterInterface 22 | { 23 | /** 24 | * Require all properties to be written in camelCase. 25 | */ 26 | public const REQUIRE_CAMEL_CASE_PROPERTIES = 'require_camel_case_properties'; 27 | 28 | /** 29 | * @param string[]|null $attributes The list of attributes to rename or null for all attributes 30 | * @param bool $lowerCamelCase Use lowerCamelCase style 31 | */ 32 | public function __construct( 33 | private ?array $attributes = null, 34 | private bool $lowerCamelCase = true, 35 | ) { 36 | } 37 | 38 | /** 39 | * @param class-string|null $class 40 | * @param array $context 41 | */ 42 | public function normalize(string $propertyName, ?string $class = null, ?string $format = null, array $context = []): string 43 | { 44 | if (null !== $this->attributes && !\in_array($propertyName, $this->attributes, true)) { 45 | return $propertyName; 46 | } 47 | 48 | $camelCasedName = preg_replace_callback( 49 | '/(^|_|\.)+(.)/', 50 | fn ($match) => ('.' === $match[1] ? '_' : '').strtoupper($match[2]), 51 | $propertyName 52 | ); 53 | 54 | if ($this->lowerCamelCase) { 55 | $camelCasedName = lcfirst($camelCasedName); 56 | } 57 | 58 | return $camelCasedName; 59 | } 60 | 61 | /** 62 | * @param class-string|null $class 63 | * @param array $context 64 | */ 65 | public function denormalize(string $propertyName, ?string $class = null, ?string $format = null, array $context = []): string 66 | { 67 | if (($context[self::REQUIRE_CAMEL_CASE_PROPERTIES] ?? false) && $propertyName !== $this->normalize($propertyName, $class, $format, $context)) { 68 | throw new UnexpectedPropertyException($propertyName); 69 | } 70 | 71 | $snakeCased = strtolower(preg_replace('/[A-Z]/', '_\\0', lcfirst($propertyName))); 72 | if (null === $this->attributes || \in_array($snakeCased, $this->attributes, true)) { 73 | return $snakeCased; 74 | } 75 | 76 | return $propertyName; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Normalizer/ArrayDenormalizer.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\Normalizer; 13 | 14 | use Symfony\Component\PropertyInfo\Type as LegacyType; 15 | use Symfony\Component\Serializer\Exception\BadMethodCallException; 16 | use Symfony\Component\Serializer\Exception\InvalidArgumentException; 17 | use Symfony\Component\Serializer\Exception\NotNormalizableValueException; 18 | use Symfony\Component\TypeInfo\Type; 19 | use Symfony\Component\TypeInfo\Type\BuiltinType; 20 | use Symfony\Component\TypeInfo\Type\UnionType; 21 | use Symfony\Component\TypeInfo\TypeIdentifier; 22 | 23 | /** 24 | * Denormalizes arrays of objects. 25 | * 26 | * @author Alexander M. Turek 27 | * 28 | * @final 29 | */ 30 | class ArrayDenormalizer implements DenormalizerInterface, DenormalizerAwareInterface 31 | { 32 | use DenormalizerAwareTrait; 33 | 34 | public function getSupportedTypes(?string $format): array 35 | { 36 | return ['object' => null, '*' => false]; 37 | } 38 | 39 | /** 40 | * @throws NotNormalizableValueException 41 | */ 42 | public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): array 43 | { 44 | if (!isset($this->denormalizer)) { 45 | throw new BadMethodCallException('Please set a denormalizer before calling denormalize()!'); 46 | } 47 | if (!\is_array($data)) { 48 | throw NotNormalizableValueException::createForUnexpectedDataType(\sprintf('Data expected to be "%s", "%s" given.', $type, get_debug_type($data)), $data, ['array'], $context['deserialization_path'] ?? null); 49 | } 50 | if (!str_ends_with($type, '[]')) { 51 | throw new InvalidArgumentException('Unsupported class: '.$type); 52 | } 53 | 54 | $type = substr($type, 0, -2); 55 | 56 | $typeIdentifiers = []; 57 | if (null !== $keyType = ($context['key_type'] ?? null)) { 58 | if ($keyType instanceof Type) { 59 | // BC layer for type-info < 7.2 60 | if (method_exists(Type::class, 'getBaseType')) { 61 | $typeIdentifiers = array_map(fn (Type $t): string => $t->getBaseType()->getTypeIdentifier()->value, $keyType instanceof UnionType ? $keyType->getTypes() : [$keyType]); 62 | } else { 63 | /** @var list|BuiltinType> */ 64 | $keyTypes = $keyType instanceof UnionType ? $keyType->getTypes() : [$keyType]; 65 | 66 | $typeIdentifiers = array_map(fn (BuiltinType $t): string => $t->getTypeIdentifier()->value, $keyTypes); 67 | } 68 | } else { 69 | $typeIdentifiers = array_map(fn (LegacyType $t): string => $t->getBuiltinType(), \is_array($keyType) ? $keyType : [$keyType]); 70 | } 71 | } 72 | 73 | foreach ($data as $key => $value) { 74 | $subContext = $context; 75 | $subContext['deserialization_path'] = ($context['deserialization_path'] ?? false) ? \sprintf('%s[%s]', $context['deserialization_path'], $key) : "[$key]"; 76 | 77 | $this->validateKeyType($typeIdentifiers, $key, $subContext['deserialization_path']); 78 | 79 | $data[$key] = $this->denormalizer->denormalize($value, $type, $format, $subContext); 80 | } 81 | 82 | return $data; 83 | } 84 | 85 | public function supportsDenormalization(mixed $data, string $type, ?string $format = null, array $context = []): bool 86 | { 87 | if (!isset($this->denormalizer)) { 88 | throw new BadMethodCallException(\sprintf('The nested denormalizer needs to be set to allow "%s()" to be used.', __METHOD__)); 89 | } 90 | 91 | return str_ends_with($type, '[]') 92 | && $this->denormalizer->supportsDenormalization($data, substr($type, 0, -2), $format, $context); 93 | } 94 | 95 | /** 96 | * @param list $typeIdentifiers 97 | */ 98 | private function validateKeyType(array $typeIdentifiers, mixed $key, string $path): void 99 | { 100 | if (!$typeIdentifiers) { 101 | return; 102 | } 103 | 104 | foreach ($typeIdentifiers as $typeIdentifier) { 105 | if (('is_'.$typeIdentifier)($key)) { 106 | return; 107 | } 108 | } 109 | 110 | throw NotNormalizableValueException::createForUnexpectedDataType(\sprintf('The type of the key "%s" must be "%s" ("%s" given).', $key, implode('", "', $typeIdentifiers), get_debug_type($key)), $key, $typeIdentifiers, $path, true); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /Normalizer/BackedEnumNormalizer.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\Normalizer; 13 | 14 | use Symfony\Component\Serializer\Exception\InvalidArgumentException; 15 | use Symfony\Component\Serializer\Exception\NotNormalizableValueException; 16 | 17 | /** 18 | * Normalizes a {@see \BackedEnum} enumeration to a string or an integer. 19 | * 20 | * @author Alexandre Daubois 21 | */ 22 | final class BackedEnumNormalizer implements NormalizerInterface, DenormalizerInterface 23 | { 24 | /** 25 | * If true, will denormalize any invalid value into null. 26 | */ 27 | public const ALLOW_INVALID_VALUES = 'allow_invalid_values'; 28 | 29 | public function getSupportedTypes(?string $format): array 30 | { 31 | return [ 32 | \BackedEnum::class => true, 33 | ]; 34 | } 35 | 36 | public function normalize(mixed $data, ?string $format = null, array $context = []): int|string 37 | { 38 | if (!$data instanceof \BackedEnum) { 39 | throw new InvalidArgumentException('The data must belong to a backed enumeration.'); 40 | } 41 | 42 | return $data->value; 43 | } 44 | 45 | public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool 46 | { 47 | return $data instanceof \BackedEnum; 48 | } 49 | 50 | /** 51 | * @throws NotNormalizableValueException 52 | */ 53 | public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): mixed 54 | { 55 | if (!is_subclass_of($type, \BackedEnum::class)) { 56 | throw new InvalidArgumentException('The data must belong to a backed enumeration.'); 57 | } 58 | 59 | if ($context[self::ALLOW_INVALID_VALUES] ?? false) { 60 | if (null === $data || (!\is_int($data) && !\is_string($data))) { 61 | return null; 62 | } 63 | 64 | try { 65 | return $type::tryFrom($data); 66 | } catch (\TypeError) { 67 | return null; 68 | } 69 | } 70 | 71 | if (!\is_int($data) && !\is_string($data)) { 72 | throw NotNormalizableValueException::createForUnexpectedDataType('The data is neither an integer nor a string, you should pass an integer or a string that can be parsed as an enumeration case of type '.$type.'.', $data, ['int', 'string'], $context['deserialization_path'] ?? null, true); 73 | } 74 | 75 | try { 76 | return $type::from($data); 77 | } catch (\ValueError $e) { 78 | if (isset($context['has_constructor'])) { 79 | throw new InvalidArgumentException('The data must belong to a backed enumeration of type '.$type, 0, $e); 80 | } 81 | 82 | throw NotNormalizableValueException::createForUnexpectedDataType('The data must belong to a backed enumeration of type '.$type, $data, [$type], $context['deserialization_path'] ?? null, true, 0, $e); 83 | } 84 | } 85 | 86 | public function supportsDenormalization(mixed $data, string $type, ?string $format = null, array $context = []): bool 87 | { 88 | return is_subclass_of($type, \BackedEnum::class); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Normalizer/ConstraintViolationListNormalizer.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\Normalizer; 13 | 14 | use Symfony\Component\Serializer\NameConverter\NameConverterInterface; 15 | use Symfony\Component\Validator\ConstraintViolationListInterface; 16 | 17 | /** 18 | * A normalizer that normalizes a ConstraintViolationListInterface instance. 19 | * 20 | * This Normalizer implements RFC7807 {@link https://tools.ietf.org/html/rfc7807}. 21 | * 22 | * @author Grégoire Pineau 23 | * @author Kévin Dunglas 24 | */ 25 | final class ConstraintViolationListNormalizer implements NormalizerInterface 26 | { 27 | public const INSTANCE = 'instance'; 28 | public const STATUS = 'status'; 29 | public const TITLE = 'title'; 30 | public const TYPE = 'type'; 31 | public const PAYLOAD_FIELDS = 'payload_fields'; 32 | 33 | public function __construct( 34 | private readonly array $defaultContext = [], 35 | private readonly ?NameConverterInterface $nameConverter = null, 36 | ) { 37 | } 38 | 39 | public function getSupportedTypes(?string $format): array 40 | { 41 | return [ 42 | ConstraintViolationListInterface::class => true, 43 | ]; 44 | } 45 | 46 | public function normalize(mixed $data, ?string $format = null, array $context = []): array 47 | { 48 | if (\array_key_exists(self::PAYLOAD_FIELDS, $context)) { 49 | $payloadFieldsToSerialize = $context[self::PAYLOAD_FIELDS]; 50 | } elseif (\array_key_exists(self::PAYLOAD_FIELDS, $this->defaultContext)) { 51 | $payloadFieldsToSerialize = $this->defaultContext[self::PAYLOAD_FIELDS]; 52 | } else { 53 | $payloadFieldsToSerialize = []; 54 | } 55 | 56 | if (\is_array($payloadFieldsToSerialize) && [] !== $payloadFieldsToSerialize) { 57 | $payloadFieldsToSerialize = array_flip($payloadFieldsToSerialize); 58 | } 59 | 60 | $violations = []; 61 | $messages = []; 62 | foreach ($data as $violation) { 63 | $propertyPath = $this->nameConverter ? $this->nameConverter->normalize($violation->getPropertyPath(), null, $format, $context) : $violation->getPropertyPath(); 64 | 65 | $violationEntry = [ 66 | 'propertyPath' => $propertyPath, 67 | 'title' => $violation->getMessage(), 68 | 'template' => $violation->getMessageTemplate(), 69 | 'parameters' => $violation->getParameters(), 70 | ]; 71 | if (null !== $code = $violation->getCode()) { 72 | $violationEntry['type'] = \sprintf('urn:uuid:%s', $code); 73 | } 74 | 75 | $constraint = $violation->getConstraint(); 76 | if ( 77 | [] !== $payloadFieldsToSerialize 78 | && $constraint 79 | && $constraint->payload 80 | // If some or all payload fields are whitelisted, add them 81 | && $payloadFields = null === $payloadFieldsToSerialize || true === $payloadFieldsToSerialize ? $constraint->payload : array_intersect_key($constraint->payload, $payloadFieldsToSerialize) 82 | ) { 83 | $violationEntry['payload'] = $payloadFields; 84 | } 85 | 86 | $violations[] = $violationEntry; 87 | 88 | $prefix = $propertyPath ? \sprintf('%s: ', $propertyPath) : ''; 89 | $messages[] = $prefix.$violation->getMessage(); 90 | } 91 | 92 | $result = [ 93 | 'type' => $context[self::TYPE] ?? $this->defaultContext[self::TYPE] ?? 'https://symfony.com/errors/validation', 94 | 'title' => $context[self::TITLE] ?? $this->defaultContext[self::TITLE] ?? 'Validation Failed', 95 | ]; 96 | if (null !== $status = ($context[self::STATUS] ?? $this->defaultContext[self::STATUS] ?? null)) { 97 | $result['status'] = $status; 98 | } 99 | if ($messages) { 100 | $result['detail'] = implode("\n", $messages); 101 | } 102 | if (null !== $instance = ($context[self::INSTANCE] ?? $this->defaultContext[self::INSTANCE] ?? null)) { 103 | $result['instance'] = $instance; 104 | } 105 | 106 | return $result + ['violations' => $violations]; 107 | } 108 | 109 | public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool 110 | { 111 | return $data instanceof ConstraintViolationListInterface; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /Normalizer/CustomNormalizer.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\Normalizer; 13 | 14 | use Symfony\Component\Serializer\SerializerAwareInterface; 15 | use Symfony\Component\Serializer\SerializerAwareTrait; 16 | 17 | /** 18 | * @author Jordi Boggiano 19 | */ 20 | final class CustomNormalizer implements NormalizerInterface, DenormalizerInterface, SerializerAwareInterface 21 | { 22 | use ObjectToPopulateTrait; 23 | use SerializerAwareTrait; 24 | 25 | public function getSupportedTypes(?string $format): array 26 | { 27 | return [ 28 | NormalizableInterface::class => true, 29 | DenormalizableInterface::class => true, 30 | ]; 31 | } 32 | 33 | public function normalize(mixed $data, ?string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null 34 | { 35 | return $data->normalize($this->serializer, $format, $context); 36 | } 37 | 38 | public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): mixed 39 | { 40 | $object = $this->extractObjectToPopulate($type, $context) ?? new $type(); 41 | $object->denormalize($this->serializer, $data, $format, $context); 42 | 43 | return $object; 44 | } 45 | 46 | /** 47 | * Checks if the given class implements the NormalizableInterface. 48 | * 49 | * @param mixed $data Data to normalize 50 | * @param string|null $format The format being (de-)serialized from or into 51 | */ 52 | public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool 53 | { 54 | return $data instanceof NormalizableInterface; 55 | } 56 | 57 | /** 58 | * Checks if the given class implements the DenormalizableInterface. 59 | * 60 | * @param mixed $data Data to denormalize from 61 | * @param string $type The class to which the data should be denormalized 62 | * @param string|null $format The format being deserialized from 63 | */ 64 | public function supportsDenormalization(mixed $data, string $type, ?string $format = null, array $context = []): bool 65 | { 66 | return is_subclass_of($type, DenormalizableInterface::class); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Normalizer/DateIntervalNormalizer.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\Normalizer; 13 | 14 | use Symfony\Component\Serializer\Exception\InvalidArgumentException; 15 | use Symfony\Component\Serializer\Exception\NotNormalizableValueException; 16 | 17 | /** 18 | * Normalizes an instance of {@see \DateInterval} to an interval string. 19 | * Denormalizes an interval string to an instance of {@see \DateInterval}. 20 | * 21 | * @author Jérôme Parmentier 22 | */ 23 | final class DateIntervalNormalizer implements NormalizerInterface, DenormalizerInterface 24 | { 25 | public const FORMAT_KEY = 'dateinterval_format'; 26 | 27 | private array $defaultContext = [ 28 | self::FORMAT_KEY => '%rP%yY%mM%dDT%hH%iM%sS', 29 | ]; 30 | 31 | public function __construct(array $defaultContext = []) 32 | { 33 | $this->defaultContext = array_merge($this->defaultContext, $defaultContext); 34 | } 35 | 36 | public function getSupportedTypes(?string $format): array 37 | { 38 | return [ 39 | \DateInterval::class => true, 40 | ]; 41 | } 42 | 43 | /** 44 | * @throws InvalidArgumentException 45 | */ 46 | public function normalize(mixed $data, ?string $format = null, array $context = []): string 47 | { 48 | if (!$data instanceof \DateInterval) { 49 | throw new InvalidArgumentException('The object must be an instance of "\DateInterval".'); 50 | } 51 | 52 | return $data->format($context[self::FORMAT_KEY] ?? $this->defaultContext[self::FORMAT_KEY]); 53 | } 54 | 55 | public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool 56 | { 57 | return $data instanceof \DateInterval; 58 | } 59 | 60 | /** 61 | * @throws NotNormalizableValueException 62 | */ 63 | public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): \DateInterval 64 | { 65 | if (!\is_string($data)) { 66 | throw NotNormalizableValueException::createForUnexpectedDataType('Data expected to be a string.', $data, ['string'], $context['deserialization_path'] ?? null, true); 67 | } 68 | 69 | if (!$this->isISO8601($data)) { 70 | throw NotNormalizableValueException::createForUnexpectedDataType('Expected a valid ISO 8601 interval string.', $data, ['string'], $context['deserialization_path'] ?? null, true); 71 | } 72 | 73 | $dateIntervalFormat = $context[self::FORMAT_KEY] ?? $this->defaultContext[self::FORMAT_KEY]; 74 | 75 | $signPattern = ''; 76 | switch (substr($dateIntervalFormat, 0, 2)) { 77 | case '%R': 78 | $signPattern = '[-+]'; 79 | $dateIntervalFormat = substr($dateIntervalFormat, 2); 80 | break; 81 | case '%r': 82 | $signPattern = '-?'; 83 | $dateIntervalFormat = substr($dateIntervalFormat, 2); 84 | break; 85 | } 86 | $valuePattern = '/^'.$signPattern.preg_replace('/%([yYmMdDhHiIsSwW])(\w)/', '(?:(?P<$1>\d+)$2)?', preg_replace('/(T.*)$/', '($1)?', $dateIntervalFormat)).'$/'; 87 | if (!preg_match($valuePattern, $data)) { 88 | throw NotNormalizableValueException::createForUnexpectedDataType(\sprintf('Value "%s" contains intervals not accepted by format "%s".', $data, $dateIntervalFormat), $data, ['string'], $context['deserialization_path'] ?? null, false); 89 | } 90 | 91 | try { 92 | if ('-' === $data[0]) { 93 | $interval = new \DateInterval(substr($data, 1)); 94 | $interval->invert = 1; 95 | 96 | return $interval; 97 | } 98 | 99 | if ('+' === $data[0]) { 100 | return new \DateInterval(substr($data, 1)); 101 | } 102 | 103 | return new \DateInterval($data); 104 | } catch (\Exception $e) { 105 | throw NotNormalizableValueException::createForUnexpectedDataType($e->getMessage(), $data, ['string'], $context['deserialization_path'] ?? null, false, $e->getCode(), $e); 106 | } 107 | } 108 | 109 | public function supportsDenormalization(mixed $data, string $type, ?string $format = null, array $context = []): bool 110 | { 111 | return \DateInterval::class === $type; 112 | } 113 | 114 | private function isISO8601(string $string): bool 115 | { 116 | return preg_match('/^[\-+]?P(?=\w*(?:\d|%\w))(?:\d+Y|%[yY]Y)?(?:\d+M|%[mM]M)?(?:\d+W|%[wW]W)?(?:\d+D|%[dD]D)?(?:T(?:\d+H|[hH]H)?(?:\d+M|[iI]M)?(?:\d+S|[sS]S)?)?$/', $string); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /Normalizer/DateTimeZoneNormalizer.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\Normalizer; 13 | 14 | use Symfony\Component\Serializer\Exception\InvalidArgumentException; 15 | use Symfony\Component\Serializer\Exception\NotNormalizableValueException; 16 | 17 | /** 18 | * Normalizes a {@see \DateTimeZone} object to a timezone string. 19 | * 20 | * @author Jérôme Desjardins 21 | */ 22 | final class DateTimeZoneNormalizer implements NormalizerInterface, DenormalizerInterface 23 | { 24 | public function getSupportedTypes(?string $format): array 25 | { 26 | return [ 27 | \DateTimeZone::class => true, 28 | ]; 29 | } 30 | 31 | /** 32 | * @throws InvalidArgumentException 33 | */ 34 | public function normalize(mixed $data, ?string $format = null, array $context = []): string 35 | { 36 | if (!$data instanceof \DateTimeZone) { 37 | throw new InvalidArgumentException('The object must be an instance of "\DateTimeZone".'); 38 | } 39 | 40 | return $data->getName(); 41 | } 42 | 43 | public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool 44 | { 45 | return $data instanceof \DateTimeZone; 46 | } 47 | 48 | /** 49 | * @throws NotNormalizableValueException 50 | */ 51 | public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): \DateTimeZone 52 | { 53 | if ('' === $data || null === $data) { 54 | throw NotNormalizableValueException::createForUnexpectedDataType('The data is either an empty string or null, you should pass a string that can be parsed as a DateTimeZone.', $data, ['string'], $context['deserialization_path'] ?? null, true); 55 | } 56 | 57 | try { 58 | return new \DateTimeZone($data); 59 | } catch (\Exception $e) { 60 | throw NotNormalizableValueException::createForUnexpectedDataType($e->getMessage(), $data, ['string'], $context['deserialization_path'] ?? null, true, $e->getCode(), $e); 61 | } 62 | } 63 | 64 | public function supportsDenormalization(mixed $data, string $type, ?string $format = null, array $context = []): bool 65 | { 66 | return \DateTimeZone::class === $type; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Normalizer/DenormalizableInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\Normalizer; 13 | 14 | /** 15 | * Defines the most basic interface a class must implement to be denormalizable. 16 | * 17 | * If a denormalizer is registered for the class and it doesn't implement 18 | * the Denormalizable interfaces, the normalizer will be used instead 19 | * 20 | * @author Jordi Boggiano 21 | */ 22 | interface DenormalizableInterface 23 | { 24 | /** 25 | * Denormalizes the object back from an array of scalars|arrays. 26 | * 27 | * It is important to understand that the denormalize() call should denormalize 28 | * recursively all child objects of the implementor. 29 | * 30 | * @param DenormalizerInterface $denormalizer The denormalizer is given so that you 31 | * can use it to denormalize objects contained within this object 32 | * @param array|string|int|float|bool $data The data from which to re-create the object 33 | * @param string|null $format The format is optionally given to be able to denormalize 34 | * differently based on different input formats 35 | * @param array $context Options for denormalizing 36 | */ 37 | public function denormalize(DenormalizerInterface $denormalizer, array|string|int|float|bool $data, ?string $format = null, array $context = []): void; 38 | } 39 | -------------------------------------------------------------------------------- /Normalizer/DenormalizerAwareInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\Normalizer; 13 | 14 | /** 15 | * @author Joel Wurtz 16 | */ 17 | interface DenormalizerAwareInterface 18 | { 19 | /** 20 | * Sets the owning Denormalizer object. 21 | */ 22 | public function setDenormalizer(DenormalizerInterface $denormalizer): void; 23 | } 24 | -------------------------------------------------------------------------------- /Normalizer/DenormalizerAwareTrait.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\Normalizer; 13 | 14 | /** 15 | * @author Joel Wurtz 16 | */ 17 | trait DenormalizerAwareTrait 18 | { 19 | protected DenormalizerInterface $denormalizer; 20 | 21 | public function setDenormalizer(DenormalizerInterface $denormalizer): void 22 | { 23 | $this->denormalizer = $denormalizer; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Normalizer/DenormalizerInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\Normalizer; 13 | 14 | use Symfony\Component\Serializer\Exception\BadMethodCallException; 15 | use Symfony\Component\Serializer\Exception\ExceptionInterface; 16 | use Symfony\Component\Serializer\Exception\ExtraAttributesException; 17 | use Symfony\Component\Serializer\Exception\InvalidArgumentException; 18 | use Symfony\Component\Serializer\Exception\LogicException; 19 | use Symfony\Component\Serializer\Exception\RuntimeException; 20 | use Symfony\Component\Serializer\Exception\UnexpectedValueException; 21 | 22 | /** 23 | * @author Jordi Boggiano 24 | */ 25 | interface DenormalizerInterface 26 | { 27 | public const COLLECT_DENORMALIZATION_ERRORS = 'collect_denormalization_errors'; 28 | 29 | /** 30 | * Denormalizes data back into an object of the given class. 31 | * 32 | * @param mixed $data Data to restore 33 | * @param string $type The expected class to instantiate 34 | * @param string|null $format Format the given data was extracted from 35 | * @param array $context Options available to the denormalizer 36 | * 37 | * @throws BadMethodCallException Occurs when the normalizer is not called in an expected context 38 | * @throws InvalidArgumentException Occurs when the arguments are not coherent or not supported 39 | * @throws UnexpectedValueException Occurs when the item cannot be hydrated with the given data 40 | * @throws ExtraAttributesException Occurs when the item doesn't have attribute to receive given data 41 | * @throws LogicException Occurs when the normalizer is not supposed to denormalize 42 | * @throws RuntimeException Occurs if the class cannot be instantiated 43 | * @throws ExceptionInterface Occurs for all the other cases of errors 44 | */ 45 | public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): mixed; 46 | 47 | /** 48 | * Checks whether the given class is supported for denormalization by this normalizer. 49 | * 50 | * @param mixed $data Data to denormalize from 51 | * @param string $type The class to which the data should be denormalized 52 | * @param string|null $format The format being deserialized from 53 | */ 54 | public function supportsDenormalization(mixed $data, string $type, ?string $format = null, array $context = []): bool; 55 | 56 | /** 57 | * Returns the types potentially supported by this denormalizer. 58 | * 59 | * For each supported formats (if applicable), the supported types should be 60 | * returned as keys, and each type should be mapped to a boolean indicating 61 | * if the result of supportsDenormalization() can be cached or not 62 | * (a result cannot be cached when it depends on the context or on the data.) 63 | * A null value means that the denormalizer does not support the corresponding 64 | * type. 65 | * 66 | * Use type "object" to match any classes or interfaces, 67 | * and type "*" to match any types. 68 | * 69 | * @return array 70 | */ 71 | public function getSupportedTypes(?string $format): array; 72 | } 73 | -------------------------------------------------------------------------------- /Normalizer/FormErrorNormalizer.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\Normalizer; 13 | 14 | use Symfony\Component\Form\FormInterface; 15 | 16 | /** 17 | * Normalizes invalid Form instances. 18 | */ 19 | final class FormErrorNormalizer implements NormalizerInterface 20 | { 21 | public const TITLE = 'title'; 22 | public const TYPE = 'type'; 23 | public const CODE = 'status_code'; 24 | 25 | public function normalize(mixed $data, ?string $format = null, array $context = []): array 26 | { 27 | $error = [ 28 | 'title' => $context[self::TITLE] ?? 'Validation Failed', 29 | 'type' => $context[self::TYPE] ?? 'https://symfony.com/errors/form', 30 | 'code' => $context[self::CODE] ?? null, 31 | 'errors' => $this->convertFormErrorsToArray($data), 32 | ]; 33 | 34 | if (0 !== \count($data->all())) { 35 | $error['children'] = $this->convertFormChildrenToArray($data); 36 | } 37 | 38 | return $error; 39 | } 40 | 41 | public function getSupportedTypes(?string $format): array 42 | { 43 | return [ 44 | FormInterface::class => false, 45 | ]; 46 | } 47 | 48 | public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool 49 | { 50 | return $data instanceof FormInterface && $data->isSubmitted() && !$data->isValid(); 51 | } 52 | 53 | private function convertFormErrorsToArray(FormInterface $data): array 54 | { 55 | $errors = []; 56 | 57 | foreach ($data->getErrors() as $error) { 58 | $errors[] = [ 59 | 'message' => $error->getMessage(), 60 | 'cause' => $error->getCause(), 61 | ]; 62 | } 63 | 64 | return $errors; 65 | } 66 | 67 | private function convertFormChildrenToArray(FormInterface $data): array 68 | { 69 | $children = []; 70 | 71 | foreach ($data->all() as $child) { 72 | $childData = [ 73 | 'errors' => $this->convertFormErrorsToArray($child), 74 | ]; 75 | 76 | if ($child->all()) { 77 | $childData['children'] = $this->convertFormChildrenToArray($child); 78 | } 79 | 80 | $children[$child->getName()] = $childData; 81 | } 82 | 83 | return $children; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /Normalizer/JsonSerializableNormalizer.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\Normalizer; 13 | 14 | use Symfony\Component\Serializer\Exception\InvalidArgumentException; 15 | use Symfony\Component\Serializer\Exception\LogicException; 16 | 17 | /** 18 | * A normalizer that uses an objects own JsonSerializable implementation. 19 | * 20 | * @author Fred Cox 21 | */ 22 | final class JsonSerializableNormalizer extends AbstractNormalizer 23 | { 24 | public function normalize(mixed $data, ?string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null 25 | { 26 | if ($this->isCircularReference($data, $context)) { 27 | return $this->handleCircularReference($data, $format, $context); 28 | } 29 | 30 | if (!$data instanceof \JsonSerializable) { 31 | throw new InvalidArgumentException(\sprintf('The object must implement "%s".', \JsonSerializable::class)); 32 | } 33 | 34 | if (!$this->serializer instanceof NormalizerInterface) { 35 | throw new LogicException('Cannot normalize object because injected serializer is not a normalizer.'); 36 | } 37 | 38 | return $this->serializer->normalize($data->jsonSerialize(), $format, $context); 39 | } 40 | 41 | public function getSupportedTypes(?string $format): array 42 | { 43 | return [ 44 | \JsonSerializable::class => true, 45 | ]; 46 | } 47 | 48 | public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool 49 | { 50 | return $data instanceof \JsonSerializable; 51 | } 52 | 53 | public function supportsDenormalization(mixed $data, string $type, ?string $format = null, array $context = []): bool 54 | { 55 | return false; 56 | } 57 | 58 | public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): mixed 59 | { 60 | throw new LogicException(\sprintf('Cannot denormalize with "%s".', \JsonSerializable::class)); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Normalizer/MimeMessageNormalizer.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\Normalizer; 13 | 14 | use Symfony\Component\Mime\Address; 15 | use Symfony\Component\Mime\Header\HeaderInterface; 16 | use Symfony\Component\Mime\Header\Headers; 17 | use Symfony\Component\Mime\Header\UnstructuredHeader; 18 | use Symfony\Component\Mime\Message; 19 | use Symfony\Component\Mime\Part\AbstractPart; 20 | use Symfony\Component\Mime\RawMessage; 21 | use Symfony\Component\Serializer\Exception\LogicException; 22 | use Symfony\Component\Serializer\SerializerAwareInterface; 23 | use Symfony\Component\Serializer\SerializerInterface; 24 | 25 | /** 26 | * Normalize Mime message classes. 27 | * 28 | * It forces the use of a PropertyNormalizer instance for normalization 29 | * of all data objects composing a Message. 30 | * 31 | * Emails using resources for any parts are not serializable. 32 | */ 33 | final class MimeMessageNormalizer implements NormalizerInterface, DenormalizerInterface, SerializerAwareInterface 34 | { 35 | private NormalizerInterface&DenormalizerInterface $serializer; 36 | private array $headerClassMap; 37 | private \ReflectionProperty $headersProperty; 38 | 39 | public function __construct(private readonly PropertyNormalizer $normalizer) 40 | { 41 | $this->headerClassMap = (new \ReflectionClassConstant(Headers::class, 'HEADER_CLASS_MAP'))->getValue(); 42 | $this->headersProperty = new \ReflectionProperty(Headers::class, 'headers'); 43 | } 44 | 45 | public function getSupportedTypes(?string $format): array 46 | { 47 | return [ 48 | Message::class => true, 49 | Headers::class => true, 50 | HeaderInterface::class => true, 51 | Address::class => true, 52 | AbstractPart::class => true, 53 | ]; 54 | } 55 | 56 | public function setSerializer(SerializerInterface $serializer): void 57 | { 58 | if (!$serializer instanceof NormalizerInterface || !$serializer instanceof DenormalizerInterface) { 59 | throw new LogicException(\sprintf('The passed serializer should implement both NormalizerInterface and DenormalizerInterface, "%s" given.', get_debug_type($serializer))); 60 | } 61 | $this->serializer = $serializer; 62 | $this->normalizer->setSerializer($serializer); 63 | } 64 | 65 | public function normalize(mixed $data, ?string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null 66 | { 67 | if ($data instanceof Headers) { 68 | $ret = []; 69 | foreach ($this->headersProperty->getValue($data) as $name => $header) { 70 | $ret[$name] = $this->serializer->normalize($header, $format, $context); 71 | } 72 | 73 | return $ret; 74 | } 75 | 76 | $ret = $this->normalizer->normalize($data, $format, $context); 77 | 78 | if ($data instanceof AbstractPart) { 79 | $ret['class'] = $data::class; 80 | unset($ret['seekable'], $ret['cid'], $ret['handle']); 81 | } 82 | 83 | if ($data instanceof RawMessage && \array_key_exists('message', $ret) && null === $ret['message']) { 84 | unset($ret['message']); 85 | } 86 | 87 | return $ret; 88 | } 89 | 90 | public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): mixed 91 | { 92 | if (Headers::class === $type) { 93 | $ret = []; 94 | foreach ($data as $headers) { 95 | foreach ($headers as $header) { 96 | $ret[] = $this->serializer->denormalize($header, $this->headerClassMap[strtolower($header['name'])] ?? UnstructuredHeader::class, $format, $context); 97 | } 98 | } 99 | 100 | return new Headers(...$ret); 101 | } 102 | 103 | if (AbstractPart::class === $type) { 104 | $type = $data['class']; 105 | unset($data['class']); 106 | $data['headers'] = $this->serializer->denormalize($data['headers'], Headers::class, $format, $context); 107 | } 108 | 109 | return $this->normalizer->denormalize($data, $type, $format, $context); 110 | } 111 | 112 | public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool 113 | { 114 | return $data instanceof Message || $data instanceof Headers || $data instanceof HeaderInterface || $data instanceof Address || $data instanceof AbstractPart; 115 | } 116 | 117 | public function supportsDenormalization(mixed $data, string $type, ?string $format = null, array $context = []): bool 118 | { 119 | return is_a($type, Message::class, true) || Headers::class === $type || AbstractPart::class === $type; 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /Normalizer/NormalizableInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\Normalizer; 13 | 14 | /** 15 | * Defines the most basic interface a class must implement to be normalizable. 16 | * 17 | * If a normalizer is registered for the class and it doesn't implement 18 | * the Normalizable interfaces, the normalizer will be used instead. 19 | * 20 | * @author Jordi Boggiano 21 | */ 22 | interface NormalizableInterface 23 | { 24 | /** 25 | * Normalizes the object into an array of scalars|arrays. 26 | * 27 | * It is important to understand that the normalize() call should normalize 28 | * recursively all child objects of the implementor. 29 | * 30 | * @param NormalizerInterface $normalizer The normalizer is given so that you 31 | * can use it to normalize objects contained within this object 32 | * @param string|null $format The format is optionally given to be able to normalize differently 33 | * based on different output formats 34 | * @param array $context Options for normalizing this object 35 | */ 36 | public function normalize(NormalizerInterface $normalizer, ?string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null; 37 | } 38 | -------------------------------------------------------------------------------- /Normalizer/NormalizerAwareInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\Normalizer; 13 | 14 | /** 15 | * @author Joel Wurtz 16 | */ 17 | interface NormalizerAwareInterface 18 | { 19 | /** 20 | * Sets the owning Normalizer object. 21 | */ 22 | public function setNormalizer(NormalizerInterface $normalizer): void; 23 | } 24 | -------------------------------------------------------------------------------- /Normalizer/NormalizerAwareTrait.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\Normalizer; 13 | 14 | /** 15 | * @author Joel Wurtz 16 | */ 17 | trait NormalizerAwareTrait 18 | { 19 | protected NormalizerInterface $normalizer; 20 | 21 | public function setNormalizer(NormalizerInterface $normalizer): void 22 | { 23 | $this->normalizer = $normalizer; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Normalizer/NormalizerInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\Normalizer; 13 | 14 | use Symfony\Component\Serializer\Exception\CircularReferenceException; 15 | use Symfony\Component\Serializer\Exception\ExceptionInterface; 16 | use Symfony\Component\Serializer\Exception\InvalidArgumentException; 17 | use Symfony\Component\Serializer\Exception\LogicException; 18 | 19 | /** 20 | * @author Jordi Boggiano 21 | */ 22 | interface NormalizerInterface 23 | { 24 | /** 25 | * Normalizes data into a set of arrays/scalars. 26 | * 27 | * @param mixed $data Data to normalize 28 | * @param string|null $format Format the normalization result will be encoded as 29 | * @param array $context Context options for the normalizer 30 | * 31 | * @return array|string|int|float|bool|\ArrayObject|null \ArrayObject is used to make sure an empty object is encoded as an object not an array 32 | * 33 | * @throws InvalidArgumentException Occurs when the object given is not a supported type for the normalizer 34 | * @throws CircularReferenceException Occurs when the normalizer detects a circular reference when no circular 35 | * reference handler can fix it 36 | * @throws LogicException Occurs when the normalizer is not called in an expected context 37 | * @throws ExceptionInterface Occurs for all the other cases of errors 38 | */ 39 | public function normalize(mixed $data, ?string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null; 40 | 41 | /** 42 | * Checks whether the given class is supported for normalization by this normalizer. 43 | * 44 | * @param mixed $data Data to normalize 45 | * @param string|null $format The format being (de-)serialized from or into 46 | */ 47 | public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool; 48 | 49 | /** 50 | * Returns the types potentially supported by this normalizer. 51 | * 52 | * For each supported formats (if applicable), the supported types should be 53 | * returned as keys, and each type should be mapped to a boolean indicating 54 | * if the result of supportsNormalization() can be cached or not 55 | * (a result cannot be cached when it depends on the context or on the data.) 56 | * A null value means that the normalizer does not support the corresponding 57 | * type. 58 | * 59 | * Use type "object" to match any classes or interfaces, 60 | * and type "*" to match any types. 61 | * 62 | * @return array 63 | */ 64 | public function getSupportedTypes(?string $format): array; 65 | } 66 | -------------------------------------------------------------------------------- /Normalizer/NumberNormalizer.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\Normalizer; 13 | 14 | use BcMath\Number; 15 | use Symfony\Component\Serializer\Exception\InvalidArgumentException; 16 | use Symfony\Component\Serializer\Exception\NotNormalizableValueException; 17 | 18 | /** 19 | * Normalizes {@see Number} and {@see \GMP} to a string. 20 | */ 21 | final class NumberNormalizer implements NormalizerInterface, DenormalizerInterface 22 | { 23 | public function getSupportedTypes(?string $format): array 24 | { 25 | return [ 26 | Number::class => true, 27 | \GMP::class => true, 28 | ]; 29 | } 30 | 31 | public function normalize(mixed $data, ?string $format = null, array $context = []): string 32 | { 33 | if (!$data instanceof Number && !$data instanceof \GMP) { 34 | throw new InvalidArgumentException(\sprintf('The data must be an instance of "%s" or "%s".', Number::class, \GMP::class)); 35 | } 36 | 37 | return (string) $data; 38 | } 39 | 40 | public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool 41 | { 42 | return $data instanceof Number || $data instanceof \GMP; 43 | } 44 | 45 | /** 46 | * @throws NotNormalizableValueException 47 | */ 48 | public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): Number|\GMP 49 | { 50 | if (!\is_string($data) && !\is_int($data)) { 51 | throw $this->createNotNormalizableValueException($type, $data, $context); 52 | } 53 | 54 | try { 55 | return match ($type) { 56 | Number::class => new Number($data), 57 | \GMP::class => new \GMP($data), 58 | default => throw new InvalidArgumentException(\sprintf('Only "%s" and "%s" types are supported.', Number::class, \GMP::class)), 59 | }; 60 | } catch (\ValueError $e) { 61 | throw $this->createNotNormalizableValueException($type, $data, $context, $e); 62 | } 63 | } 64 | 65 | public function supportsDenormalization(mixed $data, string $type, ?string $format = null, array $context = []): bool 66 | { 67 | return \in_array($type, [Number::class, \GMP::class], true) && null !== $data; 68 | } 69 | 70 | private function createNotNormalizableValueException(string $type, mixed $data, array $context, ?\Throwable $previous = null): NotNormalizableValueException 71 | { 72 | $message = match ($type) { 73 | Number::class => 'The data must be a "string" representing a decimal number, or an "int".', 74 | \GMP::class => 'The data must be a "string" representing an integer, or an "int".', 75 | }; 76 | 77 | return NotNormalizableValueException::createForUnexpectedDataType($message, $data, ['string', 'int'], $context['deserialization_path'] ?? null, true, 0, $previous); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Normalizer/ObjectToPopulateTrait.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\Normalizer; 13 | 14 | trait ObjectToPopulateTrait 15 | { 16 | /** 17 | * Extract the `object_to_populate` field from the context if it exists 18 | * and is an instance of the provided $class. 19 | * 20 | * @param string $class The class the object should be 21 | * @param string|null $key They in which to look for the object to populate. 22 | * Keeps backwards compatibility with `AbstractNormalizer`. 23 | */ 24 | protected function extractObjectToPopulate(string $class, array $context, ?string $key = null): ?object 25 | { 26 | $key ??= AbstractNormalizer::OBJECT_TO_POPULATE; 27 | 28 | if (isset($context[$key]) && \is_object($context[$key]) && $context[$key] instanceof $class) { 29 | return $context[$key]; 30 | } 31 | 32 | return null; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Normalizer/ProblemNormalizer.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\Normalizer; 13 | 14 | use Symfony\Component\ErrorHandler\Exception\FlattenException; 15 | use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface; 16 | use Symfony\Component\Messenger\Exception\ValidationFailedException as MessageValidationFailedException; 17 | use Symfony\Component\Serializer\Exception\InvalidArgumentException; 18 | use Symfony\Component\Serializer\Exception\PartialDenormalizationException; 19 | use Symfony\Component\Serializer\SerializerAwareInterface; 20 | use Symfony\Component\Serializer\SerializerAwareTrait; 21 | use Symfony\Component\Validator\Exception\ValidationFailedException; 22 | use Symfony\Contracts\Translation\TranslatorInterface; 23 | 24 | /** 25 | * Normalizes errors according to the API Problem spec (RFC 7807). 26 | * 27 | * @see https://tools.ietf.org/html/rfc7807 28 | * 29 | * @author Kévin Dunglas 30 | * @author Yonel Ceruto 31 | */ 32 | class ProblemNormalizer implements NormalizerInterface, SerializerAwareInterface 33 | { 34 | use SerializerAwareTrait; 35 | 36 | public const TITLE = 'title'; 37 | public const TYPE = 'type'; 38 | public const STATUS = 'status'; 39 | 40 | public function __construct( 41 | private bool $debug = false, 42 | private array $defaultContext = [], 43 | private ?TranslatorInterface $translator = null, 44 | ) { 45 | } 46 | 47 | public function getSupportedTypes(?string $format): array 48 | { 49 | return [ 50 | FlattenException::class => __CLASS__ === self::class, 51 | ]; 52 | } 53 | 54 | public function normalize(mixed $object, ?string $format = null, array $context = []): array 55 | { 56 | if (!$object instanceof FlattenException) { 57 | throw new InvalidArgumentException(\sprintf('The object must implement "%s".', FlattenException::class)); 58 | } 59 | 60 | $error = []; 61 | $context += $this->defaultContext; 62 | $debug = $this->debug && ($context['debug'] ?? true); 63 | $exception = $context['exception'] ?? null; 64 | if ($exception instanceof HttpExceptionInterface) { 65 | $exception = $exception->getPrevious(); 66 | 67 | if ($exception instanceof PartialDenormalizationException) { 68 | $trans = $this->translator ? $this->translator->trans(...) : fn ($m, $p) => strtr($m, $p); 69 | $template = 'This value should be of type {{ type }}.'; 70 | $error = [ 71 | self::TYPE => 'https://symfony.com/errors/validation', 72 | self::TITLE => 'Validation Failed', 73 | 'violations' => array_map( 74 | fn ($e) => [ 75 | 'propertyPath' => $e->getPath(), 76 | 'title' => $trans($template, [ 77 | '{{ type }}' => implode('|', $e->getExpectedTypes() ?? ['?']), 78 | ], 'validators'), 79 | 'template' => $template, 80 | 'parameters' => [ 81 | '{{ type }}' => implode('|', $e->getExpectedTypes() ?? ['?']), 82 | ], 83 | ] + ($debug || $e->canUseMessageForUser() ? ['hint' => $e->getMessage()] : []), 84 | $exception->getErrors() 85 | ), 86 | ]; 87 | $error['detail'] = implode("\n", array_map(fn ($e) => $e['propertyPath'].': '.$e['title'], $error['violations'])); 88 | } elseif (($exception instanceof ValidationFailedException || $exception instanceof MessageValidationFailedException) 89 | && $this->serializer instanceof NormalizerInterface 90 | && $this->serializer->supportsNormalization($exception->getViolations(), $format, $context) 91 | ) { 92 | $error = $this->serializer->normalize($exception->getViolations(), $format, $context); 93 | } 94 | } 95 | 96 | $error = [ 97 | self::TYPE => $error[self::TYPE] ?? $context[self::TYPE] ?? 'https://tools.ietf.org/html/rfc2616#section-10', 98 | self::TITLE => $error[self::TITLE] ?? $context[self::TITLE] ?? 'An error occurred', 99 | self::STATUS => $context[self::STATUS] ?? $object->getStatusCode(), 100 | 'detail' => $error['detail'] ?? ($debug ? $object->getMessage() : $object->getStatusText()), 101 | ] + $error; 102 | if ($debug) { 103 | $error['class'] = $object->getClass(); 104 | $error['trace'] = $object->getTrace(); 105 | } 106 | 107 | return $error; 108 | } 109 | 110 | public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool 111 | { 112 | return $data instanceof FlattenException; 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /Normalizer/TranslatableNormalizer.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\Normalizer; 13 | 14 | use Symfony\Component\Serializer\Exception\InvalidArgumentException; 15 | use Symfony\Component\Serializer\Exception\NotNormalizableValueException; 16 | use Symfony\Contracts\Translation\TranslatableInterface; 17 | use Symfony\Contracts\Translation\TranslatorInterface; 18 | 19 | final class TranslatableNormalizer implements NormalizerInterface 20 | { 21 | public const NORMALIZATION_LOCALE_KEY = 'translatable_normalization_locale'; 22 | 23 | private array $defaultContext = [ 24 | self::NORMALIZATION_LOCALE_KEY => null, 25 | ]; 26 | 27 | public function __construct( 28 | private readonly TranslatorInterface $translator, 29 | array $defaultContext = [], 30 | ) { 31 | $this->defaultContext = array_merge($this->defaultContext, $defaultContext); 32 | } 33 | 34 | /** 35 | * @throws InvalidArgumentException 36 | */ 37 | public function normalize(mixed $data, ?string $format = null, array $context = []): string 38 | { 39 | if (!$data instanceof TranslatableInterface) { 40 | throw NotNormalizableValueException::createForUnexpectedDataType(\sprintf('The object must implement the "%s".', TranslatableInterface::class), $data, [TranslatableInterface::class]); 41 | } 42 | 43 | return $data->trans($this->translator, $context[self::NORMALIZATION_LOCALE_KEY] ?? $this->defaultContext[self::NORMALIZATION_LOCALE_KEY]); 44 | } 45 | 46 | public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool 47 | { 48 | return $data instanceof TranslatableInterface; 49 | } 50 | 51 | public function getSupportedTypes(?string $format): array 52 | { 53 | return [TranslatableInterface::class => true]; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Normalizer/UidNormalizer.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\Normalizer; 13 | 14 | use Symfony\Component\Serializer\Exception\LogicException; 15 | use Symfony\Component\Serializer\Exception\NotNormalizableValueException; 16 | use Symfony\Component\Uid\AbstractUid; 17 | 18 | final class UidNormalizer implements NormalizerInterface, DenormalizerInterface 19 | { 20 | public const NORMALIZATION_FORMAT_KEY = 'uid_normalization_format'; 21 | 22 | public const NORMALIZATION_FORMAT_CANONICAL = 'canonical'; 23 | public const NORMALIZATION_FORMAT_BASE58 = 'base58'; 24 | public const NORMALIZATION_FORMAT_BASE32 = 'base32'; 25 | public const NORMALIZATION_FORMAT_RFC4122 = 'rfc4122'; 26 | public const NORMALIZATION_FORMAT_RFC9562 = self::NORMALIZATION_FORMAT_RFC4122; // RFC 9562 obsoleted RFC 4122 but the format is the same 27 | 28 | public const NORMALIZATION_FORMATS = [ 29 | self::NORMALIZATION_FORMAT_CANONICAL, 30 | self::NORMALIZATION_FORMAT_BASE58, 31 | self::NORMALIZATION_FORMAT_BASE32, 32 | self::NORMALIZATION_FORMAT_RFC4122, 33 | ]; 34 | 35 | private array $defaultContext = [ 36 | self::NORMALIZATION_FORMAT_KEY => self::NORMALIZATION_FORMAT_CANONICAL, 37 | ]; 38 | 39 | public function __construct(array $defaultContext = []) 40 | { 41 | $this->defaultContext = array_merge($this->defaultContext, $defaultContext); 42 | } 43 | 44 | public function getSupportedTypes(?string $format): array 45 | { 46 | return [ 47 | AbstractUid::class => true, 48 | ]; 49 | } 50 | 51 | public function normalize(mixed $data, ?string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null 52 | { 53 | return match ($context[self::NORMALIZATION_FORMAT_KEY] ?? $this->defaultContext[self::NORMALIZATION_FORMAT_KEY]) { 54 | self::NORMALIZATION_FORMAT_CANONICAL => (string) $data, 55 | self::NORMALIZATION_FORMAT_BASE58 => $data->toBase58(), 56 | self::NORMALIZATION_FORMAT_BASE32 => $data->toBase32(), 57 | self::NORMALIZATION_FORMAT_RFC4122 => $data->toRfc4122(), 58 | default => throw new LogicException(\sprintf('The "%s" format is not valid.', $context[self::NORMALIZATION_FORMAT_KEY] ?? $this->defaultContext[self::NORMALIZATION_FORMAT_KEY])), 59 | }; 60 | } 61 | 62 | public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool 63 | { 64 | return $data instanceof AbstractUid; 65 | } 66 | 67 | public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): mixed 68 | { 69 | try { 70 | return $type::fromString($data); 71 | } catch (\InvalidArgumentException|\TypeError) { 72 | throw NotNormalizableValueException::createForUnexpectedDataType(\sprintf('The data is not a valid "%s" string representation.', $type), $data, ['string'], $context['deserialization_path'] ?? null, true); 73 | } 74 | } 75 | 76 | public function supportsDenormalization(mixed $data, string $type, ?string $format = null, array $context = []): bool 77 | { 78 | return is_subclass_of($type, AbstractUid::class, true); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Normalizer/UnwrappingDenormalizer.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer\Normalizer; 13 | 14 | use Symfony\Component\PropertyAccess\PropertyAccess; 15 | use Symfony\Component\PropertyAccess\PropertyAccessorInterface; 16 | use Symfony\Component\Serializer\Exception\LogicException; 17 | use Symfony\Component\Serializer\SerializerAwareInterface; 18 | use Symfony\Component\Serializer\SerializerAwareTrait; 19 | 20 | /** 21 | * @author Eduard Bulava 22 | */ 23 | final class UnwrappingDenormalizer implements DenormalizerInterface, SerializerAwareInterface 24 | { 25 | use SerializerAwareTrait; 26 | 27 | public const UNWRAP_PATH = 'unwrap_path'; 28 | 29 | private readonly PropertyAccessorInterface $propertyAccessor; 30 | 31 | public function __construct(?PropertyAccessorInterface $propertyAccessor = null) 32 | { 33 | $this->propertyAccessor = $propertyAccessor ?? PropertyAccess::createPropertyAccessor(); 34 | } 35 | 36 | public function getSupportedTypes(?string $format): array 37 | { 38 | return ['*' => false]; 39 | } 40 | 41 | public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): mixed 42 | { 43 | $propertyPath = $context[self::UNWRAP_PATH]; 44 | $context['unwrapped'] = true; 45 | 46 | if ($propertyPath) { 47 | if (!$this->propertyAccessor->isReadable($data, $propertyPath)) { 48 | return null; 49 | } 50 | 51 | $data = $this->propertyAccessor->getValue($data, $propertyPath); 52 | } 53 | 54 | if (!$this->serializer instanceof DenormalizerInterface) { 55 | throw new LogicException('Cannot unwrap path because the injected serializer is not a denormalizer.'); 56 | } 57 | 58 | return $this->serializer->denormalize($data, $type, $format, $context); 59 | } 60 | 61 | public function supportsDenormalization(mixed $data, string $type, ?string $format = null, array $context = []): bool 62 | { 63 | return \array_key_exists(self::UNWRAP_PATH, $context) && !isset($context['unwrapped']); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Serializer Component 2 | ==================== 3 | 4 | The Serializer component handles serializing and deserializing data structures, 5 | including object graphs, into array structures or other formats like XML and 6 | JSON. 7 | 8 | Resources 9 | --------- 10 | 11 | * [Documentation](https://symfony.com/doc/current/components/serializer.html) 12 | * [Contributing](https://symfony.com/doc/current/contributing/index.html) 13 | * [Report issues](https://github.com/symfony/symfony/issues) and 14 | [send Pull Requests](https://github.com/symfony/symfony/pulls) 15 | in the [main Symfony repository](https://github.com/symfony/symfony) 16 | -------------------------------------------------------------------------------- /SerializerAwareInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer; 13 | 14 | /** 15 | * @author Jordi Boggiano 16 | */ 17 | interface SerializerAwareInterface 18 | { 19 | /** 20 | * Sets the owning Serializer object. 21 | */ 22 | public function setSerializer(SerializerInterface $serializer): void; 23 | } 24 | -------------------------------------------------------------------------------- /SerializerAwareTrait.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer; 13 | 14 | /** 15 | * @author Joel Wurtz 16 | */ 17 | trait SerializerAwareTrait 18 | { 19 | protected ?SerializerInterface $serializer = null; 20 | 21 | public function setSerializer(SerializerInterface $serializer): void 22 | { 23 | $this->serializer = $serializer; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /SerializerInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Serializer; 13 | 14 | use Symfony\Component\Serializer\Exception\ExceptionInterface; 15 | use Symfony\Component\Serializer\Exception\NotNormalizableValueException; 16 | use Symfony\Component\Serializer\Exception\UnexpectedValueException; 17 | 18 | /** 19 | * @author Jordi Boggiano 20 | */ 21 | interface SerializerInterface 22 | { 23 | /** 24 | * Serializes data in the appropriate format. 25 | * 26 | * @param array $context Options normalizers/encoders have access to 27 | * 28 | * @throws NotNormalizableValueException Occurs when a value cannot be normalized 29 | * @throws UnexpectedValueException Occurs when a value cannot be encoded 30 | * @throws ExceptionInterface Occurs for all the other cases of serialization-related errors 31 | */ 32 | public function serialize(mixed $data, string $format, array $context = []): string; 33 | 34 | /** 35 | * Deserializes data into the given type. 36 | * 37 | * @template TObject of object 38 | * @template TType of string|class-string 39 | * 40 | * @param TType $type 41 | * @param array $context 42 | * 43 | * @psalm-return (TType is class-string ? TObject : mixed) 44 | * 45 | * @phpstan-return ($type is class-string ? TObject : mixed) 46 | * 47 | * @throws NotNormalizableValueException Occurs when a value cannot be denormalized 48 | * @throws UnexpectedValueException Occurs when a value cannot be decoded 49 | * @throws ExceptionInterface Occurs for all the other cases of serialization-related errors 50 | */ 51 | public function deserialize(mixed $data, string $type, string $format, array $context = []): mixed; 52 | } 53 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "symfony/serializer", 3 | "type": "library", 4 | "description": "Handles serializing and deserializing data structures, including object graphs, into array structures or other formats like XML and JSON.", 5 | "keywords": [], 6 | "homepage": "https://symfony.com", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "Fabien Potencier", 11 | "email": "fabien@symfony.com" 12 | }, 13 | { 14 | "name": "Symfony Community", 15 | "homepage": "https://symfony.com/contributors" 16 | } 17 | ], 18 | "require": { 19 | "php": ">=8.2", 20 | "symfony/deprecation-contracts": "^2.5|^3", 21 | "symfony/polyfill-ctype": "~1.8" 22 | }, 23 | "require-dev": { 24 | "phpdocumentor/reflection-docblock": "^3.2|^4.0|^5.0", 25 | "phpstan/phpdoc-parser": "^1.0|^2.0", 26 | "seld/jsonlint": "^1.10", 27 | "symfony/cache": "^6.4|^7.0", 28 | "symfony/config": "^6.4|^7.0", 29 | "symfony/console": "^6.4|^7.0", 30 | "symfony/dependency-injection": "^7.2", 31 | "symfony/error-handler": "^6.4|^7.0", 32 | "symfony/filesystem": "^6.4|^7.0", 33 | "symfony/form": "^6.4|^7.0", 34 | "symfony/http-foundation": "^6.4|^7.0", 35 | "symfony/http-kernel": "^6.4|^7.0", 36 | "symfony/messenger": "^6.4|^7.0", 37 | "symfony/mime": "^6.4|^7.0", 38 | "symfony/property-access": "^6.4|^7.0", 39 | "symfony/property-info": "^6.4|^7.0", 40 | "symfony/translation-contracts": "^2.5|^3", 41 | "symfony/type-info": "^7.1", 42 | "symfony/uid": "^6.4|^7.0", 43 | "symfony/validator": "^6.4|^7.0", 44 | "symfony/var-dumper": "^6.4|^7.0", 45 | "symfony/var-exporter": "^6.4|^7.0", 46 | "symfony/yaml": "^6.4|^7.0" 47 | }, 48 | "conflict": { 49 | "phpdocumentor/reflection-docblock": "<3.2.2", 50 | "phpdocumentor/type-resolver": "<1.4.0", 51 | "symfony/dependency-injection": "<6.4", 52 | "symfony/property-access": "<6.4", 53 | "symfony/property-info": "<6.4", 54 | "symfony/uid": "<6.4", 55 | "symfony/validator": "<6.4", 56 | "symfony/yaml": "<6.4" 57 | }, 58 | "autoload": { 59 | "psr-4": { "Symfony\\Component\\Serializer\\": "" }, 60 | "exclude-from-classmap": [ 61 | "/Tests/" 62 | ] 63 | }, 64 | "minimum-stability": "dev" 65 | } 66 | --------------------------------------------------------------------------------