From 353504a0cc22938248d68a9b7c5210448bdcc725 Mon Sep 17 00:00:00 2001 From: Benoit Galati Date: Fri, 21 Nov 2025 14:52:10 +0100 Subject: [PATCH 1/6] feat: support for custom message generation in `ZddMessageConfigInterface` --- README.md | 17 +++++++++++++ src/Config/ZddMessageConfigInterface.php | 14 +++++++++++ src/Factory/ZddMessageFactory.php | 13 +++++++--- .../App/Messages/Config/MessageConfig.php | 14 +++++++++++ .../App/Messages/DummyCustomMessage.php | 18 +++++++++++++ tests/Func/GenerateZddMessageCommandTest.php | 1 + tests/Func/ListZddMessageCommandTest.php | 1 + tests/Unit/ZddMessageFactoryTest.php | 25 ++++++++++++++++++- 8 files changed, 99 insertions(+), 4 deletions(-) create mode 100644 tests/Fixtures/App/Messages/DummyCustomMessage.php diff --git a/README.md b/README.md index 5c45173..735de6a 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ class MessageConfig implements ZddMessageConfigInterface /** * Return the list of messages to assert. */ + #[\Override] public function getMessageToAssert(): array { return [ @@ -41,6 +42,7 @@ class MessageConfig implements ZddMessageConfigInterface * If your message contains no scalar value as parameter such like value enums, value object more complex object, * you should use this method to return value for each type hint. */ + #[\Override] public function generateValueForCustomPropertyType(string $type): mixed { return match ($type) { @@ -49,6 +51,21 @@ class MessageConfig implements ZddMessageConfigInterface default => null, }; } + + /** + * If you need full control over how a specific message instance is created, + * use this method to return a fully instantiated message object. + * This is useful when the default instantiation (using reflection and property injection) + * is not sufficient or when your message requires specific constructor logic. + */ + #[\Override] + public function generateCustomMessage(string $className): ?object + { + return match ($className) { + App\Message\ComplexMessage::class => new App\Message\ComplexMessage('custom data'), + default => null, + }; + } } ``` diff --git a/src/Config/ZddMessageConfigInterface.php b/src/Config/ZddMessageConfigInterface.php index 4e70ae1..73112f5 100644 --- a/src/Config/ZddMessageConfigInterface.php +++ b/src/Config/ZddMessageConfigInterface.php @@ -51,4 +51,18 @@ public function getMessageToAssert(): array; * @see MessageConfig in ZddMessageFakerTest.php for a concret examples */ public function generateValueForCustomPropertyType(string $type): mixed; + + /** + * If you need full control over how a specific message instance is created, + * use this method to return a fully instantiated message object. + * This is useful when the default instantiation (using reflection and property injection) + * is not sufficient or when your message requires specific constructor logic. + * + * @template T of object + * + * @param class-string $className + * + * @return ?T The fully instantiated message object, or null if the default instantiation should be used + */ + public function generateCustomMessage(string $className): ?object; } diff --git a/src/Factory/ZddMessageFactory.php b/src/Factory/ZddMessageFactory.php index a2b6adb..3253b5b 100644 --- a/src/Factory/ZddMessageFactory.php +++ b/src/Factory/ZddMessageFactory.php @@ -12,10 +12,12 @@ */ final class ZddMessageFactory { + private ZddMessageConfigInterface $config; private ZddPropertyExtractor $propertyExtractor; public function __construct(ZddMessageConfigInterface $config, private readonly SerializerInterface $serializer) { + $this->config = $config; $this->propertyExtractor = new ZddPropertyExtractor($config); } @@ -26,10 +28,15 @@ public function create(string $className): ZddMessage { try { $propertyList = $this->propertyExtractor->extractPropertiesFromClass($className); - $message = (new \ReflectionClass($className))->newInstanceWithoutConstructor(); - foreach ($propertyList->getProperties() as $property) { - $this->forcePropertyValue($message, $property->name, $property->value); + + $message = $this->config->generateCustomMessage($className); + if (null === $message) { + $message = (new \ReflectionClass($className))->newInstanceWithoutConstructor(); + foreach ($propertyList->getProperties() as $property) { + $this->forcePropertyValue($message, $property->name, $property->value); + } } + $serializedMessage = $this->serializer->serialize($message); } catch (\Throwable $e) { throw new \LogicException('Unable to create ZddMessage for class "'.$className.'"', previous: $e); diff --git a/tests/Fixtures/App/Messages/Config/MessageConfig.php b/tests/Fixtures/App/Messages/Config/MessageConfig.php index a09f596..1240bee 100644 --- a/tests/Fixtures/App/Messages/Config/MessageConfig.php +++ b/tests/Fixtures/App/Messages/Config/MessageConfig.php @@ -3,6 +3,7 @@ namespace Yousign\ZddMessageBundle\Tests\Fixtures\App\Messages\Config; use Yousign\ZddMessageBundle\Config\ZddMessageConfigInterface; +use Yousign\ZddMessageBundle\Tests\Fixtures\App\Messages\DummyCustomMessage; use Yousign\ZddMessageBundle\Tests\Fixtures\App\Messages\DummyMessage; use Yousign\ZddMessageBundle\Tests\Fixtures\App\Messages\DummyMessageWithAllManagedTypes; use Yousign\ZddMessageBundle\Tests\Fixtures\App\Messages\DummyMessageWithNullableNumberProperty; @@ -15,6 +16,7 @@ class MessageConfig implements ZddMessageConfigInterface { public static array $messagesToAssert = []; + #[\Override] public function getMessageToAssert(): array { return [] !== self::$messagesToAssert ? self::$messagesToAssert : [ @@ -23,9 +25,11 @@ public function getMessageToAssert(): array DummyMessageWithPrivateConstructor::class, DummyMessageWithAllManagedTypes::class, Other\DummyMessage::class, + DummyCustomMessage::class, ]; } + #[\Override] public function generateValueForCustomPropertyType(string $type): mixed { return match ($type) { @@ -40,4 +44,14 @@ public static function reset(): void { self::$messagesToAssert = []; } + + #[\Override] + public function generateCustomMessage(string $className): ?object + { + if (DummyCustomMessage::class === $className) { + return new DummyCustomMessage('custom message data'); + } + + return null; + } } diff --git a/tests/Fixtures/App/Messages/DummyCustomMessage.php b/tests/Fixtures/App/Messages/DummyCustomMessage.php new file mode 100644 index 0000000..b614e4f --- /dev/null +++ b/tests/Fixtures/App/Messages/DummyCustomMessage.php @@ -0,0 +1,18 @@ +content = $content; + } + + public function getContent(): string + { + return $this->content; + } +} diff --git a/tests/Func/GenerateZddMessageCommandTest.php b/tests/Func/GenerateZddMessageCommandTest.php index 4961145..c78a5a1 100644 --- a/tests/Func/GenerateZddMessageCommandTest.php +++ b/tests/Func/GenerateZddMessageCommandTest.php @@ -50,6 +50,7 @@ public function testThatCommandIsSuccessful(): void 3 Yousign\ZddMessageBundle\Tests\Fixtures\App\Messages\DummyMessageWithPrivateConstructor 4 Yousign\ZddMessageBundle\Tests\Fixtures\App\Messages\DummyMessageWithAllManagedTypes 5 Yousign\ZddMessageBundle\Tests\Fixtures\App\Messages\Other\DummyMessage + 6 Yousign\ZddMessageBundle\Tests\Fixtures\App\Messages\DummyCustomMessage --- --------------------------------------------------------------------------------------------- EOF; diff --git a/tests/Func/ListZddMessageCommandTest.php b/tests/Func/ListZddMessageCommandTest.php index 3670f7a..a2681e4 100644 --- a/tests/Func/ListZddMessageCommandTest.php +++ b/tests/Func/ListZddMessageCommandTest.php @@ -31,6 +31,7 @@ public function testCommandIsSuccess(): void 3 Yousign\ZddMessageBundle\Tests\Fixtures\App\Messages\DummyMessageWithPrivateConstructor 4 Yousign\ZddMessageBundle\Tests\Fixtures\App\Messages\DummyMessageWithAllManagedTypes 5 Yousign\ZddMessageBundle\Tests\Fixtures\App\Messages\Other\DummyMessage + 6 Yousign\ZddMessageBundle\Tests\Fixtures\App\Messages\DummyCustomMessage --- --------------------------------------------------------------------------------------------- EOF; diff --git a/tests/Unit/ZddMessageFactoryTest.php b/tests/Unit/ZddMessageFactoryTest.php index cb18815..9364fef 100644 --- a/tests/Unit/ZddMessageFactoryTest.php +++ b/tests/Unit/ZddMessageFactoryTest.php @@ -8,6 +8,7 @@ use Yousign\ZddMessageBundle\Exceptions\MissingValueForTypeException; use Yousign\ZddMessageBundle\Factory\ZddMessageFactory; use Yousign\ZddMessageBundle\Tests\Fixtures\App\Messages\Config\MessageConfig; +use Yousign\ZddMessageBundle\Tests\Fixtures\App\Messages\DummyCustomMessage; use Yousign\ZddMessageBundle\Tests\Fixtures\App\Messages\DummyMessageWithAllManagedTypes; use Yousign\ZddMessageBundle\Tests\Fixtures\App\Messages\DummyMessageWithInheritedReadonlyProperty; use Yousign\ZddMessageBundle\Tests\Fixtures\App\Messages\DummyMessageWithNullableNumberProperty; @@ -19,13 +20,18 @@ class ZddMessageFactoryTest extends TestCase { use SerializerTrait; - private readonly ZddMessageFactory $zddMessageFactory; + private ZddMessageFactory $zddMessageFactory; public function setUp(): void { $this->zddMessageFactory = new ZddMessageFactory(new MessageConfig(), $this->getSerializer()); } + protected function tearDown(): void + { + unset($this->zddMessageFactory); + } + public function testItGeneratesSerializedMessageWithNullAndNotNullableProperties(): void { $zddMessage = $this->zddMessageFactory->create(DummyMessageWithNullableNumberProperty::class); @@ -112,6 +118,15 @@ public function testItThrownAMissingValueForTypeException(): void ); $factory->create(WithoutValue::class); } + + public function testGenerateCustomMessage(): void + { + $zddMessage = $this->zddMessageFactory->create(DummyCustomMessage::class); + self::assertEquals(1, $zddMessage->propertyList()->count()); + self::assertSame('string', $zddMessage->propertyList()->get('content')->type); + // See the expected message in Fixtures\App\Messages\Config\MessageConfig::generateCustomMessage(...) + self::assertEquals(new DummyCustomMessage('custom message data'), $zddMessage->message()); + } } namespace App\WithoutValue; @@ -128,6 +143,7 @@ public function __construct(private DummyMessage $dummyMessage) final class WithoutValueConfig implements ZddMessageConfigInterface { + #[\Override] public function getMessageToAssert(): array { return [ @@ -135,8 +151,15 @@ public function getMessageToAssert(): array ]; } + #[\Override] public function generateValueForCustomPropertyType(string $type): mixed { return null; } + + #[\Override] + public function generateCustomMessage(string $className): ?object + { + return null; + } } From e1408e420418ba6eff5c78a49ae1188d7348b697 Mon Sep 17 00:00:00 2001 From: Benoit Galati Date: Fri, 21 Nov 2025 22:15:06 +0100 Subject: [PATCH 2/6] refactor: extract `MessageGenerator` from `ZddMessageFactory` This refacto will clearly split responsibilities between these 2 concerns. Moreover, it will enable the introduction of the custom message feature to handle edge cases. --- src/Factory/MessageGenerator.php | 76 ++++++++++++++++++++ src/Factory/Property.php | 2 +- src/Factory/PropertyList.php | 13 ++-- src/Factory/ZddMessageFactory.php | 28 ++------ src/Factory/ZddPropertyExtractor.php | 37 ++-------- tests/Func/ValidateZddMessageCommandTest.php | 6 +- tests/Unit/ZddMessageAsserterTest.php | 18 +++-- tests/Unit/ZddMessageFactoryTest.php | 4 -- 8 files changed, 114 insertions(+), 70 deletions(-) create mode 100644 src/Factory/MessageGenerator.php diff --git a/src/Factory/MessageGenerator.php b/src/Factory/MessageGenerator.php new file mode 100644 index 0000000..4607414 --- /dev/null +++ b/src/Factory/MessageGenerator.php @@ -0,0 +1,76 @@ +propertyExtractor = new ZddPropertyExtractor(); + } + + /** + * @param class-string $className + */ + public function generate(string $className): object + { + $message = $this->config->generateCustomMessage($className); + if (null !== $message) { + return $message; + } + + $message = (new \ReflectionClass($className))->newInstanceWithoutConstructor(); + $propertyList = $this->propertyExtractor->extractPropertiesFromClass($className); + + foreach ($propertyList->getProperties() as $property) { + $value = $property->isNullable ? null : $this->generateValueForProperty($property); + $this->forcePropertyValue($message, $property->name, $value); + } + + return $message; + } + + /** + * @throws MissingValueForTypeException + */ + private function generateValueForProperty(Property $property): mixed + { + $value = $this->config->generateValueForCustomPropertyType($property->type); + if (null !== $value) { + return $value; + } + + return match ($property->type) { + 'string' => 'Hello World!', + 'int' => 42, + 'float' => 42.42, + 'bool' => true, + 'array' => ['PHP', 'For The Win'], + default => throw MissingValueForTypeException::missingValue($property->type, $this->config), + }; + } + + private function forcePropertyValue(object $object, string $property, mixed $value): void + { + $reflectionClass = new \ReflectionClass($object); + $reflectionProperty = $reflectionClass->getProperty($property); + + // Readonly properties can only be set on the declaring class in case of inheritance + if ($reflectionProperty->isReadOnly()) { + $reflectionProperty = $reflectionProperty->getDeclaringClass()->getProperty($property); + } + + $reflectionProperty->setAccessible(true); + $reflectionProperty->setValue($object, $value); + } +} diff --git a/src/Factory/Property.php b/src/Factory/Property.php index 0ecd324..c477961 100644 --- a/src/Factory/Property.php +++ b/src/Factory/Property.php @@ -4,7 +4,7 @@ final class Property { - public function __construct(public readonly string $name, public readonly string $type, public readonly mixed $value) + public function __construct(public readonly string $name, public readonly string $type, public readonly bool $isNullable) { } } diff --git a/src/Factory/PropertyList.php b/src/Factory/PropertyList.php index 10d7e23..22d8cb6 100644 --- a/src/Factory/PropertyList.php +++ b/src/Factory/PropertyList.php @@ -29,17 +29,21 @@ public function addProperty(Property $property): void public static function fromJson(string $data): self { - /** @var array> $decodedProperties */ + /** @var array< + * array{name?: string, type?: string, isNullable?: bool} + * > $decodedProperties + */ $decodedProperties = \json_decode($data, true); $properties = []; foreach ($decodedProperties as $decodedProperty) { $name = $decodedProperty['name'] ?? null; $type = $decodedProperty['type'] ?? null; + $isNullable = $decodedProperty['isNullable'] ?? null; - if (null === $name || null === $type) { - throw new \LogicException(sprintf('Missing keys name and/or type in decoded properties from data: "%s"', $data)); + if (null === $name || null === $type || null === $isNullable) { + throw new \LogicException(sprintf('Missing keys name and/or type and/or isNullable in decoded properties from data: "%s"', $data)); } - $properties[] = new Property($decodedProperty['name'], $decodedProperty['type'], null); + $properties[] = new Property($name, $type, $isNullable); } return new self($properties); @@ -94,6 +98,7 @@ public function toJson(): string $data[] = [ 'name' => $property->name, 'type' => $property->type, + 'isNullable' => $property->isNullable, ]; } diff --git a/src/Factory/ZddMessageFactory.php b/src/Factory/ZddMessageFactory.php index 3253b5b..905a317 100644 --- a/src/Factory/ZddMessageFactory.php +++ b/src/Factory/ZddMessageFactory.php @@ -12,13 +12,13 @@ */ final class ZddMessageFactory { - private ZddMessageConfigInterface $config; private ZddPropertyExtractor $propertyExtractor; + private MessageGenerator $messageGenerator; public function __construct(ZddMessageConfigInterface $config, private readonly SerializerInterface $serializer) { - $this->config = $config; - $this->propertyExtractor = new ZddPropertyExtractor($config); + $this->propertyExtractor = new ZddPropertyExtractor(); + $this->messageGenerator = new MessageGenerator($config); } /** @@ -29,13 +29,7 @@ public function create(string $className): ZddMessage try { $propertyList = $this->propertyExtractor->extractPropertiesFromClass($className); - $message = $this->config->generateCustomMessage($className); - if (null === $message) { - $message = (new \ReflectionClass($className))->newInstanceWithoutConstructor(); - foreach ($propertyList->getProperties() as $property) { - $this->forcePropertyValue($message, $property->name, $property->value); - } - } + $message = $this->messageGenerator->generate($className); $serializedMessage = $this->serializer->serialize($message); } catch (\Throwable $e) { @@ -44,18 +38,4 @@ public function create(string $className): ZddMessage return new ZddMessage($className, $serializedMessage, $propertyList, $message); } - - private function forcePropertyValue(object $object, string $property, mixed $value): void - { - $reflectionClass = new \ReflectionClass($object); - $reflectionProperty = $reflectionClass->getProperty($property); - - // Readonly properties can only be set on the declaring class in case of inheritance - if ($reflectionProperty->isReadOnly()) { - $reflectionProperty = $reflectionProperty->getDeclaringClass()->getProperty($property); - } - - $reflectionProperty->setAccessible(true); - $reflectionProperty->setValue($object, $value); - } } diff --git a/src/Factory/ZddPropertyExtractor.php b/src/Factory/ZddPropertyExtractor.php index 38d1f5b..e3f22cc 100644 --- a/src/Factory/ZddPropertyExtractor.php +++ b/src/Factory/ZddPropertyExtractor.php @@ -2,24 +2,17 @@ namespace Yousign\ZddMessageBundle\Factory; -use Yousign\ZddMessageBundle\Config\ZddMessageConfigInterface; use Yousign\ZddMessageBundle\Exceptions\InvalidTypeException; -use Yousign\ZddMessageBundle\Exceptions\MissingValueForTypeException; /** * @internal */ final class ZddPropertyExtractor { - public function __construct(private readonly ZddMessageConfigInterface $config) - { - } - /** * @param class-string $className * * @throws InvalidTypeException - * @throws MissingValueForTypeException * @throws \ReflectionException */ public function extractPropertiesFromClass(string $className): PropertyList @@ -40,31 +33,15 @@ public function extractPropertiesFromClass(string $className): PropertyList throw InvalidTypeException::typeNotSupported(); } - $typeHint = $propertyType->getName(); - $value = $propertyType->allowsNull() ? null : $this->generateFakeValueFromType($typeHint); - $propertyList->addProperty(new Property($propertyName, $typeHint, $value)); + $propertyList->addProperty( + new Property( + $propertyName, + $propertyType->getName(), + $propertyType->allowsNull(), + ), + ); } return $propertyList; } - - /** - * @throws MissingValueForTypeException - */ - private function generateFakeValueFromType(string $typeHint): mixed - { - $value = $this->config->generateValueForCustomPropertyType($typeHint); - if (null !== $value) { - return $value; - } - - return match ($typeHint) { - 'string' => 'Hello World!', - 'int' => 42, - 'float' => 42.42, - 'bool' => true, - 'array' => ['PHP', 'For The Win'], - default => throw MissingValueForTypeException::missingValue($typeHint, $this->config), - }; - } } diff --git a/tests/Func/ValidateZddMessageCommandTest.php b/tests/Func/ValidateZddMessageCommandTest.php index 3b1d366..5e1c044 100644 --- a/tests/Func/ValidateZddMessageCommandTest.php +++ b/tests/Func/ValidateZddMessageCommandTest.php @@ -49,7 +49,7 @@ public function testThatCommandIsSuccessful(): void { mkdir($this->serializedMessagesDir); file_put_contents($this->serializedMessagesDir.'/DummyMessage.txt', $this->getSerializer()->serialize(new DummyMessage('Hi'))); - file_put_contents($this->serializedMessagesDir.'/DummyMessage.properties.json', '[{"name":"content","type":"string"}]'); + file_put_contents($this->serializedMessagesDir.'/DummyMessage.properties.json', '[{"name":"content","type":"string","isNullable":false}]'); $this->assertSerializedFilesExist($this->serializedMessagesDir); $this->command->execute([]); @@ -92,10 +92,12 @@ public function testThatCommandFailsWhenMessageIsNotZddCompliant(): void [ 'name' => 'content', 'type' => 'string', + 'isNullable' => false, ], [ 'name' => 'number', 'type' => 'int', + 'isNullable' => false, ], ]; mkdir($this->serializedMessagesDir); @@ -126,10 +128,12 @@ public function testThatErrorTableIsShownInVerboseMode(): void [ 'name' => 'content', 'type' => 'string', + 'isNullable' => false, ], [ 'name' => 'number', 'type' => 'int', + 'isNullable' => false, ], ]; mkdir($this->serializedMessagesDir); diff --git a/tests/Unit/ZddMessageAsserterTest.php b/tests/Unit/ZddMessageAsserterTest.php index 7071a27..a7fab96 100644 --- a/tests/Unit/ZddMessageAsserterTest.php +++ b/tests/Unit/ZddMessageAsserterTest.php @@ -42,7 +42,8 @@ public function provideValidAssertion(): iterable [ { "name": "content", - "type": "string" + "type": "string", + "isNullable": false } ] JSON, @@ -55,11 +56,13 @@ public function provideValidAssertion(): iterable [ { "name": "content", - "type": "string" + "type": "string", + "isNullable": false }, { "name": "number", - "type": "int" + "type": "int", + "isNullable": false } ] JSON, @@ -86,11 +89,13 @@ private function getSerializedMessageForPreviousVersionOfDummyMessageWithNumberP [ { "name": "content", - "type": "string" + "type": "string", + "isNullable": false }, { "name": "number", - "type": "int" + "type": "int", + "isNullable": false } ] JSON; @@ -124,7 +129,8 @@ public function testItThrowsExceptionForInvalidIntegrationDueToPropertyTypeMisma [ { "name": "content", - "type": "int" + "type": "int", + "isNullable": false } ] JSON diff --git a/tests/Unit/ZddMessageFactoryTest.php b/tests/Unit/ZddMessageFactoryTest.php index 9364fef..6c0f10b 100644 --- a/tests/Unit/ZddMessageFactoryTest.php +++ b/tests/Unit/ZddMessageFactoryTest.php @@ -44,11 +44,9 @@ public function testItGeneratesSerializedMessageWithNullAndNotNullableProperties self::assertTrue($zddMessage->propertyList()->has('content')); $property = $zddMessage->propertyList()->get('content'); self::assertSame('string', $property->type); - self::assertSame('Hello World!', $property->value); $propertyNumber = $zddMessage->propertyList()->get('number'); self::assertSame('int', $propertyNumber->type); - self::assertNull($propertyNumber->value); } public function testItGeneratesSerializedMessageForDummyMessageWithInheritedReadonlyProperty(): void @@ -64,7 +62,6 @@ public function testItGeneratesSerializedMessageForDummyMessageWithInheritedRead self::assertTrue($zddMessage->propertyList()->has('content')); $property = $zddMessage->propertyList()->get('content'); self::assertSame('string', $property->type); - self::assertSame('Hello World!', $property->value); } public function testItGeneratesSerializedMessageForDummyMessageWithPrivateConstructor(): void @@ -79,7 +76,6 @@ public function testItGeneratesSerializedMessageForDummyMessageWithPrivateConstr self::assertTrue($zddMessage->propertyList()->has('content')); $property = $zddMessage->propertyList()->get('content'); self::assertSame('string', $property->type); - self::assertSame('Hello World!', $property->value); } public function testItGeneratesSerializedMessageForDummyMessageContainingAllManagedTypesWithoutError(): void From 72eaa1da61ba640ed39d0a185719dc436ca88516 Mon Sep 17 00:00:00 2001 From: Benoit Galati Date: Fri, 21 Nov 2025 14:53:48 +0100 Subject: [PATCH 3/6] docs: update README wording and fix `message_config_service` registration --- README.md | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 735de6a..4723326 100644 --- a/README.md +++ b/README.md @@ -4,17 +4,18 @@ A Symfony Bundle to use when you want to assert that messages used with Message ## Getting started ### Installation -You can easily install Zdd Message bundle by composer +First, install the bundle with composer: ``` $ composer require yousign/zdd-message-bundle ``` -Then, bundle should be registered. Just verify that `config\bundles.php` is containing : + +Then, verify that the bundle has been registered in `config/bundles.php`: ```php Yousign\ZddMessageBundle\ZddMessageBundle::class => ['all' => true], ``` ### Configuration -Once the bundle is installed, you should create a class to configure the messages to assert and how to create them: +Create a class to configure the messages to assert and how to create them: ```php ' ``` +**Custom directory for serialized messages** + +Option to specify a custom directory where serialized messages will be stored. + +```yaml +# config/packages/zdd_message.yaml +zdd_message: + # ... + serialized_messages_dir: '%kernel.project_dir%/custom/path' # Default: '%kernel.project_dir%/var/zdd-message' +``` + **Detect messages not tracked** Option to write a log message if an asynchronous message has been sent (using symfony messenger) and is not present in your configuration. From 0497be5c08fdfd1be2077b149c0e38b109708eb3 Mon Sep 17 00:00:00 2001 From: Benoit Galati Date: Fri, 19 Dec 2025 10:29:27 +0100 Subject: [PATCH 4/6] feat: extract `generateCustomMessage` into optional `CustomMessageGeneratorInterface` --- README.md | 10 ++++--- .../CustomMessageGeneratorInterface.php | 26 +++++++++++++++++++ src/Config/ZddMessageConfigInterface.php | 14 ---------- src/Factory/MessageGenerator.php | 9 ++++--- .../App/Messages/Config/MessageConfig.php | 3 ++- tests/Unit/ZddMessageFactoryTest.php | 6 ----- 6 files changed, 41 insertions(+), 27 deletions(-) create mode 100644 src/Config/CustomMessageGeneratorInterface.php diff --git a/README.md b/README.md index 4723326..9d8993c 100644 --- a/README.md +++ b/README.md @@ -22,9 +22,10 @@ Create a class to configure the messages to assert and how to create them: namespace App\Message; +use Yousign\ZddMessageBundle\Config\CustomMessageGeneratorInterface; use Yousign\ZddMessageBundle\Config\ZddMessageConfigInterface; -class MessageConfig implements ZddMessageConfigInterface +class MessageConfig implements ZddMessageConfigInterface, CustomMessageGeneratorInterface { /** * Return the list of messages to assert. @@ -54,10 +55,13 @@ class MessageConfig implements ZddMessageConfigInterface } /** - * If you need full control over how a specific message instance is created, - * use this method to return a fully instantiated message object. + * Optional: Implement CustomMessageGeneratorInterface if you need full control + * over how a specific message instance is created. * This is useful when the default instantiation (using reflection and property injection) * is not sufficient or when your message requires specific constructor logic. + * + * WARNING: The object must be instantiated with minimum requirements (i.e., nullable properties + * must be set as null) in order to ensure a good ZDD test. */ #[\Override] public function generateCustomMessage(string $className): ?object diff --git a/src/Config/CustomMessageGeneratorInterface.php b/src/Config/CustomMessageGeneratorInterface.php new file mode 100644 index 0000000..309df45 --- /dev/null +++ b/src/Config/CustomMessageGeneratorInterface.php @@ -0,0 +1,26 @@ + $className + * + * @return ?T The fully instantiated message object, or null if the default instantiation should be used + */ + public function generateCustomMessage(string $className): ?object; +} diff --git a/src/Config/ZddMessageConfigInterface.php b/src/Config/ZddMessageConfigInterface.php index 73112f5..4e70ae1 100644 --- a/src/Config/ZddMessageConfigInterface.php +++ b/src/Config/ZddMessageConfigInterface.php @@ -51,18 +51,4 @@ public function getMessageToAssert(): array; * @see MessageConfig in ZddMessageFakerTest.php for a concret examples */ public function generateValueForCustomPropertyType(string $type): mixed; - - /** - * If you need full control over how a specific message instance is created, - * use this method to return a fully instantiated message object. - * This is useful when the default instantiation (using reflection and property injection) - * is not sufficient or when your message requires specific constructor logic. - * - * @template T of object - * - * @param class-string $className - * - * @return ?T The fully instantiated message object, or null if the default instantiation should be used - */ - public function generateCustomMessage(string $className): ?object; } diff --git a/src/Factory/MessageGenerator.php b/src/Factory/MessageGenerator.php index 4607414..cbb2bbe 100644 --- a/src/Factory/MessageGenerator.php +++ b/src/Factory/MessageGenerator.php @@ -4,6 +4,7 @@ namespace Yousign\ZddMessageBundle\Factory; +use Yousign\ZddMessageBundle\Config\CustomMessageGeneratorInterface; use Yousign\ZddMessageBundle\Config\ZddMessageConfigInterface; use Yousign\ZddMessageBundle\Exceptions\MissingValueForTypeException; @@ -24,9 +25,11 @@ public function __construct(private readonly ZddMessageConfigInterface $config) */ public function generate(string $className): object { - $message = $this->config->generateCustomMessage($className); - if (null !== $message) { - return $message; + if ($this->config instanceof CustomMessageGeneratorInterface) { + $message = $this->config->generateCustomMessage($className); + if (null !== $message) { + return $message; + } } $message = (new \ReflectionClass($className))->newInstanceWithoutConstructor(); diff --git a/tests/Fixtures/App/Messages/Config/MessageConfig.php b/tests/Fixtures/App/Messages/Config/MessageConfig.php index 1240bee..a271f66 100644 --- a/tests/Fixtures/App/Messages/Config/MessageConfig.php +++ b/tests/Fixtures/App/Messages/Config/MessageConfig.php @@ -2,6 +2,7 @@ namespace Yousign\ZddMessageBundle\Tests\Fixtures\App\Messages\Config; +use Yousign\ZddMessageBundle\Config\CustomMessageGeneratorInterface; use Yousign\ZddMessageBundle\Config\ZddMessageConfigInterface; use Yousign\ZddMessageBundle\Tests\Fixtures\App\Messages\DummyCustomMessage; use Yousign\ZddMessageBundle\Tests\Fixtures\App\Messages\DummyMessage; @@ -12,7 +13,7 @@ use Yousign\ZddMessageBundle\Tests\Fixtures\App\Messages\Input\Status; use Yousign\ZddMessageBundle\Tests\Fixtures\App\Messages\Other; -class MessageConfig implements ZddMessageConfigInterface +class MessageConfig implements ZddMessageConfigInterface, CustomMessageGeneratorInterface { public static array $messagesToAssert = []; diff --git a/tests/Unit/ZddMessageFactoryTest.php b/tests/Unit/ZddMessageFactoryTest.php index 6c0f10b..bc83d0c 100644 --- a/tests/Unit/ZddMessageFactoryTest.php +++ b/tests/Unit/ZddMessageFactoryTest.php @@ -152,10 +152,4 @@ public function generateValueForCustomPropertyType(string $type): mixed { return null; } - - #[\Override] - public function generateCustomMessage(string $className): ?object - { - return null; - } } From d1a74f6af5f95bc3b69072fe7cc4c7519bd2094b Mon Sep 17 00:00:00 2001 From: Benoit Galati Date: Fri, 19 Dec 2025 10:44:52 +0100 Subject: [PATCH 5/6] doc: add phpstan target to Makefile help message --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index d43cf34..c85e2d6 100644 --- a/Makefile +++ b/Makefile @@ -7,7 +7,7 @@ phpcs: ## Run PHP CS Fixer test: ## Run code tests ./vendor/bin/phpunit --testdox -phpstan: +phpstan: ## Run PHPStan static analysis ./vendor/bin/phpstan analyse src --level=9 test-phpcs: ## Run coding standard tests From 912a5ffbd2e82096e44084fb143da2c094d6e509 Mon Sep 17 00:00:00 2001 From: Benoit Galati Date: Fri, 19 Dec 2025 10:51:59 +0100 Subject: [PATCH 6/6] chore: remove useless PHPStan ignore --- src/ZddMessageBundle.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ZddMessageBundle.php b/src/ZddMessageBundle.php index d63148b..b513966 100644 --- a/src/ZddMessageBundle.php +++ b/src/ZddMessageBundle.php @@ -21,7 +21,6 @@ final class ZddMessageBundle extends AbstractBundle { public function configure(DefinitionConfigurator $definition): void { - /* @phpstan-ignore-next-line */ $definition ->rootNode() ->children()