From d7febf6ec15bd2311e16c6ffda45f3ebc385a246 Mon Sep 17 00:00:00 2001 From: Alexey Rogachev Date: Wed, 7 Aug 2024 13:20:02 +0500 Subject: [PATCH] Add support for collections (#90) undefined --- CHANGELOG.md | 3 + docs/guide/en/typecasting.md | 59 +++- src/Attribute/Parameter/Collection.php | 28 ++ .../Parameter/CollectionResolver.php | 47 ++++ .../ParameterAttributeResolveContext.php | 13 + .../ParameterAttributesHandler.php | 16 +- src/Hydrator.php | 2 +- tests/Attribute/Parameter/CollectionTest.php | 252 ++++++++++++++++++ tests/Attribute/Parameter/LeftTrimTest.php | 1 + tests/Attribute/Parameter/RightTrimTest.php | 1 + tests/Attribute/Parameter/ToDateTimeTest.php | 3 + tests/Attribute/Parameter/ToStringTest.php | 1 + tests/Attribute/Parameter/TrimTest.php | 1 + .../ParameterAttributeResolveContextTest.php | 18 +- .../ParameterAttributesHandlerTest.php | 32 ++- tests/Support/Classes/Chart/Chart.php | 16 ++ tests/Support/Classes/Chart/ChartSet.php | 16 ++ tests/Support/Classes/Chart/Coordinates.php | 14 + tests/Support/Classes/Chart/Point.php | 14 + tests/Support/Classes/Post/Post.php | 14 + tests/Support/Classes/Post/PostCategory.php | 21 ++ 21 files changed, 556 insertions(+), 16 deletions(-) create mode 100644 src/Attribute/Parameter/Collection.php create mode 100644 src/Attribute/Parameter/CollectionResolver.php create mode 100644 tests/Attribute/Parameter/CollectionTest.php create mode 100644 tests/Support/Classes/Chart/Chart.php create mode 100644 tests/Support/Classes/Chart/ChartSet.php create mode 100644 tests/Support/Classes/Chart/Coordinates.php create mode 100644 tests/Support/Classes/Chart/Point.php create mode 100644 tests/Support/Classes/Post/Post.php create mode 100644 tests/Support/Classes/Post/PostCategory.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ed6fc2..0e106ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## 1.3.0 under development +- New #49: Add `Collection` PHP attribute (@arogachev) +- New #49: Add hydrator dependency and `withHydrator()` method to `ParameterAttributesHandler` (@arogachev) +- New #49: Add hydrator dependency and `getHydrator()` method to `ParameterAttributeResolveContext` (@arogachev) - Enh #85: Allow to hydrate non-initialized readonly properties (@vjik) ## 1.2.0 April 03, 2024 diff --git a/docs/guide/en/typecasting.md b/docs/guide/en/typecasting.md index 8f39370..ccda587 100644 --- a/docs/guide/en/typecasting.md +++ b/docs/guide/en/typecasting.md @@ -20,9 +20,10 @@ $lock = $hydrator->create(Lock::class, ['name' => 'The lock', 'isLocked' => 1]); You can adjust type-casting by passing a type-caster to the hydrator: ```php +use Yiisoft\Hydrator\Hydrator; use Yiisoft\Hydrator\TypeCaster\CompositeTypeCaster; use Yiisoft\Hydrator\TypeCaster\PhpNativeTypeCaster; -use Yiisoft\Hydrator\TypeCaster\HydratorTypeCaster +use Yiisoft\Hydrator\TypeCaster\HydratorTypeCaster; $typeCaster = new CompositeTypeCaster( new PhpNativeTypeCaster(), @@ -119,6 +120,8 @@ echo $post->getAuthor()->getNickName(); ## Using attributes +### `ToString` + To cast a value to string explicitly, you can use `ToString` attribute: ```php @@ -139,6 +142,28 @@ $money = $hydrator->create(Money::class, [ ]); ``` +### `Trim` / `LeftTrim` / `RightTrim` + +To strip whitespace (or other characters) from the beginning and/or end of a resolved string value, you can use `Trim`, +`LeftTrim` or `RightTrim` attributes: + +```php +use DateTimeImmutable; +use Yiisoft\Hydrator\Attribute\Parameter\Trim; + +class Person +{ + public function __construct( + #[Trim] // ' John ' → 'John' + private ?string $name = null, + ) {} +} + +$person = $hydrator->create(Person::class, ['name' => ' John ']); +``` + +### `ToDatetime` + To cast a value to `DateTimeImmutable` or `DateTime` object explicitly, you can use `ToDateTime` attribute: ```php @@ -156,20 +181,34 @@ class Person $person = $hydrator->create(Person::class, ['birthday' => '27.01.1986']); ``` -To strip whitespace (or other characters) from the beginning and/or end of a resolved string value, you can use `Trim`, -`LeftTrim` or `RightTrim` attributes: +### `Collection` + +Hydrator supports collections via `Collection` attribute. The class name of related collection must be specified: ```php -use DateTimeImmutable; -use Yiisoft\Hydrator\Attribute\Parameter\Trim; +final class PostCategory +{ + public function __construct( + #[Collection(Post::class)] + private array $posts = [], + ) { + } +} -class Person +final class Post { public function __construct( - #[Trim] // ' John ' → 'John' - private ?string $name = null, - ) {} + private string $name, + private string $description = '', + ) { + } } -$person = $hydrator->create(Person::class, ['name' => ' John ']); +$category = $hydrator->create( + PostCategory::class, + [ + ['name' => 'Post 1'], + ['name' => 'Post 2', 'description' => 'Description for post 2'], + ], +); ``` diff --git a/src/Attribute/Parameter/Collection.php b/src/Attribute/Parameter/Collection.php new file mode 100644 index 0000000..7100ffe --- /dev/null +++ b/src/Attribute/Parameter/Collection.php @@ -0,0 +1,28 @@ +isResolved()) { + return Result::fail(); + } + + $resolvedValue = $context->getResolvedValue(); + if (!is_iterable($resolvedValue)) { + return Result::fail(); + } + + $collection = []; + foreach ($resolvedValue as $item) { + if (!is_array($item) && !$item instanceof DataInterface) { + continue; + } + + try { + $collection[] = $context->getHydrator()->create($attribute->className, $item); + } catch (NonInstantiableException) { + continue; + } + } + + return Result::success($collection); + } +} diff --git a/src/AttributeHandling/ParameterAttributeResolveContext.php b/src/AttributeHandling/ParameterAttributeResolveContext.php index 3567b67..ed2e12d 100644 --- a/src/AttributeHandling/ParameterAttributeResolveContext.php +++ b/src/AttributeHandling/ParameterAttributeResolveContext.php @@ -4,9 +4,11 @@ namespace Yiisoft\Hydrator\AttributeHandling; +use LogicException; use ReflectionParameter; use ReflectionProperty; use Yiisoft\Hydrator\DataInterface; +use Yiisoft\Hydrator\HydratorInterface; use Yiisoft\Hydrator\Result; /** @@ -18,11 +20,13 @@ final class ParameterAttributeResolveContext * @param ReflectionParameter|ReflectionProperty $parameter Resolved parameter or property reflection. * @param Result $resolveResult The resolved value object. * @param DataInterface $data Data to be used for resolving. + * @param ?HydratorInterface Hydrator instance. */ public function __construct( private ReflectionParameter|ReflectionProperty $parameter, private Result $resolveResult, private DataInterface $data, + private ?HydratorInterface $hydrator = null, ) { } @@ -66,4 +70,13 @@ public function getData(): DataInterface { return $this->data; } + + public function getHydrator(): HydratorInterface + { + if ($this->hydrator === null) { + throw new LogicException('Hydrator is not set in parameter attribute resolve context.'); + } + + return $this->hydrator; + } } diff --git a/src/AttributeHandling/ParameterAttributesHandler.php b/src/AttributeHandling/ParameterAttributesHandler.php index 10158ed..2e187aa 100644 --- a/src/AttributeHandling/ParameterAttributesHandler.php +++ b/src/AttributeHandling/ParameterAttributesHandler.php @@ -4,6 +4,7 @@ namespace Yiisoft\Hydrator\AttributeHandling; +use LogicException; use ReflectionAttribute; use ReflectionParameter; use ReflectionProperty; @@ -13,6 +14,7 @@ use Yiisoft\Hydrator\Attribute\Parameter\ParameterAttributeResolverInterface; use Yiisoft\Hydrator\ArrayData; use Yiisoft\Hydrator\DataInterface; +use Yiisoft\Hydrator\HydratorInterface; use Yiisoft\Hydrator\Result; /** @@ -22,6 +24,7 @@ final class ParameterAttributesHandler { public function __construct( private AttributeResolverFactoryInterface $attributeResolverFactory, + private ?HydratorInterface $hydrator = null, ) { } @@ -40,6 +43,10 @@ public function handle( ?Result $resolveResult = null, ?DataInterface $data = null ): Result { + if ($this->hydrator === null) { + throw new LogicException('Hydrator is not set in parameter attributes handler.'); + } + $resolveResult ??= Result::fail(); $data ??= new ArrayData(); @@ -60,7 +67,7 @@ public function handle( ); } - $context = new ParameterAttributeResolveContext($parameter, $resolveResult, $data); + $context = new ParameterAttributeResolveContext($parameter, $resolveResult, $data, $this->hydrator); $tryResolveResult = $resolver->getParameterValue($attribute, $context); if ($tryResolveResult->isResolved()) { @@ -70,4 +77,11 @@ public function handle( return $resolveResult; } + + public function withHydrator(HydratorInterface $hydrator): self + { + $new = clone $this; + $new->hydrator = $hydrator; + return $new; + } } diff --git a/src/Hydrator.php b/src/Hydrator.php index 49c7041..ce73c78 100644 --- a/src/Hydrator.php +++ b/src/Hydrator.php @@ -56,7 +56,7 @@ public function __construct( $attributeResolverFactory ??= new ReflectionAttributeResolverFactory(); $this->dataAttributesHandler = new DataAttributesHandler($attributeResolverFactory); - $this->parameterAttributesHandler = new ParameterAttributesHandler($attributeResolverFactory); + $this->parameterAttributesHandler = new ParameterAttributesHandler($attributeResolverFactory, $this); $this->objectFactory = $objectFactory ?? new ReflectionObjectFactory(); diff --git a/tests/Attribute/Parameter/CollectionTest.php b/tests/Attribute/Parameter/CollectionTest.php new file mode 100644 index 0000000..8a2a8d5 --- /dev/null +++ b/tests/Attribute/Parameter/CollectionTest.php @@ -0,0 +1,252 @@ + new CollectionResolver(), + ]), + ), + ); + $object = new CounterClass(); + + $this->expectException(UnexpectedAttributeException::class); + $this->expectExceptionMessage( + 'Expected "' . Collection::class . '", but "' . Counter::class . '" given.' + ); + $hydrator->hydrate($object); + } + + public function testNotResolvedValue(): void + { + $hydrator = new Hydrator(); + $object = new PostCategory(); + + $hydrator->hydrate($object, ['post' => []]); + $this->assertEmpty($object->getPosts()); + } + + public function testInvalidValue(): void + { + $hydrator = new Hydrator(); + $object = new PostCategory(); + + $hydrator->hydrate($object, ['posts' => new stdClass()]); + $this->assertEmpty($object->getPosts()); + } + + public function testInvalidValueItem(): void + { + $hydrator = new Hydrator(); + $object = new PostCategory(); + + $hydrator->hydrate( + $object, + [ + 'posts' => [ + ['name' => 'Post 1'], + new stdClass(), + ['name' => 'Post 2', 'description' => 'Description for post 2'], + ], + ], + ); + $this->assertEquals( + [ + new Post(name: 'Post 1'), + new Post(name: 'Post 2', description: 'Description for post 2'), + ], + $object->getPosts(), + ); + } + + public function testNonInstantiableValueItem(): void + { + $hydrator = new Hydrator(); + $object = new PostCategory(); + + $hydrator->hydrate( + $object, + [ + 'posts' => [ + ['name' => 'Post 1'], + ['name' => []], + ['name' => 'Post 2', 'description' => 'Description for post 2'], + ], + ], + ); + $this->assertEquals( + [ + new Post(name: 'Post 1'), + new Post(name: 'Post 2', description: 'Description for post 2'), + ], + $object->getPosts(), + ); + } + + public static function dataBase(): array + { + return [ + 'basic' => [ + new Collection(Post::class), + [ + ['name' => 'Post 1'], + ['name' => 'Post 2', 'description' => 'Description for post 2'], + ], + [ + new Post(name: 'Post 1'), + new Post(name: 'Post 2', description: 'Description for post 2'), + ], + ], + 'nested, one to one and one to many relations' => [ + new Collection(Chart::class), + [ + [ + 'points' => [ + ['coordinates' => ['x' => 1, 'y' => 1], 'rgb' => [255, 0, 0]], + ['coordinates' => ['x' => 2, 'y' => 2], 'rgb' => [255, 0, 0]], + ], + ], + [ + 'points' => [ + ['coordinates' => ['x' => 3, 'y' => 3], 'rgb' => [0, 255, 0]], + ['coordinates' => ['x' => 4, 'y' => 4], 'rgb' => [0, 255, 0]], + ], + ], + [ + 'points' => [ + ['coordinates' => ['x' => 5, 'y' => 5], 'rgb' => [0, 0, 255]], + ['coordinates' => ['x' => 6, 'y' => 6], 'rgb' => [0, 0, 255]], + ], + ], + ], + [ + new Chart([ + new Point(new Coordinates(1, 1), [255, 0, 0]), + new Point(new Coordinates(2, 2), [255, 0, 0]), + ]), + new Chart([ + new Point(new Coordinates(3, 3), [0, 255, 0]), + new Point(new Coordinates(4, 4), [0, 255, 0]), + ]), + new Chart([ + new Point(new Coordinates(5, 5), [0, 0, 255]), + new Point(new Coordinates(6, 6), [0, 0, 255]), + ]), + ], + ], + 'value item provided by class' => [ + new Collection(Post::class), + [ + ['name' => 'Post 1'], + new class () implements DataInterface { + public function getValue(string $name): Result + { + $value = $name === 'name' ? 'Post 2' : 'Description for post 2'; + + return Result::success($value); + } + }, + ], + [ + new Post(name: 'Post 1'), + new Post(name: 'Post 2', description: 'Description for post 2'), + ], + ], + ]; + } + + #[DataProvider('dataBase')] + public function testBase(Collection $attribute, array $value, mixed $expectedValue): void + { + $resolver = new CollectionResolver(); + $context = new ParameterAttributeResolveContext( + TestHelper::getFirstParameter(static fn(?string $a) => null), + Result::success($value), + new ArrayData(), + new Hydrator(), + ); + $result = $resolver->getParameterValue($attribute, $context); + + $this->assertTrue($result->isResolved()); + $this->assertEquals($expectedValue, $result->getValue()); + } + + public function testWithHydrator(): void + { + $hydrator = new Hydrator(); + $object = $hydrator->create( + ChartSet::class, + [ + 'charts' => [ + [ + 'points' => [ + ['coordinates' => ['x' => 1, 'y' => 1], 'rgb' => [255, 0, 0]], + ['coordinates' => ['x' => 2, 'y' => 2], 'rgb' => [255, 0, 0]], + ], + ], + [ + 'points' => [ + ['coordinates' => ['x' => 3, 'y' => 3], 'rgb' => [0, 255, 0]], + ['coordinates' => ['x' => 4, 'y' => 4], 'rgb' => [0, 255, 0]], + ], + ], + [ + 'points' => [ + ['coordinates' => ['x' => 5, 'y' => 5], 'rgb' => [0, 0, 255]], + ['coordinates' => ['x' => 6, 'y' => 6], 'rgb' => [0, 0, 255]], + ], + ], + ], + ], + ); + + $this->assertEquals( + new ChartSet([ + new Chart([ + new Point(new Coordinates(1, 1), [255, 0, 0]), + new Point(new Coordinates(2, 2), [255, 0, 0]), + ]), + new Chart([ + new Point(new Coordinates(3, 3), [0, 255, 0]), + new Point(new Coordinates(4, 4), [0, 255, 0]), + ]), + new Chart([ + new Point(new Coordinates(5, 5), [0, 0, 255]), + new Point(new Coordinates(6, 6), [0, 0, 255]), + ]), + ]), + $object, + ); + } +} diff --git a/tests/Attribute/Parameter/LeftTrimTest.php b/tests/Attribute/Parameter/LeftTrimTest.php index ac832f0..ea5dc99 100644 --- a/tests/Attribute/Parameter/LeftTrimTest.php +++ b/tests/Attribute/Parameter/LeftTrimTest.php @@ -38,6 +38,7 @@ public function testBase(string $expected, LeftTrim $attribute, mixed $value): v TestHelper::getFirstParameter(static fn(?string $a) => null), Result::success($value), new ArrayData(), + new Hydrator(), ); $result = $resolver->getParameterValue($attribute, $context); diff --git a/tests/Attribute/Parameter/RightTrimTest.php b/tests/Attribute/Parameter/RightTrimTest.php index dbad4b3..8ea738c 100644 --- a/tests/Attribute/Parameter/RightTrimTest.php +++ b/tests/Attribute/Parameter/RightTrimTest.php @@ -38,6 +38,7 @@ public function testBase(string $expected, RightTrim $attribute, mixed $value): TestHelper::getFirstParameter(static fn(?string $a) => null), Result::success($value), new ArrayData(), + new Hydrator(), ); $result = $resolver->getParameterValue($attribute, $context); diff --git a/tests/Attribute/Parameter/ToDateTimeTest.php b/tests/Attribute/Parameter/ToDateTimeTest.php index ad7ebf6..19e5c64 100644 --- a/tests/Attribute/Parameter/ToDateTimeTest.php +++ b/tests/Attribute/Parameter/ToDateTimeTest.php @@ -75,6 +75,7 @@ public function testBase(DateTimeImmutable $expected, ToDateTime $attribute, mix TestHelper::getFirstParameter(static fn(?DateTimeImmutable $a) => null), Result::success($value), new ArrayData(), + new Hydrator(), ); $result = $resolver->getParameterValue($attribute, $context); @@ -346,6 +347,7 @@ public function testResultType(string $expected, Closure $closure, mixed $value) TestHelper::getFirstParameter($closure), Result::success($value), new ArrayData(), + new Hydrator(), ); $result = $resolver->getParameterValue(new ToDateTime(format: 'php:d.m.Y'), $context); @@ -362,6 +364,7 @@ public function testResultTypeWithIntlFormat(string $expected, Closure $closure, TestHelper::getFirstParameter($closure), Result::success($value), new ArrayData(), + new Hydrator(), ); $result = $resolver->getParameterValue(new ToDateTime(format: 'dd.MM.yyyy'), $context); diff --git a/tests/Attribute/Parameter/ToStringTest.php b/tests/Attribute/Parameter/ToStringTest.php index fc433b4..395623d 100644 --- a/tests/Attribute/Parameter/ToStringTest.php +++ b/tests/Attribute/Parameter/ToStringTest.php @@ -43,6 +43,7 @@ public function testBase(mixed $expected, mixed $value): void TestHelper::getFirstParameter(static fn(string $a) => null), Result::success($value), new ArrayData(), + new Hydrator(), ); $result = $attribute->getParameterValue($attribute, $context); diff --git a/tests/Attribute/Parameter/TrimTest.php b/tests/Attribute/Parameter/TrimTest.php index 1fc776c..1809536 100644 --- a/tests/Attribute/Parameter/TrimTest.php +++ b/tests/Attribute/Parameter/TrimTest.php @@ -38,6 +38,7 @@ public function testBase(string $expected, Trim $attribute, mixed $value): void TestHelper::getFirstParameter(static fn(?string $a) => null), Result::success($value), new ArrayData(), + new Hydrator(), ); $result = $resolver->getParameterValue($attribute, $context); diff --git a/tests/AttributeHandling/ParameterAttributeResolveContextTest.php b/tests/AttributeHandling/ParameterAttributeResolveContextTest.php index 9a9ba91..0987add 100644 --- a/tests/AttributeHandling/ParameterAttributeResolveContextTest.php +++ b/tests/AttributeHandling/ParameterAttributeResolveContextTest.php @@ -4,11 +4,14 @@ namespace Yiisoft\Hydrator\Tests\AttributeHandling; +use LogicException; use PHPUnit\Framework\TestCase; use ReflectionClass; use Yiisoft\Hydrator\ArrayData; use Yiisoft\Hydrator\AttributeHandling\ParameterAttributeResolveContext; +use Yiisoft\Hydrator\Hydrator; use Yiisoft\Hydrator\Result; +use Yiisoft\Hydrator\Tests\Support\TestHelper; final class ParameterAttributeResolveContextTest extends TestCase { @@ -21,11 +24,24 @@ public function testBase(): void ))->getProperties()[0]; $data = new ArrayData(['a' => 5, 'b' => 6]); - $context = new ParameterAttributeResolveContext($parameter, Result::success(7), $data); + $context = new ParameterAttributeResolveContext($parameter, Result::success(7), $data, new Hydrator()); $this->assertSame($parameter, $context->getParameter()); $this->assertTrue($context->isResolved()); $this->assertSame(7, $context->getResolvedValue()); $this->assertSame($data, $context->getData()); } + + public function testGetHydratorNull(): void + { + $context = new ParameterAttributeResolveContext( + TestHelper::getFirstParameter(static fn(?string $a) => null), + Result::success(1), + new ArrayData(), + ); + + $this->expectException(LogicException::class); + $this->expectExceptionMessage('Hydrator is not set in parameter attribute resolve context.'); + $context->getHydrator(); + } } diff --git a/tests/AttributeHandling/ParameterAttributesHandlerTest.php b/tests/AttributeHandling/ParameterAttributesHandlerTest.php index 9c663ae..32d7292 100644 --- a/tests/AttributeHandling/ParameterAttributesHandlerTest.php +++ b/tests/AttributeHandling/ParameterAttributesHandlerTest.php @@ -4,11 +4,13 @@ namespace Yiisoft\Hydrator\Tests\AttributeHandling; +use LogicException; use PHPUnit\Framework\TestCase; use Yiisoft\Hydrator\AttributeHandling\ParameterAttributeResolveContext; use Yiisoft\Hydrator\AttributeHandling\ParameterAttributesHandler; use Yiisoft\Hydrator\AttributeHandling\ResolverFactory\ContainerAttributeResolverFactory; use Yiisoft\Hydrator\AttributeHandling\ResolverFactory\ReflectionAttributeResolverFactory; +use Yiisoft\Hydrator\Hydrator; use Yiisoft\Hydrator\Tests\Support\Attribute\ContextViewer; use Yiisoft\Hydrator\Tests\Support\Attribute\ContextViewerResolver; use Yiisoft\Hydrator\Tests\Support\Attribute\CustomValue; @@ -26,7 +28,8 @@ public function testDefaultsHandleParameters(): void new SimpleContainer([ ContextViewerResolver::class => $contextViewerResolver, ]) - ) + ), + new Hydrator(), ); $parameter = TestHelper::getFirstParameter(static fn(#[ContextViewer] int $a) => null); @@ -41,7 +44,7 @@ public function testDefaultsHandleParameters(): void public function testBase(): void { - $handler = new ParameterAttributesHandler(new ReflectionAttributeResolverFactory()); + $handler = new ParameterAttributesHandler(new ReflectionAttributeResolverFactory(), new Hydrator()); $parameter = TestHelper::getFirstParameter(static fn(#[CustomValue('42')] int $a) => null); @@ -52,7 +55,7 @@ public function testBase(): void public function testNotResolvedAttributeAfterResolved(): void { - $handler = new ParameterAttributesHandler(new ReflectionAttributeResolverFactory()); + $handler = new ParameterAttributesHandler(new ReflectionAttributeResolverFactory(), new Hydrator()); $parameter = TestHelper::getFirstParameter( static function( @@ -68,4 +71,27 @@ static function( $this->assertSame('42', $result->getValue()); } + + public function testWithHydrator(): void + { + $handler = new ParameterAttributesHandler(new ReflectionAttributeResolverFactory()); + $hydrator = new Hydrator(); + + $newHandler = $handler->withHydrator($hydrator); + $this->assertNotSame($handler, $newHandler); + + $parameter = TestHelper::getFirstParameter(static fn(#[CustomValue('42')] int $a) => null); + $result = $newHandler->handle($parameter); + $this->assertSame('42', $result->getValue()); + } + + public function testHydratorNotSet(): void + { + $handler = new ParameterAttributesHandler(new ReflectionAttributeResolverFactory()); + $parameter = TestHelper::getFirstParameter(static fn(#[CustomValue('42')] int $a) => null); + + $this->expectException(LogicException::class); + $this->expectExceptionMessage('Hydrator is not set in parameter attributes handler.'); + $handler->handle($parameter); + } } diff --git a/tests/Support/Classes/Chart/Chart.php b/tests/Support/Classes/Chart/Chart.php new file mode 100644 index 0000000..b90cba1 --- /dev/null +++ b/tests/Support/Classes/Chart/Chart.php @@ -0,0 +1,16 @@ +posts; + } +}