Skip to content

[Serializer] Support serialized names and paths configuration per group #58236

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 23 commits into
base: 7.4
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
c31acc5
[Serializer] Support serialized names and paths configuration per group
Sep 11, 2024
5f869f9
CS fixes
Sep 11, 2024
c7d2c4d
Fix static analysis
Sep 11, 2024
bb65ab8
Revert changes to exception message
Sep 13, 2024
899dc71
Merge branch '7.2' into serialized-names-and-paths-per-group
daniser Sep 16, 2024
223ad89
Attribute metadata: default asterisk group in serialized name/path ge…
Sep 19, 2024
796ed93
Attribute metadata: properly unset data if given serialized name/path…
Sep 19, 2024
0b12b51
Attribute metadata: get rid of unsafe array_filter function usage
Sep 19, 2024
7f6ac69
Attribute metadata: update merging logic
Sep 19, 2024
a46ed52
Default asterisk group in serialized name/path attributes
Sep 19, 2024
dcab6c1
Attribute metadata: fall back to asterisk group if group list is empt…
Sep 19, 2024
8492935
Name converter: add default groups to context during normalization
Sep 19, 2024
db5cc11
CS fix
Sep 20, 2024
d2d075e
Update CHANGELOG.md
Sep 20, 2024
4a044f0
Update tests
Sep 20, 2024
3911bd4
Merge branch '7.2' into serialized-names-and-paths-per-group
daniser Sep 20, 2024
fa5badf
Merge branch '7.2' into serialized-names-and-paths-per-group
Nov 5, 2024
3b3c2ba
Minor changes
Nov 5, 2024
719120a
Do not fall back to '*' group in serialized name/path attributes
Nov 5, 2024
d4bcf65
Change way of retrieving BC-friendly arguments
Nov 5, 2024
cf43396
Merge branch '7.2' into serialized-names-and-paths-per-group
Nov 11, 2024
3fbe2c6
Partial revert due to failing tests
Nov 11, 2024
b3f789f
Partial revert due to failing tests
Nov 11, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
class_exists(\Symfony\Component\Serializer\Attribute\SerializedName::class);

if (false) {
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::TARGET_PROPERTY)]
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::TARGET_PROPERTY | \Attribute::IS_REPEATABLE)]
class SerializedName extends \Symfony\Component\Serializer\Attribute\SerializedName
{
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
class_exists(\Symfony\Component\Serializer\Attribute\SerializedPath::class);

if (false) {
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::TARGET_PROPERTY)]
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::TARGET_PROPERTY | \Attribute::IS_REPEATABLE)]
class SerializedPath extends \Symfony\Component\Serializer\Attribute\SerializedPath
{
}
Expand Down
26 changes: 22 additions & 4 deletions src/Symfony/Component/Serializer/Attribute/SerializedName.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,23 +16,41 @@
/**
* @author Fabien Bourigault <bourigaultfabien@gmail.com>
*/
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::TARGET_PROPERTY)]
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::TARGET_PROPERTY | \Attribute::IS_REPEATABLE)]
class SerializedName
{
private array $groups;

/**
* @param string $serializedName The name of the property as it will be serialized
* @param string $serializedName The name of the property as it will be serialized
* @param string|string[] $groups The groups to use when serializing or deserializing
*/
public function __construct(private readonly string $serializedName)
{
public function __construct(
private readonly string $serializedName,
string|array $groups = ['*'],
) {
if ('' === $serializedName) {
throw new InvalidArgumentException(\sprintf('Parameter given to "%s" must be a non-empty string.', self::class));
}

$this->groups = ((array) $groups) ?: ['*'];

foreach ($this->groups as $group) {
if (!\is_string($group)) {
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)));
}
}
}

public function getSerializedName(): string
{
return $this->serializedName;
}

public function getGroups(): array
{
return $this->groups;
}
}

if (!class_exists(\Symfony\Component\Serializer\Annotation\SerializedName::class, false)) {
Expand Down
22 changes: 19 additions & 3 deletions src/Symfony/Component/Serializer/Attribute/SerializedPath.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,27 +18,43 @@
/**
* @author Tobias Bönner <tobi@boenner.family>
*/
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::TARGET_PROPERTY)]
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::TARGET_PROPERTY | \Attribute::IS_REPEATABLE)]
class SerializedPath
{
private PropertyPath $serializedPath;

private array $groups;

/**
* @param string $serializedPath A path using a valid PropertyAccess syntax where the value is stored in a normalized representation
* @param string $serializedPath A path using a valid PropertyAccess syntax where the value is stored in a normalized representation
* @param string|string[] $groups The groups to use when serializing or deserializing
*/
public function __construct(string $serializedPath)
public function __construct(string $serializedPath, string|array $groups = ['*'])
{
try {
$this->serializedPath = new PropertyPath($serializedPath);
} catch (InvalidPropertyPathException $pathException) {
throw new InvalidArgumentException(\sprintf('Parameter given to "%s" must be a valid property path.', self::class));
}

$this->groups = ((array) $groups) ?: ['*'];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure that we must fall back to ['*'] here, maybe an empty array can be valid in some cases (same for SerializedName)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've tried that but it led to more failing tests. So I reverted it back until we decide what it could actually mean, so we could change the tests.
In my point of view, empty array here is nonsense, as it means "apply serialized name/path to no groups".
As alternative to fallback to ['*'], we can throw exception. WDYT?


foreach ($this->groups as $group) {
if (!\is_string($group)) {
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)));
}
}
}

public function getSerializedPath(): PropertyPath
{
return $this->serializedPath;
}

public function getGroups(): array
{
return $this->groups;
}
}

if (!class_exists(\Symfony\Component\Serializer\Annotation\SerializedPath::class, false)) {
Expand Down
1 change: 1 addition & 0 deletions src/Symfony/Component/Serializer/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ CHANGELOG
default contexts, name converters, sets of normalizers and encoders
* Add support for collection profiles of multiple serializer instances
* Deprecate `AdvancedNameConverterInterface`, use `NameConverterInterface` instead
* Add support for serialized names and paths configuration per group

7.1
---
Expand Down
5 changes: 3 additions & 2 deletions src/Symfony/Component/Serializer/Command/DebugCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Serializer\Mapping\AttributeMetadata;
use Symfony\Component\Serializer\Mapping\ClassMetadataInterface;
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;

Expand Down Expand Up @@ -100,8 +101,8 @@ private function getAttributesData(ClassMetadataInterface $classMetadata): array
$data[$attributeMetadata->getName()] = [
'groups' => $attributeMetadata->getGroups(),
'maxDepth' => $attributeMetadata->getMaxDepth(),
'serializedName' => $attributeMetadata->getSerializedName(),
'serializedPath' => $attributeMetadata->getSerializedPath() ? (string) $attributeMetadata->getSerializedPath() : null,
'serializedNames' => AttributeMetadata::getSerializedNamesFromAttributeMetadata($attributeMetadata),
'serializedPaths' => array_map('strval', AttributeMetadata::getSerializedPathsFromAttributeMetadata($attributeMetadata)),
'ignore' => $attributeMetadata->isIgnored(),
'normalizationContexts' => $attributeMetadata->getNormalizationContexts(),
'denormalizationContexts' => $attributeMetadata->getDenormalizationContexts(),
Expand Down
130 changes: 115 additions & 15 deletions src/Symfony/Component/Serializer/Mapping/AttributeMetadata.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,18 +40,22 @@ class AttributeMetadata implements AttributeMetadataInterface
public ?int $maxDepth = null;

/**
* @var array<string, string> Serialized names per group name ("*" applies to all groups)
*
* @internal This property is public in order to reduce the size of the
* class' serialized representation. Do not access it. Use
* {@link getSerializedName()} instead.
* {@link getSerializedNames()} instead.
*/
public ?string $serializedName = null;
public array $serializedNames = [];

/**
* @var array<string, PropertyPath> Serialized paths per group name ("*" applies to all groups)
*
* @internal This property is public in order to reduce the size of the
* class' serialized representation. Do not access it. Use
* {@link getSerializedPath()} instead.
* {@link getSerializedPaths()} instead.
*/
public ?PropertyPath $serializedPath = null;
public array $serializedPaths = [];

/**
* @internal This property is public in order to reduce the size of the
Expand Down Expand Up @@ -110,24 +114,70 @@ public function getMaxDepth(): ?int
return $this->maxDepth;
}

public function setSerializedName(?string $serializedName): void
public function setSerializedName(?string $serializedName /* , array $groups = ['*'] */): void
{
$groups = 2 <= \func_num_args() ? (func_get_arg(1) ?: ['*']) : ['*'];

if (isset($serializedName)) {
foreach ($groups as $group) {
$this->serializedNames[$group] = $serializedName;
}
} else {
foreach ($groups as $group) {
unset($this->serializedNames[$group]);
}
}
}

public function getSerializedName(/* array $groups = ['*'] */): ?string
{
$groups = 1 <= \func_num_args() ? func_get_arg(0) : ['*'];

foreach ($groups as $group) {
if (isset($this->serializedNames[$group])) {
return $this->serializedNames[$group];
}
}

return $this->serializedNames['*'] ?? null;
}

public function getSerializedNames(): array
{
$this->serializedName = $serializedName;
return $this->serializedNames;
}

public function getSerializedName(): ?string
public function setSerializedPath(?PropertyPath $serializedPath = null /* , array $groups = ['*'] */): void
{
return $this->serializedName;
$groups = 2 <= \func_num_args() ? (func_get_arg(1) ?: ['*']) : ['*'];

if (isset($serializedPath)) {
foreach ($groups as $group) {
$this->serializedPaths[$group] = $serializedPath;
}
} else {
foreach ($groups as $group) {
unset($this->serializedPaths[$group]);
}
}
}

public function setSerializedPath(?PropertyPath $serializedPath = null): void
public function getSerializedPath(/* array $groups = ['*'] */): ?PropertyPath
{
$this->serializedPath = $serializedPath;
$groups = 1 <= \func_num_args() ? func_get_arg(0) : ['*'];

foreach ($groups as $group) {
if (isset($this->serializedPaths[$group])) {
return $this->serializedPaths[$group];
}
}

return $this->serializedPaths['*'] ?? null;
}

public function getSerializedPath(): ?PropertyPath
public function getSerializedPaths(): array
{
return $this->serializedPath;
return $this->serializedPaths;
}

public function setIgnore(bool $ignore): void
Expand Down Expand Up @@ -200,8 +250,16 @@ public function merge(AttributeMetadataInterface $attributeMetadata): void

// Overwrite only if not defined
$this->maxDepth ??= $attributeMetadata->getMaxDepth();
$this->serializedName ??= $attributeMetadata->getSerializedName();
$this->serializedPath ??= $attributeMetadata->getSerializedPath();

// Overwrite only if serialized names are empty
if (!$this->serializedNames) {
$this->serializedNames = self::getSerializedNamesFromAttributeMetadata($attributeMetadata);
}

// Overwrite only if serialized paths are empty
if (!$this->serializedPaths) {
$this->serializedPaths = self::getSerializedPathsFromAttributeMetadata($attributeMetadata);
}

// Overwrite only if both contexts are empty
if (!$this->normalizationContexts && !$this->denormalizationContexts) {
Expand All @@ -214,13 +272,55 @@ public function merge(AttributeMetadataInterface $attributeMetadata): void
}
}

/**
* BC layer for extraction of serialized names from attribute metadata.
* Can be removed as soon as AttributeMetadataInterface::getSerializedNames() become part of the interface.
*
* @internal
*
* @return array<string, string>
*/
public static function getSerializedNamesFromAttributeMetadata(AttributeMetadataInterface $attributeMetadata): array
{
if (method_exists($attributeMetadata, 'getSerializedNames')) {
return $attributeMetadata->getSerializedNames();
}

if (null !== $serializedName = $attributeMetadata->getSerializedName()) {
return ['*' => $serializedName];
}

return [];
}

/**
* BC layer for extraction of serialized paths from attribute metadata.
* Can be removed as soon as AttributeMetadataInterface::getSerializedPaths() become part of the interface.
*
* @internal
*
* @return array<string, PropertyPath>
*/
public static function getSerializedPathsFromAttributeMetadata(AttributeMetadataInterface $attributeMetadata): array
{
if (method_exists($attributeMetadata, 'getSerializedPaths')) {
return $attributeMetadata->getSerializedPaths();
}

if (null !== $serializedPath = $attributeMetadata->getSerializedPath()) {
return ['*' => $serializedPath];
}

return [];
}

/**
* Returns the names of the properties that should be serialized.
*
* @return string[]
*/
public function __sleep(): array
{
return ['name', 'groups', 'maxDepth', 'serializedName', 'serializedPath', 'ignore', 'normalizationContexts', 'denormalizationContexts'];
return ['name', 'groups', 'maxDepth', 'serializedNames', 'serializedPaths', 'ignore', 'normalizationContexts', 'denormalizationContexts'];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@
* @internal
*
* @author Kévin Dunglas <dunglas@gmail.com>
*
* @method string[] getSerializedNames() Gets all the serialized names per group.
* @method PropertyPath[] getSerializedPaths() Gets all the serialized paths per group.
*/
interface AttributeMetadataInterface
{
Expand Down Expand Up @@ -53,17 +56,31 @@ public function getMaxDepth(): ?int;

/**
* Sets the serialization name for this attribute.
*
* @param string[] $groups
*/
public function setSerializedName(?string $serializedName): void;
public function setSerializedName(?string $serializedName /* , array $groups = ['*'] */): void;

/**
* Gets the serialization name for this attribute.
*
* @param string[] $groups
*/
public function getSerializedName(): ?string;
public function getSerializedName(/* array $groups = ['*'] */): ?string;

public function setSerializedPath(?PropertyPath $serializedPath): void;
/**
* Sets the serialization path for this attribute.
*
* @param string[] $groups
*/
public function setSerializedPath(?PropertyPath $serializedPath /* , array $groups = ['*'] */): void;

public function getSerializedPath(): ?PropertyPath;
/**
* Gets the serialization path for this attribute.
*
* @param string[] $groups
*/
public function getSerializedPath(/* array $groups = ['*'] */): ?PropertyPath;

/**
* Sets if this attribute must be ignored or not.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,10 @@ private function generateDeclaredClassMetadata(array $classMetadatas): string
$attributesMetadata[$attributeMetadata->getName()] = [
$attributeMetadata->getGroups(),
$attributeMetadata->getMaxDepth(),
$attributeMetadata->getSerializedName(),
$attributeMetadata->getSerializedPath(),
method_exists($attributeMetadata, 'getSerializedNames')
? $attributeMetadata->getSerializedNames() : $attributeMetadata->getSerializedName(),
method_exists($attributeMetadata, 'getSerializedPaths')
? $attributeMetadata->getSerializedPaths() : $attributeMetadata->getSerializedPath(),
];
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,12 @@ public function getMetadataFor(string|object $value): ClassMetadataInterface
$classMetadata = new ClassMetadata($className);
foreach ($this->compiledClassMetadata[$className][0] as $name => $compiledAttributesMetadata) {
$classMetadata->attributesMetadata[$name] = $attributeMetadata = new AttributeMetadata($name);
[$attributeMetadata->groups, $attributeMetadata->maxDepth, $attributeMetadata->serializedName] = $compiledAttributesMetadata;
[$attributeMetadata->groups, $attributeMetadata->maxDepth, $serializedNames] = $compiledAttributesMetadata;
$attributeMetadata->serializedNames = match (true) {
\is_array($serializedNames) => $serializedNames,
null === $serializedNames => [],
default => ['*' => $serializedNames],
};
}
$classMetadata->classDiscriminatorMapping = $this->compiledClassMetadata[$className][1]
? new ClassDiscriminatorMapping(...$this->compiledClassMetadata[$className][1])
Expand Down
Loading
Loading
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy