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"
 |