Skip to content

Commit 8482d29

Browse files
committed
Feature: Allow to normalize and denormalize custom serialized objects.
1 parent 5627d6b commit 8482d29

File tree

13 files changed

+295
-14
lines changed

13 files changed

+295
-14
lines changed

docs/VOM.md

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ The Versatile Object Mapper - or in short VOM - is a PHP library to transform an
1919
<!-- toc -->
2020

2121
- [The Object Mapper](#the-object-mapper)
22-
* [Without Framework](#without-framework)
22+
* [Without Framework](#plain-old-php)
2323
* [Laravel Framework](#laravel-framework)
2424
* [Symfony Framework](#symfony-framework)
2525
* [Denormalization](#denormalization)
@@ -322,6 +322,33 @@ class ConstructorArguments
322322
> [!NOTE]
323323
> When using the [`object_to_populate` context](#object-to-populate), the constructor arguments and constructor property promotion will be skipped.
324324
325+
#### Serialized Objects
326+
327+
In case the source data is a serialized representation of the model, the constructor (or factory) can have a single string argument with the `serialized` option set to `true`.
328+
329+
See [Custom serialization](#custom-serialization) to implement the equivalent normalizer.
330+
331+
```php
332+
use Zolex\VOM\Mapping as VOM;
333+
334+
#[VOM\Model]
335+
class SerializedObject
336+
{
337+
private int $id;
338+
private string $name;
339+
340+
public function __construct(
341+
#[VOM\Argument(serialized: true)]
342+
string $data,
343+
) {
344+
[$this->id, $this->name] = explode(':', $data);
345+
}
346+
}
347+
```
348+
349+
```php
350+
$objectMapper->denormalize('123:test', SerializedObject::class);
351+
```
325352

326353
### Constructor Property Promotion
327354

@@ -642,6 +669,30 @@ class Calls
642669
}
643670
```
644671

672+
##### Custom serialization
673+
674+
The normalizer attribute can be added on the `__toString()` method. Note that in this case it must be the only normalizer on the model and return a string representation of the object.
675+
676+
See [Serialized Objects](#serialized-objects) to implement the equivalent denormalizer.
677+
678+
```php
679+
use Symfony\Component\Serializer\Attribute\Groups;
680+
use Zolex\VOM\Mapping as VOM;
681+
682+
#[VOM\Model]
683+
class SerializedObject
684+
{
685+
public int $id;
686+
public string $name;
687+
688+
#[VOM\Mormalizer]
689+
public function __toString(): string
690+
{
691+
return $this->id . ':' . $this->name;
692+
}
693+
}
694+
```
695+
645696
> [!CAUTION]
646697
> It is possible to normalize data and denormalize a model while maintaining the exact same results, no matter how often you repeat this process.
647698
> If this is one of your requirements, and you are using Normalizer and Denormalizer methods, you have to be careful how you store the injected data

src/Laravel/Providers/VersatileObjectMapperProvider.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,7 @@ class VersatileObjectMapperProvider extends ServiceProvider
4040
{
4141
public function register(): void
4242
{
43-
$this->app->singleton(VersatileObjectMapper::class, function(Application $app): VersatileObjectMapper
44-
{
43+
$this->app->singleton(VersatileObjectMapper::class, function (Application $app): VersatileObjectMapper {
4544
return VersatileObjectMapperFactory::create();
4645
});
4746
}

src/Mapping/AbstractProperty.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ public function __construct(
2525
private ?string $defaultOrder = null,
2626
private ?string $dateTimeFormat = null,
2727
private ?array $map = null,
28+
private bool $serialized = false,
2829
) {
2930
}
3031

@@ -87,4 +88,9 @@ public function getMap(): ?array
8788
{
8889
return $this->map;
8990
}
91+
92+
public function isSerialized(): bool
93+
{
94+
return $this->serialized;
95+
}
9096
}

src/Metadata/Factory/ModelMetadataFactory.php

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,8 @@ public function getMetadataFor(string|\ReflectionClass $class, ?ModelMetadata $m
118118
throw new MissingMetadataException(\sprintf('The class "%s" does not have the "VOM\Model" attribute.', $class->getName()));
119119
}
120120

121+
$hasSerializer = false;
122+
$normalizerCount = 0;
121123
foreach ($class->getMethods() as $reflectionMethod) {
122124
if ($reflectionMethod->isConstructor()) {
123125
continue;
@@ -127,7 +129,12 @@ public function getMetadataFor(string|\ReflectionClass $class, ?ModelMetadata $m
127129
$attribute = $reflectionAttribute->newInstance();
128130

129131
if ($attribute instanceof Normalizer) {
130-
$modelMetadata->addNormalizer($this->createNormalizerMetadata($class, $reflectionMethod, $attribute));
132+
++$normalizerCount;
133+
$normalizer = $this->createNormalizerMetadata($class, $reflectionMethod, $attribute);
134+
if ('__toString' === $normalizer->getMethod()) {
135+
$hasSerializer = true;
136+
}
137+
$modelMetadata->addNormalizer($normalizer);
131138
continue;
132139
}
133140

@@ -143,6 +150,10 @@ public function getMetadataFor(string|\ReflectionClass $class, ?ModelMetadata $m
143150
}
144151
}
145152

153+
if ($hasSerializer && $normalizerCount > 1) {
154+
throw new MappingException(\sprintf('The "__toString()" method on model "%s" is configured as a normalizer. There must be no additional normalizer methods.', $class->getName()));
155+
}
156+
146157
foreach ($class->getProperties() as $reflectionProperty) {
147158
if ($modelMetadata->isConstructorArgumentPromoted($reflectionProperty->getName())) {
148159
continue;
@@ -180,10 +191,12 @@ private function createNormalizerMetadata(
180191
throw new MappingException(\sprintf('Normalizer method %s::%s() should not be static.', $reflectionClass->getName(), $reflectionMethod->getName()));
181192
}
182193

183-
if (preg_match('/^(get|has|is|normalize)(.+)$/i', $reflectionMethod->getName(), $matches)) {
194+
if ('__toString' === $reflectionMethod->getName()) {
195+
$virtualPropertyName = null;
196+
} elseif (preg_match('/^(get|has|is|normalize)(.+)$/i', $reflectionMethod->getName(), $matches)) {
184197
$virtualPropertyName = lcfirst($matches[2]);
185198
} else {
186-
throw new MappingException(\sprintf('Normalizer on "%s::%s()" cannot be added. Normalizer can only be added on methods beginning with "get", "has", "is" or "normalize".', $reflectionClass->getName(), $reflectionMethod->getName()));
199+
throw new MappingException(\sprintf('Normalizer on "%s::%s()" cannot be added. Normalizer can only be added on methods beginning with "get", "has", "is" or "normalize" or on the "__toString" method.', $reflectionClass->getName(), $reflectionMethod->getName()));
187200
}
188201

189202
return new NormalizerMetadata($reflectionClass->getName(), $reflectionMethod->getName(), $virtualPropertyName, $normalizer);

src/Metadata/NormalizerMetadata.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ class NormalizerMetadata extends AbstractCallableMetadata
2020
public function __construct(
2121
string $class,
2222
string $method,
23-
private readonly string $virtualPropertyName,
23+
private readonly ?string $virtualPropertyName,
2424
private readonly Normalizer $attribute,
2525
) {
2626
parent::__construct($class, $method);

src/Metadata/PropertyMetadata.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,11 @@ public function hasMap(): bool
139139
return $this->attribute->hasMap();
140140
}
141141

142+
public function isSerialized(): bool
143+
{
144+
return $this->attribute->isSerialized();
145+
}
146+
142147
public function getMappedValue(mixed $value): mixed
143148
{
144149
$map = $this->attribute->getMap();

src/Serializer/Normalizer/ObjectNormalizer.php

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ public function getSupportedTypes(?string $format): array
101101
{
102102
return [
103103
'object' => true,
104+
'*' => true,
104105
];
105106
}
106107

@@ -119,7 +120,7 @@ public function supportsDenormalization(mixed $data, string $type, ?string $form
119120
return false;
120121
}
121122

122-
return \is_array($data) || \is_object($data);
123+
return \is_array($data) || \is_object($data) || \is_string($data);
123124
}
124125

125126
/**
@@ -209,7 +210,7 @@ public function denormalize(mixed $data, string $type, ?string $format = null, a
209210
* @throws FactoryException When at least one factory method is configured but failed to instantiate the model
210211
* @throws ExceptionInterface For any other type of exception
211212
*/
212-
protected function createInstance(array &$data, string $class, array &$context, ?string $format): object
213+
protected function createInstance(array|string &$data, string $class, array &$context, ?string $format): object
213214
{
214215
if (null !== $object = $this->extractObjectToPopulate($class, $context, self::OBJECT_TO_POPULATE)) {
215216
return $object;
@@ -286,6 +287,10 @@ private function denormalizeProperty(string $type, mixed $data, PropertyMetadata
286287
$data = &$context[self::ROOT_DATA];
287288
}
288289

290+
if (\is_string($data) && $property->isSerialized()) {
291+
return $data;
292+
}
293+
289294
if ($accessor = $property->getAccessor()) {
290295
$value = $this->propertyAccessor->getValue($data, $accessor);
291296
} else {
@@ -478,13 +483,17 @@ public function supportsNormalization(mixed $data, ?string $format = null, array
478483
return false;
479484
}
480485

486+
if (!\is_object($data)) {
487+
return false;
488+
}
489+
481490
try {
482491
$this->modelMetadataFactory->getMetadataFor($data::class);
483492
} catch (MissingMetadataException) {
484493
return false;
485494
}
486495

487-
return \is_object($data);
496+
return true;
488497
}
489498

490499
/**
@@ -599,7 +608,9 @@ public function normalize(mixed $object, ?string $format = null, array $context
599608
throw new BadMethodCallException(\sprintf('Bad normalizer method call: %s', $e->getMessage()), 0, $e);
600609
}
601610

602-
if (null !== $accessor = $normalizer->getAccessor()) {
611+
if ('__toString' === $normalizer->getMethod()) {
612+
return $normalized;
613+
} elseif (null !== $accessor = $normalizer->getAccessor()) {
603614
$this->propertyAccessor->setValue($data, $accessor, $normalized);
604615
} else {
605616
if (!\is_array($normalized)) {
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* This file is part of the VOM package.
7+
*
8+
* (c) Andreas Linden <zlx@gmx.de>
9+
*
10+
* For the full copyright and license information, please view the LICENSE
11+
* file that was distributed with this source code.
12+
*/
13+
14+
namespace Zolex\VOM\Test\Fixtures;
15+
16+
use Zolex\VOM\Mapping as VOM;
17+
18+
#[VOM\Model]
19+
class SerializedObject
20+
{
21+
public string $filename;
22+
public string $tag;
23+
public string $description;
24+
25+
public function __construct(
26+
#[VOM\Argument(serialized: true)]
27+
string $data,
28+
) {
29+
$parts = explode(',', $data);
30+
$this->filename = array_shift($parts);
31+
foreach ($parts as $part) {
32+
[$key, $value] = explode(':', $part);
33+
if (property_exists($this, $key)) {
34+
$this->{$key} = $value;
35+
}
36+
}
37+
}
38+
39+
#[VOM\Normalizer]
40+
public function __toString(): string
41+
{
42+
return $this->filename.',tag:'.$this->tag.',description:'.$this->description;
43+
}
44+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* This file is part of the VOM package.
7+
*
8+
* (c) Andreas Linden <zlx@gmx.de>
9+
*
10+
* For the full copyright and license information, please view the LICENSE
11+
* file that was distributed with this source code.
12+
*/
13+
14+
namespace Zolex\VOM\Test\Fixtures;
15+
16+
use Zolex\VOM\Mapping as VOM;
17+
18+
#[VOM\Model]
19+
class SerializedObjectArray
20+
{
21+
/**
22+
* @var SerializedObject[]
23+
*/
24+
#[VOM\Property]
25+
public array $images;
26+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* This file is part of the VOM package.
7+
*
8+
* (c) Andreas Linden <zlx@gmx.de>
9+
*
10+
* For the full copyright and license information, please view the LICENSE
11+
* file that was distributed with this source code.
12+
*/
13+
14+
namespace Zolex\VOM\Test\Fixtures;
15+
16+
use Zolex\VOM\Mapping as VOM;
17+
18+
#[VOM\Model]
19+
class SerializedObjectWithAdditionalNormalizer
20+
{
21+
#[VOM\Normalizer]
22+
public function __toString(): string
23+
{
24+
return 'serialized object';
25+
}
26+
27+
#[VOM\Normalizer(accessor: 'something')]
28+
public function getSomething(): string
29+
{
30+
return 'something';
31+
}
32+
}

0 commit comments

Comments
 (0)