Skip to content

Commit

Permalink
Merge pull request #1 from andreypostal/json-object-attribute
Browse files Browse the repository at this point in the history
Support class as json object with single attribute
  • Loading branch information
andreypostal authored Aug 18, 2024
2 parents 5a7f1e1 + 8bd6469 commit 49c6dae
Show file tree
Hide file tree
Showing 13 changed files with 126 additions and 46 deletions.
29 changes: 24 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@ composer require andreypostal/json-handler-php
When creating your **Value Objects** that represent a **JSON entity** you just need
to add the ``JsonItemAttribute`` to each property that will be present in the JSON.
```php
use \Andrey\JsonHandler\JsonItemAttribute;
use \Andrey\JsonHandler\Attributes\JsonItemAttribute;

// { "id": 123, "name": "my name" }
class MyObject {
#[JsonItemAttribute]
public int $id;
Expand All @@ -27,11 +28,24 @@ class MyObject {
}
```

In the case of the entire object being a JsonObject with a direct 1:1 match (or perfect mirror of the keys), you can use the ``JsonObjectAttribute``
```php
use \Andrey\JsonHandler\Attributes\JsonObjectAttribute;

// { "id": 123, "name": "my name" }
#[JsonObjectAttribute]
class MyObject {
public int $id;
public string $name;
}
```

If your **Value Object** has some property that **won't be present** in the JSON, you can
just omit the attribute for it and the other ones will be processed normally.
```php
use \Andrey\JsonHandler\JsonItemAttribute;
use \Andrey\JsonHandler\Attributes\JsonItemAttribute;

// { "id": 123 }
class MyObject {
#[JsonItemAttribute]
public int $id;
Expand All @@ -41,21 +55,25 @@ class MyObject {

In case the items are required to exist in the JSON being processed, you must add the required flag in the attribute.
```php
use \Andrey\JsonHandler\JsonItemAttribute;
use \Andrey\JsonHandler\Attributes\JsonItemAttribute;

// { "id": 123 } or { "id": 123, "name": "my name" }
class MyObject {
#[JsonItemAttribute(required: true)]
public int $id;
#[JsonItemAttribute]
public string $name;
}
```

When some of the keys in your JSON are different from your object, you can include the JSON key in the attribute.
```php
use \Andrey\JsonHandler\JsonItemAttribute;
use \Andrey\JsonHandler\Attributes\JsonItemAttribute;

// { "customer_name": "the customer name" }
class MyObject {
#[JsonItemAttribute(key: 'customer_name')]
public name $name;
public string $name;
}
```

Expand All @@ -65,6 +83,7 @@ This will work as a hint so the hydrator can instantiate the appropriate object.
use \Andrey\JsonHandler\JsonItemAttribute;
use \MyNamespace\MyOtherObj;

// { "list": [ { "key": "value" } ] }
class MyObject {
/** @var MyOtherObj[] */
#[JsonItemAttribute(type: MyOtherObj::class)]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<?php
namespace Andrey\JsonHandler;
namespace Andrey\JsonHandler\Attributes;

use Attribute;

Expand Down
8 changes: 8 additions & 0 deletions src/Attributes/JsonObjectAttribute.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php
namespace Andrey\JsonHandler\Attributes;

use Attribute;

#[Attribute(Attribute::TARGET_CLASS)]
class JsonObjectAttribute
{ }
11 changes: 7 additions & 4 deletions src/JsonHydratorTrait.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
<?php
namespace Andrey\JsonHandler;

use Andrey\JsonHandler\Attributes\JsonItemAttribute;
use Andrey\JsonHandler\Attributes\JsonObjectAttribute;
use InvalidArgumentException;
use JsonException;
use LogicException;
Expand Down Expand Up @@ -40,27 +42,28 @@ public function hydrateObject(string|array $json, object $obj): object
*/
private function processClass(ReflectionClass $class, array $jsonArr): array
{
$skipAttributeCheck = ($class->getAttributes(JsonObjectAttribute::class)[0] ?? null) !== null;
$output = [];
$properties = $class->getProperties();
foreach ($properties as $property) {
$output[$property->getName()] = $this->processProperty($property, $jsonArr);
$output[$property->getName()] = $this->processProperty($property, $jsonArr, $skipAttributeCheck);
}
return $output;
}

/**
* @throws JsonException
*/
private function processProperty(ReflectionProperty $property, array $jsonArr): mixed
private function processProperty(ReflectionProperty $property, array $jsonArr, bool $skipAttributeCheck): mixed
{
$attributes = $property->getAttributes(JsonItemAttribute::class);
$attr = $attributes[0] ?? null;
if ($attr === null) {
if ($attr === null && !$skipAttributeCheck) {
return null;
}

/** @var JsonItemAttribute $item */
$item = $attr->newInstance();
$item = $skipAttributeCheck ? new JsonItemAttribute() : $attr->newInstance();
$key = $item->key ?? $property->getName();
if ($item->required && !array_key_exists($key, $jsonArr)) {
throw new InvalidArgumentException(sprintf('required item <%s> not found', $key));
Expand Down
7 changes: 5 additions & 2 deletions src/JsonSerializerTrait.php
Original file line number Diff line number Diff line change
@@ -1,23 +1,26 @@
<?php
namespace Andrey\JsonHandler;

use Andrey\JsonHandler\Attributes\JsonItemAttribute;
use Andrey\JsonHandler\Attributes\JsonObjectAttribute;
use ReflectionClass;

trait JsonSerializerTrait
{
public function serialize(object $obj): array
{
$class = new ReflectionClass($obj);
$skipAttributeCheck = ($class->getAttributes(JsonObjectAttribute::class)[0] ?? null) !== null;
$output = [];
$properties = $class->getProperties();
foreach ($properties as $property) {
$attributes = $property->getAttributes(JsonItemAttribute::class);
$attr = $attributes[0] ?? null;
if ($attr === null) {
if ($attr === null && !$skipAttributeCheck) {
continue;
}
/** @var JsonItemAttribute $item */
$item = $attr->newInstance();
$item = $skipAttributeCheck ? new JsonItemAttribute() : $attr->newInstance();
$key = $item->key ?? $property->name;

if ($property->getType()?->isBuiltin()) {
Expand Down
21 changes: 20 additions & 1 deletion tests/HydratorTest.php
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
<?php

use Andrey\JsonHandler\Attributes\JsonObjectAttribute;
use Andrey\JsonHandler\JsonHandler;
use Andrey\JsonHandler\JsonHydratorTrait;
use Andrey\JsonHandler\JsonItemAttribute;
use Andrey\JsonHandler\Attributes\JsonItemAttribute;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\CoversMethod;
use PHPUnit\Framework\Attributes\CoversTrait;
Expand All @@ -11,6 +12,7 @@
#[CoversTrait(JsonHydratorTrait::class)]
#[CoversMethod(JsonHandler::class, 'Decode')]
#[CoversClass(JsonItemAttribute::class)]
#[CoversClass(JsonObjectAttribute::class)]
final class HydratorTest extends TestCase
{
/**
Expand Down Expand Up @@ -150,4 +152,21 @@ public function testHydrateWithArrayOfObjects(): void
$this->assertIsObject($obj->children[0]);
$this->assertEquals('abc', $obj->children[0]->string);
}

/**
* @throws JsonException
*/
public function testHydrateWithObjectAttr(): void
{
$json = '{"string": "str", "int": 1, "float": 1.50, "bool": false}';
$obj = new SimpleTestWithObjectAttr();

$handler = new JsonHandler();
$handler->hydrateObject($json, $obj);

$this->assertEquals('str', $obj->string);
$this->assertEquals(1, $obj->int);
$this->assertEquals(1.5, $obj->float);
$this->assertFalse($obj->bool);
}
}
74 changes: 45 additions & 29 deletions tests/SerializerTest.php
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
<?php

use Andrey\JsonHandler\Attributes\JsonItemAttribute;
use Andrey\JsonHandler\Attributes\JsonObjectAttribute;
use Andrey\JsonHandler\JsonHandler;
use Andrey\JsonHandler\JsonItemAttribute;
use Andrey\JsonHandler\JsonSerializerTrait;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\CoversMethod;
Expand All @@ -11,41 +12,15 @@
#[CoversTrait(JsonSerializerTrait::class)]
#[CoversMethod(JsonHandler::class, 'Encode')]
#[CoversClass(JsonItemAttribute::class)]
#[CoversClass(JsonObjectAttribute::class)]
final class SerializerTest extends TestCase
{
/**
* @throws JsonException
*/
public function testSimpleSerialize(): void
{
$obj = new SimpleTestObject();

$handler = new JsonHandler();
$arr = $handler->serialize($obj);

$this->assertArrayHasKey('string', $arr);
$this->assertArrayHasKey('int', $arr);
$this->assertArrayHasKey('float', $arr);
$this->assertArrayHasKey('bool', $arr);

$this->assertIsBool($arr['bool']);
$this->assertTrue($arr['bool']);

$this->assertIsInt($arr['int']);
$this->assertEquals(11, $arr['int']);

$this->assertIsFloat($arr['float']);
$this->assertEquals(11.50, $arr['float']);

$this->assertIsString($arr['string']);
$this->assertEquals('string', $arr['string']);

// No exceptions
$json = JsonHandler::Encode($arr);
$this->assertStringContainsString('float', $json);
$this->assertStringContainsString('int', $json);
$this->assertStringContainsString('bool', $json);
$this->assertStringContainsString('string', $json);
$this->assertSimpleSerializedObject(new SimpleTestObject());
}

/**
Expand Down Expand Up @@ -117,4 +92,45 @@ public function testSerializeWithExtraItems(): void

JsonHandler::Encode($arr);
}

/**
* @throws JsonException
*/
public function testSerializeWithObjectAttr(): void
{
$this->assertSimpleSerializedObject(new SimpleTestWithObjectAttr());
}

/**
* @throws JsonException
*/
private function assertSimpleSerializedObject(object $obj): void
{
$handler = new JsonHandler();
$arr = $handler->serialize($obj);

$this->assertArrayHasKey('string', $arr);
$this->assertArrayHasKey('int', $arr);
$this->assertArrayHasKey('float', $arr);
$this->assertArrayHasKey('bool', $arr);

$this->assertIsBool($arr['bool']);
$this->assertTrue($arr['bool']);

$this->assertIsInt($arr['int']);
$this->assertEquals(11, $arr['int']);

$this->assertIsFloat($arr['float']);
$this->assertEquals(11.50, $arr['float']);

$this->assertIsString($arr['string']);
$this->assertEquals('string', $arr['string']);

// No exceptions
$json = JsonHandler::Encode($arr);
$this->assertStringContainsString('float', $json);
$this->assertStringContainsString('int', $json);
$this->assertStringContainsString('bool', $json);
$this->assertStringContainsString('string', $json);
}
}
1 change: 1 addition & 0 deletions tests/bootstrap.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@
require_once __DIR__ . '/utils/WithChildObject.php';
require_once __DIR__ . '/utils/SimpleTestWithArrayObject.php';
require_once __DIR__ . '/utils/WithArrayOfChildObject.php';
require_once __DIR__ . '/utils/SimpleTestWithObjectAttr.php';

2 changes: 1 addition & 1 deletion tests/utils/SimpleTestObject.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?php

use Andrey\JsonHandler\JsonItemAttribute;
use Andrey\JsonHandler\Attributes\JsonItemAttribute;

class SimpleTestObject
{
Expand Down
2 changes: 1 addition & 1 deletion tests/utils/SimpleTestWithArrayObject.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?php

use Andrey\JsonHandler\JsonItemAttribute;
use Andrey\JsonHandler\Attributes\JsonItemAttribute;

class SimpleTestWithArrayObject
{
Expand Down
11 changes: 11 additions & 0 deletions tests/utils/SimpleTestWithObjectAttr.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php
use Andrey\JsonHandler\Attributes\JsonObjectAttribute;

#[JsonObjectAttribute]
class SimpleTestWithObjectAttr
{
public string $string = 'string';
public ?int $int = 11;
public ?float $float = 11.50;
public ?bool $bool = true;
}
2 changes: 1 addition & 1 deletion tests/utils/WithArrayOfChildObject.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?php

use Andrey\JsonHandler\JsonItemAttribute;
use Andrey\JsonHandler\Attributes\JsonItemAttribute;

class WithArrayOfChildObject
{
Expand Down
2 changes: 1 addition & 1 deletion tests/utils/WithChildObject.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?php

use Andrey\JsonHandler\JsonItemAttribute;
use Andrey\JsonHandler\Attributes\JsonItemAttribute;

class WithChildObject
{
Expand Down

0 comments on commit 49c6dae

Please # to comment.