Skip to content

Commit

Permalink
Merge pull request #7 from nutgram/abstract-class-resolver
Browse files Browse the repository at this point in the history
add ability to resolve abstract class
  • Loading branch information
sergix44 authored Apr 29, 2022
2 parents 4a36ac7 + 6336957 commit 9ae7d88
Show file tree
Hide file tree
Showing 10 changed files with 183 additions and 19 deletions.
6 changes: 6 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,9 @@ insert_final_newline = true

[*.yml]
indent_size = 2

[*.php]
ij_php_keep_indents_on_empty_lines = false
ij_php_concat_spaces = false
ij_php_space_after_type_cast = true
ij_php_align_key_value_pairs = false
13 changes: 13 additions & 0 deletions src/ConcreteResolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

namespace SergiX44\Hydrator;

interface ConcreteResolver
{
/**
* @param array $data
*
* @return class-string
*/
public static function resolveAbstractClass(array $data): string;
}
20 changes: 18 additions & 2 deletions src/Hydrator.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
use SergiX44\Hydrator\Annotation\Alias;
use SergiX44\Hydrator\Annotation\ArrayType;
use SergiX44\Hydrator\Annotation\UnionResolver;
use SergiX44\Hydrator\Exception\InvalidObjectException;
use function sprintf;
use function strtotime;

Expand Down Expand Up @@ -72,7 +73,7 @@ public function hydrate(string|object $object, array|object $data): object
$data = get_object_vars($data);
}

$object = $this->initializeObject($object);
$object = $this->initializeObject($object, $data);

$class = new ReflectionClass($object);
foreach ($class->getProperties() as $property) {
Expand Down Expand Up @@ -180,7 +181,7 @@ public function hydrateWithJson(string|object $object, string $json, ?int $flags
*
* @template T
*/
private function initializeObject(string|object $object): object
private function initializeObject(string|object $object, array|object $data): object
{
if (is_object($object)) {
return $object;
Expand All @@ -194,6 +195,21 @@ private function initializeObject(string|object $object): object
}

$class = new ReflectionClass($object);

if ($class->isAbstract()) {
if (!$class->implementsInterface(ConcreteResolver::class)) {
throw new InvalidObjectException(sprintf(
'The given abstract object must implement %s.',
ConcreteResolver::class
));
}

$resolveMethod = $class->getMethod('resolveAbstractClass');
$realClass = $resolveMethod->invoke(null, $data);

return $this->initializeObject($realClass, $data);
}

$constructor = $class->getConstructor();
if (isset($constructor) && $constructor->getNumberOfRequiredParameters() > 0) {
throw new InvalidArgumentException(sprintf(
Expand Down
10 changes: 10 additions & 0 deletions tests/Fixtures/ObjectWithAbstract.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

namespace SergiX44\Hydrator\Tests\Fixtures;

use SergiX44\Hydrator\Tests\Fixtures\Store\Apple;

final class ObjectWithAbstract
{
public Apple $value;
}
10 changes: 10 additions & 0 deletions tests/Fixtures/ObjectWithInvalidAbstract.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

namespace SergiX44\Hydrator\Tests\Fixtures;

use SergiX44\Hydrator\Tests\Fixtures\Store\Fruit;

final class ObjectWithInvalidAbstract
{
public Fruit $value;
}
20 changes: 20 additions & 0 deletions tests/Fixtures/Store/Apple.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

namespace SergiX44\Hydrator\Tests\Fixtures\Store;

use Exception;
use SergiX44\Hydrator\ConcreteResolver;

abstract class Apple implements ConcreteResolver
{
public string $type;

public static function resolveAbstractClass(array $data): string
{
return match ($data['type']) {
'jack' => AppleJack::class,
'sauce' => AppleSauce::class,
default => throw new Exception('Invalid apple type'),
};
}
}
8 changes: 8 additions & 0 deletions tests/Fixtures/Store/AppleJack.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

namespace SergiX44\Hydrator\Tests\Fixtures\Store;

final class AppleJack extends Apple
{
public string $category;
}
8 changes: 8 additions & 0 deletions tests/Fixtures/Store/AppleSauce.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

namespace SergiX44\Hydrator\Tests\Fixtures\Store;

final class AppleSauce extends Apple
{
public int $sweetness;
}
8 changes: 8 additions & 0 deletions tests/Fixtures/Store/Fruit.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

namespace SergiX44\Hydrator\Tests\Fixtures\Store;

abstract class Fruit
{
public string $name;
}
99 changes: 82 additions & 17 deletions tests/HydratorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,15 @@
use InvalidArgumentException;
use PHPUnit\Framework\TestCase;
use SergiX44\Hydrator\Exception;
use SergiX44\Hydrator\Exception\InvalidObjectException;
use SergiX44\Hydrator\Hydrator;
use SergiX44\Hydrator\HydratorInterface;
use SergiX44\Hydrator\Tests\Fixtures\ObjectWithAbstract;
use SergiX44\Hydrator\Tests\Fixtures\ObjectWithInvalidAbstract;
use SergiX44\Hydrator\Tests\Fixtures\Store\Apple;
use SergiX44\Hydrator\Tests\Fixtures\Store\AppleJack;
use SergiX44\Hydrator\Tests\Fixtures\Store\AppleSauce;
use SergiX44\Hydrator\Tests\Fixtures\Store\Fruit;
use SergiX44\Hydrator\Tests\Fixtures\Store\Tag;
use SergiX44\Hydrator\Tests\Fixtures\Store\TagPrice;
use TypeError;
Expand Down Expand Up @@ -127,7 +134,10 @@ public function testHydrateAttributedProperty(): void
$this->markTestSkipped('php >= 8 is required.');
}

$object = (new Hydrator())->hydrate(Fixtures\ObjectWithAttributedAlias::class, ['non-normalized-value' => 'foo']);
$object = (new Hydrator())->hydrate(
Fixtures\ObjectWithAttributedAlias::class,
['non-normalized-value' => 'foo']
);

$this->assertSame('foo', $object->value);
}
Expand Down Expand Up @@ -335,10 +345,12 @@ public function testHydrateArrayableProperty(): void

public function testHydrateTypedArrayableProperty(): void
{
$object = (new Hydrator())->hydrate(Fixtures\ObjectWithTypedArray::class, ['value' => [
['name' => 'foo'],
['name' => 'bar'],
]]);
$object = (new Hydrator())->hydrate(Fixtures\ObjectWithTypedArray::class, [
'value' => [
['name' => 'foo'],
['name' => 'bar'],
],
]);

$this->assertIsArray($object->value);
$this->assertInstanceOf(Tag::class, $object->value[0]);
Expand All @@ -350,10 +362,12 @@ public function testHydrateTypedArrayableProperty(): void

public function testHydrateTypedNestedArrayableProperty(): void
{
$object = (new Hydrator())->hydrate(Fixtures\ObjectWithTypedArrayOfArray::class, ['value' => [
[['name' => 'foo'], ['name' => 'fef']],
[['name' => 'bar'], ['name' => 'fif']],
]]);
$object = (new Hydrator())->hydrate(Fixtures\ObjectWithTypedArrayOfArray::class, [
'value' => [
[['name' => 'foo'], ['name' => 'fef']],
[['name' => 'bar'], ['name' => 'fif']],
],
]);

$this->assertIsArray($object->value);
$this->assertIsArray($object->value[0]);
Expand Down Expand Up @@ -604,10 +618,12 @@ public function testHydrateAssociatedPropertyWithInvalidData(): void

public function testHydrateAssociationCollectionProperty(): void
{
$o = (new Hydrator())->hydrate(Fixtures\ObjectWithAssociations::class, ['value' => [
'foo' => ['value' => 'foo'],
'bar' => ['value' => 'bar'],
]]);
$o = (new Hydrator())->hydrate(Fixtures\ObjectWithAssociations::class, [
'value' => [
'foo' => ['value' => 'foo'],
'bar' => ['value' => 'bar'],
],
]);

$this->assertNotNull($o->value['foo']);
$this->assertSame('foo', $o->value['foo']->value);
Expand All @@ -618,10 +634,12 @@ public function testHydrateAssociationCollectionProperty(): void

public function testHydrateAssociationCollectionPropertyUsingDataObject(): void
{
$o = (new Hydrator())->hydrate(Fixtures\ObjectWithAssociations::class, ['value' => (object) [
'foo' => (object) ['value' => 'foo'],
'bar' => (object) ['value' => 'bar'],
]]);
$o = (new Hydrator())->hydrate(Fixtures\ObjectWithAssociations::class, [
'value' => (object) [
'foo' => (object) ['value' => 'foo'],
'bar' => (object) ['value' => 'bar'],
],
]);

$this->assertNotNull($o->value['foo']);
$this->assertSame('foo', $o->value['foo']->value);
Expand Down Expand Up @@ -721,4 +739,51 @@ public function testHydrateProductWithJsonAsObject(): void
$this->assertSame('dccd816f-bb28-41f3-b1a9-ddaff1fdec5b', $product->tags[1]->name);
$this->assertSame(2, $product->status->value);
}

public function testHydrateAbstractObject(): void
{
$o = (new Hydrator())->hydrate(Apple::class, [
'type' => 'sauce',
'sweetness' => 100,
'category' => null,
]);

$this->assertInstanceOf(AppleSauce::class, $o);
$this->assertSame('sauce', $o->type);
$this->assertSame(100, $o->sweetness);
}

public function testHydrateAbstractObjectWithoutInterface(): void
{
$this->expectException(InvalidObjectException::class);

(new Hydrator())->hydrate(Fruit::class, ['name' => 'apple']);
}

public function testHydrateAbstractProperty(): void
{
$o = (new Hydrator())->hydrate(new ObjectWithAbstract(), [
'value' => [
'type' => 'jack',
'sweetness' => null,
'category' => 'brandy',
],
]);

$this->assertInstanceOf(ObjectWithAbstract::class, $o);
$this->assertInstanceOf(AppleJack::class, $o->value);
$this->assertSame('jack', $o->value->type);
$this->assertSame('brandy', $o->value->category);
}

public function testHydrateInvalidAbstractObject(): void
{
$this->expectException(InvalidObjectException::class);

(new Hydrator())->hydrate(new ObjectWithInvalidAbstract(), [
'value' => [
'name' => 'apple',
],
]);
}
}

0 comments on commit 9ae7d88

Please sign in to comment.