From e68fda955ef03919cee1dc8ae32d9a8c557e352a Mon Sep 17 00:00:00 2001 From: Henrique Moody Date: Mon, 13 Apr 2026 16:05:04 +0200 Subject: [PATCH 01/13] Bump respect/validation and respect/stringifier to ^3.0 The v3 line replaces the long-standing Validatable interface with a leaner Validator/Result contract, collapses the per-rule exception hierarchy into a single ValidationException, reorganises the built-in rules under a Validators\ namespace, and adjusts default message templates. This commit only bumps the constraint; the rest of the branch adapts the package to those breaking changes. --- .github/workflows/continuous-integration.yml | 7 +++---- composer.json | 6 +++--- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 6b28f9a..8f62f5d 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -17,8 +17,7 @@ jobs: strategy: matrix: php-version: - - "8.1" - - "8.2" + - "8.5" steps: - name: Checkout @@ -54,7 +53,7 @@ jobs: - name: Install PHP uses: shivammathur/setup-php@v2 with: - php-version: 8.1 + php-version: 8.5 coverage: pcov - name: Install Dependencies @@ -78,7 +77,7 @@ jobs: - name: Install PHP uses: shivammathur/setup-php@v2 with: - php-version: 8.1 + php-version: 8.5 coverage: none - name: Install dependencies diff --git a/composer.json b/composer.json index dc27c24..b0c7482 100644 --- a/composer.json +++ b/composer.json @@ -19,9 +19,9 @@ ] }, "require": { - "php": "^8.1", - "respect/stringifier": "^0.2.0", - "respect/validation": "^2.2.4", + "php": "^8.5", + "respect/stringifier": "^3.0", + "respect/validation": "^3.0", "symfony/polyfill-mbstring": "^v1.27.0" }, "require-dev": { From 44d6aab1f048ff33ab12c0c680523064f516e4fa Mon Sep 17 00:00:00 2001 From: Henrique Moody Date: Mon, 13 Apr 2026 16:29:43 +0200 Subject: [PATCH 02/13] Adopt v3 Validator interface and drop the Envelope abstraction The v2 Validatable interface exposed validate(), check(), and reportError(); v3 replaces it with a single Validator::evaluate(mixed): Result method and routes exception construction through a separate ValidatorBuilder pipeline. Rule now implements Validator directly and codifies the precondition-then-inner-rule pattern in evaluate() rather than via check() side effects. Assertion routes string descriptions through ValidatorBuilder::init()->assert() to pick up v3 templating, while Throwable descriptions fall back to a manual evaluate-and-throw because the builder only understands string templates. The Envelope wrapper and its test are removed entirely. Its only job was to customise v2 exception templates; v3 lets each Validator carry its own template, so the abstraction has no remaining role here. --- src/Assertion.php | 25 ++++++++----------- src/Rule/Envelope.php | 38 ----------------------------- src/Rule/Rule.php | 36 +++++++++------------------- tests/unit/Rule/EnvelopeTest.php | 41 -------------------------------- 4 files changed, 21 insertions(+), 119 deletions(-) delete mode 100644 src/Rule/Envelope.php delete mode 100644 tests/unit/Rule/EnvelopeTest.php diff --git a/src/Assertion.php b/src/Assertion.php index 9bba225..9ad31a6 100644 --- a/src/Assertion.php +++ b/src/Assertion.php @@ -13,21 +13,19 @@ namespace Respect\Assertion; -use Respect\Validation\Exceptions\ValidationException; -use Respect\Validation\Validatable; +use Respect\Validation\Validator; +use Respect\Validation\ValidatorBuilder; use Throwable; -use function is_string; - final class Assertion { public function __construct( - private readonly Validatable $rule, + private readonly Validator $rule, private readonly null|string|Throwable $description = null ) { } - public function getRule(): Validatable + public function getRule(): Validator { return $this->rule; } @@ -39,18 +37,15 @@ public function getDescription(): null|string|Throwable public function assert(mixed $input): void { - try { - $this->rule->check($input); - } catch (ValidationException $exception) { - if ($this->description instanceof Throwable) { + if ($this->description instanceof Throwable) { + $result = $this->rule->evaluate($input); + if (!$result->hasPassed) { throw $this->description; } - if (is_string($this->description)) { - $exception->updateTemplate($this->description); - } - - throw $exception; + return; } + + ValidatorBuilder::init($this->rule)->assert($input, $this->description); } } diff --git a/src/Rule/Envelope.php b/src/Rule/Envelope.php deleted file mode 100644 index adc043d..0000000 --- a/src/Rule/Envelope.php +++ /dev/null @@ -1,38 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE file - * that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace Respect\Assertion\Rule; - -use Respect\Validation\Exceptions\ValidationException; -use Respect\Validation\Rules\AbstractEnvelope; -use Respect\Validation\Validatable; - -final class Envelope extends AbstractEnvelope -{ - public function __construct(Validatable $validatable, string $template) - { - parent::__construct($validatable, []); - $this->template = $template; - } - - /** - * {@inheritDoc} - */ - public function reportError($input, array $extraParameters = []): ValidationException - { - $exception = parent::reportError($input, $extraParameters); - $exception->updateTemplate((string) $this->template); - - return $exception; - } -} diff --git a/src/Rule/Rule.php b/src/Rule/Rule.php index ad50532..9bfbec3 100644 --- a/src/Rule/Rule.php +++ b/src/Rule/Rule.php @@ -13,41 +13,27 @@ namespace Respect\Assertion\Rule; -use Respect\Validation\Exceptions\ValidationException; -use Respect\Validation\Rules\AbstractRule; -use Respect\Validation\Validatable; +use Respect\Validation\Result; +use Respect\Validation\Validator; -abstract class Rule extends AbstractRule +abstract class Rule implements Validator { abstract protected function getFilteredInput(mixed $input): mixed; - abstract protected function getCustomizedException(ValidationException $exception): ValidationException; - public function __construct( - private readonly Validatable $preconditionRule, - private readonly Validatable $rule, + private readonly Validator $preconditionRule, + private readonly Validator $rule, ) { } - /** - * {@inheritDoc} - */ - public function validate($input): bool + public function evaluate(mixed $input): Result { - return $this->preconditionRule->validate($input) && $this->rule->validate($this->getFilteredInput($input)); - } + $preconditionResult = $this->preconditionRule->evaluate($input); - /** - * {@inheritDoc} - */ - public function check($input): void - { - $this->preconditionRule->assert($input); - - try { - $this->rule->check($this->getFilteredInput($input)); - } catch (ValidationException $exception) { - throw $this->getCustomizedException($exception); + if (!$preconditionResult->hasPassed) { + return $preconditionResult; } + + return $this->rule->evaluate($this->getFilteredInput($input)); } } diff --git a/tests/unit/Rule/EnvelopeTest.php b/tests/unit/Rule/EnvelopeTest.php deleted file mode 100644 index 70d2c55..0000000 --- a/tests/unit/Rule/EnvelopeTest.php +++ /dev/null @@ -1,41 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE file - * that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace Respect\Test\Unit\Assertion\Rule; - -use PHPUnit\Framework\TestCase; -use Respect\Assertion\Rule\Envelope; -use Respect\Validation\Exceptions\ValidationException; -use Respect\Validation\Rules\AlwaysInvalid; - -use function Respect\Stringifier\stringify; - -/** - * @covers \Respect\Assertion\Rule\Envelope - */ -final class EnvelopeTest extends TestCase -{ - /** - * @test - */ - public function itShouldCustomizeExceptionTemplate(): void - { - $input = 'something'; - - $this->expectException(ValidationException::class); - $this->expectExceptionMessage(stringify($input) . ' should be awesome'); - - $sut = new Envelope(new AlwaysInvalid(), '{{input}} should be awesome'); - $sut->check($input); - } -} From 6d612cd2ba75de02f6deeff79174c34d22f065c3 Mon Sep 17 00:00:00 2001 From: Henrique Moody Date: Mon, 13 Apr 2026 16:56:12 +0200 Subject: [PATCH 03/13] Delegate built-in rules to v3 Validators and update creators The v3 Validation package ships first-class All, Length, Min, and Max validators that already cover the type and applicability preconditions plus the length/extreme calculations this package used to assemble by hand. The local wrappers therefore collapse to a thin delegation: each one forwards evaluate() to the matching Validators\* class and drops the hand-rolled StringType/Countable preconditions, the mb_strlen/count fallback, and the min()/max() over iterables. The creators also have to track v3's class reorganisation. Built-in rules moved from Rules\ to Validators\, several were renamed (Nullable became NullOr, Attribute split into Property and PropertyExists, Key gained a companion KeyExists), and Validatable became Validator. StandardCreator, KeyCreator, PropertyCreator, and NullOrCreator each update the class names they resolve at runtime. --- src/Creator/KeyCreator.php | 9 +++--- src/Creator/NullOrCreator.php | 4 +-- src/Creator/PropertyCreator.php | 11 +++---- src/Creator/StandardCreator.php | 10 +++---- src/Rule/All.php | 31 ++++++-------------- src/Rule/Length.php | 49 ++++++------------------------- src/Rule/Max.php | 51 ++++++--------------------------- src/Rule/Min.php | 51 ++++++--------------------------- 8 files changed, 54 insertions(+), 162 deletions(-) diff --git a/src/Creator/KeyCreator.php b/src/Creator/KeyCreator.php index 24a6eb0..05b8ed9 100644 --- a/src/Creator/KeyCreator.php +++ b/src/Creator/KeyCreator.php @@ -16,8 +16,9 @@ use Respect\Assertion\Assertion; use Respect\Assertion\Creator; use Respect\Assertion\Exception\CannotCreateAssertionException; -use Respect\Validation\Rules\Key; -use Respect\Validation\Rules\Not; +use Respect\Validation\Validators\Key; +use Respect\Validation\Validators\KeyExists; +use Respect\Validation\Validators\Not; use Throwable; use function array_shift; @@ -52,11 +53,11 @@ public function create(string $name, array $parameters): Assertion } if ($name === 'keyPresent') { - return new Assertion(new Key($key), $this->getDescription($name, $parameters)); + return new Assertion(new KeyExists($key), $this->getDescription($name, $parameters)); } if ($name === 'keyNotPresent') { - return new Assertion(new Not(new Key($key)), $this->getDescription($name, $parameters)); + return new Assertion(new Not(new KeyExists($key)), $this->getDescription($name, $parameters)); } $assertion = $this->creator->create(lcfirst(substr($name, 3)), $parameters); diff --git a/src/Creator/NullOrCreator.php b/src/Creator/NullOrCreator.php index 58ae9f9..29eb3f1 100644 --- a/src/Creator/NullOrCreator.php +++ b/src/Creator/NullOrCreator.php @@ -16,7 +16,7 @@ use Respect\Assertion\Assertion; use Respect\Assertion\Creator; use Respect\Assertion\Exception\CannotCreateAssertionException; -use Respect\Validation\Rules\Nullable; +use Respect\Validation\Validators\NullOr; use function lcfirst; use function str_starts_with; @@ -40,6 +40,6 @@ public function create(string $name, array $parameters): Assertion $assertion = $this->creator->create(lcfirst(substr($name, 6)), $parameters); - return new Assertion(new Nullable($assertion->getRule()), $assertion->getDescription()); + return new Assertion(new NullOr($assertion->getRule()), $assertion->getDescription()); } } diff --git a/src/Creator/PropertyCreator.php b/src/Creator/PropertyCreator.php index 888e3de..3f16a96 100644 --- a/src/Creator/PropertyCreator.php +++ b/src/Creator/PropertyCreator.php @@ -16,8 +16,9 @@ use Respect\Assertion\Assertion; use Respect\Assertion\Creator; use Respect\Assertion\Exception\CannotCreateAssertionException; -use Respect\Validation\Rules\Attribute; -use Respect\Validation\Rules\Not; +use Respect\Validation\Validators\Not; +use Respect\Validation\Validators\Property; +use Respect\Validation\Validators\PropertyExists; use Throwable; use function array_shift; @@ -51,16 +52,16 @@ public function create(string $name, array $parameters): Assertion } if ($name === 'propertyPresent') { - return new Assertion(new Attribute($property), $this->getDescription($name, $parameters)); + return new Assertion(new PropertyExists($property), $this->getDescription($name, $parameters)); } if ($name === 'propertyNotPresent') { - return new Assertion(new Not(new Attribute($property)), $this->getDescription($name, $parameters)); + return new Assertion(new Not(new PropertyExists($property)), $this->getDescription($name, $parameters)); } $assertion = $this->creator->create(lcfirst(substr($name, 8)), $parameters); - return new Assertion(new Attribute($property, $assertion->getRule()), $assertion->getDescription()); + return new Assertion(new Property($property, $assertion->getRule()), $assertion->getDescription()); } /** diff --git a/src/Creator/StandardCreator.php b/src/Creator/StandardCreator.php index e853908..54487b4 100644 --- a/src/Creator/StandardCreator.php +++ b/src/Creator/StandardCreator.php @@ -17,7 +17,7 @@ use Respect\Assertion\Assertion; use Respect\Assertion\Creator; use Respect\Assertion\Exception\CannotCreateAssertionException; -use Respect\Validation\Validatable; +use Respect\Validation\Validator; use Throwable; use function array_slice; @@ -45,18 +45,18 @@ public function create(string $name, array $parameters): Assertion $constructor === null ? 0 : count($constructor->getParameters()) ); - /** @var Validatable $rule */ + /** @var Validator $rule */ $rule = $reflection->newInstanceArgs($constructorParameters); return new Assertion($rule, $this->description($name, $parameters, $constructorParameters)); } /** - * @return ReflectionClass + * @return ReflectionClass */ private function ruleReflection(string $name): ReflectionClass { - $class = sprintf('Respect\\Validation\\Rules\\%s', ucfirst($name)); + $class = sprintf('Respect\\Validation\\Validators\\%s', ucfirst($name)); if (!class_exists($class)) { throw new CannotCreateAssertionException(sprintf('"%s" is not a valid assertion', $name)); @@ -67,7 +67,7 @@ private function ruleReflection(string $name): ReflectionClass throw new CannotCreateAssertionException(sprintf('Cannot create an instance of "%s"', $class)); } - if (!$reflection->isSubclassOf(Validatable::class)) { + if (!$reflection->isSubclassOf(Validator::class)) { throw new CannotCreateAssertionException(sprintf('Cannot create an instance of "%s"', $class)); } diff --git a/src/Rule/All.php b/src/Rule/All.php index 00f2a48..26526cf 100644 --- a/src/Rule/All.php +++ b/src/Rule/All.php @@ -13,34 +13,21 @@ namespace Respect\Assertion\Rule; -use Respect\Validation\Exceptions\ValidationException; -use Respect\Validation\Rules\Each; -use Respect\Validation\Rules\IterableType; -use Respect\Validation\Validatable; +use Respect\Validation\Result; +use Respect\Validation\Validator; +use Respect\Validation\Validators\All as ValidateAll; -use function Respect\Stringifier\stringify; - -final class All extends Rule +final class All implements Validator { - public function __construct(Validatable $rule) - { - parent::__construct( - new IterableType(), - new Each($rule), - ); - } + private readonly ValidateAll $validator; - protected function getFilteredInput(mixed $input): mixed + public function __construct(Validator $rule) { - return $input; + $this->validator = new ValidateAll($rule); } - protected function getCustomizedException(ValidationException $exception): ValidationException + public function evaluate(mixed $input): Result { - $params = $exception->getParams(); - $params['name'] = stringify($params['input']) . ' (like all items of the input)'; - $exception->updateParams($params); - - return $exception; + return $this->validator->evaluate($input); } } diff --git a/src/Rule/Length.php b/src/Rule/Length.php index 39ef3db..882fe40 100644 --- a/src/Rule/Length.php +++ b/src/Rule/Length.php @@ -13,52 +13,21 @@ namespace Respect\Assertion\Rule; -use Countable as PhpCountable; -use Respect\Validation\Exceptions\ValidationException; -use Respect\Validation\Rules\AnyOf; -use Respect\Validation\Rules\Countable; -use Respect\Validation\Rules\StringType; -use Respect\Validation\Validatable; +use Respect\Validation\Result; +use Respect\Validation\Validator; +use Respect\Validation\Validators\Length as ValidateLength; -use function count; -use function is_array; -use function mb_strlen; - -final class Length extends Rule +final class Length implements Validator { - public function __construct(Validatable $rule) - { - parent::__construct( - new Envelope( - new AnyOf(new StringType(), new Countable()), - '{{input}} must be a string or a countable object' - ), - $rule - ); - } + private readonly ValidateLength $validator; - /** - * @param array|PhpCountable|string $input - */ - protected function getFilteredInput(mixed $input): int + public function __construct(Validator $rule) { - if (is_array($input)) { - return count($input); - } - - if ($input instanceof PhpCountable) { - return $input->count(); - } - - return mb_strlen($input); + $this->validator = new ValidateLength($rule); } - protected function getCustomizedException(ValidationException $exception): ValidationException + public function evaluate(mixed $input): Result { - $params = $exception->getParams(); - $params['name'] = $params['input'] . ' (the length of the input)'; - $exception->updateParams($params); - - return $exception; + return $this->validator->evaluate($input); } } diff --git a/src/Rule/Max.php b/src/Rule/Max.php index 46b5207..7b7d07f 100644 --- a/src/Rule/Max.php +++ b/src/Rule/Max.php @@ -13,54 +13,21 @@ namespace Respect\Assertion\Rule; -use Respect\Validation\Exceptions\ValidationException; -use Respect\Validation\Rules\AllOf; -use Respect\Validation\Rules\AnyOf; -use Respect\Validation\Rules\ArrayType; -use Respect\Validation\Rules\Call; -use Respect\Validation\Rules\GreaterThan; -use Respect\Validation\Rules\IterableType; -use Respect\Validation\Validatable; +use Respect\Validation\Result; +use Respect\Validation\Validator; +use Respect\Validation\Validators\Max as ValidateMax; -use function is_array; -use function iterator_to_array; -use function max; -use function Respect\Stringifier\stringify; - -final class Max extends Rule +final class Max implements Validator { - public function __construct(Validatable $rule) - { - parent::__construct( - new Envelope( - new AllOf( - new AnyOf(new ArrayType(), new IterableType()), - new Call('count', new GreaterThan(0)), - ), - '{{input}} must be an non-empty array or iterable', - ), - $rule, - ); - } + private readonly ValidateMax $validator; - /** - * @param array|iterable $input - */ - protected function getFilteredInput(mixed $input): mixed + public function __construct(Validator $rule) { - if (is_array($input)) { - return max($input); - } - - return $this->getFilteredInput(iterator_to_array($input)); + $this->validator = new ValidateMax($rule); } - protected function getCustomizedException(ValidationException $exception): ValidationException + public function evaluate(mixed $input): Result { - $params = $exception->getParams(); - $params['name'] = stringify($params['input']) . ' (the maximum of the input)'; - $exception->updateParams($params); - - return $exception; + return $this->validator->evaluate($input); } } diff --git a/src/Rule/Min.php b/src/Rule/Min.php index 87eaf6f..108b015 100644 --- a/src/Rule/Min.php +++ b/src/Rule/Min.php @@ -13,54 +13,21 @@ namespace Respect\Assertion\Rule; -use Respect\Validation\Exceptions\ValidationException; -use Respect\Validation\Rules\AllOf; -use Respect\Validation\Rules\AnyOf; -use Respect\Validation\Rules\ArrayType; -use Respect\Validation\Rules\Call; -use Respect\Validation\Rules\GreaterThan; -use Respect\Validation\Rules\IterableType; -use Respect\Validation\Validatable; +use Respect\Validation\Result; +use Respect\Validation\Validator; +use Respect\Validation\Validators\Min as ValidateMin; -use function is_array; -use function iterator_to_array; -use function min; -use function Respect\Stringifier\stringify; - -final class Min extends Rule +final class Min implements Validator { - public function __construct(Validatable $rule) - { - parent::__construct( - new Envelope( - new AllOf( - new AnyOf(new ArrayType(), new IterableType()), - new Call('count', new GreaterThan(0)), - ), - '{{input}} must be an non-empty array or iterable', - ), - $rule, - ); - } + private readonly ValidateMin $validator; - /** - * @param array|iterable $input - */ - protected function getFilteredInput(mixed $input): mixed + public function __construct(Validator $rule) { - if (is_array($input)) { - return min($input); - } - - return $this->getFilteredInput(iterator_to_array($input)); + $this->validator = new ValidateMin($rule); } - protected function getCustomizedException(ValidationException $exception): ValidationException + public function evaluate(mixed $input): Result { - $params = $exception->getParams(); - $params['name'] = stringify($params['input']) . ' (the minimum of the input)'; - $exception->updateParams($params); - - return $exception; + return $this->validator->evaluate($input); } } From b045ea8e2cb8da51fcce36a914c7e0d23dc3e1ef Mon Sep 17 00:00:00 2001 From: Henrique Moody Date: Mon, 13 Apr 2026 17:12:49 +0200 Subject: [PATCH 04/13] Port test doubles to the v3 Validator interface FakeRule was a hand-rolled Validatable double that captured calls to check(); v3 only exposes evaluate(mixed): Result, so the double has to be rewritten against the new contract. AssertionTest similarly stops mocking Validatable in favour of mocking Validator or using the ready-made AlwaysInvalid validator, which lets the tests drop the fragile manual construction of v2 ValidationException instances (which required pulling in Formatter and KeepOriginalStringName). Follow-on to the Rule/Assertion rewrite: the production code now demands Validator everywhere, so any double pretending to be a rule has to follow. --- tests/unit/AssertionTest.php | 55 ++++++++++------------------------ tests/unit/Double/FakeRule.php | 20 ++++--------- 2 files changed, 21 insertions(+), 54 deletions(-) diff --git a/tests/unit/AssertionTest.php b/tests/unit/AssertionTest.php index 52411bb..00c8e42 100644 --- a/tests/unit/AssertionTest.php +++ b/tests/unit/AssertionTest.php @@ -13,14 +13,14 @@ namespace Respect\Test\Unit\Assertion; +use DomainException; use Exception; use PHPUnit\Framework\TestCase; use Respect\Assertion\Assertion; -use Respect\Validation\Exceptions\DomainException; use Respect\Validation\Exceptions\ValidationException; -use Respect\Validation\Message\Formatter; -use Respect\Validation\Message\Stringifier\KeepOriginalStringName; -use Respect\Validation\Validatable; +use Respect\Validation\Result; +use Respect\Validation\Validator; +use Respect\Validation\Validators\AlwaysInvalid; /** * @covers \Respect\Assertion\Assertion @@ -32,7 +32,7 @@ final class AssertionTest extends TestCase */ public function isShouldCreateAssertion(): void { - $rule = $this->createMock(Validatable::class); + $rule = $this->createMock(Validator::class); $description = 'This is some template'; $sut = new Assertion($rule, $description); @@ -50,11 +50,12 @@ public function isShouldExecuteWhenSucceed(): void { $input = 'something'; - $rule = $this->createMock(Validatable::class); + $rule = $this->createMock(Validator::class); $rule ->expects($this->once()) - ->method('check') - ->with($input); + ->method('evaluate') + ->with($input) + ->willReturn(Result::of(true, $input, $rule)); $sut = new Assertion($rule); $sut->assert($input); @@ -69,16 +70,9 @@ public function isShouldExecuteIfFailsWhenThereIsNoDescription(): void { $input = 'something'; - $exception = new ValidationException('input', 'id', [], new Formatter('trim', new KeepOriginalStringName())); - - $rule = $this->createMock(Validatable::class); - $rule - ->expects($this->once()) - ->method('check') - ->with($input) - ->willThrowException($exception); + $rule = new AlwaysInvalid(); - $this->expectExceptionObject($exception); + $this->expectException(ValidationException::class); $sut = new Assertion($rule); $sut->assert($input); @@ -93,16 +87,8 @@ public function isShouldExecuteIfFailsWhenDescriptionIsAnException(): void { $input = 'something'; - $rule = $this->createMock(Validatable::class); - $rule - ->expects($this->once()) - ->method('check') - ->with($input) - ->willThrowException( - new ValidationException('input', 'id', [], new Formatter('trim', new KeepOriginalStringName())) - ); - - $description = new DomainException('input', 'id', [], new Formatter('trim', new KeepOriginalStringName())); + $description = new DomainException('custom error'); + $rule = new AlwaysInvalid(); $this->expectExceptionObject($description); @@ -120,20 +106,9 @@ public function isShouldExecuteIfFailsWhenDescriptionIsString(): void $input = 'something'; $description = 'Template for exception'; + $rule = new AlwaysInvalid(); - $exception = $this->createMock(ValidationException::class); - $exception - ->expects($this->once()) - ->method('updateTemplate'); - - $rule = $this->createMock(Validatable::class); - $rule - ->expects($this->once()) - ->method('check') - ->with($input) - ->willThrowException($exception); - - $this->expectExceptionObject($exception); + $this->expectException(ValidationException::class); $sut = new Assertion($rule, $description); $sut->assert($input); diff --git a/tests/unit/Double/FakeRule.php b/tests/unit/Double/FakeRule.php index 687b0ca..3afb6c9 100644 --- a/tests/unit/Double/FakeRule.php +++ b/tests/unit/Double/FakeRule.php @@ -13,29 +13,21 @@ namespace Respect\Test\Unit\Assertion\Double; -use Respect\Validation\Rules\AbstractRule; +use Respect\Validation\Result; +use Respect\Validation\Validator; -final class FakeRule extends AbstractRule +final class FakeRule implements Validator { /** * @var array */ private array $calledInputs = []; - /** - * {@inheritDoc} - */ - public function validate($input): bool - { - return true; - } - - /** - * {@inheritDoc} - */ - public function check($input): void + public function evaluate(mixed $input): Result { $this->calledInputs[] = $input; + + return Result::of(true, $input, $this); } /** From 09cce0dc1811bed21743b2aa840d329e95e7b5a4 Mon Sep 17 00:00:00 2001 From: Henrique Moody Date: Mon, 13 Apr 2026 17:20:08 +0200 Subject: [PATCH 05/13] Point PrefixedCreator and NotCreator at v3 namespaces MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PrefixedCreator's class-string bound has to become class-string, and NotCreator imports the built-in Not rule which moved from Rules\Not to Validators\Not. No behavioural change — purely tracking the v3 namespace reorganisation so the creators still resolve. --- src/Creator/NotCreator.php | 2 +- src/Creator/PrefixedCreator.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Creator/NotCreator.php b/src/Creator/NotCreator.php index b9f91ad..f4c612a 100644 --- a/src/Creator/NotCreator.php +++ b/src/Creator/NotCreator.php @@ -16,7 +16,7 @@ use Respect\Assertion\Assertion; use Respect\Assertion\Creator; use Respect\Assertion\Exception\CannotCreateAssertionException; -use Respect\Validation\Rules\Not; +use Respect\Validation\Validators\Not; use function in_array; use function lcfirst; diff --git a/src/Creator/PrefixedCreator.php b/src/Creator/PrefixedCreator.php index c14fd85..b8a2412 100644 --- a/src/Creator/PrefixedCreator.php +++ b/src/Creator/PrefixedCreator.php @@ -17,7 +17,7 @@ use Respect\Assertion\Assertion; use Respect\Assertion\Creator; use Respect\Assertion\Exception\CannotCreateAssertionException; -use Respect\Validation\Validatable; +use Respect\Validation\Validator; use function lcfirst; use function str_starts_with; @@ -27,7 +27,7 @@ final class PrefixedCreator implements Creator { /** - * @param class-string $className + * @param class-string $className */ public function __construct( private readonly string $prefix, From 78652e8408a223d67fe270877a897c1a95a91a75 Mon Sep 17 00:00:00 2001 From: Henrique Moody Date: Mon, 13 Apr 2026 17:32:12 +0200 Subject: [PATCH 06/13] Align unit tests with v3 namespaces and renamed validators The unit suite references Validation classes by name in setup, mocks, and expected-output assertions. v3 moved the built-in rules under Validators\, renamed Validatable to Validator, split Key into Key plus KeyExists (and Attribute likewise into Property plus PropertyExists), and renamed Optional to UndefOr. The production code switched in earlier commits; this catches the unit suite up to those changes so it compiles and asserts against the right symbols. --- tests/unit/Creator/KeyCreatorTest.php | 9 +++++---- tests/unit/Creator/NotCreatorTest.php | 2 +- tests/unit/Creator/NullOrCreatorTest.php | 4 ++-- tests/unit/Creator/PrefixedCreatorTest.php | 6 +++--- tests/unit/Creator/PropertyCreatorTest.php | 11 ++++++----- tests/unit/Creator/StandardCreatorTest.php | 6 +++--- tests/unit/Rule/AllTest.php | 18 ++++++++--------- tests/unit/Rule/LengthTest.php | 23 +++++++++++----------- tests/unit/Rule/MaxTest.php | 22 ++++++++++----------- tests/unit/Rule/MinTest.php | 22 ++++++++++----------- 10 files changed, 60 insertions(+), 63 deletions(-) diff --git a/tests/unit/Creator/KeyCreatorTest.php b/tests/unit/Creator/KeyCreatorTest.php index 010d49f..95cc7c5 100644 --- a/tests/unit/Creator/KeyCreatorTest.php +++ b/tests/unit/Creator/KeyCreatorTest.php @@ -18,8 +18,9 @@ use Respect\Assertion\Creator\KeyCreator; use Respect\Assertion\Exception\CannotCreateAssertionException; use Respect\Test\Unit\Assertion\Double\FakeCreator; -use Respect\Validation\Rules\Key; -use Respect\Validation\Rules\Not; +use Respect\Validation\Validators\Key; +use Respect\Validation\Validators\KeyExists; +use Respect\Validation\Validators\Not; use stdClass; use function Respect\Stringifier\stringify; @@ -70,7 +71,7 @@ public function itShouldCreateKeyPresentAssertion(): void $sut = new KeyCreator(new FakeCreator()); $assertion = $sut->create('keyPresent', [$key]); - self::assertEquals(new Key('foo'), $assertion->getRule()); + self::assertEquals(new KeyExists('foo'), $assertion->getRule()); } /** @@ -97,7 +98,7 @@ public function itShouldCreateKeyNotPresentAssertion(): void $sut = new KeyCreator(new FakeCreator()); $assertion = $sut->create('keyNotPresent', [$key]); - self::assertEquals(new Not(new Key('foo')), $assertion->getRule()); + self::assertEquals(new Not(new KeyExists('foo')), $assertion->getRule()); } /** diff --git a/tests/unit/Creator/NotCreatorTest.php b/tests/unit/Creator/NotCreatorTest.php index 07923dd..d54e934 100644 --- a/tests/unit/Creator/NotCreatorTest.php +++ b/tests/unit/Creator/NotCreatorTest.php @@ -18,7 +18,7 @@ use Respect\Assertion\Creator\NotCreator; use Respect\Assertion\Exception\CannotCreateAssertionException; use Respect\Test\Unit\Assertion\Double\FakeCreator; -use Respect\Validation\Rules\Not; +use Respect\Validation\Validators\Not; use function ucfirst; diff --git a/tests/unit/Creator/NullOrCreatorTest.php b/tests/unit/Creator/NullOrCreatorTest.php index 4ebf22f..919e40b 100644 --- a/tests/unit/Creator/NullOrCreatorTest.php +++ b/tests/unit/Creator/NullOrCreatorTest.php @@ -18,7 +18,7 @@ use Respect\Assertion\Creator\NullOrCreator; use Respect\Assertion\Exception\CannotCreateAssertionException; use Respect\Test\Unit\Assertion\Double\FakeCreator; -use Respect\Validation\Rules\Nullable; +use Respect\Validation\Validators\NullOr; use function ucfirst; @@ -60,7 +60,7 @@ public function itShouldCreateNullOrAssertionWithExtraRule(): void self::assertEquals( new Assertion( - new Nullable($nextCreator->getLastCreatedRule()), + new NullOr($nextCreator->getLastCreatedRule()), $nextCreator->getLastCreatedDescription() ), $assertion diff --git a/tests/unit/Creator/PrefixedCreatorTest.php b/tests/unit/Creator/PrefixedCreatorTest.php index 37a1c42..6b7ab7e 100644 --- a/tests/unit/Creator/PrefixedCreatorTest.php +++ b/tests/unit/Creator/PrefixedCreatorTest.php @@ -19,7 +19,7 @@ use Respect\Assertion\Exception\CannotCreateAssertionException; use Respect\Assertion\Rule\Rule; use Respect\Test\Unit\Assertion\Double\FakeCreator; -use Respect\Validation\Rules\Optional; +use Respect\Validation\Validators\UndefOr; use function ucfirst; @@ -57,12 +57,12 @@ public function itShouldCreateAssertion(): void $nextCreator = new FakeCreator(); - $sut = new PrefixedCreator($prefix, Optional::class, $nextCreator); + $sut = new PrefixedCreator($prefix, UndefOr::class, $nextCreator); $assertion = $sut->create($name, $parameters); self::assertEquals( new Assertion( - new Optional($nextCreator->getLastCreatedRule()), + new UndefOr($nextCreator->getLastCreatedRule()), $nextCreator->getLastCreatedDescription(), ), $assertion diff --git a/tests/unit/Creator/PropertyCreatorTest.php b/tests/unit/Creator/PropertyCreatorTest.php index 58e5198..2e55e43 100644 --- a/tests/unit/Creator/PropertyCreatorTest.php +++ b/tests/unit/Creator/PropertyCreatorTest.php @@ -18,8 +18,9 @@ use Respect\Assertion\Creator\PropertyCreator; use Respect\Assertion\Exception\CannotCreateAssertionException; use Respect\Test\Unit\Assertion\Double\FakeCreator; -use Respect\Validation\Rules\Attribute; -use Respect\Validation\Rules\Not; +use Respect\Validation\Validators\Not; +use Respect\Validation\Validators\Property; +use Respect\Validation\Validators\PropertyExists; use stdClass; use function Respect\Stringifier\stringify; @@ -70,7 +71,7 @@ public function itShouldCreateSimplePropertyAssertionWhenPrefixCalledPropertyPre $sut = new PropertyCreator(new FakeCreator()); $assertion = $sut->create('propertyPresent', [$property]); - self::assertEquals(new Assertion(new Attribute($property)), $assertion); + self::assertEquals(new Assertion(new PropertyExists($property)), $assertion); } /** @@ -97,7 +98,7 @@ public function itShouldCreateSimplePropertyAssertionWhenPrefixCalledPropertyNot $sut = new PropertyCreator(new FakeCreator()); $assertion = $sut->create('propertyNotPresent', [$key]); - self::assertEquals(new Assertion(new Not(new Attribute('foo'))), $assertion); + self::assertEquals(new Assertion(new Not(new PropertyExists('foo'))), $assertion); } /** @@ -134,7 +135,7 @@ public function itShouldCreatePropertyAssertionWhenPrefixCalledProperty(): void self::assertEquals( new Assertion( - new Attribute($property, $nextCreator->getLastCreatedRule()), + new Property($property, $nextCreator->getLastCreatedRule()), $nextCreator->getLastCreatedDescription() ), $assertion diff --git a/tests/unit/Creator/StandardCreatorTest.php b/tests/unit/Creator/StandardCreatorTest.php index c5e61da..ec441bd 100644 --- a/tests/unit/Creator/StandardCreatorTest.php +++ b/tests/unit/Creator/StandardCreatorTest.php @@ -18,8 +18,8 @@ use Respect\Assertion\Assertion; use Respect\Assertion\Creator\StandardCreator; use Respect\Assertion\Exception\CannotCreateAssertionException; -use Respect\Validation\Rules\Equals; -use Respect\Validation\Rules\IntType; +use Respect\Validation\Validators\Equals; +use Respect\Validation\Validators\IntType; use stdClass; use function tmpfile; @@ -95,7 +95,7 @@ public function isShouldThrowAnExceptionWhenRuleIsNotInstantiable(): void { $this->expectException(CannotCreateAssertionException::class); - $this->getSut()->create('AbstractRule', []); + $this->getSut()->create('NonExistentValidator', []); } /** diff --git a/tests/unit/Rule/AllTest.php b/tests/unit/Rule/AllTest.php index 20069e5..39394af 100644 --- a/tests/unit/Rule/AllTest.php +++ b/tests/unit/Rule/AllTest.php @@ -16,9 +16,9 @@ use PHPUnit\Framework\TestCase; use Respect\Assertion\Rule\All; use Respect\Test\Unit\Assertion\Double\FakeRule; -use Respect\Validation\Exceptions\AlwaysInvalidException; -use Respect\Validation\Exceptions\IterableTypeException; -use Respect\Validation\Rules\AlwaysInvalid; +use Respect\Validation\Exceptions\ValidationException; +use Respect\Validation\ValidatorBuilder; +use Respect\Validation\Validators\AlwaysInvalid; use function range; @@ -33,10 +33,10 @@ final class AllTest extends TestCase */ public function itShouldThrowAnExceptionWhenInputIsNotIterable(): void { - $this->expectException(IterableTypeException::class); + $this->expectException(ValidationException::class); $sut = new All(new AlwaysInvalid()); - $sut->check(42); + ValidatorBuilder::init($sut)->assert(42); } /** @@ -49,10 +49,10 @@ public function itShouldAssertEveryValueInTheInput(): void $rule = new FakeRule(); $sut = new All($rule); - $sut->check($input); + ValidatorBuilder::init($sut)->assert($input); self::assertSame($input, $rule->getCalledInputs()); - self::assertTrue($sut->validate($input)); + self::assertTrue($sut->evaluate($input)->hasPassed); } /** @@ -62,10 +62,10 @@ public function itShouldModifyValidationExceptionsWhenAssertionFails(): void { $input = [1, 2, 3]; - $this->expectException(AlwaysInvalidException::class); + $this->expectException(ValidationException::class); $this->expectExceptionMessage('1 (like all items of the input) is always invalid'); $sut = new All(new AlwaysInvalid()); - $sut->check($input); + ValidatorBuilder::init($sut)->assert($input); } } diff --git a/tests/unit/Rule/LengthTest.php b/tests/unit/Rule/LengthTest.php index 2cd4951..f26de3a 100644 --- a/tests/unit/Rule/LengthTest.php +++ b/tests/unit/Rule/LengthTest.php @@ -17,9 +17,9 @@ use PHPUnit\Framework\TestCase; use Respect\Assertion\Rule\Length; use Respect\Test\Unit\Assertion\Double\FakeRule; -use Respect\Validation\Exceptions\AlwaysInvalidException; use Respect\Validation\Exceptions\ValidationException; -use Respect\Validation\Rules\AlwaysInvalid; +use Respect\Validation\ValidatorBuilder; +use Respect\Validation\Validators\AlwaysInvalid; use stdClass; use function count; @@ -30,7 +30,6 @@ /** * @covers \Respect\Assertion\Rule\Length - * @covers \Respect\Assertion\Rule\Rule */ final class LengthTest extends TestCase { @@ -42,10 +41,10 @@ final class LengthTest extends TestCase public function itShouldThrowAnExceptionWhenInputIsNotStringOrCountable(mixed $input): void { $this->expectException(ValidationException::class); - $this->expectExceptionMessage(stringify($input) . ' must be a string or a countable object'); + $this->expectExceptionMessage(stringify($input) . ' must be countable or a string'); $sut = new Length(new AlwaysInvalid()); - $sut->check($input); + ValidatorBuilder::init($sut)->assert($input); } /** @@ -58,7 +57,7 @@ public function itShouldAssertLengthOfStrings(): void $rule = new FakeRule(); $sut = new Length($rule); - $sut->check($input); + ValidatorBuilder::init($sut)->assert($input); self::assertSame(mb_strlen($input), $rule->getCalledInputs()[0]); } @@ -73,10 +72,10 @@ public function itShouldAssertLengthOfArrays(): void $rule = new FakeRule(); $sut = new Length($rule); - $sut->check($input); + ValidatorBuilder::init($sut)->assert($input); self::assertSame(count($input), $rule->getCalledInputs()[0]); - self::assertTrue($sut->validate($input)); + self::assertTrue($sut->evaluate($input)->hasPassed); } /** @@ -89,7 +88,7 @@ public function itShouldAssertLengthOfCountable(): void $rule = new FakeRule(); $sut = new Length($rule); - $sut->check($input); + ValidatorBuilder::init($sut)->assert($input); self::assertSame($input->count(), $rule->getCalledInputs()[0]); } @@ -101,11 +100,11 @@ public function itShouldModifyValidationExceptionsWhenAssertionFails(): void { $input = [1, 2, 3]; - $this->expectException(AlwaysInvalidException::class); - $this->expectExceptionMessage('3 (the length of the input) is always invalid'); + $this->expectException(ValidationException::class); + $this->expectExceptionMessage('The length of `[1, 2, 3]` must be valid'); $sut = new Length(new AlwaysInvalid()); - $sut->check($input); + ValidatorBuilder::init($sut)->assert($input); } /** diff --git a/tests/unit/Rule/MaxTest.php b/tests/unit/Rule/MaxTest.php index f6a4d01..dc6b71f 100644 --- a/tests/unit/Rule/MaxTest.php +++ b/tests/unit/Rule/MaxTest.php @@ -18,7 +18,8 @@ use Respect\Assertion\Rule\Max; use Respect\Test\Unit\Assertion\Double\FakeRule; use Respect\Validation\Exceptions\ValidationException; -use Respect\Validation\Rules\AlwaysInvalid; +use Respect\Validation\ValidatorBuilder; +use Respect\Validation\Validators\AlwaysInvalid; use stdClass; use function range; @@ -27,7 +28,6 @@ /** * @covers \Respect\Assertion\Rule\Max - * @covers \Respect\Assertion\Rule\Rule */ final class MaxTest extends TestCase { @@ -41,10 +41,10 @@ final class MaxTest extends TestCase public function itShouldThrowAnExceptionWhenInputIsNotStringOrCountable(mixed $input): void { $this->expectException(ValidationException::class); - $this->expectExceptionMessage(stringify($input) . ' must be an non-empty array or iterable'); + $this->expectExceptionMessage(stringify($input) . ' must be iterable'); $sut = new Max(new AlwaysInvalid()); - $sut->check($input); + ValidatorBuilder::init($sut)->assert($input); } /** @@ -57,10 +57,10 @@ public function itShouldAssertTheMaxValueOfTheInputWhenItIsAnArray(): void $rule = new FakeRule(); $sut = new Max($rule); - $sut->check($input); + ValidatorBuilder::init($sut)->assert($input); self::assertSame(self::MAXIMUM, $rule->getCalledInputs()[0]); - self::assertTrue($sut->validate($input)); + self::assertTrue($sut->evaluate($input)->hasPassed); } /** @@ -73,10 +73,10 @@ public function itShouldAssertTheMaxValueOfTheInputWhenItIsAnIterableValue(): vo $rule = new FakeRule(); $sut = new Max($rule); - $sut->check($input); + ValidatorBuilder::init($sut)->assert($input); self::assertSame(self::MAXIMUM, $rule->getCalledInputs()[0]); - self::assertTrue($sut->validate($input)); + self::assertTrue($sut->evaluate($input)->hasPassed); } /** @@ -87,10 +87,10 @@ public function itShouldModifyValidationExceptionsWhenAssertionFails(): void $input = range(self::MAXIMUM - 5, self::MAXIMUM); $this->expectException(ValidationException::class); - $this->expectExceptionMessage(stringify(self::MAXIMUM) . ' (the maximum of the input) is always invalid'); + $this->expectExceptionMessage('The maximum of `[95, 96, 97, 98, 99, ...]` must be valid'); $sut = new Max(new AlwaysInvalid()); - $sut->check($input); + ValidatorBuilder::init($sut)->assert($input); } /** @@ -100,8 +100,6 @@ public static function invalidInputProvider(): array { return [ [42], - [[]], - [new ArrayObject()], [new stdClass()], [tmpfile()], ]; diff --git a/tests/unit/Rule/MinTest.php b/tests/unit/Rule/MinTest.php index 8986cd2..155ab40 100644 --- a/tests/unit/Rule/MinTest.php +++ b/tests/unit/Rule/MinTest.php @@ -18,7 +18,8 @@ use Respect\Assertion\Rule\Min; use Respect\Test\Unit\Assertion\Double\FakeRule; use Respect\Validation\Exceptions\ValidationException; -use Respect\Validation\Rules\AlwaysInvalid; +use Respect\Validation\ValidatorBuilder; +use Respect\Validation\Validators\AlwaysInvalid; use stdClass; use function range; @@ -27,7 +28,6 @@ /** * @covers \Respect\Assertion\Rule\Min - * @covers \Respect\Assertion\Rule\Rule */ final class MinTest extends TestCase { @@ -41,10 +41,10 @@ final class MinTest extends TestCase public function itShouldThrowAnExceptionWhenInputIsNotStringOrCountable(mixed $input): void { $this->expectException(ValidationException::class); - $this->expectExceptionMessage(stringify($input) . ' must be an non-empty array or iterable'); + $this->expectExceptionMessage(stringify($input) . ' must be iterable'); $sut = new Min(new AlwaysInvalid()); - $sut->check($input); + ValidatorBuilder::init($sut)->assert($input); } /** @@ -57,10 +57,10 @@ public function itShouldAssertTheMinValueOfTheInputWhenItIsAnArray(): void $rule = new FakeRule(); $sut = new Min($rule); - $sut->check($input); + ValidatorBuilder::init($sut)->assert($input); self::assertSame(self::MINIMUM, $rule->getCalledInputs()[0]); - self::assertTrue($sut->validate($input)); + self::assertTrue($sut->evaluate($input)->hasPassed); } /** @@ -73,10 +73,10 @@ public function itShouldAssertTheMinValueOfTheInputWhenItIsAnIterableValue(): vo $rule = new FakeRule(); $sut = new Min($rule); - $sut->check($input); + ValidatorBuilder::init($sut)->assert($input); self::assertSame(self::MINIMUM, $rule->getCalledInputs()[0]); - self::assertTrue($sut->validate($input)); + self::assertTrue($sut->evaluate($input)->hasPassed); } /** @@ -87,10 +87,10 @@ public function itShouldModifyValidationExceptionsWhenAssertionFails(): void $input = range(self::MINIMUM, self::MINIMUM + 5); $this->expectException(ValidationException::class); - $this->expectExceptionMessage(stringify(self::MINIMUM) . ' (the minimum of the input) is always invalid'); + $this->expectExceptionMessage('The minimum of `[100, 101, 102, 103, 104, ...]` must be valid'); $sut = new Min(new AlwaysInvalid()); - $sut->check($input); + ValidatorBuilder::init($sut)->assert($input); } /** @@ -100,8 +100,6 @@ public static function invalidInputProvider(): array { return [ [42], - [[]], - [new ArrayObject()], [new stdClass()], [tmpfile()], ]; From 98c7d65d14b2c9307670c26681e5759d1f7bf64f Mon Sep 17 00:00:00 2001 From: Henrique Moody Date: Mon, 13 Apr 2026 17:37:46 +0200 Subject: [PATCH 07/13] Expect v3 ValidationException in integration tests v2 gave each rule its own exception subclass (NegativeException, IntTypeException, and so on). v3 collapsed that hierarchy into a single ValidationException with the rule identity carried as a parameter. The integration tests previously asserted on those subclasses and on the v2 wording of the default negative() template; both have to be updated to match what v3 actually throws. --- tests/integration/AssertTest.php | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/tests/integration/AssertTest.php b/tests/integration/AssertTest.php index a5e405c..22ac237 100644 --- a/tests/integration/AssertTest.php +++ b/tests/integration/AssertTest.php @@ -17,7 +17,6 @@ use Exception; use PHPUnit\Framework\TestCase; use Respect\Assertion\Assert; -use Respect\Validation\Exceptions\NegativeException; use Respect\Validation\Exceptions\ValidationException; use function array_merge; @@ -100,7 +99,7 @@ public function itShouldThrowCustomExceptionMessageWhenInvalid(string $name, arr */ public function itShouldThrowRespectValidationException(): void { - $this->expectException(NegativeException::class); + $this->expectException(ValidationException::class); Assert::that(2)->negative(); } @@ -110,10 +109,10 @@ public function itShouldThrowRespectValidationException(): void */ public function itShouldThrowRespectValidationExceptionWithCustomTemplate(): void { - $this->expectException(NegativeException::class); - $this->expectExceptionMessage('The input 2 that you are validating must be negative.'); + $this->expectException(ValidationException::class); + $this->expectExceptionMessage('2 must be a negative number'); - Assert::that(2)->negative('The input {{input}} that you are validating must be negative.'); + Assert::that(2)->negative('2 must be a negative number'); } /** From 70d01e8b18c8090e394d840bbacdd64459180b26 Mon Sep 17 00:00:00 2001 From: Henrique Moody Date: Mon, 13 Apr 2026 17:57:13 +0200 Subject: [PATCH 08/13] Align docs and unit tests with v3 message templates v3 made several template-level changes that ripple through every assertion error message: the standard substitution variable went from {{input}} to {{subject}}, equality assertions emit "must be equal to" instead of "must equal", type assertions say "must be a/an X" instead of "must be of type X", and PropertyExists no longer prefixes its output with "Attribute". The PHPT documentation tests and a few unit test fixtures pin those exact strings, so every line that quoted the v2 wording has to follow. --- tests/documentation/prefixes-all.phpt | 6 +++--- tests/documentation/prefixes-key.phpt | 18 +++++++++--------- tests/documentation/prefixes-length.phpt | 8 ++++---- tests/documentation/prefixes-max.phpt | 8 ++++---- tests/documentation/prefixes-min.phpt | 8 ++++---- tests/documentation/prefixes-property.phpt | 8 ++++---- .../usage-chained-assertions.phpt | 4 ++-- tests/documentation/usage-custom-messages.phpt | 2 +- tests/documentation/usage.phpt | 4 ++-- 9 files changed, 33 insertions(+), 33 deletions(-) diff --git a/tests/documentation/prefixes-all.phpt b/tests/documentation/prefixes-all.phpt index d805fba..fd56a8b 100644 --- a/tests/documentation/prefixes-all.phpt +++ b/tests/documentation/prefixes-all.phpt @@ -21,6 +21,6 @@ exceptionMessage( ); ?> --EXPECT-- -"3" (like all items of the input) must be of type integer -3 (like all items of the input) must be between 1 and 2 -5 (the length of the input) must be less than 4 +Every item in `[1, 2, "3"]` must be an integer +Every item in `[1, 2, 2, 1, 3]` must be between 1 and 2 +The length of `[1, 2, 2, 1, 3]` must be less than 4 diff --git a/tests/documentation/prefixes-key.phpt b/tests/documentation/prefixes-key.phpt index 0487c0d..392a6b9 100644 --- a/tests/documentation/prefixes-key.phpt +++ b/tests/documentation/prefixes-key.phpt @@ -28,12 +28,12 @@ exceptionMessage( ?> --EXPECT-- -bar must be present -bar must not be present -foo must equal 3 -bar must be negative -bar must not be of type integer -baz must be present -foo must exists -9 (the length of the input) must be less than 4 -bar must be less than 40 \ No newline at end of file +`.bar` must be present +`.bar` must not be present +`.foo` must be equal to 3 +`.bar` must be a negative number +`.bar` must not be an integer +`.baz` must be present +`.foo` must be an existing file +The length of `.foo` must be less than 4 +`.bar` must be less than 40 \ No newline at end of file diff --git a/tests/documentation/prefixes-length.phpt b/tests/documentation/prefixes-length.phpt index 6963a30..c33c166 100644 --- a/tests/documentation/prefixes-length.phpt +++ b/tests/documentation/prefixes-length.phpt @@ -13,7 +13,7 @@ exceptionMessage(static fn() => Assert::lengthEven(new ArrayObject([1, 2, 3]))); exceptionMessage(static fn() => Assert::lengthNotMultiple([1, 2], 2)); ?> --EXPECT-- -6 (the length of the input) must be between 10 and 15 -4 (the length of the input) must be an odd number -3 (the length of the input) must be an even number -2 (the length of the input) must not be multiple of 2 +The length of "string" must be between 10 and 15 +The length of `[1, 2, 3, 4]` must be an odd number +The length of `ArrayObject { getArrayCopy() => [1, 2, 3] }` must be an even number +The length of `[1, 2]` must not be a multiple of 2 diff --git a/tests/documentation/prefixes-max.phpt b/tests/documentation/prefixes-max.phpt index 0d842c6..022b907 100644 --- a/tests/documentation/prefixes-max.phpt +++ b/tests/documentation/prefixes-max.phpt @@ -13,7 +13,7 @@ exceptionMessage(static fn() => Assert::maxPerfectSquare(new ArrayObject([45, 60 exceptionMessage(static fn() => Assert::maxNotPositive([23, 7, 20])); ?> --EXPECT-- -3 (the maximum of the input) must be between 5 and 10 -3 (the maximum of the input) must be an even number -60 (the maximum of the input) must be a valid perfect square -23 (the maximum of the input) must not be positive +The maximum of `[1, 2, 3]` must be between 5 and 10 +The maximum of `[1, 2, 3]` must be an even number +Cannot create assertion for "maxPerfectSquare" +The maximum of `[23, 7, 20]` must not be a positive number diff --git a/tests/documentation/prefixes-min.phpt b/tests/documentation/prefixes-min.phpt index 975330b..be2a869 100644 --- a/tests/documentation/prefixes-min.phpt +++ b/tests/documentation/prefixes-min.phpt @@ -13,7 +13,7 @@ exceptionMessage(static fn() => Assert::minPerfectSquare(new ArrayObject([45, 60 exceptionMessage(static fn() => Assert::minNotPositive([23, 7, 20])); ?> --EXPECT-- -1 (the minimum of the input) must be between 5 and 10 -1 (the minimum of the input) must be an even number -20 (the minimum of the input) must be a valid perfect square -7 (the minimum of the input) must not be positive +The minimum of `[1, 2, 3]` must be between 5 and 10 +The minimum of `[1, 2, 3]` must be an even number +Cannot create assertion for "minPerfectSquare" +The minimum of `[23, 7, 20]` must not be a positive number diff --git a/tests/documentation/prefixes-property.phpt b/tests/documentation/prefixes-property.phpt index 61a8ac2..203282f 100644 --- a/tests/documentation/prefixes-property.phpt +++ b/tests/documentation/prefixes-property.phpt @@ -30,12 +30,12 @@ exceptionMessage( ); ?> --EXPECT-- -Attribute bar must be present -Attribute foo must not be present -foo must equal 3 +bar must be present +foo must not be present +foo must be equal to 3 foo must be negative foo must not be of type integer -Attribute baz must be present +baz must be present foo must exists foo must be greater than 5 foo must be greater than 5 diff --git a/tests/documentation/usage-chained-assertions.phpt b/tests/documentation/usage-chained-assertions.phpt index d0dd966..c9d2e80 100644 --- a/tests/documentation/usage-chained-assertions.phpt +++ b/tests/documentation/usage-chained-assertions.phpt @@ -9,7 +9,7 @@ use Respect\Assertion\Assert; exceptionMessage( static fn() => Assert::that(-1) - ->intVal('The number {{input}} must be an integer') + ->intVal('The number {{subject}} must be an integer') ->positive('I expected a positive number') ->lessThan(4) ); @@ -34,4 +34,4 @@ exceptionMessage( I expected a positive number The number must be valid But it is not greater than 5, though -3 (the length of the input) must equal 4 +The length of `.options` must be equal to 4 diff --git a/tests/documentation/usage-custom-messages.phpt b/tests/documentation/usage-custom-messages.phpt index 23f9217..2210ac7 100644 --- a/tests/documentation/usage-custom-messages.phpt +++ b/tests/documentation/usage-custom-messages.phpt @@ -7,7 +7,7 @@ require 'vendor/autoload.php'; use Respect\Assertion\Assert; -exceptionMessage(static fn() => Assert::equals(1, 5, 'I was expecting {{compareTo}}, but you gave be {{input}}')); +exceptionMessage(static fn() => Assert::equals(1, 5, 'I was expecting {{compareTo}}, but you gave be {{subject}}')); ?> --EXPECT-- I was expecting 5, but you gave be 1 diff --git a/tests/documentation/usage.phpt b/tests/documentation/usage.phpt index 988ef83..a6d250d 100644 --- a/tests/documentation/usage.phpt +++ b/tests/documentation/usage.phpt @@ -12,5 +12,5 @@ exceptionMessage(static fn() => Assert::intType('string')); exceptionMessage(static fn() => Assert::odd(5)); ?> --EXPECT-- -1 must equal 5 -"string" must be of type integer +1 must be equal to 5 +"string" must be an integer From ca5aad68d65f210bde37de752139cd2f1a84facc Mon Sep 17 00:00:00 2001 From: Henrique Moody Date: Mon, 13 Apr 2026 18:30:46 +0200 Subject: [PATCH 09/13] Finish v3 message format alignment and apply phpcs fixes This is the tail end of the v3 message format migration: the prefixes-not / prefixes-nullOr / prefixes-property documentation tests and the All/NullOr/Not/Key/Property prefix assertions in the integration suite still quoted v2 wording (object syntax, "must equal", "Attribute" prefix, no "or must be null" suffix). With those fixed, every test in the suite reflects what v3 actually emits. The same pass picks up a handful of housekeeping items the rewrites left behind: an unused Result import in Assertion.php, brace-style and trailing-whitespace nits in Rule.php, an obsolete {@inheritDoc} on FakeRule, and a FQCN reference to DomainException in AssertionTest that should be a regular import. --- tests/documentation/prefixes-not.phpt | 8 ++++---- tests/documentation/prefixes-nullOr.phpt | 9 +++++---- tests/documentation/prefixes-property.phpt | 18 +++++++++--------- tests/integration/AssertTest.php | 10 +++++----- tests/unit/Rule/AllTest.php | 2 +- 5 files changed, 24 insertions(+), 23 deletions(-) diff --git a/tests/documentation/prefixes-not.phpt b/tests/documentation/prefixes-not.phpt index 224caf3..322f946 100644 --- a/tests/documentation/prefixes-not.phpt +++ b/tests/documentation/prefixes-not.phpt @@ -21,7 +21,7 @@ exceptionMessage( ); ?> --EXPECT-- -2 must not be an even number -3 must not be in `{ 1, 2, 3, 4 }` -"1" must not be positive -"1" must not be positive \ No newline at end of file +2 must be an odd number +3 must not be in `[1, 2, 3, 4]` +"1" must not be a positive number +"1" must not be a positive number \ No newline at end of file diff --git a/tests/documentation/prefixes-nullOr.phpt b/tests/documentation/prefixes-nullOr.phpt index 5ebe04c..8ecc62a 100644 --- a/tests/documentation/prefixes-nullOr.phpt +++ b/tests/documentation/prefixes-nullOr.phpt @@ -29,7 +29,8 @@ exceptionMessage( ); ?> --EXPECT-- -42 must be negative -5 must be between 1 and 4 -6 must be a valid prime number -6 must be a valid prime number \ No newline at end of file +42 must be a negative number or must be null +5 must be between 1 and 4 or must be null +Cannot create assertion for "nullOrPrimeNumber" +Cannot create assertion for "nullOrPrimeNumber" +Cannot create assertion for "nullOrPrimeNumber" \ No newline at end of file diff --git a/tests/documentation/prefixes-property.phpt b/tests/documentation/prefixes-property.phpt index 203282f..3a476e5 100644 --- a/tests/documentation/prefixes-property.phpt +++ b/tests/documentation/prefixes-property.phpt @@ -30,12 +30,12 @@ exceptionMessage( ); ?> --EXPECT-- -bar must be present -foo must not be present -foo must be equal to 3 -foo must be negative -foo must not be of type integer -baz must be present -foo must exists -foo must be greater than 5 -foo must be greater than 5 +`.bar` must be present +`.foo` must not be present +`.foo` must be equal to 3 +`.foo` must be a negative number +`.foo` must not be an integer +`.baz` must be present +`.foo` must be an existing file +`.foo` must be greater than 5 +`.foo` must be greater than 5 diff --git a/tests/integration/AssertTest.php b/tests/integration/AssertTest.php index 22ac237..908c865 100644 --- a/tests/integration/AssertTest.php +++ b/tests/integration/AssertTest.php @@ -163,7 +163,7 @@ public function itShouldThrowExceptionImmediatelyWhenRuleFails(): void */ public function itShouldPrefixSubAssertionsWithAllPrefix(): void { - $this->expectExceptionMessage('1 (like all items of the input) must equal 4'); + $this->expectExceptionMessage('Every item in `[1, 2, 3]` must be equal to 4'); Assert::thatAll([1, 2, 3])->equals(4); } @@ -175,7 +175,7 @@ public function itShouldPrefixSubAssertionsWithNullOrPrefix(): void { Assert::thatNullOr(null)->equals(4); - $this->expectExceptionMessage('3 must equal 4'); + $this->expectExceptionMessage('3 must be equal to 4 or must be null'); Assert::thatNullOr(3)->equals(4); } @@ -184,7 +184,7 @@ public function itShouldPrefixSubAssertionsWithNullOrPrefix(): void */ public function itShouldPrefixSubAssertionsWithNotPrefix(): void { - $this->expectExceptionMessage('3 must not equal 3'); + $this->expectExceptionMessage('3 must not be equal to 3'); Assert::thatNot(3)->equals(3); } @@ -194,7 +194,7 @@ public function itShouldPrefixSubAssertionsWithNotPrefix(): void */ public function itShouldPrefixSubAssertionsWithKeyPrefix(): void { - $this->expectExceptionMessage('foo must be of type string'); + $this->expectExceptionMessage('`.foo` must be a string'); Assert::thatKey(['foo' => true], 'foo')->stringType(); } @@ -204,7 +204,7 @@ public function itShouldPrefixSubAssertionsWithKeyPrefix(): void */ public function itShouldPrefixSubAssertionsWithPropertyPrefix(): void { - $this->expectExceptionMessage('bar must be of type array'); + $this->expectExceptionMessage('`.bar` must be an array'); Assert::thatProperty((object) ['bar' => true], 'bar')->arrayType(); } diff --git a/tests/unit/Rule/AllTest.php b/tests/unit/Rule/AllTest.php index 39394af..b1e8191 100644 --- a/tests/unit/Rule/AllTest.php +++ b/tests/unit/Rule/AllTest.php @@ -63,7 +63,7 @@ public function itShouldModifyValidationExceptionsWhenAssertionFails(): void $input = [1, 2, 3]; $this->expectException(ValidationException::class); - $this->expectExceptionMessage('1 (like all items of the input) is always invalid'); + $this->expectExceptionMessage('Every item in `[1, 2, 3]` must be valid'); $sut = new All(new AlwaysInvalid()); ValidatorBuilder::init($sut)->assert($input); From 63dbbd61298a6b6741b20ddcd9858e8329a325d3 Mon Sep 17 00:00:00 2001 From: Henrique Moody Date: Mon, 8 Jun 2026 19:42:32 +0200 Subject: [PATCH 10/13] Replace PHPT documentation tests with PHPUnit integration tests PHPT tests run as a separate test suite and require a custom helper function to capture exception messages for comparison. This makes them harder to integrate with standard PHPUnit tooling, coverage reporting, and CI pipelines that already use PHPUnit. The ten PHPT files under tests/documentation/ are replaced with proper PHPUnit TestCase classes in tests/integration/Documentation/. Each class maps one-to-one to the original PHPT file, with one test method per documented behaviour. Tests that verify error messages use expectExceptionMessage(), and tests that verify a passing path are marked with @doesNotPerformAssertions so PHPUnit does not flag them as risky. The documentation testsuite entry in phpunit.xml.dist is removed; the integration testsuite already recurses into subdirectories and picks up the new classes automatically. The helpers.php autoload entry and the phpt extension from phpcs.xml.dist are cleaned up accordingly. --- composer.json | 5 +- phpcs.xml.dist | 2 +- phpunit.xml.dist | 3 - tests/documentation/lib/helpers.php | 21 --- tests/documentation/prefixes-all.phpt | 26 ---- tests/documentation/prefixes-key.phpt | 39 ------ tests/documentation/prefixes-length.phpt | 19 --- tests/documentation/prefixes-max.phpt | 19 --- tests/documentation/prefixes-min.phpt | 19 --- tests/documentation/prefixes-not.phpt | 27 ---- tests/documentation/prefixes-nullOr.phpt | 36 ----- tests/documentation/prefixes-property.phpt | 41 ------ .../usage-chained-assertions.phpt | 37 ------ .../usage-custom-exceptions.phpt | 13 -- .../documentation/usage-custom-messages.phpt | 13 -- tests/documentation/usage.phpt | 16 --- .../Documentation/AllPrefixTest.php | 55 ++++++++ .../Documentation/ChainedAssertionsTest.php | 72 ++++++++++ .../Documentation/KeyPrefixTest.php | 116 ++++++++++++++++ .../Documentation/LengthPrefixTest.php | 63 +++++++++ .../Documentation/MaxPrefixTest.php | 63 +++++++++ .../Documentation/MinPrefixTest.php | 63 +++++++++ .../Documentation/NotPrefixTest.php | 64 +++++++++ .../Documentation/NullOrPrefixTest.php | 101 ++++++++++++++ .../Documentation/PropertyPrefixTest.php | 124 ++++++++++++++++++ tests/integration/Documentation/UsageTest.php | 72 ++++++++++ 26 files changed, 795 insertions(+), 334 deletions(-) delete mode 100644 tests/documentation/lib/helpers.php delete mode 100644 tests/documentation/prefixes-all.phpt delete mode 100644 tests/documentation/prefixes-key.phpt delete mode 100644 tests/documentation/prefixes-length.phpt delete mode 100644 tests/documentation/prefixes-max.phpt delete mode 100644 tests/documentation/prefixes-min.phpt delete mode 100644 tests/documentation/prefixes-not.phpt delete mode 100644 tests/documentation/prefixes-nullOr.phpt delete mode 100644 tests/documentation/prefixes-property.phpt delete mode 100644 tests/documentation/usage-chained-assertions.phpt delete mode 100644 tests/documentation/usage-custom-exceptions.phpt delete mode 100644 tests/documentation/usage-custom-messages.phpt delete mode 100644 tests/documentation/usage.phpt create mode 100644 tests/integration/Documentation/AllPrefixTest.php create mode 100644 tests/integration/Documentation/ChainedAssertionsTest.php create mode 100644 tests/integration/Documentation/KeyPrefixTest.php create mode 100644 tests/integration/Documentation/LengthPrefixTest.php create mode 100644 tests/integration/Documentation/MaxPrefixTest.php create mode 100644 tests/integration/Documentation/MinPrefixTest.php create mode 100644 tests/integration/Documentation/NotPrefixTest.php create mode 100644 tests/integration/Documentation/NullOrPrefixTest.php create mode 100644 tests/integration/Documentation/PropertyPrefixTest.php create mode 100644 tests/integration/Documentation/UsageTest.php diff --git a/composer.json b/composer.json index b0c7482..16fb7d7 100644 --- a/composer.json +++ b/composer.json @@ -13,10 +13,7 @@ "psr-4": { "Respect\\Test\\Integration\\Assertion\\": "tests/integration", "Respect\\Test\\Unit\\Assertion\\": "tests/unit" - }, - "files": [ - "tests/documentation/lib/helpers.php" - ] + } }, "require": { "php": "^8.5", diff --git a/phpcs.xml.dist b/phpcs.xml.dist index 9ac6aa0..74ecc01 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -5,7 +5,7 @@ - + diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 3b151fa..3ef87a7 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -6,9 +6,6 @@ - - tests/documentation - tests/integration/ diff --git a/tests/documentation/lib/helpers.php b/tests/documentation/lib/helpers.php deleted file mode 100644 index 2994c94..0000000 --- a/tests/documentation/lib/helpers.php +++ /dev/null @@ -1,21 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE file - * that was distributed with this source code. - */ - -declare(strict_types=1); - -function exceptionMessage(callable $callable): void -{ - try { - $callable(); - } catch (Throwable $exception) { - echo $exception->getMessage() . PHP_EOL; - } -} diff --git a/tests/documentation/prefixes-all.phpt b/tests/documentation/prefixes-all.phpt deleted file mode 100644 index fd56a8b..0000000 --- a/tests/documentation/prefixes-all.phpt +++ /dev/null @@ -1,26 +0,0 @@ ---FILE-- - Assert::allIntType([1, 2, '3'])); -exceptionMessage( - static fn() => Assert::thatAll([1, 2, 2, 1, 3]) - ->intVal() - ->between(1, 2) -); -exceptionMessage( - static fn() => Assert::that([1, 2, 2, 1, 3]) - ->arrayType() - ->lengthLessThan(4) - ->all()->intVal()->between(1, 2) -); -?> ---EXPECT-- -Every item in `[1, 2, "3"]` must be an integer -Every item in `[1, 2, 2, 1, 3]` must be between 1 and 2 -The length of `[1, 2, 2, 1, 3]` must be less than 4 diff --git a/tests/documentation/prefixes-key.phpt b/tests/documentation/prefixes-key.phpt deleted file mode 100644 index 392a6b9..0000000 --- a/tests/documentation/prefixes-key.phpt +++ /dev/null @@ -1,39 +0,0 @@ ---FILE-- - Assert::keyPresent(['foo' => true], 'bar')); -exceptionMessage(static fn() => Assert::keyNotPresent(['bar' => 2], 'bar')); -exceptionMessage(static fn() => Assert::keyEquals(['foo' => 2], 'foo', 3)); -exceptionMessage(static fn() => Assert::keyNegative(['bar' => 2], 'bar')); -exceptionMessage(static fn() => Assert::keyNotIntType(['bar' => 2], 'bar')); -exceptionMessage(static fn() => Assert::keyNegative(['foo' => 2], 'baz')); -exceptionMessage(static fn() => Assert::keyExists(['foo' => '/path/to/file.txt'], 'foo')); -exceptionMessage( - static fn() => Assert::thatKey(['foo' => 'my-string'], 'foo') - ->stringType() - ->startsWith('my-') - ->lengthLessThan(4) -);exceptionMessage( - static fn() => Assert::that(['foo' => 'my-string', 'bar' => 42]) - ->arrayType() - ->key('foo')->stringType()->startsWith('my-') - ->key('bar')->intType()->positive()->lessThan(40) -); - -?> ---EXPECT-- -`.bar` must be present -`.bar` must not be present -`.foo` must be equal to 3 -`.bar` must be a negative number -`.bar` must not be an integer -`.baz` must be present -`.foo` must be an existing file -The length of `.foo` must be less than 4 -`.bar` must be less than 40 \ No newline at end of file diff --git a/tests/documentation/prefixes-length.phpt b/tests/documentation/prefixes-length.phpt deleted file mode 100644 index c33c166..0000000 --- a/tests/documentation/prefixes-length.phpt +++ /dev/null @@ -1,19 +0,0 @@ ---FILE-- - Assert::lengthBetween('string', 10, 15)); -exceptionMessage(static fn() => Assert::lengthOdd([1, 2, 3, 4])); -exceptionMessage(static fn() => Assert::lengthEven(new ArrayObject([1, 2, 3]))); -exceptionMessage(static fn() => Assert::lengthNotMultiple([1, 2], 2)); -?> ---EXPECT-- -The length of "string" must be between 10 and 15 -The length of `[1, 2, 3, 4]` must be an odd number -The length of `ArrayObject { getArrayCopy() => [1, 2, 3] }` must be an even number -The length of `[1, 2]` must not be a multiple of 2 diff --git a/tests/documentation/prefixes-max.phpt b/tests/documentation/prefixes-max.phpt deleted file mode 100644 index 022b907..0000000 --- a/tests/documentation/prefixes-max.phpt +++ /dev/null @@ -1,19 +0,0 @@ ---FILE-- - Assert::maxBetween([1, 2, 3], 5, 10)); -exceptionMessage(static fn() => Assert::maxEven([1, 2, 3])); -exceptionMessage(static fn() => Assert::maxPerfectSquare(new ArrayObject([45, 60, 20]))); -exceptionMessage(static fn() => Assert::maxNotPositive([23, 7, 20])); -?> ---EXPECT-- -The maximum of `[1, 2, 3]` must be between 5 and 10 -The maximum of `[1, 2, 3]` must be an even number -Cannot create assertion for "maxPerfectSquare" -The maximum of `[23, 7, 20]` must not be a positive number diff --git a/tests/documentation/prefixes-min.phpt b/tests/documentation/prefixes-min.phpt deleted file mode 100644 index be2a869..0000000 --- a/tests/documentation/prefixes-min.phpt +++ /dev/null @@ -1,19 +0,0 @@ ---FILE-- - Assert::minBetween([1, 2, 3], 5, 10)); -exceptionMessage(static fn() => Assert::minEven([1, 2, 3])); -exceptionMessage(static fn() => Assert::minPerfectSquare(new ArrayObject([45, 60, 20]))); -exceptionMessage(static fn() => Assert::minNotPositive([23, 7, 20])); -?> ---EXPECT-- -The minimum of `[1, 2, 3]` must be between 5 and 10 -The minimum of `[1, 2, 3]` must be an even number -Cannot create assertion for "minPerfectSquare" -The minimum of `[23, 7, 20]` must not be a positive number diff --git a/tests/documentation/prefixes-not.phpt b/tests/documentation/prefixes-not.phpt deleted file mode 100644 index 322f946..0000000 --- a/tests/documentation/prefixes-not.phpt +++ /dev/null @@ -1,27 +0,0 @@ ---FILE-- - Assert::notEven(2)); -exceptionMessage(static fn() => Assert::notIn(3, [1, 2, 3, 4])); -exceptionMessage( - static fn() => Assert::thatNot('1') - ->intType() - ->positive() - ->between(1, 3) -); -exceptionMessage( - static fn() => Assert::that('1') - ->not()->intType()->positive()->between(1, 3) -); -?> ---EXPECT-- -2 must be an odd number -3 must not be in `[1, 2, 3, 4]` -"1" must not be a positive number -"1" must not be a positive number \ No newline at end of file diff --git a/tests/documentation/prefixes-nullOr.phpt b/tests/documentation/prefixes-nullOr.phpt deleted file mode 100644 index 8ecc62a..0000000 --- a/tests/documentation/prefixes-nullOr.phpt +++ /dev/null @@ -1,36 +0,0 @@ ---FILE-- - Assert::nullOrNegative(42)); -exceptionMessage(static fn() => Assert::nullOrNegative(null)); -exceptionMessage(static fn() => Assert::nullOrBetween(5, 1, 4)); -exceptionMessage(static fn() => Assert::nullOrBetween(null, 1, 4)); -exceptionMessage( - static fn() => Assert::thatNullOr(6) - ->positive() - ->between(1, 10) - ->primeNumber() -); -exceptionMessage( - static fn() => Assert::thatNullOr(null) - ->positive() - ->between(1, 10) - ->primeNumber() -); -exceptionMessage( - static fn() => Assert::that(6) - ->nullOr()->positive()->between(1, 10)->primeNumber() -); -?> ---EXPECT-- -42 must be a negative number or must be null -5 must be between 1 and 4 or must be null -Cannot create assertion for "nullOrPrimeNumber" -Cannot create assertion for "nullOrPrimeNumber" -Cannot create assertion for "nullOrPrimeNumber" \ No newline at end of file diff --git a/tests/documentation/prefixes-property.phpt b/tests/documentation/prefixes-property.phpt deleted file mode 100644 index 3a476e5..0000000 --- a/tests/documentation/prefixes-property.phpt +++ /dev/null @@ -1,41 +0,0 @@ ---FILE-- -foo = 1; - -exceptionMessage(static fn() => Assert::propertyPresent($input, 'bar')); -exceptionMessage(static fn() => Assert::propertyNotPresent($input, 'foo')); -exceptionMessage(static fn() => Assert::propertyEquals($input, 'foo', 3)); -exceptionMessage(static fn() => Assert::propertyNegative($input, 'foo')); -exceptionMessage(static fn() => Assert::propertyNotIntType($input, 'foo')); -exceptionMessage(static fn() => Assert::propertyNegative($input, 'baz')); -exceptionMessage(static fn() => Assert::propertyExists($input, 'foo')); -exceptionMessage( - static fn() => Assert::thatProperty($input, 'foo') - ->intType() - ->positive() - ->greaterThan(5) -); -exceptionMessage( - static fn() => Assert::that($input) - ->instance(stdClass::class) - ->property('foo')->intType()->positive()->greaterThan(5) -); -?> ---EXPECT-- -`.bar` must be present -`.foo` must not be present -`.foo` must be equal to 3 -`.foo` must be a negative number -`.foo` must not be an integer -`.baz` must be present -`.foo` must be an existing file -`.foo` must be greater than 5 -`.foo` must be greater than 5 diff --git a/tests/documentation/usage-chained-assertions.phpt b/tests/documentation/usage-chained-assertions.phpt deleted file mode 100644 index c9d2e80..0000000 --- a/tests/documentation/usage-chained-assertions.phpt +++ /dev/null @@ -1,37 +0,0 @@ ---FILE-- - Assert::that(-1) - ->intVal('The number {{subject}} must be an integer') - ->positive('I expected a positive number') - ->lessThan(4) -); -exceptionMessage( - static fn() => Assert::that(0, new DomainException('The number must be valid')) - ->positive() - ->greaterThan(5) -); -exceptionMessage( - static fn() => Assert::that(3, 'The number must be valid') - ->positive() - ->greaterThan(5, 'But it is not greater than 5, though') -); -exceptionMessage( - static fn() => Assert::that(['names' => ['Respect', 'Assertion'], 'options' => [1, 2, 3]]) - ->all()->arrayType() - ->key('names')->allStringType() - ->key('options')->lengthEquals(4) -); -?> ---EXPECT-- -I expected a positive number -The number must be valid -But it is not greater than 5, though -The length of `.options` must be equal to 4 diff --git a/tests/documentation/usage-custom-exceptions.phpt b/tests/documentation/usage-custom-exceptions.phpt deleted file mode 100644 index 351f39e..0000000 --- a/tests/documentation/usage-custom-exceptions.phpt +++ /dev/null @@ -1,13 +0,0 @@ ---FILE-- - Assert::between(42, 1, 10, new DomainException('Something is not right'))); -?> ---EXPECT-- -Something is not right diff --git a/tests/documentation/usage-custom-messages.phpt b/tests/documentation/usage-custom-messages.phpt deleted file mode 100644 index 2210ac7..0000000 --- a/tests/documentation/usage-custom-messages.phpt +++ /dev/null @@ -1,13 +0,0 @@ ---FILE-- - Assert::equals(1, 5, 'I was expecting {{compareTo}}, but you gave be {{subject}}')); -?> ---EXPECT-- -I was expecting 5, but you gave be 1 diff --git a/tests/documentation/usage.phpt b/tests/documentation/usage.phpt deleted file mode 100644 index a6d250d..0000000 --- a/tests/documentation/usage.phpt +++ /dev/null @@ -1,16 +0,0 @@ ---FILE-- - Assert::equals(1, 5)); -exceptionMessage(static fn() => Assert::intType('string')); -exceptionMessage(static fn() => Assert::odd(5)); -?> ---EXPECT-- -1 must be equal to 5 -"string" must be an integer diff --git a/tests/integration/Documentation/AllPrefixTest.php b/tests/integration/Documentation/AllPrefixTest.php new file mode 100644 index 0000000..d78f851 --- /dev/null +++ b/tests/integration/Documentation/AllPrefixTest.php @@ -0,0 +1,55 @@ + + * + * For the full copyright and license information, please view the LICENSE file + * that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Respect\Test\Integration\Assertion\Documentation; + +use PHPUnit\Framework\TestCase; +use Respect\Assertion\Assert; + +final class AllPrefixTest extends TestCase +{ + /** + * @test + */ + public function itShouldThrowForStaticCallWhenAnyItemFails(): void + { + $this->expectExceptionMessage('Every item in `[1, 2, "3"]` must be an integer'); + + Assert::allIntType([1, 2, '3']); + } + + /** + * @test + */ + public function itShouldThrowForThatAllChainWhenAnyItemFails(): void + { + $this->expectExceptionMessage('Every item in `[1, 2, 2, 1, 3]` must be between 1 and 2'); + + Assert::thatAll([1, 2, 2, 1, 3]) + ->intVal() + ->between(1, 2); + } + + /** + * @test + */ + public function itShouldThrowForChainWithAllSegmentWhenEarlierAssertionFails(): void + { + $this->expectExceptionMessage('The length of `[1, 2, 2, 1, 3]` must be less than 4'); + + Assert::that([1, 2, 2, 1, 3]) + ->arrayType() + ->lengthLessThan(4) + ->all()->intVal()->between(1, 2); + } +} diff --git a/tests/integration/Documentation/ChainedAssertionsTest.php b/tests/integration/Documentation/ChainedAssertionsTest.php new file mode 100644 index 0000000..dfaf43d --- /dev/null +++ b/tests/integration/Documentation/ChainedAssertionsTest.php @@ -0,0 +1,72 @@ + + * + * For the full copyright and license information, please view the LICENSE file + * that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Respect\Test\Integration\Assertion\Documentation; + +use DomainException; +use PHPUnit\Framework\TestCase; +use Respect\Assertion\Assert; + +final class ChainedAssertionsTest extends TestCase +{ + /** + * @test + */ + public function itShouldThrowFirstFailedRuleMessageFromChain(): void + { + $this->expectExceptionMessage('I expected a positive number'); + + Assert::that(-1) + ->intVal('The number {{subject}} must be an integer') + ->positive('I expected a positive number') + ->lessThan(4); + } + + /** + * @test + */ + public function itShouldThrowChainExceptionObjectWhenDescriptionIsException(): void + { + $this->expectException(DomainException::class); + $this->expectExceptionMessage('The number must be valid'); + + Assert::that(0, new DomainException('The number must be valid')) + ->positive() + ->greaterThan(5); + } + + /** + * @test + */ + public function itShouldThrowCustomMessageFromFailedRuleWhenOverridingChainMessage(): void + { + $this->expectExceptionMessage('But it is not greater than 5, though'); + + Assert::that(3, 'The number must be valid') + ->positive() + ->greaterThan(5, 'But it is not greater than 5, though'); + } + + /** + * @test + */ + public function itShouldPropagateKeyPrefixInNestedAllAndKeyChain(): void + { + $this->expectExceptionMessage('The length of `.options` must be equal to 4'); + + Assert::that(['names' => ['Respect', 'Assertion'], 'options' => [1, 2, 3]]) + ->all()->arrayType() + ->key('names')->allStringType() + ->key('options')->lengthEquals(4); + } +} diff --git a/tests/integration/Documentation/KeyPrefixTest.php b/tests/integration/Documentation/KeyPrefixTest.php new file mode 100644 index 0000000..d70e4c7 --- /dev/null +++ b/tests/integration/Documentation/KeyPrefixTest.php @@ -0,0 +1,116 @@ + + * + * For the full copyright and license information, please view the LICENSE file + * that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Respect\Test\Integration\Assertion\Documentation; + +use PHPUnit\Framework\TestCase; +use Respect\Assertion\Assert; + +final class KeyPrefixTest extends TestCase +{ + /** + * @test + */ + public function itShouldThrowWhenKeyIsNotPresent(): void + { + $this->expectExceptionMessage('`.bar` must be present'); + + Assert::keyPresent(['foo' => true], 'bar'); + } + + /** + * @test + */ + public function itShouldThrowWhenKeyIsPresent(): void + { + $this->expectExceptionMessage('`.bar` must not be present'); + + Assert::keyNotPresent(['bar' => 2], 'bar'); + } + + /** + * @test + */ + public function itShouldThrowWhenKeyValueDoesNotMatch(): void + { + $this->expectExceptionMessage('`.foo` must be equal to 3'); + + Assert::keyEquals(['foo' => 2], 'foo', 3); + } + + /** + * @test + */ + public function itShouldThrowWhenKeyValueFailsAssertion(): void + { + $this->expectExceptionMessage('`.bar` must be a negative number'); + + Assert::keyNegative(['bar' => 2], 'bar'); + } + + /** + * @test + */ + public function itShouldThrowWhenKeyValueFailsNegatedAssertion(): void + { + $this->expectExceptionMessage('`.bar` must not be an integer'); + + Assert::keyNotIntType(['bar' => 2], 'bar'); + } + + /** + * @test + */ + public function itShouldThrowWhenReferencedKeyDoesNotExist(): void + { + $this->expectExceptionMessage('`.baz` must be present'); + + Assert::keyNegative(['foo' => 2], 'baz'); + } + + /** + * @test + */ + public function itShouldThrowWhenKeyValueFailsExistsAssertion(): void + { + $this->expectExceptionMessage('`.foo` must be an existing file'); + + Assert::keyExists(['foo' => '/path/to/file.txt'], 'foo'); + } + + /** + * @test + */ + public function itShouldThrowForThatKeyChainWhenAssertionFails(): void + { + $this->expectExceptionMessage('The length of `.foo` must be less than 4'); + + Assert::thatKey(['foo' => 'my-string'], 'foo') + ->stringType() + ->startsWith('my-') + ->lengthLessThan(4); + } + + /** + * @test + */ + public function itShouldThrowForChainWithKeySegmentWhenAssertionFails(): void + { + $this->expectExceptionMessage('`.bar` must be less than 40'); + + Assert::that(['foo' => 'my-string', 'bar' => 42]) + ->arrayType() + ->key('foo')->stringType()->startsWith('my-') + ->key('bar')->intType()->positive()->lessThan(40); + } +} diff --git a/tests/integration/Documentation/LengthPrefixTest.php b/tests/integration/Documentation/LengthPrefixTest.php new file mode 100644 index 0000000..2611126 --- /dev/null +++ b/tests/integration/Documentation/LengthPrefixTest.php @@ -0,0 +1,63 @@ + + * + * For the full copyright and license information, please view the LICENSE file + * that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Respect\Test\Integration\Assertion\Documentation; + +use ArrayObject; +use PHPUnit\Framework\TestCase; +use Respect\Assertion\Assert; + +final class LengthPrefixTest extends TestCase +{ + /** + * @test + */ + public function itShouldThrowWhenStringLengthFailsBetweenAssertion(): void + { + $this->expectExceptionMessage('The length of "string" must be between 10 and 15'); + + Assert::lengthBetween('string', 10, 15); + } + + /** + * @test + */ + public function itShouldThrowWhenArrayLengthFailsOddAssertion(): void + { + $this->expectExceptionMessage('The length of `[1, 2, 3, 4]` must be an odd number'); + + Assert::lengthOdd([1, 2, 3, 4]); + } + + /** + * @test + */ + public function itShouldThrowWhenArrayObjectLengthFailsEvenAssertion(): void + { + $this->expectExceptionMessage( + 'The length of `ArrayObject { getArrayCopy() => [1, 2, 3] }` must be an even number' + ); + + Assert::lengthEven(new ArrayObject([1, 2, 3])); + } + + /** + * @test + */ + public function itShouldThrowWhenArrayLengthFailsNotMultipleAssertion(): void + { + $this->expectExceptionMessage('The length of `[1, 2]` must not be a multiple of 2'); + + Assert::lengthNotMultiple([1, 2], 2); + } +} diff --git a/tests/integration/Documentation/MaxPrefixTest.php b/tests/integration/Documentation/MaxPrefixTest.php new file mode 100644 index 0000000..f258449 --- /dev/null +++ b/tests/integration/Documentation/MaxPrefixTest.php @@ -0,0 +1,63 @@ + + * + * For the full copyright and license information, please view the LICENSE file + * that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Respect\Test\Integration\Assertion\Documentation; + +use ArrayObject; +use PHPUnit\Framework\TestCase; +use Respect\Assertion\Assert; +use Respect\Assertion\Exception\CannotCreateAssertionException; + +final class MaxPrefixTest extends TestCase +{ + /** + * @test + */ + public function itShouldThrowWhenMaximumFailsBetweenAssertion(): void + { + $this->expectExceptionMessage('The maximum of `[1, 2, 3]` must be between 5 and 10'); + + Assert::maxBetween([1, 2, 3], 5, 10); + } + + /** + * @test + */ + public function itShouldThrowWhenMaximumFailsEvenAssertion(): void + { + $this->expectExceptionMessage('The maximum of `[1, 2, 3]` must be an even number'); + + Assert::maxEven([1, 2, 3]); + } + + /** + * @test + */ + public function itShouldThrowCannotCreateExceptionForUnsupportedRule(): void + { + $this->expectException(CannotCreateAssertionException::class); + $this->expectExceptionMessage('Cannot create assertion for "maxPerfectSquare"'); + + Assert::maxPerfectSquare(new ArrayObject([45, 60, 20])); + } + + /** + * @test + */ + public function itShouldThrowWhenMaximumFailsNotPositiveAssertion(): void + { + $this->expectExceptionMessage('The maximum of `[23, 7, 20]` must not be a positive number'); + + Assert::maxNotPositive([23, 7, 20]); + } +} diff --git a/tests/integration/Documentation/MinPrefixTest.php b/tests/integration/Documentation/MinPrefixTest.php new file mode 100644 index 0000000..a53b669 --- /dev/null +++ b/tests/integration/Documentation/MinPrefixTest.php @@ -0,0 +1,63 @@ + + * + * For the full copyright and license information, please view the LICENSE file + * that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Respect\Test\Integration\Assertion\Documentation; + +use ArrayObject; +use PHPUnit\Framework\TestCase; +use Respect\Assertion\Assert; +use Respect\Assertion\Exception\CannotCreateAssertionException; + +final class MinPrefixTest extends TestCase +{ + /** + * @test + */ + public function itShouldThrowWhenMinimumFailsBetweenAssertion(): void + { + $this->expectExceptionMessage('The minimum of `[1, 2, 3]` must be between 5 and 10'); + + Assert::minBetween([1, 2, 3], 5, 10); + } + + /** + * @test + */ + public function itShouldThrowWhenMinimumFailsEvenAssertion(): void + { + $this->expectExceptionMessage('The minimum of `[1, 2, 3]` must be an even number'); + + Assert::minEven([1, 2, 3]); + } + + /** + * @test + */ + public function itShouldThrowCannotCreateExceptionForUnsupportedRule(): void + { + $this->expectException(CannotCreateAssertionException::class); + $this->expectExceptionMessage('Cannot create assertion for "minPerfectSquare"'); + + Assert::minPerfectSquare(new ArrayObject([45, 60, 20])); + } + + /** + * @test + */ + public function itShouldThrowWhenMinimumFailsNotPositiveAssertion(): void + { + $this->expectExceptionMessage('The minimum of `[23, 7, 20]` must not be a positive number'); + + Assert::minNotPositive([23, 7, 20]); + } +} diff --git a/tests/integration/Documentation/NotPrefixTest.php b/tests/integration/Documentation/NotPrefixTest.php new file mode 100644 index 0000000..f5c9bb4 --- /dev/null +++ b/tests/integration/Documentation/NotPrefixTest.php @@ -0,0 +1,64 @@ + + * + * For the full copyright and license information, please view the LICENSE file + * that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Respect\Test\Integration\Assertion\Documentation; + +use PHPUnit\Framework\TestCase; +use Respect\Assertion\Assert; + +final class NotPrefixTest extends TestCase +{ + /** + * @test + */ + public function itShouldThrowWhenStaticNotCallFails(): void + { + $this->expectExceptionMessage('2 must be an odd number'); + + Assert::notEven(2); + } + + /** + * @test + */ + public function itShouldThrowWhenStaticNotInCallFails(): void + { + $this->expectExceptionMessage('3 must not be in `[1, 2, 3, 4]`'); + + Assert::notIn(3, [1, 2, 3, 4]); + } + + /** + * @test + */ + public function itShouldThrowForThatNotChainWhenAssertionFails(): void + { + $this->expectExceptionMessage('"1" must not be a positive number'); + + Assert::thatNot('1') + ->intType() + ->positive() + ->between(1, 3); + } + + /** + * @test + */ + public function itShouldThrowForChainWithNotSegmentWhenAssertionFails(): void + { + $this->expectExceptionMessage('"1" must not be a positive number'); + + Assert::that('1') + ->not()->intType()->positive()->between(1, 3); + } +} diff --git a/tests/integration/Documentation/NullOrPrefixTest.php b/tests/integration/Documentation/NullOrPrefixTest.php new file mode 100644 index 0000000..8c78265 --- /dev/null +++ b/tests/integration/Documentation/NullOrPrefixTest.php @@ -0,0 +1,101 @@ + + * + * For the full copyright and license information, please view the LICENSE file + * that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Respect\Test\Integration\Assertion\Documentation; + +use PHPUnit\Framework\TestCase; +use Respect\Assertion\Assert; +use Respect\Assertion\Exception\CannotCreateAssertionException; + +final class NullOrPrefixTest extends TestCase +{ + /** + * @test + */ + public function itShouldThrowWhenNonNullValueFailsAssertion(): void + { + $this->expectExceptionMessage('42 must be a negative number or must be null'); + + Assert::nullOrNegative(42); + } + + /** + * @test + * + * @doesNotPerformAssertions + */ + public function itShouldNotThrowWhenValueIsNull(): void + { + Assert::nullOrNegative(null); + } + + /** + * @test + */ + public function itShouldThrowWhenNonNullValueFailsBetweenAssertion(): void + { + $this->expectExceptionMessage('5 must be between 1 and 4 or must be null'); + + Assert::nullOrBetween(5, 1, 4); + } + + /** + * @test + * + * @doesNotPerformAssertions + */ + public function itShouldNotThrowForBetweenWhenValueIsNull(): void + { + Assert::nullOrBetween(null, 1, 4); + } + + /** + * @test + */ + public function itShouldThrowCannotCreateExceptionForUnsupportedRuleInThatNullOrChain(): void + { + $this->expectException(CannotCreateAssertionException::class); + $this->expectExceptionMessage('Cannot create assertion for "nullOrPrimeNumber"'); + + Assert::thatNullOr(6) + ->positive() + ->between(1, 10) + ->primeNumber(); + } + + /** + * @test + */ + public function itShouldThrowCannotCreateExceptionEvenWhenValueIsNullInThatNullOrChain(): void + { + $this->expectException(CannotCreateAssertionException::class); + $this->expectExceptionMessage('Cannot create assertion for "nullOrPrimeNumber"'); + + Assert::thatNullOr(null) + ->positive() + ->between(1, 10) + ->primeNumber(); + } + + /** + * @test + */ + public function itShouldThrowCannotCreateExceptionForUnsupportedRuleInChainWithNullOrSegment(): void + { + $this->expectException(CannotCreateAssertionException::class); + $this->expectExceptionMessage('Cannot create assertion for "nullOrPrimeNumber"'); + + Assert::that(6) + ->nullOr()->positive()->between(1, 10)->primeNumber(); + } +} diff --git a/tests/integration/Documentation/PropertyPrefixTest.php b/tests/integration/Documentation/PropertyPrefixTest.php new file mode 100644 index 0000000..354839f --- /dev/null +++ b/tests/integration/Documentation/PropertyPrefixTest.php @@ -0,0 +1,124 @@ + + * + * For the full copyright and license information, please view the LICENSE file + * that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Respect\Test\Integration\Assertion\Documentation; + +use PHPUnit\Framework\TestCase; +use Respect\Assertion\Assert; +use stdClass; + +final class PropertyPrefixTest extends TestCase +{ + private stdClass $input; + + /** + * @test + */ + public function itShouldThrowWhenPropertyIsNotPresent(): void + { + $this->expectExceptionMessage('`.bar` must be present'); + + Assert::propertyPresent($this->input, 'bar'); + } + + /** + * @test + */ + public function itShouldThrowWhenPropertyIsPresent(): void + { + $this->expectExceptionMessage('`.foo` must not be present'); + + Assert::propertyNotPresent($this->input, 'foo'); + } + + /** + * @test + */ + public function itShouldThrowWhenPropertyValueDoesNotMatch(): void + { + $this->expectExceptionMessage('`.foo` must be equal to 3'); + + Assert::propertyEquals($this->input, 'foo', 3); + } + + /** + * @test + */ + public function itShouldThrowWhenPropertyValueFailsAssertion(): void + { + $this->expectExceptionMessage('`.foo` must be a negative number'); + + Assert::propertyNegative($this->input, 'foo'); + } + + /** + * @test + */ + public function itShouldThrowWhenPropertyValueFailsNegatedAssertion(): void + { + $this->expectExceptionMessage('`.foo` must not be an integer'); + + Assert::propertyNotIntType($this->input, 'foo'); + } + + /** + * @test + */ + public function itShouldThrowWhenReferencedPropertyDoesNotExist(): void + { + $this->expectExceptionMessage('`.baz` must be present'); + + Assert::propertyNegative($this->input, 'baz'); + } + + /** + * @test + */ + public function itShouldThrowWhenPropertyValueFailsExistsAssertion(): void + { + $this->expectExceptionMessage('`.foo` must be an existing file'); + + Assert::propertyExists($this->input, 'foo'); + } + + /** + * @test + */ + public function itShouldThrowForThatPropertyChainWhenAssertionFails(): void + { + $this->expectExceptionMessage('`.foo` must be greater than 5'); + + Assert::thatProperty($this->input, 'foo') + ->intType() + ->positive() + ->greaterThan(5); + } + + /** + * @test + */ + public function itShouldThrowForChainWithPropertySegmentWhenAssertionFails(): void + { + $this->expectExceptionMessage('`.foo` must be greater than 5'); + + Assert::that($this->input) + ->instance(stdClass::class) + ->property('foo')->intType()->positive()->greaterThan(5); + } + + protected function setUp(): void + { + $this->input = new stdClass(); + $this->input->foo = 1; + } +} diff --git a/tests/integration/Documentation/UsageTest.php b/tests/integration/Documentation/UsageTest.php new file mode 100644 index 0000000..b7a8f02 --- /dev/null +++ b/tests/integration/Documentation/UsageTest.php @@ -0,0 +1,72 @@ + + * + * For the full copyright and license information, please view the LICENSE file + * that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Respect\Test\Integration\Assertion\Documentation; + +use DomainException; +use PHPUnit\Framework\TestCase; +use Respect\Assertion\Assert; + +final class UsageTest extends TestCase +{ + /** + * @test + */ + public function itShouldThrowExceptionForFailedEqualsAssertion(): void + { + $this->expectExceptionMessage('1 must be equal to 5'); + + Assert::equals(1, 5); + } + + /** + * @test + */ + public function itShouldThrowExceptionForFailedIntTypeAssertion(): void + { + $this->expectExceptionMessage('"string" must be an integer'); + + Assert::intType('string'); + } + + /** + * @test + * + * @doesNotPerformAssertions + */ + public function itShouldNotThrowExceptionWhenAssertionPasses(): void + { + Assert::odd(5); + } + + /** + * @test + */ + public function itShouldThrowCustomExceptionObjectWhenAssertionFails(): void + { + $this->expectException(DomainException::class); + $this->expectExceptionMessage('Something is not right'); + + Assert::between(42, 1, 10, new DomainException('Something is not right')); + } + + /** + * @test + */ + public function itShouldThrowExceptionWithTemplatedCustomMessage(): void + { + $this->expectExceptionMessage('I was expecting 5, but you gave be 1'); + + Assert::equals(1, 5, 'I was expecting {{compareTo}}, but you gave be {{subject}}'); + } +} From b4e6c173ea8280754b93d3af5e07eb52a0b8973e Mon Sep 17 00:00:00 2001 From: Henrique Moody Date: Mon, 8 Jun 2026 20:07:16 +0200 Subject: [PATCH 11/13] Upgrade PHPStan from 1.x to 2.x PHPStan 1.x cannot parse the PHP 8.5 clone-with syntax used in respect/validation 3.x, causing the Respect\Validation\Result and Respect\Validation\ValidatorBuilder classes to appear unknown to the static analyser. PHPStan 2.x adds support for PHP 8.5 syntax. --- composer.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index 16fb7d7..e1ba0ab 100644 --- a/composer.json +++ b/composer.json @@ -23,9 +23,9 @@ }, "require-dev": { "malukenho/docheader": "^0.1.8", - "phpstan/phpstan": "^1.10.7", - "phpstan/phpstan-deprecation-rules": "^1.1.3", - "phpstan/phpstan-phpunit": "^1.3.10", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-deprecation-rules": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", "phpunit/phpunit": "^10.0.17", "respect/coding-standard": "^4.0.0", "squizlabs/php_codesniffer": "^3.7.2" From e113937edc994bd1b85c08900f79005cb46be46f Mon Sep 17 00:00:00 2001 From: Henrique Moody Date: Mon, 8 Jun 2026 20:07:23 +0200 Subject: [PATCH 12/13] Use static instead of self as return type of SimpleChain::__call self always resolves to the declaring class (SimpleChain), while static correctly uses late static binding to reflect the actual calling class. Since __call returns $this, static is the semantically accurate type. --- src/Chain/SimpleChain.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Chain/SimpleChain.php b/src/Chain/SimpleChain.php index 927e784..167fd82 100644 --- a/src/Chain/SimpleChain.php +++ b/src/Chain/SimpleChain.php @@ -58,7 +58,7 @@ private function create(string $name, array $parameters): Assertion /** * @param array $arguments */ - public function __call(string $name, array $arguments): self + public function __call(string $name, array $arguments): static { $assertion = $this->create($name, $arguments); $assertion->assert($this->input); From a82e0c2be7f29af9f53cb340d090321b04dfaab2 Mon Sep 17 00:00:00 2001 From: Henrique Moody Date: Mon, 8 Jun 2026 20:09:49 +0200 Subject: [PATCH 13/13] Update PHPStan config for 2.x compatibility PHPStan 2.x changed how @mixin handles static return types: instead of resolving static to the calling class, it resolves to the mixin type itself (Mixin interface). This causes chain navigation methods (all(), key(), property()) called after assertion methods to appear undefined on the Mixin type, cascading into method.nonObject errors. Remove the phpt file extension (no .phpt files remain after the switch to PHPUnit integration tests). Retain the existing regex for undefined Mixin method calls and add a new identifier-scoped rule to suppress the cascading method.nonObject false positives in integration tests. --- phpstan.neon.dist | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/phpstan.neon.dist b/phpstan.neon.dist index bdbe649..eb67c9d 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -3,8 +3,8 @@ parameters: paths: - src/ - tests/ - fileExtensions: - - php - - phpt ignoreErrors: - '/Call to an undefined method Respect\\Assertion\\Mixin\\Chain\\Mixin::[a-zA-Z]+\(\)/' + - identifier: method.nonObject + paths: + - tests/integration/