+
Skip to content

Improve tests and fix type handling #3

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 31, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions src/Payload.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

namespace Andrey\PancakeObject;

use Throwable;

readonly class Payload
{
public function __construct(
public mixed $data = null,
public bool $skipped = false,
public ?Throwable $error = null,
) {
}
}
57 changes: 42 additions & 15 deletions src/SimpleHydrator.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,17 @@ private function processClass(ReflectionClass $class, array $data): object

$properties = $class->getProperties();
foreach ($properties as $property) {
$value = $this->processProperty($property, $data, $skipAttributeCheck);
if ($value !== null) {
$property->setValue($instance, $value);
$payload = $this->processProperty($property, $data, $skipAttributeCheck);

if ($payload->skipped) {
continue;
}

if ($payload->error !== null) {
throw $payload->error;
}

$property->setValue($instance, $payload->data);
}

return $instance;
Expand All @@ -54,7 +61,7 @@ private function processClass(ReflectionClass $class, array $data): object
/**
* @throws ReflectionException
*/
private function processProperty(ReflectionProperty $property, array $jsonArr, bool $skipAttributeCheck): mixed
private function processProperty(ReflectionProperty $property, array $jsonArr, bool $skipAttributeCheck): Payload
{
// Check if property has ItemAttribute
$attributes = $property->getAttributes(Item::class);
Expand All @@ -66,7 +73,7 @@ private function processProperty(ReflectionProperty $property, array $jsonArr, b

// We do not touch this property in this case
if ($hasSkipAttribute || ($attr === null && !$skipAttributeCheck)) {
return null;
return new Payload(skipped: true);
}

/** @var Item $item we set to an empty new item, means that it has ValueObject attribute */
Expand All @@ -78,11 +85,11 @@ private function processProperty(ReflectionProperty $property, array $jsonArr, b
// Simple validation for required items
$arrKeyExists = array_key_exists($key, $jsonArr);
if ($item->required && !$arrKeyExists) {
throw new InvalidArgumentException(sprintf('required item <%s> not found', $key));
return new Payload(error: new InvalidArgumentException(sprintf('required item <%s> not found', $key)));
}

if (!$arrKeyExists) {
return null;
return $this->buildNullableResponse($property, null);
}

if ($property->getType()?->isBuiltin()) {
Expand All @@ -96,7 +103,7 @@ private function processProperty(ReflectionProperty $property, array $jsonArr, b
/**
* @throws ReflectionException
*/
private function handleBuiltin(array $data, string $key, ReflectionProperty $property, Item $item): mixed
private function handleBuiltin(array $data, string $key, ReflectionProperty $property, Item $item): Payload
{
// If we define a type in the attribute and the property is an array, we perform simple type validation
if ($item->type !== null && $property->getType()?->getName() === 'array') {
Expand All @@ -105,17 +112,29 @@ private function handleBuiltin(array $data, string $key, ReflectionProperty $pro
foreach ($data[$key] ?? [] as $k => $v) {
$value = $v;
if ($classExists) {
$value = $this->handleCustomType($value, $item->type);
$payload = $this->handleCustomType($value, $item->type);

if ($payload->skipped) {
continue;
}

if ($payload->error !== null) {
return $payload;
}

$value = $payload->data;
} elseif (gettype($v) !== $item->type) {
throw new LogicException(sprintf('expected array with items of type <%s> but found <%s>', $item->type, gettype($v)));
return new Payload(
error: new LogicException(sprintf('expected array with items of type <%s> but found <%s>', $item->type, gettype($v))),
);
}
$output[$k] = $value;
}
return $output;
return new Payload(data: $output);
}

// If no data is set, returns null which will be ignored by the set value, falling back to undefined or default value
return $data[$key] ?? null;
return $this->buildNullableResponse($property, $data[$key] ?? null);
}

/**
Expand All @@ -125,13 +144,21 @@ private function handleCustomType(mixed $value, string $type): mixed
{
$typeReflection = new ReflectionClass($type);
if ($typeReflection->isEnum()) {
return call_user_func($type.'::tryFrom', $value);
return new Payload(data: call_user_func($type.'::tryFrom', $value));
}

// Recursively hydrate child/internal value objects
return $this->hydrate(
return new Payload(data: $this->hydrate(
data: $value,
class: $type,
);
));
}

private function buildNullableResponse(ReflectionProperty $property, mixed $value): Payload
{
if ($value !== null || $property->getType()->allowsNull()) {
return new Payload(data: $value);
}
return new Payload(skipped: true);
}
}
14 changes: 6 additions & 8 deletions src/SimpleSerializer.php
Original file line number Diff line number Diff line change
Expand Up @@ -80,21 +80,19 @@ private function handlePossibleArray(Item $item, mixed $value): mixed
return $value;
}

if (!class_exists($item->type)) {
throw new LogicException(sprintf('expected array with items of type <%s> but found <%s>', $item->type, gettype($value)));
}

$class = new ReflectionClass($item->type);
$isEnum = $class->isEnum();
$isBuiltin = !class_exists($item->type);
$isEnum = !$isBuiltin && (new ReflectionClass($item->type))->isEnum();

return array_reduce(
array: $value,
callback: function(array $l, mixed $c) use ($isEnum): array {
callback: function(array $l, mixed $c) use ($isEnum, $isBuiltin): array {
$v = $c;
if ($isEnum) {
$v = $c->value;
} else {
} else if (!$isBuiltin) {
$v = $this->serialize($c);
}

$l[] = $v;
return $l;
},
Expand Down
62 changes: 59 additions & 3 deletions tests/HydratorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@
use PHPUnit\Framework\Attributes\CoversMethod;
use PHPUnit\Framework\Attributes\CoversTrait;
use PHPUnit\Framework\TestCase;
use Utils\SimpleTestObject;
use Utils\ChildObject;
use Utils\TestObject;

#[CoversClass(Item::class)]
#[CoversClass(ValueObject::class)]
#[CoversClass(SimpleHydrator::class)]
final class HydratorTest extends TestCase
{
/**
Expand All @@ -23,18 +25,72 @@ public function testSimpleHydrate(): void
'int' => 10,
'float' => 3.14,
'bool' => false,
'missing_required' => 'Im here',
'item_name' => 'Different name',
'single_child' => [
'i_have_a_name' => 'this is the name',
'different_one' => 'other one',
'and_im_an_array_of_int' => [ 0, 1, 2],
],
'array_of_children' => [
[
'i_have_a_name' => 'c1',
'different_one' => 'o1',
'and_im_an_array_of_int' => [ 3, 4, 5],
],
[
'i_have_a_name' => 'c2',
'different_one' => 'o2',
'and_im_an_array_of_int' => [ 6, 7, 8],
]
]
];

$hydrator = new SimpleHydrator();

/** @var SimpleTestObject $obj */
$obj = $hydrator->hydrate($data, SimpleTestObject::class);
/** @var TestObject $obj */
$obj = $hydrator->hydrate($data, TestObject::class);

$this->assertEquals('str', $obj->string);
$this->assertEquals('Different name', $obj->itemName);
$this->assertEquals(10, $obj->int);
$this->assertEquals(3.14, $obj->float);
$this->assertFalse($obj->bool);
$this->assertNull($obj->nullableInt);

$this->assertInstanceOf(ChildObject::class, $obj->singleChild);
$this->assertIsArray($obj->singleChild->andImAnArrayOfInt);
$this->assertSame($data['single_child']['and_im_an_array_of_int'], $obj->singleChild->andImAnArrayOfInt);

$this->assertIsArray($obj->arrayOfChildren);
foreach ($obj->arrayOfChildren as $ci => $child) {
$childArr = $data['array_of_children'][$ci];

$this->assertInstanceOf(ChildObject::class, $child);
$this->assertEquals($childArr['i_have_a_name'], $child->iHaveAName);
$this->assertEquals($childArr['different_one'], $child->butIHaveADifferentOne);

$this->assertIsArray($child->andImAnArrayOfInt);
$this->assertSame($childArr['and_im_an_array_of_int'], $child->andImAnArrayOfInt);

foreach ($child->andImAnArrayOfInt as $i => $int) {
$this->assertIsInt($int);
$this->assertEquals($childArr['and_im_an_array_of_int'][$i], $int);
}
}
}

/**
* @throws ReflectionException
*/
public function testMissingRequiredItem(): void
{
$data = [];

$hydrator = new SimpleHydrator();

$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('required item <missing_required> not found');
$hydrator->hydrate($data, TestObject::class);
}
}
66 changes: 41 additions & 25 deletions tests/SerializerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,47 +5,63 @@
use Andrey\PancakeObject\SimpleSerializer;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\TestCase;
use Utils\SimpleTestObject;
use Utils\TestObject;

#[CoversClass(Item::class)]
#[CoversClass(ValueObject::class)]
#[CoversClass(SimpleSerializer::class)]
final class SerializerTest extends TestCase
{
/**
* @throws ReflectionException
*/
public function testSimpleSerialize(): void
{
$this->assertSimpleSerializedObject(new SimpleTestObject());
}

/**
* @throws ReflectionException
*/
private function assertSimpleSerializedObject(object $obj): void
{
$serializer = new SimpleSerializer();
$arr = $serializer->serialize($obj);
$data = $serializer->serialize(new TestObject());

$this->assertIsString($data["string"]);
$this->assertSame("string", $data["string"]);

$this->assertIsInt($data["int"]);
$this->assertSame(1, $data["int"]);

$this->assertIsFloat($data["float"]);
$this->assertSame(1.2, $data["float"]);

$this->assertIsBool($data["bool"]);
$this->assertTrue($data["bool"]);

$this->assertIsString($data["item_name"]);
$this->assertSame("Item name", $data["item_name"]);

$this->assertIsString($data["missing_required"]);
$this->assertSame("im here", $data["missing_required"]);

$this->assertIsArray($data["single_child"]);
$this->assertArrayHasKey("i_have_a_name", $data["single_child"]);
$this->assertSame("child", $data["single_child"]["i_have_a_name"]);

$this->assertArrayHasKey('string', $arr);
$this->assertArrayHasKey('int', $arr);
$this->assertArrayHasKey('float', $arr);
$this->assertArrayHasKey('bool', $arr);
$this->assertArrayHasKey('item_name', $arr);
$this->assertArrayHasKey("different_one", $data["single_child"]);
$this->assertSame("other n", $data["single_child"]["different_one"]);

$this->assertIsBool($arr['bool']);
$this->assertTrue($arr['bool']);
$this->assertArrayHasKey("and_im_an_array_of_int", $data["single_child"]);
$this->assertIsArray($data["single_child"]["and_im_an_array_of_int"]);
$this->assertSame([4, 5, 6], $data["single_child"]["and_im_an_array_of_int"]);

$this->assertIsInt($arr['int']);
$this->assertEquals(1, $arr['int']);
$this->assertNull($data["nullable_int"]);

$this->assertIsFloat($arr['float']);
$this->assertEquals(1.2, $arr['float']);
$this->assertIsArray($data["array_of_children"]);
$this->assertCount(2, $data["array_of_children"]);

$this->assertIsString($arr['string']);
$this->assertEquals('string', $arr['string']);
$this->assertSame("n1", $data["array_of_children"][0]["i_have_a_name"]);
$this->assertSame("no1", $data["array_of_children"][0]["different_one"]);
$this->assertIsArray($data["array_of_children"][0]["and_im_an_array_of_int"]);
$this->assertEmpty($data["array_of_children"][0]["and_im_an_array_of_int"]);

$this->assertIsString($arr['item_name']);
$this->assertEquals('Item name', $arr['item_name']);
$this->assertSame("n2", $data["array_of_children"][1]["i_have_a_name"]);
$this->assertSame("no2", $data["array_of_children"][1]["different_one"]);
$this->assertIsArray($data["array_of_children"][1]["and_im_an_array_of_int"]);
$this->assertSame([1, 2, 3], $data["array_of_children"][1]["and_im_an_array_of_int"]);
}
}
29 changes: 0 additions & 29 deletions tests/Utils/SimpleTestObject.php

This file was deleted.

Loading
点击 这是indexloc提供的php浏览器服务,不要输入任何密码和下载