From c0b69e325f9ba51cfc1db7f930f48a68aad1fab7 Mon Sep 17 00:00:00 2001 From: Vincent QUATREVIEUX Date: Mon, 23 Mar 2026 14:28:19 +0100 Subject: [PATCH 1/2] feat(value): Instantiate immutable DTO value using promotted properties (#FRAM-223) --- src/Aggregate/Form.php | 11 ++- src/Aggregate/FormBuilder.php | 2 +- src/Aggregate/FormBuilderInterface.php | 2 +- src/Aggregate/FormInterface.php | 2 +- src/Aggregate/Value/ClosureValueGenerator.php | 51 ++++++++++++++ .../Value/ConstructorValueGenerator.php | 62 +++++++++++++++++ .../DefaultConstructorValueGenerator.php | 62 +++++++++++++++++ src/Aggregate/Value/ObjectValueGenerator.php | 62 +++++++++++++++++ src/Aggregate/Value/SimpleValueGenerator.php | 41 ++++++++++++ src/Aggregate/Value/ValueGenerator.php | 58 +++++++++------- .../Value/ValueGeneratorInterface.php | 21 ++++-- tests/Aggregate/FormTest.php | 23 +++++++ .../Value/ClosureValueGeneratorTest.php | 35 ++++++++++ .../Value/ConstructorValueGeneratorTest.php | 67 +++++++++++++++++++ .../DefaultConstructorValueGeneratorTest.php | 55 +++++++++++++++ tests/Aggregate/Value/Fixtures/MyDto.php | 12 ++++ tests/Aggregate/Value/Fixtures/MyEntity.php | 8 +++ .../Value/ObjectValueGeneratorTest.php | 59 ++++++++++++++++ .../Value/SimpleValueGeneratorTest.php | 51 ++++++++++++++ tests/Aggregate/Value/ValueGeneratorTest.php | 51 +++++++++----- tests/Attribute/Form/GeneratesTest.php | 2 +- .../GenerateConfiguratorStrategyTest.php | 4 +- 22 files changed, 691 insertions(+), 50 deletions(-) create mode 100644 src/Aggregate/Value/ClosureValueGenerator.php create mode 100644 src/Aggregate/Value/ConstructorValueGenerator.php create mode 100644 src/Aggregate/Value/DefaultConstructorValueGenerator.php create mode 100644 src/Aggregate/Value/ObjectValueGenerator.php create mode 100644 src/Aggregate/Value/SimpleValueGenerator.php create mode 100644 tests/Aggregate/Value/ClosureValueGeneratorTest.php create mode 100644 tests/Aggregate/Value/ConstructorValueGeneratorTest.php create mode 100644 tests/Aggregate/Value/DefaultConstructorValueGeneratorTest.php create mode 100644 tests/Aggregate/Value/Fixtures/MyDto.php create mode 100644 tests/Aggregate/Value/Fixtures/MyEntity.php create mode 100644 tests/Aggregate/Value/ObjectValueGeneratorTest.php create mode 100644 tests/Aggregate/Value/SimpleValueGeneratorTest.php diff --git a/src/Aggregate/Form.php b/src/Aggregate/Form.php index 3128546..0b98138 100644 --- a/src/Aggregate/Form.php +++ b/src/Aggregate/Form.php @@ -21,6 +21,10 @@ use Override; use function assert; +use function is_array; +use function is_object; +use function sprintf; +use function trigger_error; /** * The base form element @@ -216,7 +220,7 @@ public function value(): array|object|null } /** @var T $value */ - return $this->value = $value; + return $this->value = $this->generator->finalize($value); } #[Override] @@ -296,6 +300,11 @@ public function __clone() #[Override] public function attach($entity): FormInterface { + /** @psalm-suppress RedundantCondition */ + if (!is_object($entity) && !is_array($entity)) { + @trigger_error(sprintf('Attaching a non-object and non-array value is deprecated since bdf-form 2.0 and will be removed. Got %s.', get_debug_type($entity)), E_USER_DEPRECATED); + } + $this->generator->attach($entity); $this->value = null; // The value is only attached : it must be filled when calling value() diff --git a/src/Aggregate/FormBuilder.php b/src/Aggregate/FormBuilder.php index cc41590..190c4d0 100644 --- a/src/Aggregate/FormBuilder.php +++ b/src/Aggregate/FormBuilder.php @@ -390,7 +390,7 @@ public function generator(ValueGeneratorInterface $generator): FormBuilderInterf * {@inheritdoc} */ #[Override] - public function generates($entity): FormBuilderInterface + public function generates(mixed $entity): FormBuilderInterface { return $this->generator(new ValueGenerator($entity)); } diff --git a/src/Aggregate/FormBuilderInterface.php b/src/Aggregate/FormBuilderInterface.php index 6035349..6a7993c 100644 --- a/src/Aggregate/FormBuilderInterface.php +++ b/src/Aggregate/FormBuilderInterface.php @@ -315,7 +315,7 @@ public function generator(ValueGeneratorInterface $generator): FormBuilderInterf * }); * * - * @param callable|class-string|object|array $entity The entity to generate + * @param (callable(ElementInterface):array|object)|class-string|array|object $entity The entity to generate * * @return $this * diff --git a/src/Aggregate/FormInterface.php b/src/Aggregate/FormInterface.php index 454c613..66d63f3 100644 --- a/src/Aggregate/FormInterface.php +++ b/src/Aggregate/FormInterface.php @@ -33,7 +33,7 @@ interface FormInterface extends ChildAggregateInterface * $this->repository->save($form->value()); * * - * @param T|class-string|callable():T $entity The entity object, or class name + * @param T $entity The entity object, or initial array values * * @return $this * diff --git a/src/Aggregate/Value/ClosureValueGenerator.php b/src/Aggregate/Value/ClosureValueGenerator.php new file mode 100644 index 0000000..abf9d32 --- /dev/null +++ b/src/Aggregate/Value/ClosureValueGenerator.php @@ -0,0 +1,51 @@ + + */ +final class ClosureValueGenerator implements ValueGeneratorInterface +{ + /** + * @var T|null + */ + private array|object|null $attachment = null; + + public function __construct( + /** + * @var Closure(ElementInterface):T + */ + private readonly Closure $generator, + ) {} + + #[Override] + public function attach(mixed $entity): void + { + $this->attachment = $entity; + } + + #[Override] + public function generate(ElementInterface $element): object|array + { + if ($this->attachment !== null) { + return $this->attachment; + } + + return ($this->generator)($element); + } + + #[Override] + public function finalize(object|array $value): object|array + { + /** @var T */ + return $value; + } +} diff --git a/src/Aggregate/Value/ConstructorValueGenerator.php b/src/Aggregate/Value/ConstructorValueGenerator.php new file mode 100644 index 0000000..f47654d --- /dev/null +++ b/src/Aggregate/Value/ConstructorValueGenerator.php @@ -0,0 +1,62 @@ + + */ +final class ConstructorValueGenerator implements ValueGeneratorInterface +{ + /** + * @var T|array|null + */ + private array|object|null $attachment = null; + + public function __construct( + /** + * @var class-string + */ + private readonly string $class, + ) {} + + #[Override] + public function attach(mixed $entity): void + { + if (is_array($entity) || $entity instanceof $this->class) { + $this->attachment = $entity; + return; + } + + throw new InvalidArgumentException(sprintf('Expected array or instance of %s, %s given on %s::attach()', $this->class, get_debug_type($entity), self::class)); + } + + #[Override] + public function generate(ElementInterface $element): object|array + { + return $this->attachment ?? []; + } + + #[Override] + public function finalize(object|array $value): object + { + if (is_array($value)) { + $value = new ($this->class)(...$value); + } + + assert($value instanceof $this->class); + + return $value; + } +} diff --git a/src/Aggregate/Value/DefaultConstructorValueGenerator.php b/src/Aggregate/Value/DefaultConstructorValueGenerator.php new file mode 100644 index 0000000..ebf8f4f --- /dev/null +++ b/src/Aggregate/Value/DefaultConstructorValueGenerator.php @@ -0,0 +1,62 @@ + + */ +final class DefaultConstructorValueGenerator implements ValueGeneratorInterface +{ + /** + * @var T|null + */ + private ?object $attachment = null; + + public function __construct( + /** + * @var class-string + */ + private readonly string $class, + ) {} + + #[Override] + public function attach(mixed $entity): void + { + if (!$entity instanceof $this->class) { + throw new InvalidArgumentException(sprintf('Cannot attach a value of type %s, expecting %s', get_debug_type($entity), $this->class)); + } + + $this->attachment = $entity; + } + + #[Override] + public function generate(ElementInterface $element): object + { + if ($this->attachment !== null) { + return $this->attachment; + } + + return new $this->class; + } + + #[Override] + public function finalize(object|array $value): object + { + assert($value instanceof $this->class); + + return $value; + } +} diff --git a/src/Aggregate/Value/ObjectValueGenerator.php b/src/Aggregate/Value/ObjectValueGenerator.php new file mode 100644 index 0000000..f1b97cb --- /dev/null +++ b/src/Aggregate/Value/ObjectValueGenerator.php @@ -0,0 +1,62 @@ + + */ +final class ObjectValueGenerator implements ValueGeneratorInterface +{ + /** + * @var T|null + */ + private ?object $attachment = null; + + public function __construct( + /** + * @var T + */ + private readonly object $value, + ) {} + + #[Override] + public function attach(mixed $entity): void + { + if (!$entity instanceof $this->value) { + throw new InvalidArgumentException(sprintf('Cannot attach a value of type %s, expecting %s', get_debug_type($entity), get_class($this->value))); + } + + $this->attachment = $entity; + } + + #[Override] + public function generate(ElementInterface $element): object + { + if ($this->attachment !== null) { + return $this->attachment; + } + + return clone $this->value; + } + + #[Override] + public function finalize(object|array $value): object + { + assert($value instanceof $this->value); + + /** @var T */ + return $value; + } +} diff --git a/src/Aggregate/Value/SimpleValueGenerator.php b/src/Aggregate/Value/SimpleValueGenerator.php new file mode 100644 index 0000000..aa75f76 --- /dev/null +++ b/src/Aggregate/Value/SimpleValueGenerator.php @@ -0,0 +1,41 @@ + + */ +final class SimpleValueGenerator implements ValueGeneratorInterface +{ + public function __construct( + /** + * @var T + */ + private array|object $value = [], + ) {} + + #[Override] + public function attach(mixed $entity): void + { + $this->value = $entity; + } + + #[Override] + public function generate(ElementInterface $element): object|array + { + return $this->value; + } + + #[Override] + public function finalize(object|array $value): object|array + { + /** @var T */ + return $value; + } +} diff --git a/src/Aggregate/Value/ValueGenerator.php b/src/Aggregate/Value/ValueGenerator.php index 1afedcb..439dccd 100644 --- a/src/Aggregate/Value/ValueGenerator.php +++ b/src/Aggregate/Value/ValueGenerator.php @@ -4,6 +4,7 @@ use Bdf\Form\ElementInterface; use Override; +use ReflectionClass; use function class_exists; use function is_callable; @@ -20,59 +21,70 @@ * (new ValueGenerator(function (FormInterface $form) { return new MyEntity(...); }))->generate($form); // Custom generator * * - * @template T + * @template T as array|object * @implements ValueGeneratorInterface + * + * @psalm-suppress InvalidDocblock */ final class ValueGenerator implements ValueGeneratorInterface { /** - * @var callable():T|T|class-string - */ - private mixed $value; - - /** - * @var callable():T|T|class-string|null + * @var ValueGeneratorInterface */ - private mixed $attachment = null; + private ValueGeneratorInterface $generator; /** * ValueGenerator constructor. * - * @param callable():T|T|class-string $value + * @param callable(ElementInterface):T|T|class-string $value */ public function __construct(mixed $value = []) { - /** @psalm-suppress PropertyTypeCoercion */ - $this->value = $value; + $this->generator = self::fromValue($value, true); } #[Override] public function attach(mixed $entity): void { - /** @psalm-suppress PropertyTypeCoercion */ - $this->attachment = $entity; + $this->generator = self::fromValue($entity, false); } #[Override] - public function generate(ElementInterface $element): mixed + public function generate(ElementInterface $element): array|object { - $value = $this->attachment ?? $this->value; + return $this->generator->generate($element); + } + #[Override] + public function finalize(object|array $value): object|array + { + return $this->generator->finalize($value); + } + + /** + * @param callable(ElementInterface):U|U|class-string $value + * @return ValueGeneratorInterface + * @template U as array|object + */ + private static function fromValue(mixed $value, bool $cloneObjectValue): ValueGeneratorInterface + { if (is_string($value) && class_exists($value)) { - /** @var T */ - return new $value; + $calUseDefaultConstructor = (new ReflectionClass($value)->getConstructor()?->getNumberOfRequiredParameters() ?? 0) === 0; + + return $calUseDefaultConstructor + ? new DefaultConstructorValueGenerator($value) + : new ConstructorValueGenerator($value) + ; } if (is_callable($value)) { - return ($value)($element); + return new ClosureValueGenerator($value(...)); } - // Only clone value if it's not attached - if ($this->attachment === null && is_object($value)) { - return clone $value; + if (is_object($value) && $cloneObjectValue) { + return new ObjectValueGenerator($value); } - /** @var T */ - return $value; + return new SimpleValueGenerator($value); } } diff --git a/src/Aggregate/Value/ValueGeneratorInterface.php b/src/Aggregate/Value/ValueGeneratorInterface.php index f436d2a..bcc9eca 100644 --- a/src/Aggregate/Value/ValueGeneratorInterface.php +++ b/src/Aggregate/Value/ValueGeneratorInterface.php @@ -10,7 +10,7 @@ * * @see ElementInterface::value() * - * @template T + * @template T as object|array */ interface ValueGeneratorInterface { @@ -20,7 +20,7 @@ interface ValueGeneratorInterface * * If the attached value is an object, generate() should return this object * - * @param T|callable():T|class-string $entity + * @param T $entity * @see FormInterface::attach() */ public function attach(mixed $entity): void; @@ -29,10 +29,23 @@ public function attach(mixed $entity): void; * Generate the value * This method should be stateless : calling this method multiple times with same argument should return the same value * + * If the value needs finalization, this method may return a temporary value used as builder (for example, an array of constructor arguments), + * and the finalization will be done by the form after all generators are called. + * + * The returned value must be compatible with hydrators to allows filling the properties, and must be mutable + * * @param ElementInterface $element The source element * - * @return T + * @return object|array * @see FormInterface::value() */ - public function generate(ElementInterface $element): mixed; + public function generate(ElementInterface $element): object|array; + + /** + * Finalize the value generation + * + * @param object|array $value The value generated using {@see ValueGeneratorInterface::generate()} and filled by hydrators + * @return T + */ + public function finalize(object|array $value): object|array; } diff --git a/tests/Aggregate/FormTest.php b/tests/Aggregate/FormTest.php index b45835b..416ffcd 100644 --- a/tests/Aggregate/FormTest.php +++ b/tests/Aggregate/FormTest.php @@ -1129,6 +1129,20 @@ public function test_optional_patch() $this->assertFalse($this->form->patch(0)->valid()); $this->assertFalse($this->form->patch(false)->valid()); } + + public function test_value_using_dto_constructor() + { + $this->form->attach(PersonImmutable::class); + + $this->form->submit([ + 'firstName' => 'John', + 'lastName' => 'Doe', + 'id' => 4, + ]); + + $this->assertTrue($this->form->valid()); + $this->assertEquals(new PersonImmutable(4, 'John', 'Doe'), $this->form->value()); + } } class Person @@ -1137,3 +1151,12 @@ class Person public $firstName; public $lastName; } + +readonly class PersonImmutable +{ + public function __construct( + public int $id, + public string $firstName, + public string $lastName, + ) {} +} diff --git a/tests/Aggregate/Value/ClosureValueGeneratorTest.php b/tests/Aggregate/Value/ClosureValueGeneratorTest.php new file mode 100644 index 0000000..523241d --- /dev/null +++ b/tests/Aggregate/Value/ClosureValueGeneratorTest.php @@ -0,0 +1,35 @@ + ['foo' => 'bar']); + + $value = $generator->generate($this->createMock(ElementInterface::class)); + $this->assertSame(['foo' => 'bar'], $value); + $this->assertSame($value, $generator->finalize($value)); + } + + #[Test] + public function generate_with_attachment() + { + $generator = new ClosureValueGenerator(fn (ElementInterface $e) => ['foo' => 'bar']); + $attachment = new stdClass(); + $generator->attach($attachment); + + $value = $generator->generate($this->createMock(ElementInterface::class)); + $this->assertSame($attachment, $value); + $this->assertSame($value, $generator->finalize($value)); + } +} diff --git a/tests/Aggregate/Value/ConstructorValueGeneratorTest.php b/tests/Aggregate/Value/ConstructorValueGeneratorTest.php new file mode 100644 index 0000000..5bb99ce --- /dev/null +++ b/tests/Aggregate/Value/ConstructorValueGeneratorTest.php @@ -0,0 +1,67 @@ +generate($this->createMock(ElementInterface::class)); + + $this->assertSame([], $value); + $this->assertEquals(new MyDto('bar'), $generator->finalize(['foo' => 'bar'])); + } + + #[Test] + public function generateWithAttachmentObject() + { + $generator = new ConstructorValueGenerator(MyDto::class); + $attachment = new MyDto('bar'); + + $generator->attach($attachment); + + $value = $generator->generate($this->createMock(ElementInterface::class)); + + $this->assertSame($attachment, $value); + $this->assertSame($value, $generator->finalize($value)); + } + + #[Test] + public function generateWithAttachmentArray() + { + $generator = new ConstructorValueGenerator(MyDto::class); + $attachment = ['foo' => 'bar']; + + $generator->attach($attachment); + + $value = $generator->generate($this->createMock(ElementInterface::class)); + + $this->assertSame($attachment, $value); + $this->assertEquals(new MyDto('bar'), $generator->finalize($value)); + } + + #[ + Test, + TestWith([MyDto::class]), + TestWith([new stdClass()]), + ] + public function attachOnyAllowSameType($value) + { + $this->expectException(InvalidArgumentException::class); + + $generator = new ConstructorValueGenerator(MyDto::class); + $generator->attach($value); + } +} diff --git a/tests/Aggregate/Value/DefaultConstructorValueGeneratorTest.php b/tests/Aggregate/Value/DefaultConstructorValueGeneratorTest.php new file mode 100644 index 0000000..a3c2ec7 --- /dev/null +++ b/tests/Aggregate/Value/DefaultConstructorValueGeneratorTest.php @@ -0,0 +1,55 @@ +generate($this->createMock(ElementInterface::class)); + + $this->assertInstanceOf(MyEntity::class, $value); + $this->assertSame($value, $generator->finalize($value)); + } + + #[Test] + public function generateWithAttachment() + { + $generator = new DefaultConstructorValueGenerator(MyEntity::class); + $attachment = new MyEntity(); + $attachment->foo = 'bar'; + + $generator->attach($attachment); + + $value = $generator->generate($this->createMock(ElementInterface::class)); + + $this->assertSame($attachment, $value); + $this->assertSame($value, $generator->finalize($value)); + } + + #[ + Test, + TestWith([MyEntity::class]), + TestWith([new stdClass()]), + TestWith([[]]), + ] + public function attachOnyAllowSameType($value) + { + $this->expectException(InvalidArgumentException::class); + + $generator = new DefaultConstructorValueGenerator(MyEntity::class); + $generator->attach($value); + } +} diff --git a/tests/Aggregate/Value/Fixtures/MyDto.php b/tests/Aggregate/Value/Fixtures/MyDto.php new file mode 100644 index 0000000..4db7c7c --- /dev/null +++ b/tests/Aggregate/Value/Fixtures/MyDto.php @@ -0,0 +1,12 @@ +foo = 'bar'; + + $value = $generator->generate($this->createMock(ElementInterface::class)); + + $this->assertEquals($o, $value); + $this->assertNotSame($o, $value); + $this->assertSame($value, $generator->finalize($value)); + } + + #[Test] + public function generateWithAttachmentObject() + { + $generator = new ObjectValueGenerator(new MyEntity()); + $attachment = new MyEntity(); + $attachment->foo = 'bar'; + + $generator->attach($attachment); + + $value = $generator->generate($this->createMock(ElementInterface::class)); + + $this->assertSame($attachment, $value); + $this->assertSame($value, $generator->finalize($value)); + } + + #[ + Test, + TestWith([MyDto::class]), + TestWith([[]]), + TestWith([new stdClass()]), + ] + public function attachOnyAllowSameType($value) + { + $this->expectException(InvalidArgumentException::class); + + $generator = new ObjectValueGenerator(new MyEntity()); + $generator->attach($value); + } +} diff --git a/tests/Aggregate/Value/SimpleValueGeneratorTest.php b/tests/Aggregate/Value/SimpleValueGeneratorTest.php new file mode 100644 index 0000000..dc4723a --- /dev/null +++ b/tests/Aggregate/Value/SimpleValueGeneratorTest.php @@ -0,0 +1,51 @@ +foo = 'bar'; + + $value = $generator->generate($this->createMock(ElementInterface::class)); + + $this->assertSame($o, $value); + $this->assertSame($value, $generator->finalize($value)); + } + + #[Test] + public function generateWithoutAttachmentArray() + { + $generator = new SimpleValueGenerator(['foo' => 'bar']); + + $value = $generator->generate($this->createMock(ElementInterface::class)); + + $this->assertSame(['foo' => 'bar'], $value); + $this->assertSame($value, $generator->finalize($value)); + } + + #[Test] + public function generateWithAttachmentObject() + { + $generator = new SimpleValueGenerator(new MyEntity()); + $attachment = new MyEntity(); + $attachment->foo = 'bar'; + + $generator->attach($attachment); + + $value = $generator->generate($this->createMock(ElementInterface::class)); + + $this->assertSame($attachment, $value); + $this->assertSame($value, $generator->finalize($value)); + } +} diff --git a/tests/Aggregate/Value/ValueGeneratorTest.php b/tests/Aggregate/Value/ValueGeneratorTest.php index a77783d..0cc8411 100644 --- a/tests/Aggregate/Value/ValueGeneratorTest.php +++ b/tests/Aggregate/Value/ValueGeneratorTest.php @@ -3,11 +3,13 @@ namespace Bdf\Form\Aggregate\Value; use Bdf\Form\ElementInterface; +use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; use PHPUnit\Framework\TestCase; /** * Class ValueGeneratorTest */ +#[AllowMockObjectsWithoutExpectations] class ValueGeneratorTest extends TestCase { /** @@ -16,14 +18,19 @@ class ValueGeneratorTest extends TestCase public function test_generate_default() { $this->assertSame([], (new ValueGenerator())->generate($this->createMock(ElementInterface::class))); + $this->assertSame([], (new ValueGenerator())->finalize([])); } /** * */ - public function test_generate_with_className() + public function test_generate_with_className_no_constructor() { - $this->assertInstanceOf(MyEntity::class, (new ValueGenerator(MyEntity::class))->generate($this->createMock(ElementInterface::class))); + $generator = new ValueGenerator(Fixtures\MyEntity::class); + $value = $generator->generate($this->createMock(ElementInterface::class)); + + $this->assertInstanceOf(Fixtures\MyEntity::class, $value); + $this->assertSame($value, $generator->finalize($value)); } /** @@ -31,13 +38,16 @@ public function test_generate_with_className() */ public function test_generate_with_entity() { - $entity = new MyEntity(); + $entity = new Fixtures\MyEntity(); $entity->foo = 'bar'; - $generated = (new ValueGenerator($entity))->generate($this->createMock(ElementInterface::class)); + $generator = (new ValueGenerator($entity)); + + $generated = $generator->generate($this->createMock(ElementInterface::class)); $this->assertEquals($entity, $generated); $this->assertNotSame($entity, $generated); + $this->assertSame($generated, $generator->finalize($generated)); } /** @@ -45,19 +55,21 @@ public function test_generate_with_entity() */ public function test_generate_with_callback() { - $entity = new MyEntity(); + $entity = new Fixtures\MyEntity(); $entity->foo = 'bar'; - - $element = $this->createMock(ElementInterface::class); - - $generated = (new ValueGenerator(function ($element) use(&$param) { + $generator = new ValueGenerator(function ($element) use(&$param) { $param = $element; return ['foo' => 'bar']; - }))->generate($element); + }); + + $element = $this->createMock(ElementInterface::class); + + $generated = $generator->generate($element); $this->assertEquals(['foo' => 'bar'], $generated); $this->assertSame($element, $param); + $this->assertSame($generated, $generator->finalize($generated)); } /** @@ -65,7 +77,7 @@ public function test_generate_with_callback() */ public function test_attach_entity_should_return_the_same_instance() { - $entity = new MyEntity(); + $entity = new Fixtures\MyEntity(); $entity->foo = 'bar'; $element = $this->createMock(ElementInterface::class); @@ -75,6 +87,7 @@ public function test_attach_entity_should_return_the_same_instance() $generated = $generator->generate($element); $this->assertSame($entity, $generated); + $this->assertSame($generated, $generator->finalize($generated)); } /** @@ -82,7 +95,7 @@ public function test_attach_entity_should_return_the_same_instance() */ public function test_attach_with_callback() { - $entity = new MyEntity(); + $entity = new Fixtures\MyEntity(); $entity->foo = 'bar'; $element = $this->createMock(ElementInterface::class); @@ -97,11 +110,17 @@ public function test_attach_with_callback() $this->assertEquals(['foo' => 'bar'], $generated); $this->assertSame($element, $param); + $this->assertSame($generated, $generator->finalize($generated)); } -} + public function test_with_dto_constructor() + { + $element = $this->createMock(ElementInterface::class); + $generator = new ValueGenerator(); -class MyEntity -{ - public $foo; + $generator->attach(Fixtures\MyDto::class); + + $this->assertSame([], $generator->generate($element)); + $this->assertEquals(new Fixtures\MyDto('bar'), $generator->finalize(['foo' => 'bar'])); + } } diff --git a/tests/Attribute/Form/GeneratesTest.php b/tests/Attribute/Form/GeneratesTest.php index a731fa9..af818cd 100644 --- a/tests/Attribute/Form/GeneratesTest.php +++ b/tests/Attribute/Form/GeneratesTest.php @@ -3,7 +3,7 @@ namespace Tests\Form\Attribute\Form; use Bdf\Form\Aggregate\FormBuilderInterface; -use Bdf\Form\Aggregate\Value\MyEntity; +use Bdf\Form\Aggregate\Value\Fixtures\MyEntity; use Bdf\Form\Attribute\AttributeForm; use Bdf\Form\Attribute\Form\Generates; use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; diff --git a/tests/Attribute/Processor/GenerateConfiguratorStrategyTest.php b/tests/Attribute/Processor/GenerateConfiguratorStrategyTest.php index 7ad7fc7..c87146f 100644 --- a/tests/Attribute/Processor/GenerateConfiguratorStrategyTest.php +++ b/tests/Attribute/Processor/GenerateConfiguratorStrategyTest.php @@ -3,7 +3,7 @@ namespace Tests\Form\Attribute\Processor; use Bdf\Form\Aggregate\FormBuilder; -use Bdf\Form\Aggregate\Value\MyEntity; +use Bdf\Form\Aggregate\Value\Fixtures\MyEntity; use Bdf\Form\Attribute\AttributeForm; use Bdf\Form\Attribute\Button\Groups; use Bdf\Form\Attribute\Button\Value; @@ -35,7 +35,7 @@ public function test_form_attributes() use Bdf\Form\Aggregate\FormBuilderInterface; use Bdf\Form\Aggregate\FormInterface; -use Bdf\Form\Aggregate\Value\MyEntity; +use Bdf\Form\Aggregate\Value\Fixtures\MyEntity; use Bdf\Form\Attribute\AttributeForm; use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; use Bdf\Form\Attribute\Processor\PostConfigureInterface; From 115c19a64a7b9e3e98dfaca13a0add94fb0fc7ad Mon Sep 17 00:00:00 2001 From: Vincent QUATREVIEUX Date: Wed, 15 Apr 2026 16:35:07 +0200 Subject: [PATCH 2/2] fix: psalm error on ValueGenerator --- src/Aggregate/Value/ValueGenerator.php | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Aggregate/Value/ValueGenerator.php b/src/Aggregate/Value/ValueGenerator.php index 439dccd..d2afc7c 100644 --- a/src/Aggregate/Value/ValueGenerator.php +++ b/src/Aggregate/Value/ValueGenerator.php @@ -36,7 +36,7 @@ final class ValueGenerator implements ValueGeneratorInterface /** * ValueGenerator constructor. * - * @param callable(ElementInterface):T|T|class-string $value + * @param callable(ElementInterface):T|T|class-string $value */ public function __construct(mixed $value = []) { @@ -62,9 +62,12 @@ public function finalize(object|array $value): object|array } /** - * @param callable(ElementInterface):U|U|class-string $value + * @param callable(ElementInterface):U|U|class-string $value * @return ValueGeneratorInterface * @template U as array|object + * + * @psalm-suppress InvalidReturnStatement + * @psalm-suppress InvalidReturnType */ private static function fromValue(mixed $value, bool $cloneObjectValue): ValueGeneratorInterface { @@ -78,6 +81,7 @@ private static function fromValue(mixed $value, bool $cloneObjectValue): ValueGe } if (is_callable($value)) { + /** @psalm-suppress PossiblyInvalidFunctionCall */ return new ClosureValueGenerator($value(...)); } @@ -85,6 +89,7 @@ private static function fromValue(mixed $value, bool $cloneObjectValue): ValueGe return new ObjectValueGenerator($value); } + /** @psalm-suppress PossiblyInvalidArgument */ return new SimpleValueGenerator($value); } }