Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Recursive message validation #39

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
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
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
"symfony/browser-kit": "^6.2",
"symfony/framework-bundle": "^6.2",
"symfony/yaml": "^6.2",
"symfony/messenger": "^6.2"
"symfony/messenger": "^6.2",
"thecodingmachine/safe": "^2.5"
},
"autoload": {
"psr-4": {
Expand Down
95 changes: 59 additions & 36 deletions src/Assert/ZddMessageAsserter.php
Original file line number Diff line number Diff line change
@@ -1,68 +1,91 @@
<?php

declare(strict_types=1);

namespace Yousign\ZddMessageBundle\Assert;

use Yousign\ZddMessageBundle\Factory\Property;
use Yousign\ZddMessageBundle\Factory\PropertyList;
use Yousign\ZddMessageBundle\Serializer\SerializerInterface;
use Yousign\ZddMessageBundle\Factory\ZddMessage;
use Yousign\ZddMessageBundle\Serializer\MessageSerializerInterface;
use Yousign\ZddMessageBundle\Serializer\UnableToDeserializeException;

/**
* @internal
*/
final class ZddMessageAsserter
{
public function __construct(private readonly SerializerInterface $serializer)
{
public function __construct(
private readonly MessageSerializerInterface $messageSerializer,
) {
}

/**
* @param class-string<object> $messageFqcn
*
* @throws UnableToDeserializeException
*/
public function assert(
string $messageFqcn,
string $serializedMessage,
PropertyList $propertyList
object $messageInstance,
ZddMessage $message,
): void {
// ✅ Assert message is unserializable
$objectBefore = $this->serializer->deserialize($serializedMessage);
/** @var object $objectBefore */
$objectBefore = $this->messageSerializer->deserialize($message->serializedMessage);

if (!$objectBefore instanceof $messageFqcn) {
throw new \LogicException(\sprintf('Class mismatch between $messageFqcn: "%s" and $serializedMessage: "%s". Please verify your integration.', $messageFqcn, $serializedMessage));
if ($objectBefore::class !== $messageInstance::class) {
throw new \LogicException(sprintf('Class mismatch between $messageFqcn: "%s" and $serializedMessage: "%s". Please verify your integration.', $messageInstance::class, $objectBefore::class));
}

$reflection = new \ReflectionClass($messageFqcn);
$reflectionProperties = $reflection->getProperties();
$properties = $message->properties;

// ✅ Assert property type hint has not changed and new property have a default value
foreach ($reflectionProperties as $reflectionProperty) {
// ✅ Assert error "Typed property Message::$theProperty must not be accessed before initialization".
$reflectionProperty->getValue($objectBefore); // @phpstan-ignore-line ::: Call to method ReflectionProperty::getValue() on a separate line has no effect.
$this->assertProperties($objectBefore, $properties);

// ✅ Assert property
if ($propertyList->has($reflectionProperty->getName())) {
self::assertProperty($reflectionProperty, $propertyList->get($reflectionProperty->getName()), $messageFqcn);
$propertyList->remove($reflectionProperty->getName());
}
}

if (0 !== $propertyList->count()) {
throw new \LogicException(\sprintf('⚠️ The properties "%s" in class "%s" seems to have been removed', implode(', ', $propertyList->getPropertiesName()), $messageFqcn));
if ([] !== $properties) {
throw new \LogicException(sprintf('⚠️ The properties "%s" in class "%s" seems to have been removed', implode(', ', Property::getPropertyNames($properties)), $objectBefore::class));
}
}

private static function assertProperty(\ReflectionProperty $reflectionProperty, Property $property, string $messageFqcn): void
/**
* @param Property[] $properties
*/
private function assertProperties(object $object, array &$properties): void
{
if (null === $reflectionProperty->getType()) {
throw new \LogicException(\sprintf('$reflectionProperty::getType cannot be null'));
}
if (!$reflectionProperty->getType() instanceof \ReflectionNamedType) {
throw new \LogicException(\sprintf('$reflectionProperty::getType must be an instance of ReflectionNamedType'));
}
if ($reflectionProperty->getType()->getName() !== $property->type) {
throw new \LogicException(\sprintf('Error for property "%s" in class "%s", the type mismatch between the old and the new version of class. Please verify your integration.', $reflectionProperty->getName(), $messageFqcn));
$reflectionClass = new \ReflectionClass($object);

foreach ($reflectionClass->getProperties() as $reflectionProperty) {
$value = $reflectionProperty->getValue($object);

if (null === $reflectionProperty->getType()) { // Same check as in ZddPropertyExtractor
continue;
}

// ✅ Assert property
foreach ($properties as $key => $p) {
if ($p->name === $reflectionProperty->getName()) {
$propertyIndex = $key;
$property = $p;
}
}

if (!isset($property, $propertyIndex)) {
throw new \LogicException(sprintf('Unable to find %s property in ZddMessage properties', $reflectionProperty->getName()));
}

if ($reflectionProperty->getType() instanceof \ReflectionIntersectionType) {
throw new \LogicException('$reflectionProperty::getType must not be an instance of ReflectionIntersectionType');
}

$type = is_object($value) ? $value::class : gettype($value);
if ('NULL' !== $type && $type !== $property->type) {
throw new \LogicException(sprintf('Error for property "%s" in class "%s", the type mismatch between the old and the new version of class. Please verify your integration.', $reflectionProperty->getName(), $object::class));
}

$childrenProperties = $property->children;
if (is_object($value)) {
$this->assertProperties($value, $childrenProperties);
}

if ([] === $childrenProperties) {
unset($properties[$propertyIndex]);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
<?php

declare(strict_types=1);

namespace Yousign\ZddMessageBundle\Command;

use Symfony\Component\Console\Attribute\AsCommand;
Expand All @@ -9,11 +11,14 @@
use Symfony\Component\Console\Style\SymfonyStyle;
use Yousign\ZddMessageBundle\Config\ZddMessageConfigInterface;

#[AsCommand(name: 'yousign:zdd-message:debug', description: 'List of managed messages to validate.')]
final class ListZddMessageCommand extends Command
#[AsCommand(
name: 'yousign:zdd-message:debug',
description: 'List of managed messages to validate.',
)]
final class DebugZddMessageCommand extends Command
{
public function __construct(
private readonly ZddMessageConfigInterface $zddMessageConfig
private readonly ZddMessageConfigInterface $config,
) {
parent::__construct();
}
Expand All @@ -26,8 +31,9 @@ public function execute(InputInterface $input, OutputInterface $output): int
$table->setHeaderTitle('List of tracked messages for the zdd');
$table->setHeaders(['#', 'Message']);

foreach ($this->zddMessageConfig->getMessageToAssert() as $key => $message) {
$table->addRow([$key + 1, $message]);
$row = 1;
foreach ($this->config->getMessageToAssert() as $key => $_) {
$table->addRow([$row++, $key]);
}

$table->render();
Expand Down
29 changes: 14 additions & 15 deletions src/Command/GenerateZddMessageCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,19 @@
use Yousign\ZddMessageBundle\Config\ZddMessageConfigInterface;
use Yousign\ZddMessageBundle\Factory\ZddMessageFactory;
use Yousign\ZddMessageBundle\Filesystem\ZddMessageFilesystem;
use Yousign\ZddMessageBundle\Serializer\SerializerInterface;

#[AsCommand(name: 'yousign:zdd-message:generate', description: 'Generate serialized version of managed messages to validate them afterwards.')]
#[AsCommand(
name: 'yousign:zdd-message:generate',
description: 'Generate serialized version of managed messages to validate them afterwards.',
)]
final class GenerateZddMessageCommand extends Command
{
private ZddMessageFactory $zddMessageFactory;
private ZddMessageFilesystem $zddMessageFilesystem;

public function __construct(private readonly string $zddMessagePath, private readonly ZddMessageConfigInterface $zddMessageConfig, SerializerInterface $serializer)
{
public function __construct(
private readonly ZddMessageConfigInterface $config,
private readonly ZddMessageFactory $messageFactory,
private readonly ZddMessageFilesystem $filesystem,
) {
parent::__construct();

$this->zddMessageFactory = new ZddMessageFactory($zddMessageConfig, $serializer);
$this->zddMessageFilesystem = new ZddMessageFilesystem($this->zddMessagePath);
}

public function execute(InputInterface $input, OutputInterface $output): int
Expand All @@ -33,12 +32,12 @@ public function execute(InputInterface $input, OutputInterface $output): int
$table = $io->createTable();
$table->setHeaders(['#', 'Message']);

foreach ($this->zddMessageConfig->getMessageToAssert() as $key => $messageFqcn) {
$zddMessage = $this->zddMessageFactory->create($messageFqcn);

$this->zddMessageFilesystem->write($zddMessage);
$row = 1;
foreach ($this->config->getMessageToAssert() as $key => $instance) {
$message = $this->messageFactory->create($key, $instance);
$this->filesystem->write($message);

$table->addRow([$key + 1, $messageFqcn]);
$table->addRow([$row++, $key]);
}

$table->render();
Expand Down
42 changes: 21 additions & 21 deletions src/Command/ValidateZddMessageCommand.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
<?php

declare(strict_types=1);

namespace Yousign\ZddMessageBundle\Command;

use Symfony\Component\Console\Attribute\AsCommand;
Expand All @@ -11,22 +13,17 @@
use Yousign\ZddMessageBundle\Config\ZddMessageConfigInterface;
use Yousign\ZddMessageBundle\Factory\ZddMessageFactory;
use Yousign\ZddMessageBundle\Filesystem\ZddMessageFilesystem;
use Yousign\ZddMessageBundle\Serializer\SerializerInterface;

#[AsCommand(name: 'yousign:zdd-message:validate', description: 'Validate the serialized version of managed messages with the current version.')]
final class ValidateZddMessageCommand extends Command
{
private ZddMessageFactory $zddMessageFactory;
private ZddMessageFilesystem $zddMessageFilesystem;
private ZddMessageAsserter $zddMessageAsserter;

public function __construct(private readonly string $zddMessagePath, private readonly ZddMessageConfigInterface $zddMessageConfig, SerializerInterface $serializer)
{
public function __construct(
private readonly ZddMessageConfigInterface $config,
private readonly ZddMessageFactory $messageFactory,
private readonly ZddMessageFilesystem $filesystem,
private readonly ZddMessageAsserter $messageAsserter,
) {
parent::__construct();

$this->zddMessageFactory = new ZddMessageFactory($zddMessageConfig, $serializer);
$this->zddMessageFilesystem = new ZddMessageFilesystem($this->zddMessagePath);
$this->zddMessageAsserter = new ZddMessageAsserter($serializer);
}

public function execute(InputInterface $input, OutputInterface $output): int
Expand All @@ -37,21 +34,24 @@ public function execute(InputInterface $input, OutputInterface $output): int
$table->setHeaders(['#', 'Message', 'ZDD Compliant?']);

$errorCount = 0;
foreach ($this->zddMessageConfig->getMessageToAssert() as $key => $messageFqcn) {
if (false === $this->zddMessageFilesystem->exists($messageFqcn)) {
// It happens on newly added message, the trade-off here is to validate itself on current version
$zddMessage = $this->zddMessageFactory->create($messageFqcn);
$this->zddMessageFilesystem->write($zddMessage);

$row = 1;
foreach ($this->config->getMessageToAssert() as $name => $instance) {
if (false === $this->filesystem->exists($name)) {
$message = $this->messageFactory->create(
$name,
$instance,
);
$this->filesystem->write($message);
}

$messageToAssert = $this->zddMessageFilesystem->read($messageFqcn);
$messageToAssert = $this->filesystem->read($name);

try {
$this->zddMessageAsserter->assert($messageFqcn, $messageToAssert->serializedMessage(), $messageToAssert->propertyList());

$table->addRow([$key + 1, $messageFqcn, 'Yes ✅']);
$this->messageAsserter->assert($instance, $messageToAssert);
$table->addRow([$row++, $name, 'Yes ✅']);
} catch (\Throwable $e) {
$table->addRow([$key + 1, $messageFqcn, 'No ❌']);
$table->addRow([$row++, $name, 'No ❌']);
++$errorCount;
}
}
Expand Down
47 changes: 6 additions & 41 deletions src/Config/ZddMessageConfigInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,50 +5,15 @@
interface ZddMessageConfigInterface
{
/**
* The list of FQCN message to assert.
* The method should generate the list of message instances to assert.
*
* @example getMessageToAssert(): array
* {
* return [
* 'App\Message\MyMessage',
* 'App\Message\MyOtherMessage'
* ];
* }
*
* @return array<class-string>
*/
public function getMessageToAssert(): array;

/**
* Provide a fake value for each custom property type used in your messages.
* You can also override the fake value used for scalar types.
*
* @example
* Suppose you have message which contains an object as property type:
*
* class MyMessage
* {
* private MyObject $object;
* // ...
* }
*
* class MyObject
* {
* private string $content;
* // ...
* }
*
* The implementation of generateValueForCustomPropertyType should be like this:
*
* public function generateValueForCustomPropertyType(string $type): array;
* @example getMessageToAssert(): \Generator
* {
* return match($type) {
* 'Namespace\MyObject' => new MyObject("Hi!"),
* default => null,
* };
* yield App\Message\MyMessage::class => new App\Message\MyMessage(),
* yield App\Message\MyOtherMessage::class => new App\Message\MyOtherMessage(),
* }
*
* @see MessageConfig in ZddMessageFakerTest.php for a concret examples
* @return \Generator<string, object>
*/
public function generateValueForCustomPropertyType(string $type): mixed;
public function getMessageToAssert(): \Generator;
}
Loading
Loading