Migrate docs to docfx; publish with API
This commit is contained in:
164
docs/advanced/custom-serialization.md
Normal file
164
docs/advanced/custom-serialization.md
Normal file
@@ -0,0 +1,164 @@
|
||||
# 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"
|
||||
Reference in New Issue
Block a user