Skip to content

Commit

Permalink
Merge pull request #32 from square/mommy-daddy
Browse files Browse the repository at this point in the history
Set parent relations
  • Loading branch information
khepin authored Nov 27, 2024
2 parents cf9f9a7 + 8981102 commit 369590f
Show file tree
Hide file tree
Showing 7 changed files with 185 additions and 19 deletions.
34 changes: 34 additions & 0 deletions src/Json.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ class Json
{
protected array $path;

/**
* Sometimes we want to link back to the parent object.
* This is a reference to the parent objects available for such cases.
*/
protected static array $parentStack = [];

protected string $type;

protected bool $omit_empty;
Expand Down Expand Up @@ -64,11 +70,39 @@ public function forProperty(ReflectionProperty $prop): Json
return $this;
}

/**
* Adds a parent in the stack of parents that we use to be able to link parents
* from the children objects.
*/
public static function withParent(mixed $parent, callable $cb): mixed
{
static::$parentStack[] = $parent;
try {
return $cb();
} finally {
array_pop(static::$parentStack);
}
}

/**
* Whether or not we marked this property as linking back to the parent object.
* This is done via a separate attribute JsonParent
*/
public function linksToParentObject(): bool
{
return false;
}

/**
* Builds the PHP value from the json data and a type if available
*/
public function retrieveValue(?array $data, ReflectionNamedType|ReflectionUnionType|null $type = null)
{
if ($this->linksToParentObject()) {
end(static::$parentStack);

return prev(static::$parentStack);
}
foreach ($this->path as $pathBit) {
if (! array_key_exists($pathBit, $data)) {
return $this->handleMissingValue($data);
Expand Down
36 changes: 36 additions & 0 deletions src/JsonParent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

declare(strict_types=1);

namespace Square\Pjson;

use Attribute;

/**
* Allows linking a deserialized json item to its parent
* There is no intention for this library to handle more complex backwards lookign paths than
* this one.
* Those more complex paths can easily be handled by methods that traverse the entire
* data structure once it is in memory once the parent is available.
* So encoding something like parent->property->parent->parent->property->property is left to be
* implemented in client code
*/
#[Attribute(Attribute::TARGET_PROPERTY)]
class JsonParent extends Json
{
/**
* No constructor params are made available in this case as we do not allow customizing how we link
* to the parent object. There is only one parent object available, that is the one we are linking to
* and that's it.
*/
public function __construct()
{
$this->path = [];
$this->omit_empty = false;
}

public function linksToParentObject(): bool
{
return true;
}
}
31 changes: 20 additions & 11 deletions src/JsonSerialize.php
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
<?php declare(strict_types=1);
<?php

declare(strict_types=1);

namespace Square\Pjson;

use const JSON_THROW_ON_ERROR;

use ReflectionAttribute;
use Square\Pjson\Internal\RClass;
use const JSON_THROW_ON_ERROR;

trait JsonSerialize
{
public function toJson(int $flags = 0, int $depth = 512) : string
public function toJson(int $flags = 0, int $depth = 512): string
{
$flags |= JSON_THROW_ON_ERROR;

return json_encode($this->toJsonData(), flags: $flags, depth: $depth);
}

Expand All @@ -33,14 +37,15 @@ public function toJsonData()
return $d;
}

public static function toJsonListData(array $data) : array
public static function toJsonListData(array $data): array
{
return array_map(fn ($d) => $d->toJsonData(), $data);
}

public static function toJsonList(array $data, int $flags = 0, int $depth = 512) : string
public static function toJsonList(array $data, int $flags = 0, int $depth = 512): string
{
$flags |= JSON_THROW_ON_ERROR;

return json_encode(static::toJsonListData($data), flags: $flags, depth: $depth);
}

Expand All @@ -49,9 +54,10 @@ public static function fromJsonString(
array|string $path = [],
int $depth = 512,
int $flags = 0,
) : static {
): static {
$flags |= JSON_THROW_ON_ERROR;
$jd = json_decode($json, associative: true, flags: $flags, depth: $depth);

return self::fromJsonData($jd, $path);
}

Expand All @@ -60,24 +66,26 @@ public static function listFromJsonString(
array|string $path = [],
int $depth = 512,
int $flags = 0,
) : array {
): array {
$flags |= JSON_THROW_ON_ERROR;
$jd = json_decode($json, associative: true, flags: $flags, depth: $depth);

return static::listfromJsonData($jd, $path);
}

public static function listfromJsonData(array $json, array|string $path = []) : array
public static function listfromJsonData(array $json, array|string $path = []): array
{
if (is_string($path)) {
$path = [$path];
}
foreach ($path as $pathBit) {
$json = $json[$pathBit];
}

return array_map(fn ($d) => static::fromJsonData($d), $json);
}

public static function fromJsonData($jd, array|string $path = []) : static
public static function fromJsonData($jd, array|string $path = []): static
{
if (is_string($path)) {
$path = [$path];
Expand All @@ -97,13 +105,14 @@ public static function fromJsonData($jd, array|string $path = []) : static
$a = $attrs[0];

$type = $prop->getType();
$v = $a->newInstance()->forProperty($prop)->retrieveValue($jd, $type);
if (is_null($v) && $type && !$type->allowsNull()) {
$v = Json::withParent($return, fn () => $a->newInstance()->forProperty($prop)->retrieveValue($jd, $type));
if (is_null($v) && $type && ! $type->allowsNull()) {
continue;
}

$prop->setValue($return, $v);
}

return $return;
}
}
23 changes: 23 additions & 0 deletions tests/DeSerializationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use Square\Pjson\Tests\Definitions\Category;
use Square\Pjson\Tests\Definitions\Collection;
use Square\Pjson\Tests\Definitions\Collector;
use Square\Pjson\Tests\Definitions\LinkParent;
use Square\Pjson\Tests\Definitions\MenuList;
use Square\Pjson\Tests\Definitions\Privateer;
use Square\Pjson\Tests\Definitions\Schedule;
Expand Down Expand Up @@ -672,4 +673,26 @@ public function testUnionTypesUsingArrayAndCustomObject()
],
]);
}

public function testLinkToParent()
{
$d = LinkParent::fromJsonString('{
"name": "Millie",
"child": {
"name": "bob"
},
"children": [
{"name": "Alice"},
{"name": "Caroline"}
]
}');

$this->assertEquals($d->name, 'Millie');
$this->assertEquals($d->child->name, 'bob');
$this->assertEquals($d->child->parent, $d);
$this->assertEquals($d->children[0]->name, 'Alice');
$this->assertEquals($d->children[0]->parent, $d);
$this->assertEquals($d->children[1]->name, 'Caroline');
$this->assertEquals($d->children[1]->parent, $d);
}
}
20 changes: 20 additions & 0 deletions tests/Definitions/LinkChild.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

declare(strict_types=1);

namespace Square\Pjson\Tests\Definitions;

use Square\Pjson\Json;
use Square\Pjson\JsonParent;
use Square\Pjson\JsonSerialize;

class LinkChild
{
use JsonSerialize;

#[Json]
public string $name;

#[JsonParent]
public LinkParent $parent;
}
22 changes: 22 additions & 0 deletions tests/Definitions/LinkParent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

declare(strict_types=1);

namespace Square\Pjson\Tests\Definitions;

use Square\Pjson\Json;
use Square\Pjson\JsonSerialize;

class LinkParent
{
use JsonSerialize;

#[Json]
public LinkChild $child;

#[Json(type: LinkChild::class)]
public array $children;

#[Json]
public string $name;
}
38 changes: 30 additions & 8 deletions tests/SerializationTest.php
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
<?php declare(strict_types=1);
<?php

declare(strict_types=1);

namespace Square\Pjson\Tests;

use PHPUnit\Framework\TestCase;
use Square\Pjson\JsonSerialize;
use Square\Pjson\Json;
use Square\Pjson\JsonSerialize;
use Square\Pjson\Tests\Definitions\BigCat;
use Square\Pjson\Tests\Definitions\BigInt;
use Square\Pjson\Tests\Definitions\CatalogCategory;
Expand All @@ -12,6 +15,7 @@
use Square\Pjson\Tests\Definitions\Category;
use Square\Pjson\Tests\Definitions\Child;
use Square\Pjson\Tests\Definitions\Collector;
use Square\Pjson\Tests\Definitions\LinkParent;
use Square\Pjson\Tests\Definitions\MenuList;
use Square\Pjson\Tests\Definitions\Privateer;
use Square\Pjson\Tests\Definitions\Schedule;
Expand Down Expand Up @@ -44,7 +48,7 @@ public function testNullableProperty()
public function testSerializeAcceptsJsonFlags()
{
$c = new Category;
$expected = <<<JSON
$expected = <<<'JSON'
{
"identifier": "myid",
"category_name": "Clothes",
Expand All @@ -54,13 +58,13 @@ public function testSerializeAcceptsJsonFlags()
}
JSON;


$this->assertEquals($expected, $c->toJson(flags: JSON_PRETTY_PRINT));
}

public function testSerializeThrowsOnJsonError()
{
$c = new class {
$c = new class
{
use JsonSerialize;

public function __construct(
Expand Down Expand Up @@ -239,7 +243,7 @@ public function testList()
public function testClassToScalar()
{
$stats = new Stats;
$stats->count = new BigInt("123456789876543234567898765432345678976543234567876543212345678765432");
$stats->count = new BigInt('123456789876543234567898765432345678976543234567876543212345678765432');
$this->assertEquals(
'{"count":"123456789876543234567898765432345678976543234567876543212345678765432"}',
$stats->toJson()
Expand Down Expand Up @@ -306,14 +310,32 @@ public function testMissingParent()
{
// Ensure that while the `parent` object is null, we can still serialize the parent.id property which is
// nested under `parent`.
$data = new Child();
$data = new Child;
$json = '{"identifier":null,"parent":{"id":null}}';

$this->assertEquals($this->comparableJson($json), $data->toJson());
}

protected function comparableJson(string $json) : string
protected function comparableJson(string $json): string
{
return json_encode(json_decode($json));
}

public function testLinkedParents()
{
$d = LinkParent::fromJsonString('{
"name": "Millie",
"child": {
"name": "bob"
},
"children": [
{"name": "Alice"},
{"name": "Caroline"}
]
}');

$this->assertEquals(json_encode(json_decode(
'{"child":{"name":"bob"},"children":[{"name":"Alice"},{"name":"Caroline"}],"name":"Millie"}'
)), $d->toJson());
}
}

0 comments on commit 369590f

Please sign in to comment.