From 3b2f232404ec93aba0348c7f46c51a4a45ad0be4 Mon Sep 17 00:00:00 2001 From: aymericcucherousset Date: Sun, 5 Apr 2026 13:28:14 +0200 Subject: [PATCH 1/2] Add isNotInstanceOfAny assertion --- README.md | 61 ++++++++++--------- src/Assert.php | 30 +++++++++ src/Mixin.php | 59 ++++++++++++++++++ tests/AssertTest.php | 5 ++ .../assert-isNotInstanceOfAny.php | 51 ++++++++++++++++ 5 files changed, 176 insertions(+), 30 deletions(-) create mode 100644 tests/static-analysis/assert-isNotInstanceOfAny.php diff --git a/README.md b/README.md index 7c10111b..c809ea08 100644 --- a/README.md +++ b/README.md @@ -82,36 +82,37 @@ The [`Assert`] class provides the following assertions: ### Type Assertions -Method | Description --------------------------------------------------------- | -------------------------------------------------- -`string($value, $message = '')` | Check that a value is a string -`stringNotEmpty($value, $message = '')` | Check that a value is a non-empty string -`integer($value, $message = '')` | Check that a value is an integer -`integerish($value, $message = '')` | Check that a value casts to an integer -`positiveInteger($value, $message = '')` | Check that a value is a positive (non-zero) integer -`negativeInteger($value, $message = '')` | Check that a value is a negative integer -`notNegativeInteger($value, $message = '')` | Check that a value is a non-negative integer -`float($value, $message = '')` | Check that a value is a float -`numeric($value, $message = '')` | Check that a value is numeric -`natural($value, $message = '')` | Check that a value is a non-negative integer -`boolean($value, $message = '')` | Check that a value is a boolean -`scalar($value, $message = '')` | Check that a value is a scalar -`object($value, $message = '')` | Check that a value is an object -`objectish($value, $message = '')` | Check that a value is an object or a string of a class that exists -`resource($value, $type = null, $message = '')` | Check that a value is a resource -`isInitialized($value, $property, $message = '')` | Check that a value has an initialized property -`isCallable($value, $message = '')` | Check that a value is a callable -`isArray($value, $message = '')` | Check that a value is an array -`isIterable($value, $message = '')` | Check that a value is an array or a `\Traversable` -`isCountable($value, $message = '')` | Check that a value is an array or a `\Countable` -`isInstanceOf($value, $class, $message = '')` | Check that a value is an `instanceof` a class -`isInstanceOfAny($value, array $classes, $message = '')` | Check that a value is an `instanceof` at least one class on the array of classes -`notInstanceOf($value, $class, $message = '')` | Check that a value is not an `instanceof` a class -`isAOf($value, $class, $message = '')` | Check that a value is of the class or has one of its parents -`isAnyOf($value, array $classes, $message = '')` | Check that a value is of at least one of the classes or has one of its parents -`isNotA($value, $class, $message = '')` | Check that a value is not of the class or has not one of its parents -`isArrayAccessible($value, $message = '')` | Check that a value can be accessed as an array -`uniqueValues($values, $message = '')` | Check that the given array contains unique values +Method | Description +----------------------------------------------------------- | -------------------------------------------------- +`string($value, $message = '')` | Check that a value is a string +`stringNotEmpty($value, $message = '')` | Check that a value is a non-empty string +`integer($value, $message = '')` | Check that a value is an integer +`integerish($value, $message = '')` | Check that a value casts to an integer +`positiveInteger($value, $message = '')` | Check that a value is a positive (non-zero) integer +`negativeInteger($value, $message = '')` | Check that a value is a negative integer +`notNegativeInteger($value, $message = '')` | Check that a value is a non-negative integer +`float($value, $message = '')` | Check that a value is a float +`numeric($value, $message = '')` | Check that a value is numeric +`natural($value, $message = '')` | Check that a value is a non-negative integer +`boolean($value, $message = '')` | Check that a value is a boolean +`scalar($value, $message = '')` | Check that a value is a scalar +`object($value, $message = '')` | Check that a value is an object +`objectish($value, $message = '')` | Check that a value is an object or a string of a class that exists +`resource($value, $type = null, $message = '')` | Check that a value is a resource +`isInitialized($value, $property, $message = '')` | Check that a value has an initialized property +`isCallable($value, $message = '')` | Check that a value is a callable +`isArray($value, $message = '')` | Check that a value is an array +`isIterable($value, $message = '')` | Check that a value is an array or a `\Traversable` +`isCountable($value, $message = '')` | Check that a value is an array or a `\Countable` +`isInstanceOf($value, $class, $message = '')` | Check that a value is an `instanceof` a class +`isInstanceOfAny($value, array $classes, $message = '')` | Check that a value is an `instanceof` at least one class on the array of classes +`notInstanceOf($value, $class, $message = '')` | Check that a value is not an `instanceof` a class +`isNotInstanceOfAny($value, array $classes, $message = '')` | Check that a value is not an `instanceof` at least one class on the array of classes +`isAOf($value, $class, $message = '')` | Check that a value is of the class or has one of its parents +`isAnyOf($value, array $classes, $message = '')` | Check that a value is of at least one of the classes or has one of its parents +`isNotA($value, $class, $message = '')` | Check that a value is not of the class or has not one of its parents +`isArrayAccessible($value, $message = '')` | Check that a value can be accessed as an array +`uniqueValues($values, $message = '')` | Check that the given array contains unique values ### Comparison Assertions diff --git a/src/Assert.php b/src/Assert.php index 6b9825e7..66a0c292 100644 --- a/src/Assert.php +++ b/src/Assert.php @@ -552,6 +552,36 @@ public static function isInstanceOfAny(mixed $value, mixed $classes, string $mes )); } + /** + * @template T of object + * + * @psalm-assert T $value + * + * @param T $value + * + * @return T + * + * @throws InvalidArgumentException + */ + public static function isNotInstanceOfAny(mixed $value, mixed $classes, string $message = ''): mixed + { + static::isIterable($classes); + + foreach ($classes as $class) { + static::string($class, 'Expected class as a string. Got: %s'); + + if ($value instanceof $class) { + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected not an instance of %2$s. Got: %s', + static::typeToString($value), + \implode(', ', \array_map(static::valueToString(...), \iterator_to_array($classes))) + )); + } + } + + return $value; + } + /** * @psalm-pure * diff --git a/src/Mixin.php b/src/Mixin.php index 0b42c5ae..1bb714a0 100644 --- a/src/Mixin.php +++ b/src/Mixin.php @@ -1313,6 +1313,65 @@ public static function allNullOrIsInstanceOfAny(mixed $value, mixed $classes, st return $value; } + /** + * @template T of object + * @psalm-assert T|null $value + * + * @param T|null $value + * + * @return T|null + * + * @throws InvalidArgumentException + */ + public static function nullOrIsNotInstanceOfAny(mixed $value, mixed $classes, string $message = ''): mixed + { + null === $value || static::isNotInstanceOfAny($value, $classes, $message); + + return $value; + } + + /** + * @template T of object + * @psalm-assert iterable $value + * + * @param iterable $value + * + * @return iterable + * + * @throws InvalidArgumentException + */ + public static function allIsNotInstanceOfAny(mixed $value, mixed $classes, string $message = ''): iterable + { + static::isIterable($value); + + foreach ($value as $entry) { + static::isNotInstanceOfAny($entry, $classes, $message); + } + + return $value; + } + + /** + * @template T of object + * @psalm-assert iterable $value + * + * @param iterable $value + * + * @return iterable + * + * @throws InvalidArgumentException + */ + public static function allNullOrIsNotInstanceOfAny(mixed $value, mixed $classes, string $message = ''): iterable + { + static::isIterable($value); + + foreach ($value as $entry) { + null === $entry || static::isNotInstanceOfAny($entry, $classes, $message); + } + + return $value; + } + /** * @psalm-pure * diff --git a/tests/AssertTest.php b/tests/AssertTest.php index 8fefdbcc..4a13641f 100644 --- a/tests/AssertTest.php +++ b/tests/AssertTest.php @@ -196,6 +196,11 @@ public static function getTests(): array ['isInstanceOfAny', [new Exception(), ['ArrayAccess', 'Countable']], false], ['isInstanceOfAny', [123, ['stdClass']], false], ['isInstanceOfAny', [[], ['stdClass']], false], + ['isNotInstanceOfAny', [new ArrayIterator(), ['Exception', 'Countable']], false], + ['isNotInstanceOfAny', [new Exception(), ['ArrayAccess', 'Countable']], true], + ['isNotInstanceOfAny', [new Exception(), ['Exception', 'Countable']], false], + ['isNotInstanceOfAny', [123, ['stdClass']], true], + ['isNotInstanceOfAny', [[], ['stdClass']], true], ['isAOf', ['stdClass', 'stdClass'], true], ['isAOf', ['stdClass', 123], false], ['isAOf', ['Iterator', 'ArrayIterator'], false], diff --git a/tests/static-analysis/assert-isNotInstanceOfAny.php b/tests/static-analysis/assert-isNotInstanceOfAny.php new file mode 100644 index 00000000..a55e558b --- /dev/null +++ b/tests/static-analysis/assert-isNotInstanceOfAny.php @@ -0,0 +1,51 @@ + $classes + */ +function isNotInstanceOfAny($value, array $classes): mixed +{ + Assert::isNotInstanceOfAny($value, $classes); + + return $value; +} + +/** + * @param mixed $value + * @param array $classes + */ +function nullOrIsNotInstanceOfAny($value, array $classes): mixed +{ + Assert::nullOrIsNotInstanceOfAny($value, $classes); + + return $value; +} + +/** + * @param mixed $value + * @param array $classes + */ +function allIsNotInstanceOfAny($value, array $classes): mixed +{ + Assert::allIsNotInstanceOfAny($value, $classes); + + return $value; +} + +/** + * @param mixed $value + * @param array $classes + */ +function allNullOrIsNotInstanceOfAny($value, array $classes): mixed +{ + Assert::allNullOrIsNotInstanceOfAny($value, $classes); + + return $value; +} From 163587fb60af68a5c1628fe3e253ae5a472cf43f Mon Sep 17 00:00:00 2001 From: aymericcucherousset Date: Sun, 5 Apr 2026 13:46:19 +0200 Subject: [PATCH 2/2] Fix Psalm annotation: remove incorrect object constraint --- src/Assert.php | 2 +- src/Mixin.php | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Assert.php b/src/Assert.php index 66a0c292..8c090400 100644 --- a/src/Assert.php +++ b/src/Assert.php @@ -553,7 +553,7 @@ public static function isInstanceOfAny(mixed $value, mixed $classes, string $mes } /** - * @template T of object + * @template T * * @psalm-assert T $value * diff --git a/src/Mixin.php b/src/Mixin.php index 1bb714a0..891b7549 100644 --- a/src/Mixin.php +++ b/src/Mixin.php @@ -1314,7 +1314,7 @@ public static function allNullOrIsInstanceOfAny(mixed $value, mixed $classes, st } /** - * @template T of object + * @template T * @psalm-assert T|null $value * * @param T|null $value @@ -1331,7 +1331,7 @@ public static function nullOrIsNotInstanceOfAny(mixed $value, mixed $classes, st } /** - * @template T of object + * @template T * @psalm-assert iterable $value * * @param iterable $value @@ -1352,7 +1352,7 @@ public static function allIsNotInstanceOfAny(mixed $value, mixed $classes, strin } /** - * @template T of object + * @template T * @psalm-assert iterable $value * * @param iterable $value