165 lines
5.0 KiB
Markdown
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 • Square • GitHub"
|