PDODocument
Advanced Usage: 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
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, whetherprivate
,protected
, orpublic
; 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
andfromJsonData
. - 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.
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…
$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)
{ "box_number": 6, "things": [ { "id": "one", "name": "The First Thing" }, { "id": "dos", "name": "Alternate", "notes": "spare" } ] }
Deserializing that tree, we get:
$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 DDD to define custom types (similar to the
ThingId
example above), you can implement converters to translate them to/from your preferred JSON representation.