Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
51 changes: 42 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,30 +4,33 @@ 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
<?php

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.
*/
#[\Override]
public function getMessageToAssert(): array
{
return [
Expand All @@ -41,6 +44,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) {
Expand All @@ -49,29 +53,47 @@ class MessageConfig implements ZddMessageConfigInterface
default => null,
};
}

/**
* 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
{
return match ($className) {
App\Message\ComplexMessage::class => new App\Message\ComplexMessage('custom data'),
default => null,
};
}
}
```

When the class is created, you can register it as a service.
Then, register this class as a service.

```yaml
# config/services.yaml
App\Message\MessageConfig: ~
App\Message\MessageConfig:
```

Then, you should register it in the configuration (`config/packages/zdd_message.yaml`) :
Finish by updating the configuration with this new service in `config/packages/zdd_message.yaml`:
```yaml
# config/packages/zdd_message.yaml
zdd_message:
serialized_messages_dir: 'var/serialized_messages' # The directory where the serialized messages will be stored (default: '%kernel.logs_dir%')
Comment thread
B-Galati marked this conversation as resolved.
Resolved
message_config_service: App\Message\MessageConfig
```

#### Optional configuration

**Use a custom serializer**

Option to use different serializer.
Possible options :
Possible options:
- `Yousign\ZddMessageBundle\Serializer\ZddMessageMessengerSerializer` (default, already configured for messenger serialization in messenger.yaml)
- Define your own serializer
- Create a service that implement `Yousign\ZddMessageBundle\Serializer\SerializerInterface`
Expand All @@ -82,6 +104,17 @@ Possible options :
serializer: '<your-service-id>'
```

**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.
Expand Down
26 changes: 26 additions & 0 deletions src/Config/CustomMessageGeneratorInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

namespace Yousign\ZddMessageBundle\Config;

/**
* @experimental
*/
interface CustomMessageGeneratorInterface
{
/**
* 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.
*
* 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.
*
* @template T of object
*
* @param class-string<T> $className
*
* @return ?T The fully instantiated message object, or null if the default instantiation should be used
*/
public function generateCustomMessage(string $className): ?object;
}
79 changes: 79 additions & 0 deletions src/Factory/MessageGenerator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<?php

declare(strict_types=1);

namespace Yousign\ZddMessageBundle\Factory;

use Yousign\ZddMessageBundle\Config\CustomMessageGeneratorInterface;
use Yousign\ZddMessageBundle\Config\ZddMessageConfigInterface;
use Yousign\ZddMessageBundle\Exceptions\MissingValueForTypeException;

/**
* @internal
*/
final class MessageGenerator
{
private readonly ZddPropertyExtractor $propertyExtractor;

public function __construct(private readonly ZddMessageConfigInterface $config)
{
$this->propertyExtractor = new ZddPropertyExtractor();
}

/**
* @param class-string $className
*/
public function generate(string $className): object
{
if ($this->config instanceof CustomMessageGeneratorInterface) {
$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);
}
}
2 changes: 1 addition & 1 deletion src/Factory/Property.php
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Comment thread
B-Galati marked this conversation as resolved.
Resolved
{
}
}
13 changes: 9 additions & 4 deletions src/Factory/PropertyList.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,17 +29,21 @@ public function addProperty(Property $property): void

public static function fromJson(string $data): self
{
/** @var array<array<string,string>> $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);
Expand Down Expand Up @@ -94,6 +98,7 @@ public function toJson(): string
$data[] = [
'name' => $property->name,
'type' => $property->type,
'isNullable' => $property->isNullable,
];
}

Expand Down
25 changes: 6 additions & 19 deletions src/Factory/ZddMessageFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@
final class ZddMessageFactory
{
private ZddPropertyExtractor $propertyExtractor;
private MessageGenerator $messageGenerator;

public function __construct(ZddMessageConfigInterface $config, private readonly SerializerInterface $serializer)
{
$this->propertyExtractor = new ZddPropertyExtractor($config);
$this->propertyExtractor = new ZddPropertyExtractor();
$this->messageGenerator = new MessageGenerator($config);
}

/**
Expand All @@ -26,29 +28,14 @@ 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->messageGenerator->generate($className);

$serializedMessage = $this->serializer->serialize($message);
} catch (\Throwable $e) {
throw new \LogicException('Unable to create ZddMessage for class "'.$className.'"', previous: $e);
}

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);
}
}
37 changes: 7 additions & 30 deletions src/Factory/ZddPropertyExtractor.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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),
};
}
}
1 change: 0 additions & 1 deletion src/ZddMessageBundle.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ final class ZddMessageBundle extends AbstractBundle
{
public function configure(DefinitionConfigurator $definition): void
{
/* @phpstan-ignore-next-line */
$definition
->rootNode()
->children()
Expand Down
Loading