pdo-document/docs/advanced/custom-serialization.md

165 lines
5.0 KiB
Markdown

# Custom Serialization
JSON documents are sent to and received from both PostgreSQL and SQLite as `string`s; the translation to and from PHP classes uses the built-in `json_encode` and `json_decode` functions by default. These do an acceptable job, particularly if you do not care much about how the document is represented. If you want more control, though, there is a library that can help.
## Using `square/pjson` to Control Serialization
The [`square/pjson`][pjson] library provides an attribute, a trait, and a few interfaces. If these interfaces are implemented on your classes, `PDODocument` will use them instead of the standard serialization functions. This will not be an exhaustive tutorial of the library, but a few high points:
- Properties with the `#[Json]` attribute will be de/serialized, whether `private`, `protected`, or `public`; properties without an annotation will be ignored.
- `use JsonSerialize;` brings in the trait that implements pjson's behavior; this should be present in each class.
- Array properties must include the type in the attribute, so the library knows how to handle the object.
- A strongly-typed class that is represented by a single JSON value can be wired in by implementing `toJsonData` and `fromJsonData`.
- The library will not use the class's constructor to create its instances; default values in constructor-promoted properties will not be present if they are not specifically included in the document.
An example will help here; we will demonstrate all of the above.
```php
use Square\Pjson\{Json, JsonDataSerializable, JsonSerialize};
// A strongly-typed ID for our things; it is stored as a string, but wrapped in this class
class ThingId implements JsonDataSerializable
{
public string $value = '';
public function __construct(string $value = '')
{
$this->value = $value;
}
public function toJsonData(): string
{
return $this->value;
}
// $jd = JSON data
public static function fromJsonData(mixed $jd, array|string $path = []): static
{
return new static($jd);
}
}
// A thing; note the JsonSerialize trait
class Thing
{
use JsonSerialize;
#[Json]
public ThingId $id;
#[Json]
public string $name = '';
// If the property is null, it will not be part of the JSON document
#[Json(omit_empty: true)]
public ?string $notes = null;
public function __construct()
{
$this->id = new ThingId();
}
}
class BoxOfThings
{
use JsonSerialize;
// Customize the JSON key for this field
#[Json('box_number')]
public int $boxNumber = 0;
// Specify the type of this array
#[Json(type: Thing::class)]
public array $things = [];
}
```
With these declarations, the following code...
```php
$thing1 = new Thing();
$thing1->id = new ThingId('one');
$thing1->name = 'The First Thing';
$thing2 = new Thing();
$thing2->id = new ThingId('dos');
$thing2->name = 'Alternate';
$thing2->notes = 'spare';
$box = new BoxOfThings();
$box->boxNumber = 6;
$box->things = [$thing1, $thing2];
echo $box->toJsonString();
```
...will produce this JSON: _(manually pretty-printed below)_
```json
{
"box_number": 6,
"things": [
{
"id": "one",
"name": "The First Thing"
},
{
"id": "dos",
"name": "Alternate",
"notes": "spare"
}
]
}
```
Deserializing that tree, we get:
```php
$box2 = BoxOfThings::fromJsonString('...');
var_dump($box2);
```
```
object(BoxOfThings)#13 (2) {
["boxNumber"]=>
int(6)
["things"]=>
array(2) {
[0]=>
object(Thing)#25 (3) {
["id"]=>
object(ThingId)#29 (1) {
["value"]=>
string(3) "one"
}
["name"]=>
string(15) "The First Thing"
["notes"]=>
NULL
}
[1]=>
object(Thing)#28 (3) {
["id"]=>
object(ThingId)#31 (1) {
["value"]=>
string(3) "dos"
}
["name"]=>
string(9) "Alternate"
["notes"]=>
string(5) "spare"
}
}
}
```
Our round-trip was successful!
Any object passed as a document will use `square/pjson` serialization if it is defined. When passing an array as a document, if that array only has one value, and that value implements `square/pjson` serialization, it will be used; otherwise, arrays will use the standard `json_encode`/`json_decode` pairs. In practice, the ability to call `Patch::by*` with an array of fields to be updated may not give the results you would expect; using `Document::update` will replace the entire document, but it will use the `square/pjson` serialization.
## Uses for Custom Serialization
- If you exchange JSON documents with a data partner, they may have a specific format they provide or expect; this allows you to work with objects rather than trees of parsed JSON.
- If you use <abbr title="Domain Driven Design">DDD</abbr> to define custom types (similar to the `ThingId` example above), you can implement converters to translate them to/from your preferred JSON representation.
[pjson]: https://github.com/square/pjson "PJson &bull; Square &bull; GitHub"