From c3fca94db5b3391f7cdc5ab1401505a0fbcc991a Mon Sep 17 00:00:00 2001 From: Yevhen Sidelnyk Date: Tue, 24 Mar 2026 11:30:20 +0200 Subject: [PATCH] feat: introduce included_violations and validator_violations configuration constants --- README.md | 49 +++++++------------ .../ConstantsAutoloadingCompilerPass.php | 20 ++++++++ ...AutoloadingCompilerPassIntegrationTest.php | 2 + .../Autoload/ConstantsClassLoader.php | 11 ++++- .../Uid/InvalidUidExceptionMatchCondition.php | 2 +- ...alidationFailedExceptionMatchCondition.php | 2 +- .../Value/ExceptionValueMatchCondition.php | 2 +- .../ValidationFailedExceptionFormatter.php | 5 +- .../ViolationListExceptionFormatter.php | 5 +- tests/Unit/Stub/HandleableMessageStub.php | 6 +-- tests/Unit/Stub/NestedHandleableMessage.php | 6 ++- 11 files changed, 67 insertions(+), 43 deletions(-) diff --git a/README.md b/README.md index 5a857d0..dca148a 100644 --- a/README.md +++ b/README.md @@ -544,7 +544,7 @@ class CardBlockedException extends DomainException implements ValueException This one is very similar to `ValueException` condition \ with the difference that it integrates Symfony's native `ValidationFailedException`. -Specify `ValidationFailedExceptionMatchCondition` to correlate validation exception's value with a property value: +Specify `validated_value` match condition to compare property's value against exception's validated value: ```php use PhPhD\ExceptionalMatcher\Rule\Object\Try_; @@ -552,15 +552,12 @@ use PhPhD\ExceptionalMatcher\Rule\Object\Property\Catch_; use Symfony\Component\Validator\Exception\ValidationFailedException; use const PhPhD\ExceptionalMatcher\Rule\Object\Property\Match\Condition\Validator\validated_value; +use const PhPhD\ExceptionalMatcher\Validator\Formatter\Validator\validator_violations; #[Try_] class RegisterUserCommand { - #[Catch_( - exception: ValidationFailedException::class, - from: Password::class, - match: validated_value, - )] + #[Catch_(ValidationFailedException::class, from: Password::class, match: validated_value, format: validator_violations)] public string $password; } ``` @@ -611,15 +608,13 @@ Then, specify `ViolationListExceptionFormatter` as a `format:` for the `#[Catch_ ```php use PhPhD\ExceptionalMatcher\Rule\Object\Try_; use PhPhD\ExceptionalMatcher\Rule\Object\Property\Catch_; -use PhPhD\ExceptionalMatcher\Validator\Formatter\ViolationList\ViolationListExceptionFormatter; + +use const PhPhD\ExceptionalMatcher\Validator\Formatter\ViolationList\included_violations; #[Try_] class IssueCreditCardCommand { - #[Catch_( - exception: CardNumberValidationFailedException::class, - format: ViolationListExceptionFormatter::class, - )] + #[Catch_(CardNumberValidationFailedException::class, format: included_violations)] private string $cardNumber; } ``` @@ -631,7 +626,7 @@ formatter makes sure that a proper representation of this exception in a `Constr > it would've been ignored in favour of `ConstraintViolationList` messages. -> Besides that, it's also possible to use `ValidationFailedExceptionFormatter`, \ +> Besides that, it's also possible to use `validator_violations` formatter, \ > which can format Symfony's native `ValidationFailedException`. #### Custom Violation Formatters 🎨🖌️ @@ -646,8 +641,8 @@ use PhPhD\ExceptionalMatcher\Exception\MatchedException; use PhPhD\ExceptionalMatcher\Validator\Formatter\ExceptionViolationFormatter; use Symfony\Component\Validator\ConstraintViolationInterface; -/** @implements ExceptionViolationFormatter */ -final class RegistrationViolationsFormatter implements ExceptionViolationFormatter +/** @implements ExceptionViolationFormatter */ +final class LoginAlreadyTakenViolationFormatter implements ExceptionViolationFormatter { public function __construct( #[Autowire(service: ExceptionViolationFormatter::class.'')] @@ -662,20 +657,15 @@ final class RegistrationViolationsFormatter implements ExceptionViolationFormatt // and then adjust only the necessary parts [$violation] = $this->formatter->format($matchedException); + /** @var LoginAlreadyTakenException $exception */ $exception = $matchedException->getException(); - if ($exception instanceof LoginAlreadyTakenException) { - $violation = new ConstraintViolation( - $violation->getMessage(), - $violation->getMessageTemplate(), - ['loginHolder' => $exception->getLoginHolder()], - // ... - ); - } - - if ($exception instanceof WeakPasswordException) { + $violation = new ConstraintViolation( + $violation->getMessage(), + $violation->getMessageTemplate(), + ['loginHolder' => $exception->getLoginHolder()], // ... - } + ); return [$violation]; } @@ -686,7 +676,7 @@ Then, register it as a service: ```yaml services: - App\Auth\User\Features\Registration\Validation\RegistrationViolationsFormatter: + App\Auth\User\Support\Validation\LoginAlreadyTakenViolationFormatter: autoconfigure: true ``` @@ -714,11 +704,8 @@ final class RegisterUserCommand } ``` -In this example, `LoginAlreadyTakenViolationFormatter` is used to format constraint violation for -`LoginAlreadyTakenException`, \ -and `WeakPasswordViolationFormatter` formats `WeakPasswordException`. - -Though not recommended, you might use a single formatter for the two. +In this example, `LoginAlreadyTakenViolationFormatter` formats constraint violation for `LoginAlreadyTakenException`, \ +while `WeakPasswordViolationFormatter` formats `WeakPasswordException`. ### In-depth analysis diff --git a/src/ExceptionalMatcher/Rule/Object/Assembler/Autoload/ConstantsAutoloadingCompilerPass.php b/src/ExceptionalMatcher/Rule/Object/Assembler/Autoload/ConstantsAutoloadingCompilerPass.php index 9a028a8..1fb88e1 100644 --- a/src/ExceptionalMatcher/Rule/Object/Assembler/Autoload/ConstantsAutoloadingCompilerPass.php +++ b/src/ExceptionalMatcher/Rule/Object/Assembler/Autoload/ConstantsAutoloadingCompilerPass.php @@ -4,6 +4,7 @@ namespace PhPhD\ExceptionalMatcher\Rule\Object\Assembler\Autoload; +use PhPhD\ExceptionalMatcher\Exception\Formatter\MatchedExceptionFormatter; use PhPhD\ExceptionalMatcher\Rule\Assembler\MatchingRuleSetAssemblerService; use PhPhD\ExceptionalMatcher\Rule\Object\Assembler\ObjectMatchingRuleSetAssembler; use PhPhD\ExceptionalMatcher\Rule\Object\Property\Match\Condition\MatchConditionFactory; @@ -22,6 +23,7 @@ final class ConstantsAutoloadingCompilerPass implements CompilerPassInterface public function process(ContainerBuilder $container): void { $classNamesSet = $this->getMatchConditionFactoryIds($container); + $classNamesSet += $this->getExceptionFormatterIds($container); $definition = $container->getDefinition(MatchingRuleSetAssemblerService::class.'<'.ObjectMatchingRuleSetAssembler::class.'>'); @@ -52,4 +54,22 @@ private function getMatchConditionFactoryIds(ContainerBuilder $container): array return $classNames; } + + /** @return array */ + private function getExceptionFormatterIds(ContainerBuilder $container): array + { + $classNames = []; + $taggedServiceIds = array_keys($container->findTaggedServiceIds(MatchedExceptionFormatter::class)); + + foreach ($taggedServiceIds as $taggedServiceId) { + $def = $container->getDefinition($taggedServiceId); + + /** @var class-string $className */ + $className = $def->getClass(); + + $classNames[$className] = true; + } + + return $classNames; + } } diff --git a/src/ExceptionalMatcher/Rule/Object/Assembler/Autoload/ConstantsAutoloadingCompilerPassIntegrationTest.php b/src/ExceptionalMatcher/Rule/Object/Assembler/Autoload/ConstantsAutoloadingCompilerPassIntegrationTest.php index b1fa0d9..28afbb5 100644 --- a/src/ExceptionalMatcher/Rule/Object/Assembler/Autoload/ConstantsAutoloadingCompilerPassIntegrationTest.php +++ b/src/ExceptionalMatcher/Rule/Object/Assembler/Autoload/ConstantsAutoloadingCompilerPassIntegrationTest.php @@ -10,6 +10,7 @@ use PhPhD\ExceptionalMatcher\Rule\Object\Assembler\ObjectMatchingRuleSetAssemblerService; use PhPhD\ExceptionalMatcher\Rule\Object\Property\Match\Condition\Value\ExceptionValueMatchConditionFactory; use PhPhD\ExceptionalMatcher\Tests\Unit\Stub\HandleableMessageStub; +use PhPhD\ExceptionalMatcher\Validator\Formatter\ViolationList\ViolationListExceptionFormatter; use function class_exists; @@ -43,5 +44,6 @@ public function testClassesThatDefineConstantsAreAutoloaded(): void self::assertNotNull($rule); self::assertTrue(class_exists(ExceptionValueMatchConditionFactory::class, false)); + self::assertTrue(class_exists(ViolationListExceptionFormatter::class, false)); } } diff --git a/src/ExceptionalMatcher/Rule/Object/Assembler/Autoload/ConstantsClassLoader.php b/src/ExceptionalMatcher/Rule/Object/Assembler/Autoload/ConstantsClassLoader.php index 8720bcd..5ba9398 100644 --- a/src/ExceptionalMatcher/Rule/Object/Assembler/Autoload/ConstantsClassLoader.php +++ b/src/ExceptionalMatcher/Rule/Object/Assembler/Autoload/ConstantsClassLoader.php @@ -6,6 +6,8 @@ use function array_map; use function glob; +use function implode; +use function range; use function sprintf; use function str_repeat; @@ -21,11 +23,16 @@ public static function loadClassNames(array $classNames): void /** @codeCoverageIgnore */ public static function loadFiles(string $basePath, int $depth = 7): void { - $nestingPattern = str_repeat('{,*/}', $depth); + $prefixes = array_map( + static fn (int $level): string => str_repeat('*/', $level), + range(1, $depth), + ); + + $nestingPattern = '{'.implode(',', $prefixes).'}'; /** @var list $glob */ $glob = glob( - $basePath.sprintf('/%s*MatchConditionFactory.php', $nestingPattern), + $basePath.sprintf('/%s*{MatchConditionFactory,ExceptionFormatter}.php', $nestingPattern), GLOB_BRACE | GLOB_NOSORT, ); diff --git a/src/ExceptionalMatcher/Rule/Object/Property/Match/Condition/Uid/InvalidUidExceptionMatchCondition.php b/src/ExceptionalMatcher/Rule/Object/Property/Match/Condition/Uid/InvalidUidExceptionMatchCondition.php index a60f282..2e850ed 100644 --- a/src/ExceptionalMatcher/Rule/Object/Property/Match/Condition/Uid/InvalidUidExceptionMatchCondition.php +++ b/src/ExceptionalMatcher/Rule/Object/Property/Match/Condition/Uid/InvalidUidExceptionMatchCondition.php @@ -9,7 +9,7 @@ use Throwable; /** - * @api - use uid_value constant for a class name instead + * @api - use {@see uid_value} constant for a class reference instead * * @implements MatchCondition */ diff --git a/src/ExceptionalMatcher/Rule/Object/Property/Match/Condition/Validator/ValidationFailedExceptionMatchCondition.php b/src/ExceptionalMatcher/Rule/Object/Property/Match/Condition/Validator/ValidationFailedExceptionMatchCondition.php index 675fbf7..3f2a16a 100644 --- a/src/ExceptionalMatcher/Rule/Object/Property/Match/Condition/Validator/ValidationFailedExceptionMatchCondition.php +++ b/src/ExceptionalMatcher/Rule/Object/Property/Match/Condition/Validator/ValidationFailedExceptionMatchCondition.php @@ -9,7 +9,7 @@ use Throwable; /** - * @api - use validated_value constant for a class name instead + * @api - use {@see validated_value} constant for a class reference instead * * @implements MatchCondition */ diff --git a/src/ExceptionalMatcher/Rule/Object/Property/Match/Condition/Value/ExceptionValueMatchCondition.php b/src/ExceptionalMatcher/Rule/Object/Property/Match/Condition/Value/ExceptionValueMatchCondition.php index b66ae8a..72de128 100644 --- a/src/ExceptionalMatcher/Rule/Object/Property/Match/Condition/Value/ExceptionValueMatchCondition.php +++ b/src/ExceptionalMatcher/Rule/Object/Property/Match/Condition/Value/ExceptionValueMatchCondition.php @@ -8,7 +8,7 @@ use Throwable; /** - * @api - use exception_value constant for a class name instead + * @api - use {@see exception_value} constant for a class reference instead * * @implements MatchCondition */ diff --git a/src/ExceptionalMatcher/Validator/Formatter/Validator/ValidationFailedExceptionFormatter.php b/src/ExceptionalMatcher/Validator/Formatter/Validator/ValidationFailedExceptionFormatter.php index efedcea..6051dad 100644 --- a/src/ExceptionalMatcher/Validator/Formatter/Validator/ValidationFailedExceptionFormatter.php +++ b/src/ExceptionalMatcher/Validator/Formatter/Validator/ValidationFailedExceptionFormatter.php @@ -10,8 +10,11 @@ use PhPhD\ExceptionalMatcher\Validator\Formatter\ViolationList\ViolationListException; use Symfony\Component\Validator\Exception\ValidationFailedException; +/** @api */ +const validator_violations = ValidationFailedExceptionFormatter::class; + /** - * @api + * @api - use {@see validator_violations} constant for a class reference instead * * @implements ExceptionViolationFormatter */ diff --git a/src/ExceptionalMatcher/Validator/Formatter/ViolationList/ViolationListExceptionFormatter.php b/src/ExceptionalMatcher/Validator/Formatter/ViolationList/ViolationListExceptionFormatter.php index 1880f46..8df4c16 100644 --- a/src/ExceptionalMatcher/Validator/Formatter/ViolationList/ViolationListExceptionFormatter.php +++ b/src/ExceptionalMatcher/Validator/Formatter/ViolationList/ViolationListExceptionFormatter.php @@ -14,8 +14,11 @@ use function array_map; use function iterator_to_array; +/** @api */ +const included_violations = ViolationListExceptionFormatter::class; + /** - * @api + * @api - use {@see included_violations} constant for a class reference instead * * @implements ExceptionViolationFormatter */ diff --git a/tests/Unit/Stub/HandleableMessageStub.php b/tests/Unit/Stub/HandleableMessageStub.php index 20e80e0..9708712 100644 --- a/tests/Unit/Stub/HandleableMessageStub.php +++ b/tests/Unit/Stub/HandleableMessageStub.php @@ -17,12 +17,12 @@ use PhPhD\ExceptionalMatcher\Tests\Unit\Stub\Exception\StaticPropertyMatchedException; use PhPhD\ExceptionalMatcher\Validator\Formatter\Main\Tests\Stub\MessageContainingException; use PhPhD\ExceptionalMatcher\Validator\Formatter\Main\Tests\Stub\ObjectPropertyMatchedException; -use PhPhD\ExceptionalMatcher\Validator\Formatter\Validator\ValidationFailedExceptionFormatter; use Symfony\Component\Uid\Uuid; use Symfony\Component\Validator\Exception\ValidationFailedException; use const PhPhD\ExceptionalMatcher\Rule\Object\Property\Match\Condition\Validator\validated_value; use const PhPhD\ExceptionalMatcher\Rule\Object\Property\Match\Condition\Value\exception_value; +use const PhPhD\ExceptionalMatcher\Validator\Formatter\Validator\validator_violations; /** * @psalm-suppress InvalidAttribute ("Attribute Catch_ is not repeatable") @@ -59,11 +59,11 @@ final class HandleableMessageStub private string $messageText; #[Catch_(SomeValueException::class, match: exception_value, message: 'oops')] - #[Catch_(ValidationFailedException::class, match: validated_value, format: ValidationFailedExceptionFormatter::class)] + #[Catch_(ValidationFailedException::class, match: validated_value, format: validator_violations)] private string $notMatchedProperty = 'not matched'; #[Catch_(SomeValueException::class, match: exception_value, message: 'oops')] - #[Catch_(ValidationFailedException::class, match: validated_value, format: ValidationFailedExceptionFormatter::class)] + #[Catch_(ValidationFailedException::class, match: validated_value, format: validator_violations)] private string $matchedProperty = 'matched!'; #[Catch_(SomeValueException::class, message: 'oops')] diff --git a/tests/Unit/Stub/NestedHandleableMessage.php b/tests/Unit/Stub/NestedHandleableMessage.php index 93b74a9..de67549 100644 --- a/tests/Unit/Stub/NestedHandleableMessage.php +++ b/tests/Unit/Stub/NestedHandleableMessage.php @@ -9,9 +9,11 @@ use PhPhD\ExceptionalMatcher\Rule\Object\Try_; use PhPhD\ExceptionalMatcher\Tests\Unit\Stub\Exception\NestedPropertyMatchedException; use PhPhD\ExceptionalMatcher\Validator\Formatter\ViolationList\Tests\Stub\ViolationListExampleException; -use PhPhD\ExceptionalMatcher\Validator\Formatter\ViolationList\ViolationListExceptionFormatter; use Symfony\Component\Validator\Constraints\Valid; +use const PhPhD\ExceptionalMatcher\Validator\Formatter\ViolationList\included_violations; + +/** @psalm-suppress ArgumentTypeCoercion */ #[Try_] final class NestedHandleableMessage { @@ -21,7 +23,7 @@ final class NestedHandleableMessage #[Valid] private ConditionalMessage $conditionalMessage; - #[Catch_(ViolationListExampleException::class, format: ViolationListExceptionFormatter::class)] + #[Catch_(ViolationListExampleException::class, format: included_violations)] private int $violationListCapturedProperty; public static function createWithConditionalMessage(ConditionalMessage $conditionalMessage): self