Skip to content

Commit 9ae7d88

Browse files
authored
Merge pull request #7 from nutgram/abstract-class-resolver
add ability to resolve abstract class
2 parents 4a36ac7 + 6336957 commit 9ae7d88

File tree

10 files changed

+183
-19
lines changed

10 files changed

+183
-19
lines changed

.editorconfig

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,9 @@ insert_final_newline = true
1313

1414
[*.yml]
1515
indent_size = 2
16+
17+
[*.php]
18+
ij_php_keep_indents_on_empty_lines = false
19+
ij_php_concat_spaces = false
20+
ij_php_space_after_type_cast = true
21+
ij_php_align_key_value_pairs = false

src/ConcreteResolver.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
namespace SergiX44\Hydrator;
4+
5+
interface ConcreteResolver
6+
{
7+
/**
8+
* @param array $data
9+
*
10+
* @return class-string
11+
*/
12+
public static function resolveAbstractClass(array $data): string;
13+
}

src/Hydrator.php

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
use SergiX44\Hydrator\Annotation\Alias;
3333
use SergiX44\Hydrator\Annotation\ArrayType;
3434
use SergiX44\Hydrator\Annotation\UnionResolver;
35+
use SergiX44\Hydrator\Exception\InvalidObjectException;
3536
use function sprintf;
3637
use function strtotime;
3738

@@ -72,7 +73,7 @@ public function hydrate(string|object $object, array|object $data): object
7273
$data = get_object_vars($data);
7374
}
7475

75-
$object = $this->initializeObject($object);
76+
$object = $this->initializeObject($object, $data);
7677

7778
$class = new ReflectionClass($object);
7879
foreach ($class->getProperties() as $property) {
@@ -180,7 +181,7 @@ public function hydrateWithJson(string|object $object, string $json, ?int $flags
180181
*
181182
* @template T
182183
*/
183-
private function initializeObject(string|object $object): object
184+
private function initializeObject(string|object $object, array|object $data): object
184185
{
185186
if (is_object($object)) {
186187
return $object;
@@ -194,6 +195,21 @@ private function initializeObject(string|object $object): object
194195
}
195196

196197
$class = new ReflectionClass($object);
198+
199+
if ($class->isAbstract()) {
200+
if (!$class->implementsInterface(ConcreteResolver::class)) {
201+
throw new InvalidObjectException(sprintf(
202+
'The given abstract object must implement %s.',
203+
ConcreteResolver::class
204+
));
205+
}
206+
207+
$resolveMethod = $class->getMethod('resolveAbstractClass');
208+
$realClass = $resolveMethod->invoke(null, $data);
209+
210+
return $this->initializeObject($realClass, $data);
211+
}
212+
197213
$constructor = $class->getConstructor();
198214
if (isset($constructor) && $constructor->getNumberOfRequiredParameters() > 0) {
199215
throw new InvalidArgumentException(sprintf(

tests/Fixtures/ObjectWithAbstract.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
namespace SergiX44\Hydrator\Tests\Fixtures;
4+
5+
use SergiX44\Hydrator\Tests\Fixtures\Store\Apple;
6+
7+
final class ObjectWithAbstract
8+
{
9+
public Apple $value;
10+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
namespace SergiX44\Hydrator\Tests\Fixtures;
4+
5+
use SergiX44\Hydrator\Tests\Fixtures\Store\Fruit;
6+
7+
final class ObjectWithInvalidAbstract
8+
{
9+
public Fruit $value;
10+
}

tests/Fixtures/Store/Apple.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
namespace SergiX44\Hydrator\Tests\Fixtures\Store;
4+
5+
use Exception;
6+
use SergiX44\Hydrator\ConcreteResolver;
7+
8+
abstract class Apple implements ConcreteResolver
9+
{
10+
public string $type;
11+
12+
public static function resolveAbstractClass(array $data): string
13+
{
14+
return match ($data['type']) {
15+
'jack' => AppleJack::class,
16+
'sauce' => AppleSauce::class,
17+
default => throw new Exception('Invalid apple type'),
18+
};
19+
}
20+
}

tests/Fixtures/Store/AppleJack.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php
2+
3+
namespace SergiX44\Hydrator\Tests\Fixtures\Store;
4+
5+
final class AppleJack extends Apple
6+
{
7+
public string $category;
8+
}

tests/Fixtures/Store/AppleSauce.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php
2+
3+
namespace SergiX44\Hydrator\Tests\Fixtures\Store;
4+
5+
final class AppleSauce extends Apple
6+
{
7+
public int $sweetness;
8+
}

tests/Fixtures/Store/Fruit.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php
2+
3+
namespace SergiX44\Hydrator\Tests\Fixtures\Store;
4+
5+
abstract class Fruit
6+
{
7+
public string $name;
8+
}

tests/HydratorTest.php

Lines changed: 82 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,15 @@
55
use InvalidArgumentException;
66
use PHPUnit\Framework\TestCase;
77
use SergiX44\Hydrator\Exception;
8+
use SergiX44\Hydrator\Exception\InvalidObjectException;
89
use SergiX44\Hydrator\Hydrator;
910
use SergiX44\Hydrator\HydratorInterface;
11+
use SergiX44\Hydrator\Tests\Fixtures\ObjectWithAbstract;
12+
use SergiX44\Hydrator\Tests\Fixtures\ObjectWithInvalidAbstract;
13+
use SergiX44\Hydrator\Tests\Fixtures\Store\Apple;
14+
use SergiX44\Hydrator\Tests\Fixtures\Store\AppleJack;
15+
use SergiX44\Hydrator\Tests\Fixtures\Store\AppleSauce;
16+
use SergiX44\Hydrator\Tests\Fixtures\Store\Fruit;
1017
use SergiX44\Hydrator\Tests\Fixtures\Store\Tag;
1118
use SergiX44\Hydrator\Tests\Fixtures\Store\TagPrice;
1219
use TypeError;
@@ -127,7 +134,10 @@ public function testHydrateAttributedProperty(): void
127134
$this->markTestSkipped('php >= 8 is required.');
128135
}
129136

130-
$object = (new Hydrator())->hydrate(Fixtures\ObjectWithAttributedAlias::class, ['non-normalized-value' => 'foo']);
137+
$object = (new Hydrator())->hydrate(
138+
Fixtures\ObjectWithAttributedAlias::class,
139+
['non-normalized-value' => 'foo']
140+
);
131141

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

336346
public function testHydrateTypedArrayableProperty(): void
337347
{
338-
$object = (new Hydrator())->hydrate(Fixtures\ObjectWithTypedArray::class, ['value' => [
339-
['name' => 'foo'],
340-
['name' => 'bar'],
341-
]]);
348+
$object = (new Hydrator())->hydrate(Fixtures\ObjectWithTypedArray::class, [
349+
'value' => [
350+
['name' => 'foo'],
351+
['name' => 'bar'],
352+
],
353+
]);
342354

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

351363
public function testHydrateTypedNestedArrayableProperty(): void
352364
{
353-
$object = (new Hydrator())->hydrate(Fixtures\ObjectWithTypedArrayOfArray::class, ['value' => [
354-
[['name' => 'foo'], ['name' => 'fef']],
355-
[['name' => 'bar'], ['name' => 'fif']],
356-
]]);
365+
$object = (new Hydrator())->hydrate(Fixtures\ObjectWithTypedArrayOfArray::class, [
366+
'value' => [
367+
[['name' => 'foo'], ['name' => 'fef']],
368+
[['name' => 'bar'], ['name' => 'fif']],
369+
],
370+
]);
357371

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

605619
public function testHydrateAssociationCollectionProperty(): void
606620
{
607-
$o = (new Hydrator())->hydrate(Fixtures\ObjectWithAssociations::class, ['value' => [
608-
'foo' => ['value' => 'foo'],
609-
'bar' => ['value' => 'bar'],
610-
]]);
621+
$o = (new Hydrator())->hydrate(Fixtures\ObjectWithAssociations::class, [
622+
'value' => [
623+
'foo' => ['value' => 'foo'],
624+
'bar' => ['value' => 'bar'],
625+
],
626+
]);
611627

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

619635
public function testHydrateAssociationCollectionPropertyUsingDataObject(): void
620636
{
621-
$o = (new Hydrator())->hydrate(Fixtures\ObjectWithAssociations::class, ['value' => (object) [
622-
'foo' => (object) ['value' => 'foo'],
623-
'bar' => (object) ['value' => 'bar'],
624-
]]);
637+
$o = (new Hydrator())->hydrate(Fixtures\ObjectWithAssociations::class, [
638+
'value' => (object) [
639+
'foo' => (object) ['value' => 'foo'],
640+
'bar' => (object) ['value' => 'bar'],
641+
],
642+
]);
625643

626644
$this->assertNotNull($o->value['foo']);
627645
$this->assertSame('foo', $o->value['foo']->value);
@@ -721,4 +739,51 @@ public function testHydrateProductWithJsonAsObject(): void
721739
$this->assertSame('dccd816f-bb28-41f3-b1a9-ddaff1fdec5b', $product->tags[1]->name);
722740
$this->assertSame(2, $product->status->value);
723741
}
742+
743+
public function testHydrateAbstractObject(): void
744+
{
745+
$o = (new Hydrator())->hydrate(Apple::class, [
746+
'type' => 'sauce',
747+
'sweetness' => 100,
748+
'category' => null,
749+
]);
750+
751+
$this->assertInstanceOf(AppleSauce::class, $o);
752+
$this->assertSame('sauce', $o->type);
753+
$this->assertSame(100, $o->sweetness);
754+
}
755+
756+
public function testHydrateAbstractObjectWithoutInterface(): void
757+
{
758+
$this->expectException(InvalidObjectException::class);
759+
760+
(new Hydrator())->hydrate(Fruit::class, ['name' => 'apple']);
761+
}
762+
763+
public function testHydrateAbstractProperty(): void
764+
{
765+
$o = (new Hydrator())->hydrate(new ObjectWithAbstract(), [
766+
'value' => [
767+
'type' => 'jack',
768+
'sweetness' => null,
769+
'category' => 'brandy',
770+
],
771+
]);
772+
773+
$this->assertInstanceOf(ObjectWithAbstract::class, $o);
774+
$this->assertInstanceOf(AppleJack::class, $o->value);
775+
$this->assertSame('jack', $o->value->type);
776+
$this->assertSame('brandy', $o->value->category);
777+
}
778+
779+
public function testHydrateInvalidAbstractObject(): void
780+
{
781+
$this->expectException(InvalidObjectException::class);
782+
783+
(new Hydrator())->hydrate(new ObjectWithInvalidAbstract(), [
784+
'value' => [
785+
'name' => 'apple',
786+
],
787+
]);
788+
}
724789
}

0 commit comments

Comments
 (0)