Skip to content

Commit 6c6eb3b

Browse files
committed
Move some complex examples to their own section.
1 parent 250acf7 commit 6c6eb3b

File tree

1 file changed

+126
-122
lines changed

1 file changed

+126
-122
lines changed

README.md

Lines changed: 126 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -439,128 +439,6 @@ When collecting, only the lexically last flattened array will get any data, and
439439

440440
In this case, the `$other` property has two keys, `foo` and `bar`, with values `beep` and `boop`, respectively. The same JSON will deserialize back to the same object as before.
441441

442-
#### Value objects
443-
444-
Flattening can also be used in conjunction with renaming to silently translate value objects. Consider:
445-
446-
```php
447-
class Person
448-
{
449-
public function __construct(
450-
public string $name,
451-
#[Field(flatten: true)]
452-
public Age $age,
453-
#[Field(flatten: true)]
454-
public Email $email,
455-
) {}
456-
}
457-
458-
readonly class Email
459-
{
460-
public function __construct(
461-
#[Field(serializedName: 'email')] public string $value,
462-
) {}
463-
}
464-
465-
readonly class Age
466-
{
467-
public function __construct(
468-
#[Field(serializedName: 'age')] public int $value
469-
) {
470-
$this->validate();
471-
}
472-
473-
#[PostLoad]
474-
private function validate(): void
475-
{
476-
if ($this->value < 0) {
477-
throw new \InvalidArgumentException('Age cannot be negative.');
478-
}
479-
}
480-
}
481-
```
482-
483-
In this example, `Email` and `Age` are value objects, in the latter case with extra validation. However, both are marked `flatten: true`, so their properties will be moved up a level to `Person` when serializing. However, they both use the same property name, so both have a custom serialization name specified. The above object will serialize to (and deserialize from) something like this:
484-
485-
```json
486-
{
487-
"name": "Larry",
488-
"age": 21,
489-
"email": "me@example.com"
490-
}
491-
```
492-
493-
Note that because deserialization bypasses the constructor, the extra validation in `Age` must be placed in a separate method that is called from the constructor and flagged to run automatically after deserialization.
494-
495-
It is also possible to specify a prefix for a flattened value, which will also be applied recursively. For example, assuming the same Age class above:
496-
497-
```php
498-
readonly class JobDescription
499-
{
500-
public function __construct(
501-
#[Field(flatten: true, flattenPrefix: 'min_')]
502-
public Age $minAge,
503-
#[Field(flatten: true, flattenPrefix: 'max_')]
504-
public Age $maxAge,
505-
) {}
506-
}
507-
508-
class JobEntry
509-
{
510-
public function __construct(
511-
#[Field(flatten: true, flattenPrefix: 'desc_')]
512-
public JobDescription $description,
513-
) {}
514-
}
515-
```
516-
517-
In this case, serializing `JobEntry` will first flatten the `$description` property, with `desc_` as a prefix. Then, `JobDescription` will flatten both of its age fields, giving each a separate prefix. That will result in a serialized output something like this:
518-
519-
```json
520-
{
521-
"desc_min_age": 18,
522-
"desc_max_age": 65
523-
}
524-
```
525-
526-
And it will deserialize back to the same original 3-layer-object structure.
527-
528-
#### List objects
529-
530-
It is common in some conventions to have not an object, but an array (sequence) get sent on the wire. This is especially true for JSON APIs, that may send a payload like this:
531-
532-
```json
533-
[
534-
{"x": 1, "y": 2},
535-
{"x": 3, "y": 4},
536-
{"x": 5, "y": 6}
537-
]
538-
```
539-
540-
Flattening provides a way to support those structures by reading them into an object property.
541-
542-
For example, we could model the above JSON like this:
543-
544-
```php
545-
class Point
546-
{
547-
public function(public int $x, public int $y) {}
548-
}
549-
550-
class PointList
551-
{
552-
public function __construct(
553-
#[Field(flatten: true)]
554-
#[SequenceField(arrayType: Point::class)]
555-
public array $points,
556-
) {}
557-
}
558-
```
559-
560-
On serialization, that will flatten the array of points to the level of the `PointList` object itself, producing the JSON shown above.
561-
562-
On deserialization, Serde will "collect" any otherwise undefined properties up into `$points`, as it is an array marked `flatten`. Serde will also detect that there is a `SequenceField` or `DictionaryField` defined, and deserialize each entry in the incoming array as that object. That provides a full round-trip between a JSON array-of-objects and a PHP object-with-array-property.
563-
564442
### `flattenPrefix` (string, default '')
565443

566444
When an object or array property is flattened, by default its properties will be flattened using their existing name (or `serializedName`, if specified). That may cause issues if the same class is included in a parent class twice, or if there is some other name collision. Instead, flattened fields may be given a `flattenPrefix` value. That string will be prepended to the name of the property when serializing.
@@ -1261,6 +1139,132 @@ Instead, Serde will look for any method or methods that have a `#[\Crell\Serde\A
12611139

12621140
The visibilty of the method is irrelevant. Serde will call `public`, `private`, or `protected` methods the same. Note, however, that a `private` method in a parent class of the class being deserialized to will not get called, as it is not accessible to PHP from that scope.
12631141

1142+
## Advanced patterns
1143+
1144+
Serde contains numerous dials and switches, which allows for some very powerful if not always obvious usage patterns. A collection of them are provided below.
1145+
1146+
### Value objects
1147+
1148+
Flattening can also be used in conjunction with renaming to silently translate value objects. Consider:
1149+
1150+
```php
1151+
class Person
1152+
{
1153+
public function __construct(
1154+
public string $name,
1155+
#[Field(flatten: true)]
1156+
public Age $age,
1157+
#[Field(flatten: true)]
1158+
public Email $email,
1159+
) {}
1160+
}
1161+
1162+
readonly class Email
1163+
{
1164+
public function __construct(
1165+
#[Field(serializedName: 'email')] public string $value,
1166+
) {}
1167+
}
1168+
1169+
readonly class Age
1170+
{
1171+
public function __construct(
1172+
#[Field(serializedName: 'age')] public int $value
1173+
) {
1174+
$this->validate();
1175+
}
1176+
1177+
#[PostLoad]
1178+
private function validate(): void
1179+
{
1180+
if ($this->value < 0) {
1181+
throw new \InvalidArgumentException('Age cannot be negative.');
1182+
}
1183+
}
1184+
}
1185+
```
1186+
1187+
In this example, `Email` and `Age` are value objects, in the latter case with extra validation. However, both are marked `flatten: true`, so their properties will be moved up a level to `Person` when serializing. However, they both use the same property name, so both have a custom serialization name specified. The above object will serialize to (and deserialize from) something like this:
1188+
1189+
```json
1190+
{
1191+
"name": "Larry",
1192+
"age": 21,
1193+
"email": "me@example.com"
1194+
}
1195+
```
1196+
1197+
Note that because deserialization bypasses the constructor, the extra validation in `Age` must be placed in a separate method that is called from the constructor and flagged to run automatically after deserialization.
1198+
1199+
It is also possible to specify a prefix for a flattened value, which will also be applied recursively. For example, assuming the same Age class above:
1200+
1201+
```php
1202+
readonly class JobDescription
1203+
{
1204+
public function __construct(
1205+
#[Field(flatten: true, flattenPrefix: 'min_')]
1206+
public Age $minAge,
1207+
#[Field(flatten: true, flattenPrefix: 'max_')]
1208+
public Age $maxAge,
1209+
) {}
1210+
}
1211+
1212+
class JobEntry
1213+
{
1214+
public function __construct(
1215+
#[Field(flatten: true, flattenPrefix: 'desc_')]
1216+
public JobDescription $description,
1217+
) {}
1218+
}
1219+
```
1220+
1221+
In this case, serializing `JobEntry` will first flatten the `$description` property, with `desc_` as a prefix. Then, `JobDescription` will flatten both of its age fields, giving each a separate prefix. That will result in a serialized output something like this:
1222+
1223+
```json
1224+
{
1225+
"desc_min_age": 18,
1226+
"desc_max_age": 65
1227+
}
1228+
```
1229+
1230+
And it will deserialize back to the same original 3-layer-object structure.
1231+
1232+
### List objects
1233+
1234+
It is common in some conventions to have not an object, but an array (sequence) get sent on the wire. This is especially true for JSON APIs, that may send a payload like this:
1235+
1236+
```json
1237+
[
1238+
{"x": 1, "y": 2},
1239+
{"x": 3, "y": 4},
1240+
{"x": 5, "y": 6}
1241+
]
1242+
```
1243+
1244+
Flattening provides a way to support those structures by reading them into an object property.
1245+
1246+
For example, we could model the above JSON like this:
1247+
1248+
```php
1249+
class Point
1250+
{
1251+
public function(public int $x, public int $y) {}
1252+
}
1253+
1254+
class PointList
1255+
{
1256+
public function __construct(
1257+
#[Field(flatten: true)]
1258+
#[SequenceField(arrayType: Point::class)]
1259+
public array $points,
1260+
) {}
1261+
}
1262+
```
1263+
1264+
On serialization, that will flatten the array of points to the level of the `PointList` object itself, producing the JSON shown above.
1265+
1266+
On deserialization, Serde will "collect" any otherwise undefined properties up into `$points`, as it is an array marked `flatten`. Serde will also detect that there is a `SequenceField` or `DictionaryField` defined, and deserialize each entry in the incoming array as that object. That provides a full round-trip between a JSON array-of-objects and a PHP object-with-array-property.
1267+
12641268
## Extending Serde
12651269

12661270
Internally, Serde has six types of extensions that work in concert to produce a serialized or deserialized product.

0 commit comments

Comments
 (0)