diff --git a/README.md b/README.md index 8481267..562f5ce 100755 --- a/README.md +++ b/README.md @@ -66,6 +66,26 @@ class SomeClass } ``` +### Serialization groups + +```php +class SomeClass +{ + #[SerializerContext(groups: ['group1'])] + public string $property1; + + // Has implicit group 'default' + public string $property2; +} + + +$serializer = new Serializer(); +$object = new SomeClass(); +$object->property1 = 'value1'; +$object->property2 = 'value2'; +$serializer->serialize($object, attributes: [SerializerInterface::ATTRIBUTE_GROUPS => ['group1']]); // {"property1":"value1"} +``` + ### Custom date format Per property: diff --git a/src/Attribute/SerializerContext.php b/src/Attribute/SerializerContext.php index 9514781..66ecb21 100644 --- a/src/Attribute/SerializerContext.php +++ b/src/Attribute/SerializerContext.php @@ -8,12 +8,14 @@ class SerializerContext { /** - * @param array> $typeMap + * @param array>|null $typeMap + * @param array $groups * @param array $attributes */ public function __construct( public ?string $name = null, public ?array $typeMap = null, + public array $groups = [], public array $attributes = [], ) {} } diff --git a/src/Denormalizer.php b/src/Denormalizer.php index f59f857..ad71615 100644 --- a/src/Denormalizer.php +++ b/src/Denormalizer.php @@ -11,7 +11,7 @@ { /** * @param array $denormalizers - * @param array $attributes + * @param array $attributes */ public function __construct( private array $denormalizers, diff --git a/src/Metadata/DataType.php b/src/Metadata/DataType.php index 821739c..3835fa2 100644 --- a/src/Metadata/DataType.php +++ b/src/Metadata/DataType.php @@ -14,7 +14,7 @@ class DataType * @param class-string|null $className * @param array $listType * @param array> $typeMap - * @param array $attributes + * @param array $attributes */ public function __construct( public BuiltInType $type, diff --git a/src/Metadata/MetadataExtractor.php b/src/Metadata/MetadataExtractor.php index 92580a9..eae81f3 100644 --- a/src/Metadata/MetadataExtractor.php +++ b/src/Metadata/MetadataExtractor.php @@ -108,6 +108,7 @@ private function extractPropertyMetadata( $propertyName, $serializerContext, ), + groups: [] === $serializerContext->groups ? ['default'] : $serializerContext->groups, attributes: $serializerContext->attributes, readAccess: $this->getPropertyReadAccess($reflectionProperty), writeAccess: $this->getPropertyWriteAccess($reflectionProperty), diff --git a/src/Metadata/PropertyMetadata.php b/src/Metadata/PropertyMetadata.php index b051efb..0759d45 100644 --- a/src/Metadata/PropertyMetadata.php +++ b/src/Metadata/PropertyMetadata.php @@ -11,12 +11,14 @@ class PropertyMetadata /** * @param DataType[] $types + * @param string[] $groups * @param array $attributes */ public function __construct( public string $name, public string $serializedName, public array $types, + public array $groups, public array $attributes, public ReadAccess $readAccess, public WriteAccess $writeAccess, diff --git a/src/Normalizer.php b/src/Normalizer.php index 9a2b125..8cbc47e 100644 --- a/src/Normalizer.php +++ b/src/Normalizer.php @@ -10,7 +10,7 @@ { /** * @param array $normalizers - * @param array $attributes + * @param array $attributes */ public function __construct( private array $normalizers, @@ -19,7 +19,7 @@ public function __construct( ) {} /** - * @param array $attributes + * @param array $attributes * * @throws SerializerException */ diff --git a/src/Normalizer/ObjectNormalizer.php b/src/Normalizer/ObjectNormalizer.php index 248fa69..cdfbd25 100644 --- a/src/Normalizer/ObjectNormalizer.php +++ b/src/Normalizer/ObjectNormalizer.php @@ -17,12 +17,18 @@ public function normalize(mixed $data, Normalizer $normalizer, array $attributes $classMetadata = $normalizer->getMetadataExtractor()->extractClassMetadata($data::class); $normalizedData = []; + /** @var null|string[] $groups */ + $groups = $attributes[SerializerInterface::ATTRIBUTE_GROUPS] ?? null; foreach ($classMetadata->properties as $name => $propertyMetadata) { if (ReadAccess::NONE === $propertyMetadata->readAccess) { continue; } + if (null !== $groups && [] === array_intersect($groups, $propertyMetadata->groups)) { + continue; + } + if (ReadAccess::DIRECT === $propertyMetadata->readAccess) { /** @var scalar|null|object|array $value */ $value = $data->{$name}; diff --git a/src/NormalizerInterface.php b/src/NormalizerInterface.php index a23cd1f..067511f 100644 --- a/src/NormalizerInterface.php +++ b/src/NormalizerInterface.php @@ -9,7 +9,7 @@ interface NormalizerInterface /** * @template T of object|array|scalar|null * - * @param array $attributes + * @param array $attributes * * @return T|T[] * @throws SerializerException diff --git a/src/Serializer.php b/src/Serializer.php index 1196ca5..40ce8ef 100644 --- a/src/Serializer.php +++ b/src/Serializer.php @@ -30,7 +30,7 @@ class Serializer implements SerializerInterface /** * @param array $normalizers * @param array $denormalizers - * @param array $attributes + * @param array $attributes */ public function __construct( array $normalizers = [], @@ -75,9 +75,9 @@ public function __construct( ); } - public function serialize(mixed $data): string + public function serialize(mixed $data, array $attributes = []): string { - $normalizedData = $this->normalize($data); + $normalizedData = $this->normalize($data, $attributes); try { return json_encode($normalizedData, JSON_THROW_ON_ERROR | JSON_PRESERVE_ZERO_FRACTION); @@ -99,11 +99,13 @@ public function deserialize(string $data, ?string $type = null): mixed } /** + * @param array $attributes + * * @throws SerializerException */ - public function normalize(mixed $data): mixed + public function normalize(mixed $data, array $attributes): mixed { - return $this->normalizer->normalize($data, []); + return $this->normalizer->normalize($data, $attributes); } /** diff --git a/src/SerializerInterface.php b/src/SerializerInterface.php index 5252987..734535c 100644 --- a/src/SerializerInterface.php +++ b/src/SerializerInterface.php @@ -8,13 +8,16 @@ interface SerializerInterface { public const string ATTRIBUTE_DATETIME_FORMAT = 'datetime-format'; public const string ATTRIBUTE_SKIP_NULL_VALUES = 'skip-null-values'; + public const string ATTRIBUTE_GROUPS = 'groups'; /** * Serializes data into a string. * + * @param array $attributes + * * @throws SerializerException */ - public function serialize(mixed $data): string; + public function serialize(mixed $data, array $attributes = []): string; /** * Deserializes data into the given type. diff --git a/tests/Datasets/Group/SubLevel1.php b/tests/Datasets/Group/SubLevel1.php new file mode 100644 index 0000000..6b0bdd1 --- /dev/null +++ b/tests/Datasets/Group/SubLevel1.php @@ -0,0 +1,32 @@ +propGroup1 = 'subLevel2PropGroup1'; + $subLevel2->propGroup2 = 'subLevel2PropGroup2'; + $subLevel2->propGroup1And2 = 'subLevel2PropGroup1And2'; + $subLevel2->propNoGroup = 'subLevel2PropNoGroup'; + + $subLevel1 = new \Vuryss\Serializer\Tests\Datasets\Group\SubLevel1(); + $subLevel1->nestedPropGroup1 = clone $subLevel2; + $subLevel1->nestedPropGroup2 = clone $subLevel2; + $subLevel1->nestedPropGroup1And2 = clone $subLevel2; + $subLevel1->nestedPropNoGroup = clone $subLevel2; + $subLevel1->propGroup1 = 'subLevel1PropGroup1'; + $subLevel1->propGroup2 = 'subLevel1PropGroup2'; + $subLevel1->propGroup1And2 = 'subLevel1PropGroup1And2'; + $subLevel1->propNoGroup = 'subLevel1PropNoGroup'; + + $data = new \Vuryss\Serializer\Tests\Datasets\Group\TopLevelClass(); + $data->nestedPropGroup1 = clone $subLevel1; + $data->nestedPropGroup2 = clone $subLevel1; + $data->nestedPropGroup1And2 = clone $subLevel1; + $data->nestedPropNoGroup = clone $subLevel1; + $data->propGroup1 = 'topLevelPropGroup1'; + $data->propGroup2 = 'topLevelPropGroup2'; + $data->propGroup1And2 = 'topLevelPropGroup1And2'; + $data->propNoGroup = 'topLevelPropNoGroup'; + + $serializer = new Serializer(); + expect($serializer->serialize($data, attributes: [SerializerInterface::ATTRIBUTE_GROUPS => $groups])) + ->toBe(json_encode($expected)); +})->with([ + 'no groups' => [ + null, + [ + 'nestedPropGroup1' => [ + 'nestedPropGroup1' => [ + 'propGroup1' => 'subLevel2PropGroup1', + 'propGroup2' => 'subLevel2PropGroup2', + 'propGroup1And2' => 'subLevel2PropGroup1And2', + 'propNoGroup' => 'subLevel2PropNoGroup', + ], + 'nestedPropGroup2' => [ + 'propGroup1' => 'subLevel2PropGroup1', + 'propGroup2' => 'subLevel2PropGroup2', + 'propGroup1And2' => 'subLevel2PropGroup1And2', + 'propNoGroup' => 'subLevel2PropNoGroup', + ], + 'nestedPropGroup1And2' => [ + 'propGroup1' => 'subLevel2PropGroup1', + 'propGroup2' => 'subLevel2PropGroup2', + 'propGroup1And2' => 'subLevel2PropGroup1And2', + 'propNoGroup' => 'subLevel2PropNoGroup', + ], + 'nestedPropNoGroup' => [ + 'propGroup1' => 'subLevel2PropGroup1', + 'propGroup2' => 'subLevel2PropGroup2', + 'propGroup1And2' => 'subLevel2PropGroup1And2', + 'propNoGroup' => 'subLevel2PropNoGroup', + ], + 'propGroup1' => 'subLevel1PropGroup1', + 'propGroup2' => 'subLevel1PropGroup2', + 'propGroup1And2' => 'subLevel1PropGroup1And2', + 'propNoGroup' => 'subLevel1PropNoGroup', + ], + 'nestedPropGroup2' => [ + 'nestedPropGroup1' => [ + 'propGroup1' => 'subLevel2PropGroup1', + 'propGroup2' => 'subLevel2PropGroup2', + 'propGroup1And2' => 'subLevel2PropGroup1And2', + 'propNoGroup' => 'subLevel2PropNoGroup', + ], + 'nestedPropGroup2' => [ + 'propGroup1' => 'subLevel2PropGroup1', + 'propGroup2' => 'subLevel2PropGroup2', + 'propGroup1And2' => 'subLevel2PropGroup1And2', + 'propNoGroup' => 'subLevel2PropNoGroup', + ], + 'nestedPropGroup1And2' => [ + 'propGroup1' => 'subLevel2PropGroup1', + 'propGroup2' => 'subLevel2PropGroup2', + 'propGroup1And2' => 'subLevel2PropGroup1And2', + 'propNoGroup' => 'subLevel2PropNoGroup', + ], + 'nestedPropNoGroup' => [ + 'propGroup1' => 'subLevel2PropGroup1', + 'propGroup2' => 'subLevel2PropGroup2', + 'propGroup1And2' => 'subLevel2PropGroup1And2', + 'propNoGroup' => 'subLevel2PropNoGroup', + ], + 'propGroup1' => 'subLevel1PropGroup1', + 'propGroup2' => 'subLevel1PropGroup2', + 'propGroup1And2' => 'subLevel1PropGroup1And2', + 'propNoGroup' => 'subLevel1PropNoGroup', + ], + 'nestedPropGroup1And2' => [ + 'nestedPropGroup1' => [ + 'propGroup1' => 'subLevel2PropGroup1', + 'propGroup2' => 'subLevel2PropGroup2', + 'propGroup1And2' => 'subLevel2PropGroup1And2', + 'propNoGroup' => 'subLevel2PropNoGroup', + ], + 'nestedPropGroup2' => [ + 'propGroup1' => 'subLevel2PropGroup1', + 'propGroup2' => 'subLevel2PropGroup2', + 'propGroup1And2' => 'subLevel2PropGroup1And2', + 'propNoGroup' => 'subLevel2PropNoGroup', + ], + 'nestedPropGroup1And2' => [ + 'propGroup1' => 'subLevel2PropGroup1', + 'propGroup2' => 'subLevel2PropGroup2', + 'propGroup1And2' => 'subLevel2PropGroup1And2', + 'propNoGroup' => 'subLevel2PropNoGroup', + ], + 'nestedPropNoGroup' => [ + 'propGroup1' => 'subLevel2PropGroup1', + 'propGroup2' => 'subLevel2PropGroup2', + 'propGroup1And2' => 'subLevel2PropGroup1And2', + 'propNoGroup' => 'subLevel2PropNoGroup', + ], + 'propGroup1' => 'subLevel1PropGroup1', + 'propGroup2' => 'subLevel1PropGroup2', + 'propGroup1And2' => 'subLevel1PropGroup1And2', + 'propNoGroup' => 'subLevel1PropNoGroup', + ], + 'nestedPropNoGroup' => [ + 'nestedPropGroup1' => [ + 'propGroup1' => 'subLevel2PropGroup1', + 'propGroup2' => 'subLevel2PropGroup2', + 'propGroup1And2' => 'subLevel2PropGroup1And2', + 'propNoGroup' => 'subLevel2PropNoGroup', + ], + 'nestedPropGroup2' => [ + 'propGroup1' => 'subLevel2PropGroup1', + 'propGroup2' => 'subLevel2PropGroup2', + 'propGroup1And2' => 'subLevel2PropGroup1And2', + 'propNoGroup' => 'subLevel2PropNoGroup', + ], + 'nestedPropGroup1And2' => [ + 'propGroup1' => 'subLevel2PropGroup1', + 'propGroup2' => 'subLevel2PropGroup2', + 'propGroup1And2' => 'subLevel2PropGroup1And2', + 'propNoGroup' => 'subLevel2PropNoGroup', + ], + 'nestedPropNoGroup' => [ + 'propGroup1' => 'subLevel2PropGroup1', + 'propGroup2' => 'subLevel2PropGroup2', + 'propGroup1And2' => 'subLevel2PropGroup1And2', + 'propNoGroup' => 'subLevel2PropNoGroup', + ], + 'propGroup1' => 'subLevel1PropGroup1', + 'propGroup2' => 'subLevel1PropGroup2', + 'propGroup1And2' => 'subLevel1PropGroup1And2', + 'propNoGroup' => 'subLevel1PropNoGroup', + ], + 'propGroup1' => 'topLevelPropGroup1', + 'propGroup2' => 'topLevelPropGroup2', + 'propGroup1And2' => 'topLevelPropGroup1And2', + 'propNoGroup' => 'topLevelPropNoGroup', + ], + ], + 'group1' => [ + ['group1'], + [ + 'nestedPropGroup1' => [ + 'nestedPropGroup1' => [ + 'propGroup1' => 'subLevel2PropGroup1', + 'propGroup1And2' => 'subLevel2PropGroup1And2', + ], + 'nestedPropGroup1And2' => [ + 'propGroup1' => 'subLevel2PropGroup1', + 'propGroup1And2' => 'subLevel2PropGroup1And2', + ], + 'propGroup1' => 'subLevel1PropGroup1', + 'propGroup1And2' => 'subLevel1PropGroup1And2', + ], + 'nestedPropGroup1And2' => [ + 'nestedPropGroup1' => [ + 'propGroup1' => 'subLevel2PropGroup1', + 'propGroup1And2' => 'subLevel2PropGroup1And2', + ], + 'nestedPropGroup1And2' => [ + 'propGroup1' => 'subLevel2PropGroup1', + 'propGroup1And2' => 'subLevel2PropGroup1And2', + ], + 'propGroup1' => 'subLevel1PropGroup1', + 'propGroup1And2' => 'subLevel1PropGroup1And2', + ], + 'propGroup1' => 'topLevelPropGroup1', + 'propGroup1And2' => 'topLevelPropGroup1And2', + ], + ], + 'group2' => [ + ['group2'], + [ + 'nestedPropGroup2' => [ + 'nestedPropGroup2' => [ + 'propGroup2' => 'subLevel2PropGroup2', + 'propGroup1And2' => 'subLevel2PropGroup1And2', + ], + 'nestedPropGroup1And2' => [ + 'propGroup2' => 'subLevel2PropGroup2', + 'propGroup1And2' => 'subLevel2PropGroup1And2', + ], + 'propGroup2' => 'subLevel1PropGroup2', + 'propGroup1And2' => 'subLevel1PropGroup1And2', + ], + 'nestedPropGroup1And2' => [ + 'nestedPropGroup2' => [ + 'propGroup2' => 'subLevel2PropGroup2', + 'propGroup1And2' => 'subLevel2PropGroup1And2', + ], + 'nestedPropGroup1And2' => [ + 'propGroup2' => 'subLevel2PropGroup2', + 'propGroup1And2' => 'subLevel2PropGroup1And2', + ], + 'propGroup2' => 'subLevel1PropGroup2', + 'propGroup1And2' => 'subLevel1PropGroup1And2', + ], + 'propGroup2' => 'topLevelPropGroup2', + 'propGroup1And2' => 'topLevelPropGroup1And2', + ], + ], + 'group1 and group2' => [ + ['group1', 'group2'], + [ + 'nestedPropGroup1' => [ + 'nestedPropGroup1' => [ + 'propGroup1' => 'subLevel2PropGroup1', + 'propGroup2' => 'subLevel2PropGroup2', + 'propGroup1And2' => 'subLevel2PropGroup1And2', + ], + 'nestedPropGroup2' => [ + 'propGroup1' => 'subLevel2PropGroup1', + 'propGroup2' => 'subLevel2PropGroup2', + 'propGroup1And2' => 'subLevel2PropGroup1And2', + ], + 'nestedPropGroup1And2' => [ + 'propGroup1' => 'subLevel2PropGroup1', + 'propGroup2' => 'subLevel2PropGroup2', + 'propGroup1And2' => 'subLevel2PropGroup1And2', + ], + 'propGroup1' => 'subLevel1PropGroup1', + 'propGroup2' => 'subLevel1PropGroup2', + 'propGroup1And2' => 'subLevel1PropGroup1And2', + ], + 'nestedPropGroup2' => [ + 'nestedPropGroup1' => [ + 'propGroup1' => 'subLevel2PropGroup1', + 'propGroup2' => 'subLevel2PropGroup2', + 'propGroup1And2' => 'subLevel2PropGroup1And2', + ], + 'nestedPropGroup2' => [ + 'propGroup1' => 'subLevel2PropGroup1', + 'propGroup2' => 'subLevel2PropGroup2', + 'propGroup1And2' => 'subLevel2PropGroup1And2', + ], + 'nestedPropGroup1And2' => [ + 'propGroup1' => 'subLevel2PropGroup1', + 'propGroup2' => 'subLevel2PropGroup2', + 'propGroup1And2' => 'subLevel2PropGroup1And2', + ], + 'propGroup1' => 'subLevel1PropGroup1', + 'propGroup2' => 'subLevel1PropGroup2', + 'propGroup1And2' => 'subLevel1PropGroup1And2', + ], + 'nestedPropGroup1And2' => [ + 'nestedPropGroup1' => [ + 'propGroup1' => 'subLevel2PropGroup1', + 'propGroup2' => 'subLevel2PropGroup2', + 'propGroup1And2' => 'subLevel2PropGroup1And2', + ], + 'nestedPropGroup2' => [ + 'propGroup1' => 'subLevel2PropGroup1', + 'propGroup2' => 'subLevel2PropGroup2', + 'propGroup1And2' => 'subLevel2PropGroup1And2', + ], + 'nestedPropGroup1And2' => [ + 'propGroup1' => 'subLevel2PropGroup1', + 'propGroup2' => 'subLevel2PropGroup2', + 'propGroup1And2' => 'subLevel2PropGroup1And2', + ], + 'propGroup1' => 'subLevel1PropGroup1', + 'propGroup2' => 'subLevel1PropGroup2', + 'propGroup1And2' => 'subLevel1PropGroup1And2', + ], + 'propGroup1' => 'topLevelPropGroup1', + 'propGroup2' => 'topLevelPropGroup2', + 'propGroup1And2' => 'topLevelPropGroup1And2', + ], + ], + 'group1 and default combination' => [ + ['group1', 'default'], + [ + 'nestedPropGroup1' => [ + 'nestedPropGroup1' => [ + 'propGroup1' => 'subLevel2PropGroup1', + 'propGroup1And2' => 'subLevel2PropGroup1And2', + 'propNoGroup' => 'subLevel2PropNoGroup', + ], + 'nestedPropGroup1And2' => [ + 'propGroup1' => 'subLevel2PropGroup1', + 'propGroup1And2' => 'subLevel2PropGroup1And2', + 'propNoGroup' => 'subLevel2PropNoGroup', + ], + 'nestedPropNoGroup' => [ + 'propGroup1' => 'subLevel2PropGroup1', + 'propGroup1And2' => 'subLevel2PropGroup1And2', + 'propNoGroup' => 'subLevel2PropNoGroup', + ], + 'propGroup1' => 'subLevel1PropGroup1', + 'propGroup1And2' => 'subLevel1PropGroup1And2', + 'propNoGroup' => 'subLevel1PropNoGroup', + ], + 'nestedPropGroup1And2' => [ + 'nestedPropGroup1' => [ + 'propGroup1' => 'subLevel2PropGroup1', + 'propGroup1And2' => 'subLevel2PropGroup1And2', + 'propNoGroup' => 'subLevel2PropNoGroup', + ], + 'nestedPropGroup1And2' => [ + 'propGroup1' => 'subLevel2PropGroup1', + 'propGroup1And2' => 'subLevel2PropGroup1And2', + 'propNoGroup' => 'subLevel2PropNoGroup', + ], + 'nestedPropNoGroup' => [ + 'propGroup1' => 'subLevel2PropGroup1', + 'propGroup1And2' => 'subLevel2PropGroup1And2', + 'propNoGroup' => 'subLevel2PropNoGroup', + ], + 'propGroup1' => 'subLevel1PropGroup1', + 'propGroup1And2' => 'subLevel1PropGroup1And2', + 'propNoGroup' => 'subLevel1PropNoGroup', + ], + 'nestedPropNoGroup' => [ + 'nestedPropGroup1' => [ + 'propGroup1' => 'subLevel2PropGroup1', + 'propGroup1And2' => 'subLevel2PropGroup1And2', + 'propNoGroup' => 'subLevel2PropNoGroup', + ], + 'nestedPropGroup1And2' => [ + 'propGroup1' => 'subLevel2PropGroup1', + 'propGroup1And2' => 'subLevel2PropGroup1And2', + 'propNoGroup' => 'subLevel2PropNoGroup', + ], + 'nestedPropNoGroup' => [ + 'propGroup1' => 'subLevel2PropGroup1', + 'propGroup1And2' => 'subLevel2PropGroup1And2', + 'propNoGroup' => 'subLevel2PropNoGroup', + ], + 'propGroup1' => 'subLevel1PropGroup1', + 'propGroup1And2' => 'subLevel1PropGroup1And2', + 'propNoGroup' => 'subLevel1PropNoGroup', + ], + 'propGroup1' => 'topLevelPropGroup1', + 'propGroup1And2' => 'topLevelPropGroup1And2', + 'propNoGroup' => 'topLevelPropNoGroup', + ], + ], +]);