From f6badd519146911450c3d44385061b782b9987a4 Mon Sep 17 00:00:00 2001 From: upstream Date: Thu, 3 Sep 2026 14:28:48 +0200 Subject: [PATCH] Upstream webmozarts/assert state of the touched files before ce2a73faf1 --- src/Assert.php | 2620 ++++++++++++++++++++++++++++++++++++++++++ tests/AssertTest.php | 1129 ++++++++++++++++++ 2 files changed, 3749 insertions(+) create mode 100644 src/Assert.php create mode 100644 tests/AssertTest.php diff --git a/src/Assert.php b/src/Assert.php new file mode 100644 index 0000000..31c861b --- /dev/null +++ b/src/Assert.php @@ -0,0 +1,2620 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Webmozart\Assert; + +use ArrayAccess; +use Closure; +use Countable; +use DateTime; +use DateTimeImmutable; +use ReflectionFunction; +use ReflectionProperty; +use Throwable; +use Traversable; + +/** + * Efficient assertions to validate the input/output of your methods. + * + * @since 1.0 + * + * @author Bernhard Schussek + */ +class Assert +{ + use Mixin; + + /** + * @psalm-pure + * + * @psalm-assert string $value + * @param string|callable():string $message + * + * @throws InvalidArgumentException + */ + public static function string(mixed $value, string|callable $message = ''): string + { + if (!\is_string($value)) { + $message = static::resolveMessage($message); + + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected a string. Got: %s', + static::typeToString($value) + )); + } + + return $value; + } + + /** + * @psalm-pure + * + * @psalm-assert non-empty-string $value + * @param string|callable():string $message + * + * @return non-empty-string + * + * @throws InvalidArgumentException + */ + public static function stringNotEmpty(mixed $value, string|callable $message = ''): string + { + static::string($value, $message); + static::notSame($value, '', $message); + + return $value; + } + + /** + * @psalm-pure + * + * @psalm-assert int $value + * + * @param string|callable():string $message + * + * @throws InvalidArgumentException + */ + public static function integer(mixed $value, string|callable $message = ''): int + { + if (!\is_int($value)) { + $message = self::resolveMessage($message); + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected an integer. Got: %s', + static::typeToString($value) + )); + } + + return $value; + } + + /** + * @psalm-pure + * + * @psalm-assert numeric $value + * @param string|callable():string $message + * + * @throws InvalidArgumentException + */ + public static function integerish(mixed $value, string|callable $message = ''): int|float|string + { + if (!\is_numeric($value) || $value != (int) $value) { + $message = self::resolveMessage($message); + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected an integerish value. Got: %s', + static::typeToString($value) + )); + } + + return $value; + } + + /** + * @psalm-pure + * + * @psalm-assert positive-int $value + * + * @param string|callable():string $message + * + * @return positive-int + * + * @throws InvalidArgumentException + */ + public static function positiveInteger(mixed $value, string|callable $message = ''): int + { + static::integer($value, $message); + + if ($value < 1) { + $message = self::resolveMessage($message); + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected a positive integer. Got: %s', + static::valueToString($value) + )); + } + + return $value; + } + + /** + * @psalm-pure + * @psalm-assert non-negative-int $value + * @param string|callable():string $message + * + * @return non-negative-int + * + * @throws InvalidArgumentException + */ + public static function notNegativeInteger(mixed $value, string|callable $message = ''): int + { + static::integer($value, $message); + + if ($value < 0) { + $message = self::resolveMessage($message); + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected a non negative integer. Got: %s', + static::valueToString($value) + )); + } + + return $value; + } + + /** + * @psalm-pure + * @psalm-assert negative-int $value + * @param string|callable():string $message + * + * @return negative-int + * + * @throws InvalidArgumentException + */ + public static function negativeInteger(mixed $value, string|callable $message = ''): int + { + static::integer($value, $message); + + if ($value >= 0) { + $message = self::resolveMessage($message); + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected a negative integer. Got: %s', + static::valueToString($value) + )); + } + + return $value; + } + + /** + * @psalm-pure + * + * @psalm-assert float $value + * + * @param string|callable():string $message + * + * @throws InvalidArgumentException + */ + public static function float(mixed $value, string|callable $message = ''): float + { + if (!\is_float($value)) { + $message = self::resolveMessage($message); + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected a float. Got: %s', + static::typeToString($value) + )); + } + + return $value; + } + + /** + * @psalm-pure + * + * @psalm-assert numeric $value + * + * @param string|callable():string $message + * + * @throws InvalidArgumentException + */ + public static function numeric(mixed $value, string|callable $message = ''): int|float|string + { + if (!\is_numeric($value)) { + $message = self::resolveMessage($message); + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected a numeric. Got: %s', + static::typeToString($value) + )); + } + + return $value; + } + + /** + * @psalm-pure + * + * @psalm-assert positive-int|0 $value + * + * @param string|callable():string $message + * + * @return positive-int|0 + * + * @throws InvalidArgumentException + */ + public static function natural(mixed $value, string|callable $message = ''): int + { + if (!\is_int($value) || $value < 0) { + $message = self::resolveMessage($message); + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected a non-negative integer. Got: %s', + static::valueToString($value) + )); + } + + return $value; + } + + /** + * @psalm-pure + * + * @psalm-assert bool $value + * + * @param string|callable():string $message + * + * @throws InvalidArgumentException + */ + public static function boolean(mixed $value, string|callable $message = ''): bool + { + if (!\is_bool($value)) { + $message = self::resolveMessage($message); + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected a boolean. Got: %s', + static::typeToString($value) + )); + } + + return $value; + } + + /** + * @psalm-pure + * + * @psalm-assert scalar $value + * + * @param string|callable():string $message + * + * @throws InvalidArgumentException + */ + public static function scalar(mixed $value, string|callable $message = ''): int|bool|float|string + { + if (!\is_scalar($value)) { + $message = self::resolveMessage($message); + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected a scalar. Got: %s', + static::typeToString($value) + )); + } + + return $value; + } + + /** + * @psalm-pure + * + * @psalm-assert object $value + * + * @param string|callable():string $message + * + * @throws InvalidArgumentException + */ + public static function object(mixed $value, string|callable $message = ''): object + { + if (!\is_object($value)) { + $message = self::resolveMessage($message); + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected an object. Got: %s', + static::typeToString($value) + )); + } + + return $value; + } + + /** + * @psalm-pure + * + * @psalm-assert object|class-string $value + * + * @param string|callable():string $message + * + * @return object|class-string + * + * @throws InvalidArgumentException + */ + public static function objectish(mixed $value, string|callable $message = ''): object|string + { + if (!\is_object($value) && !\is_string($value)) { + $message = self::resolveMessage($message); + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected an objectish value. Got: %s', + static::typeToString($value) + )); + } + + if (\is_string($value) && !\class_exists($value)) { + $message = self::resolveMessage($message); + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected class to be defined. Got: %s', + $value + )); + } + + return $value; + } + + /** + * @psalm-pure + * + * @psalm-assert resource $value + * + * @param string|callable():string $message + * + * @see https://www.php.net/manual/en/function.get-resource-type.php + * + * @return resource + * + * @throws InvalidArgumentException + */ + public static function resource(mixed $value, ?string $type = null, string|callable $message = ''): mixed + { + if (!\is_resource($value)) { + $message = self::resolveMessage($message); + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected a resource. Got: %s', + static::typeToString($value), + $type // User supplied message might include the second placeholder. + )); + } + + if ($type && $type !== \get_resource_type($value)) { + $message = self::resolveMessage($message); + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected a resource of type %2$s. Got: %s', + static::typeToString($value), + $type + )); + } + + return $value; + } + + /** + * @psalm-pure + * + * @psalm-assert object $value + * + * @param string|callable():string $message + * + * @throws InvalidArgumentException + */ + public static function isInitialized(mixed $value, string $property, string|callable $message = ''): object + { + Assert::object($value); + + $reflectionProperty = new ReflectionProperty($value, $property); + + if (!$reflectionProperty->isInitialized($value)) { + $message = self::resolveMessage($message); + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected property %s to be initialized.', + $property, + )); + } + + return $value; + } + + /** + * @psalm-pure + * + * @psalm-assert callable $value + * + * @param string|callable():string $message + * + * @throws InvalidArgumentException + */ + public static function isCallable(mixed $value, string|callable $message = ''): callable + { + if (!\is_callable($value)) { + $message = self::resolveMessage($message); + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected a callable. Got: %s', + static::typeToString($value) + )); + } + + return $value; + } + + /** + * @psalm-pure + * + * @psalm-assert array $value + * + * @param string|callable():string $message + * + * @throws InvalidArgumentException + */ + public static function isArray(mixed $value, string|callable $message = ''): array + { + if (!\is_array($value)) { + $message = self::resolveMessage($message); + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected an array. Got: %s', + static::typeToString($value) + )); + } + + return $value; + } + + /** + * @psalm-pure + * + * @psalm-assert array|ArrayAccess $value + * + * @param string|callable():string $message + * + * @throws InvalidArgumentException + */ + public static function isArrayAccessible(mixed $value, string|callable $message = ''): array|ArrayAccess + { + if (!\is_array($value) && !($value instanceof ArrayAccess)) { + $message = self::resolveMessage($message); + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected an array accessible. Got: %s', + static::typeToString($value) + )); + } + + return $value; + } + + /** + * @psalm-pure + * + * @psalm-assert countable $value + * + * @param string|callable():string $message + * + * @throws InvalidArgumentException + */ + public static function isCountable(mixed $value, string|callable $message = ''): array|Countable + { + if (!\is_array($value) && !($value instanceof Countable)) { + $message = self::resolveMessage($message); + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected a countable. Got: %s', + static::typeToString($value) + )); + } + + return $value; + } + + /** + * @psalm-pure + * + * @psalm-assert iterable $value + * + * @param string|callable():string $message + * + * @throws InvalidArgumentException + */ + public static function isIterable(mixed $value, string|callable $message = ''): iterable + { + if (!\is_array($value) && !($value instanceof Traversable)) { + $message = self::resolveMessage($message); + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected an iterable. Got: %s', + static::typeToString($value) + )); + } + + return $value; + } + + /** + * @psalm-pure + * + * @template T of object + * + * @psalm-assert T $value + * + * @param string|callable():string $message + * @param class-string $class + * + * @return T + * + * @throws InvalidArgumentException + */ + public static function isInstanceOf(mixed $value, mixed $class, string|callable $message = ''): object + { + static::string($class, 'Expected class as a string. Got: %s'); + + if (!($value instanceof $class)) { + $message = self::resolveMessage($message); + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected an instance of %2$s. Got: %s', + static::typeToString($value), + $class + )); + } + + return $value; + } + + /** + * @template T of object + * + * @psalm-assert object $value + * + * @param string|callable():string $message + * @param class-string $class + * + * @throws InvalidArgumentException + */ + public static function notInstanceOf(mixed $value, mixed $class, string|callable $message = ''): object + { + static::string($class, 'Expected class as a string. Got: %s'); + + if (!\is_object($value) || $value instanceof $class) { + $message = self::resolveMessage($message); + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected an instance other than %2$s. Got: %s', + static::typeToString($value), + $class + )); + } + + return $value; + } + + /** + * @template T of object + * + * @psalm-assert T $value + * + * @param iterable> $classes + * @param string|callable():string $message + * + * @return T + * + * @throws InvalidArgumentException + */ + public static function isInstanceOfAny(mixed $value, mixed $classes, string|callable $message = ''): object + { + static::isIterable($classes); + + foreach ($classes as $class) { + static::string($class, 'Expected class as a string. Got: %s'); + + if ($value instanceof $class) { + return $value; + } + } + + $message = self::resolveMessage($message); + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected an instance of any of %2$s. Got: %s', + static::typeToString($value), + \implode(', ', \array_map(static::valueToString(...), \iterator_to_array($classes))) + )); + } + + /** + * @template T + * + * @psalm-assert object|class-string $value + * + * @param T $value + * @param string|callable():string $message + * + * @return T + * + * @throws InvalidArgumentException + */ + public static function isNotInstanceOfAny(mixed $value, mixed $classes, string|callable $message = ''): mixed + { + static::isIterable($classes); + + foreach ($classes as $class) { + static::string($class, 'Expected class as a string. Got: %s'); + + if ($value instanceof $class) { + $message = self::resolveMessage($message); + 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 + * + * @template T of object + * + * @psalm-assert T|class-string $value + * + * @param string|callable():string $message + * @param class-string $class + * + * @return T|class-string + * + * @throws InvalidArgumentException + */ + public static function isAOf(mixed $value, mixed $class, string|callable $message = ''): object|string + { + static::string($class, 'Expected class as a string. Got: %s'); + + if (!\is_a($value, $class, \is_string($value))) { + $message = self::resolveMessage($message); + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected an instance of this class or to this class among its parents "%2$s". Got: %s', + static::valueToString($value), + $class + )); + } + + return $value; + } + + /** + * @psalm-pure + * + * @template T + * + * @psalm-assert object|class-string $value + * + * @param T $value + * @param string|callable():string $message + * + * @return object|class-string + * + * @throws InvalidArgumentException + */ + public static function isNotA(mixed $value, mixed $class, string|callable $message = ''): object|string + { + static::objectish($value, $message); + static::string($class, 'Expected class as a string. Got: %s'); + + if (\is_a($value, $class, \is_string($value))) { + $message = self::resolveMessage($message); + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected an instance of this class or to this class among its parents other than "%2$s". Got: %s', + static::valueToString($value), + $class + )); + } + + return $value; + } + + /** + * @psalm-pure + * + * @psalm-assert T $value + * + * @template T as object + * + * @param array> $classes + * @param string|callable():string $message + * + * @return T + * @throws InvalidArgumentException + */ + public static function isAnyOf(mixed $value, mixed $classes, string|callable $message = ''): object|string + { + static::objectish($value, $message); + static::isIterable($classes); + + foreach ($classes as $class) { + static::string($class, 'Expected class as a string. Got: %s'); + + if (\is_a($value, $class, \is_string($value))) { + return $value; + } + } + + $message = self::resolveMessage($message); + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected an instance of any of this classes or any of those classes among their parents "%2$s". Got: %s', + static::valueToString($value), + \implode(', ', \iterator_to_array($classes)) + )); + } + + /** + * @psalm-pure + * + * @psalm-assert empty $value + * + * @param string|callable():string $message + * + * @return empty + * + * @throws InvalidArgumentException + */ + public static function isEmpty(mixed $value, string|callable $message = ''): mixed + { + if (!empty($value)) { + $message = self::resolveMessage($message); + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected an empty value. Got: %s', + static::valueToString($value) + )); + } + + return $value; + } + + /** + * @psalm-pure + * + * @psalm-assert !empty $value + * + * @param string|callable():string $message + * + * @throws InvalidArgumentException + */ + public static function notEmpty(mixed $value, string|callable $message = ''): mixed + { + if (empty($value)) { + $message = self::resolveMessage($message); + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected a non-empty value. Got: %s', + static::valueToString($value) + )); + } + + return $value; + } + + /** + * @psalm-pure + * + * @psalm-assert null $value + * + * @param string|callable():string $message + * + * @throws InvalidArgumentException + */ + public static function null(mixed $value, string|callable $message = ''): null + { + if (null !== $value) { + $message = self::resolveMessage($message); + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected null. Got: %s', + static::valueToString($value) + )); + } + + return $value; + } + + /** + * @psalm-pure + * + * @psalm-assert !null $value + * + * @param string|callable():string $message + * + * @template T + * @param T|null $value + * @return T + * @throws InvalidArgumentException + */ + public static function notNull(mixed $value, string|callable $message = ''): mixed + { + if (null === $value) { + $message = self::resolveMessage($message); + static::reportInvalidArgument( + $message ?: 'Expected a value other than null.' + ); + } + + return $value; + } + + /** + * @psalm-pure + * + * @psalm-assert true $value + * + * @param string|callable():string $message + * + * @throws InvalidArgumentException + */ + public static function true(mixed $value, string|callable $message = ''): true + { + if (true !== $value) { + $message = self::resolveMessage($message); + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected a value to be true. Got: %s', + static::valueToString($value) + )); + } + + return $value; + } + + /** + * @psalm-pure + * + * @psalm-assert false $value + * + * @param string|callable():string $message + * + * @throws InvalidArgumentException + */ + public static function false(mixed $value, string|callable $message = ''): false + { + if (false !== $value) { + $message = self::resolveMessage($message); + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected a value to be false. Got: %s', + static::valueToString($value) + )); + } + + return $value; + } + + /** + * @psalm-pure + * + * @psalm-assert !false $value + * + * @param string|callable():string $message + * + * @template T + * @param T|false $value + * @return T + * @throws InvalidArgumentException + */ + public static function notFalse(mixed $value, string|callable $message = ''): mixed + { + if (false === $value) { + $message = self::resolveMessage($message); + static::reportInvalidArgument( + $message ?: 'Expected a value other than false.' + ); + } + + return $value; + } + + /** + * @psalm-pure + * + * @param string|callable():string $message + * @param string $value + * + * @throws InvalidArgumentException + */ + public static function ip(mixed $value, string|callable $message = ''): string + { + static::string($value, $message); + + if (false === \filter_var($value, \FILTER_VALIDATE_IP)) { + $message = self::resolveMessage($message); + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected a value to be an IP. Got: %s', + static::valueToString($value) + )); + } + + return $value; + } + + /** + * @psalm-pure + * + * @param string|callable():string $message + * @param string $value + * + * @throws InvalidArgumentException + */ + public static function ipv4(mixed $value, string|callable $message = ''): string + { + static::string($value, $message); + + if (false === \filter_var($value, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV4)) { + $message = self::resolveMessage($message); + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected a value to be an IPv4. Got: %s', + static::valueToString($value) + )); + } + + return $value; + } + + /** + * @psalm-pure + * + * @param string|callable():string $message + * @param string $value + * + * @throws InvalidArgumentException + */ + public static function ipv6(mixed $value, string|callable $message = ''): string + { + static::string($value, $message); + + if (false === \filter_var($value, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV6)) { + $message = self::resolveMessage($message); + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected a value to be an IPv6. Got: %s', + static::valueToString($value) + )); + } + + return $value; + } + + /** + * @psalm-pure + * + * @param string|callable():string $message + * @param string $value + * + * @throws InvalidArgumentException + */ + public static function email(mixed $value, string|callable $message = ''): string + { + static::string($value, $message); + + if (false === \filter_var($value, FILTER_VALIDATE_EMAIL, FILTER_FLAG_EMAIL_UNICODE)) { + $message = self::resolveMessage($message); + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected a value to be a valid e-mail address. Got: %s', + static::valueToString($value) + )); + } + + return $value; + } + + /** + * Does non-strict comparisons on the items, so ['3', 3] will not pass the assertion. + * Note: objects with identical properties are also considered equal. + * + * @psalm-assert array $values + * + * @param string|callable():string $message + * + * @throws InvalidArgumentException + */ + public static function uniqueValues(mixed $values, string|callable $message = ''): array + { + static::isArray($values); + + $allValues = \count($values); + $uniqueValues = \count(\array_unique($values)); + + if ($allValues !== $uniqueValues) { + $difference = $allValues - $uniqueValues; + + $message = self::resolveMessage($message); + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected an array of unique values, but %s of them %s duplicated', + $difference, + 1 === $difference ? 'is' : 'are' + )); + } + + return $values; + } + + /** + * @param string|callable():string $message + * + * @throws InvalidArgumentException + */ + public static function eq(mixed $value, mixed $expect, string|callable $message = ''): mixed + { + if ($expect != $value) { + $message = self::resolveMessage($message); + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected a value equal to %2$s. Got: %s', + static::valueToString($value), + static::valueToString($expect) + )); + } + + return $value; + } + + /** + * @param string|callable():string $message + * + * @throws InvalidArgumentException + */ + public static function notEq(mixed $value, mixed $expect, string|callable $message = ''): mixed + { + if ($expect == $value) { + $message = self::resolveMessage($message); + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected a different value than %s.', + static::valueToString($expect) + )); + } + + return $value; + } + + /** + * @psalm-pure + * + * @param string|callable():string $message + * + * @throws InvalidArgumentException + */ + public static function same(mixed $value, mixed $expect, string|callable $message = ''): mixed + { + if ($expect !== $value) { + $message = self::resolveMessage($message); + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected a value identical to %2$s. Got: %s', + static::valueToString($value), + static::valueToString($expect) + )); + } + + return $value; + } + + /** + * @psalm-pure + * + * @param string|callable():string $message + * + * @throws InvalidArgumentException + */ + public static function notSame(mixed $value, mixed $expect, string|callable $message = ''): mixed + { + if ($expect === $value) { + $message = self::resolveMessage($message); + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected a value not identical to %s.', + static::valueToString($expect) + )); + } + + return $value; + } + + /** + * @psalm-pure + * + * @param string|callable():string $message + * + * @throws InvalidArgumentException + */ + public static function greaterThan(mixed $value, mixed $limit, string|callable $message = ''): mixed + { + if ($value <= $limit) { + $message = self::resolveMessage($message); + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected a value greater than %2$s. Got: %s', + static::valueToString($value), + static::valueToString($limit) + )); + } + + return $value; + } + + /** + * @psalm-pure + * + * @param string|callable():string $message + * + * @throws InvalidArgumentException + */ + public static function greaterThanEq(mixed $value, mixed $limit, string|callable $message = ''): mixed + { + if ($value < $limit) { + $message = self::resolveMessage($message); + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected a value greater than or equal to %2$s. Got: %s', + static::valueToString($value), + static::valueToString($limit) + )); + } + + return $value; + } + + /** + * @psalm-pure + * + * @param string|callable():string $message + * + * @throws InvalidArgumentException + */ + public static function lessThan(mixed $value, mixed $limit, string|callable $message = ''): mixed + { + if ($value >= $limit) { + $message = self::resolveMessage($message); + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected a value less than %2$s. Got: %s', + static::valueToString($value), + static::valueToString($limit) + )); + } + + return $value; + } + + /** + * @psalm-pure + * + * @param string|callable():string $message + * + * @throws InvalidArgumentException + */ + public static function lessThanEq(mixed $value, mixed $limit, string|callable $message = ''): mixed + { + if ($value > $limit) { + $message = self::resolveMessage($message); + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected a value less than or equal to %2$s. Got: %s', + static::valueToString($value), + static::valueToString($limit) + )); + } + + return $value; + } + + /** + * Inclusive range, so Assert::(3, 3, 5) passes. + * + * @psalm-pure + * + * @param string|callable():string $message + * + * @throws InvalidArgumentException + */ + public static function range(mixed $value, mixed $min, mixed $max, string|callable $message = ''): mixed + { + if ($value < $min || $value > $max) { + $message = self::resolveMessage($message); + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected a value between %2$s and %3$s. Got: %s', + static::valueToString($value), + static::valueToString($min), + static::valueToString($max) + )); + } + + return $value; + } + + /** + * A more human-readable alias of Assert::inArray(). + * + * @psalm-pure + * + * @param string|callable():string $message + * + * @throws InvalidArgumentException + */ + public static function oneOf(mixed $value, mixed $values, string|callable $message = ''): mixed + { + static::inArray($value, $values, $message); + + return $value; + } + + /** + * Does strict comparison, so Assert::inArray(3, ['3']) does not pass the assertion. + * + * @psalm-pure + * + * @param string|callable():string $message + * + * @throws InvalidArgumentException + */ + public static function inArray(mixed $value, mixed $values, string|callable $message = ''): mixed + { + static::isArray($values); + + if (!\in_array($value, $values, true)) { + $message = self::resolveMessage($message); + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected one of: %2$s. Got: %s', + static::valueToString($value), + \implode(', ', \array_map(static::valueToString(...), $values)) + )); + } + + return $value; + } + + /** + * A more human-readable alias of Assert::notInArray(). + * + * @psalm-pure + * + * @param string|callable():string $message + * + * @throws InvalidArgumentException + */ + public static function notOneOf(mixed $value, mixed $values, string|callable $message = ''): mixed + { + static::notInArray($value, $values, $message); + + return $value; + } + + /** + * Check that a value is not present + * + * Does strict comparison, so Assert::notInArray(3, [1, 2, 3]) will not pass + * the assertion, but Assert::notInArray(3, ['3']) will. + * + * @psalm-pure + * + * @param string|callable():string $message + * + * @throws InvalidArgumentException + */ + public static function notInArray(mixed $value, mixed $values, string|callable $message = ''): mixed + { + static::isArray($values); + + if (\in_array($value, $values, true)) { + $message = self::resolveMessage($message); + static::reportInvalidArgument(\sprintf( + $message ?: '%2$s was not expected to contain a value. Got: %s', + static::valueToString($value), + \implode(', ', \array_map(static::valueToString(...), $values)) + )); + } + + return $value; + } + + /** + * @psalm-pure + * + * @param string|callable():string $message + * + * @throws InvalidArgumentException + */ + public static function contains(mixed $value, mixed $subString, string|callable $message = ''): string + { + static::string($value); + static::string($subString); + + if (!\str_contains($value, $subString)) { + $message = self::resolveMessage($message); + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected a value to contain %2$s. Got: %s', + static::valueToString($value), + static::valueToString($subString) + )); + } + + return $value; + } + + /** + * @psalm-pure + * + * @param string|callable():string $message + * + * @throws InvalidArgumentException + */ + public static function notContains(mixed $value, mixed $subString, string|callable $message = ''): string + { + static::string($value); + static::string($subString); + + if (\str_contains($value, $subString)) { + $message = self::resolveMessage($message); + static::reportInvalidArgument(\sprintf( + $message ?: '%2$s was not expected to be contained in a value. Got: %s', + static::valueToString($value), + static::valueToString($subString) + )); + } + + return $value; + } + + /** + * @psalm-pure + * + * @param string|callable():string $message + * + * @throws InvalidArgumentException + */ + public static function notWhitespaceOnly(mixed $value, string|callable $message = ''): string + { + static::string($value); + + if (\preg_match('/^\s*$/', $value)) { + $message = self::resolveMessage($message); + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected a non-whitespace string. Got: %s', + static::valueToString($value) + )); + } + + return $value; + } + + /** + * @psalm-pure + * + * @param string|callable():string $message + * + * @throws InvalidArgumentException + */ + public static function startsWith(mixed $value, mixed $prefix, string|callable $message = ''): string + { + static::string($value); + static::string($prefix); + + if (!\str_starts_with($value, $prefix)) { + $message = self::resolveMessage($message); + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected a value to start with %2$s. Got: %s', + static::valueToString($value), + static::valueToString($prefix) + )); + } + + return $value; + } + + /** + * @psalm-pure + * + * @param string|callable():string $message + * + * @throws InvalidArgumentException + */ + public static function notStartsWith(mixed $value, mixed $prefix, string|callable $message = ''): string + { + static::string($value); + static::string($prefix); + + if (\str_starts_with($value, $prefix)) { + $message = self::resolveMessage($message); + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected a value not to start with %2$s. Got: %s', + static::valueToString($value), + static::valueToString($prefix) + )); + } + + return $value; + } + + /** + * @psalm-pure + * + * @param string|callable():string $message + * + * @throws InvalidArgumentException + */ + public static function startsWithLetter(mixed $value, string|callable $message = ''): string + { + static::string($value); + + $valid = isset($value[0]); + + if ($valid) { + $locale = \setlocale(LC_CTYPE, '0'); + \setlocale(LC_CTYPE, 'C'); + $valid = \ctype_alpha($value[0]); + \setlocale(LC_CTYPE, $locale); + } + + if (!$valid) { + $message = self::resolveMessage($message); + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected a value to start with a letter. Got: %s', + static::valueToString($value) + )); + } + + return $value; + } + + /** + * @psalm-pure + * + * @param string|callable():string $message + * + * @throws InvalidArgumentException + */ + public static function endsWith(mixed $value, mixed $suffix, string|callable $message = ''): string + { + static::string($value); + static::string($suffix); + + if (!\str_ends_with($value, $suffix)) { + $message = self::resolveMessage($message); + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected a value to end with %2$s. Got: %s', + static::valueToString($value), + static::valueToString($suffix) + )); + } + + return $value; + } + + /** + * @psalm-pure + * + * @param string|callable():string $message + * + * @throws InvalidArgumentException + */ + public static function notEndsWith(mixed $value, mixed $suffix, string|callable $message = ''): string + { + static::string($value); + static::string($suffix); + + if (\str_ends_with($value, $suffix)) { + $message = self::resolveMessage($message); + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected a value not to end with %2$s. Got: %s', + static::valueToString($value), + static::valueToString($suffix) + )); + } + + return $value; + } + + /** + * @psalm-pure + * + * @param string|callable():string $message + * + * @throws InvalidArgumentException + */ + public static function regex(mixed $value, mixed $pattern, string|callable $message = ''): string + { + static::string($value); + static::string($pattern); + + if (!\preg_match($pattern, $value)) { + $message = self::resolveMessage($message); + static::reportInvalidArgument(\sprintf( + $message ?: 'The value %s does not match the expected pattern.', + static::valueToString($value) + )); + } + + return $value; + } + + /** + * @psalm-pure + * + * @param string|callable():string $message + * + * @throws InvalidArgumentException + */ + public static function notRegex(mixed $value, mixed $pattern, string|callable $message = ''): string + { + static::string($value); + static::string($pattern); + + if (\preg_match($pattern, $value, $matches, PREG_OFFSET_CAPTURE)) { + $message = self::resolveMessage($message); + static::reportInvalidArgument(\sprintf( + $message ?: 'The value %s matches the pattern %s (at offset %d).', + static::valueToString($value), + static::valueToString($pattern), + $matches[0][1] + )); + } + + return $value; + } + + /** + * @psalm-pure + * + * @param string|callable():string $message + * + * @throws InvalidArgumentException + */ + public static function unicodeLetters(mixed $value, string|callable $message = ''): string + { + static::string($value, $message); + + if (!\preg_match('/^\p{L}+$/u', $value)) { + $message = self::resolveMessage($message); + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected a value to contain only Unicode letters. Got: %s', + static::valueToString($value) + )); + } + + return $value; + } + + /** + * @psalm-pure + * + * @param string|callable():string $message + * + * @throws InvalidArgumentException + */ + public static function alpha(mixed $value, string|callable $message = ''): string + { + static::string($value, $message); + + $locale = \setlocale(LC_CTYPE, '0'); + \setlocale(LC_CTYPE, 'C'); + $valid = !\ctype_alpha($value); + \setlocale(LC_CTYPE, $locale); + + if ($valid) { + $message = self::resolveMessage($message); + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected a value to contain only letters. Got: %s', + static::valueToString($value) + )); + } + + return $value; + } + + /** + * @psalm-pure + * + * @param string|callable():string $message + * + * @throws InvalidArgumentException + */ + public static function digits(mixed $value, string|callable $message = ''): string + { + static::string($value, $message); + + $locale = \setlocale(LC_CTYPE, '0'); + \setlocale(LC_CTYPE, 'C'); + $valid = !\ctype_digit($value); + \setlocale(LC_CTYPE, $locale); + + if ($valid) { + $message = self::resolveMessage($message); + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected a value to contain digits only. Got: %s', + static::valueToString($value) + )); + } + + return $value; + } + + /** + * @psalm-pure + * + * @param string|callable():string $message + * + * @throws InvalidArgumentException + */ + public static function alnum(mixed $value, string|callable $message = ''): string + { + static::string($value, $message); + + $locale = \setlocale(LC_CTYPE, '0'); + \setlocale(LC_CTYPE, 'C'); + $valid = !\ctype_alnum($value); + \setlocale(LC_CTYPE, $locale); + + if ($valid) { + $message = self::resolveMessage($message); + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected a value to contain letters and digits only. Got: %s', + static::valueToString($value) + )); + } + + return $value; + } + + /** + * @psalm-pure + * + * @psalm-assert lowercase-string $value + * + * @param string|callable():string $message + * + * @return lowercase-string + * + * @throws InvalidArgumentException + */ + public static function lower(mixed $value, string|callable $message = ''): string + { + static::string($value, $message); + + $locale = \setlocale(LC_CTYPE, '0'); + \setlocale(LC_CTYPE, 'C'); + $valid = !\ctype_lower($value); + \setlocale(LC_CTYPE, $locale); + + if ($valid) { + $message = self::resolveMessage($message); + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected a value to contain lowercase characters only. Got: %s', + static::valueToString($value) + )); + } + + return $value; + } + + /** + * @psalm-pure + * + * @psalm-assert string $value + * + * @param string|callable():string $message + * + * @throws InvalidArgumentException + */ + public static function upper(mixed $value, string|callable $message = ''): string + { + static::string($value, $message); + + $locale = \setlocale(LC_CTYPE, '0'); + \setlocale(LC_CTYPE, 'C'); + $valid = !\ctype_upper($value); + \setlocale(LC_CTYPE, $locale); + + if ($valid) { + $message = self::resolveMessage($message); + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected a value to contain uppercase characters only. Got: %s', + static::valueToString($value) + )); + } + + return $value; + } + + /** + * @psalm-pure + * + * @param string|callable():string $message + * + * @throws InvalidArgumentException + */ + public static function length(mixed $value, mixed $length, string|callable $message = ''): string + { + static::string($value); + static::integerish($length); + + if ($length !== static::strlen($value)) { + $message = self::resolveMessage($message); + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected a value to contain %2$s characters. Got: %s', + static::valueToString($value), + $length + )); + } + + return $value; + } + + /** + * Inclusive min. + * + * @psalm-pure + * + * @param string|callable():string $message + * + * @throws InvalidArgumentException + */ + public static function minLength(mixed $value, mixed $min, string|callable $message = ''): string + { + static::string($value); + static::integerish($min); + + if (static::strlen($value) < $min) { + $message = self::resolveMessage($message); + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected a value to contain at least %2$s characters. Got: %s', + static::valueToString($value), + $min + )); + } + + return $value; + } + + /** + * Inclusive max. + * + * @psalm-pure + * + * @param string|callable():string $message + * + * @throws InvalidArgumentException + */ + public static function maxLength(mixed $value, mixed $max, string|callable $message = ''): string + { + static::string($value); + static::integerish($max); + + if (static::strlen($value) > $max) { + $message = self::resolveMessage($message); + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected a value to contain at most %2$s characters. Got: %s', + static::valueToString($value), + $max + )); + } + + return $value; + } + + /** + * Inclusive, so Assert::lengthBetween('asd', 3, 5); passes the assertion. + * + * @psalm-pure + * + * @param string|callable():string $message + * + * @throws InvalidArgumentException + */ + public static function lengthBetween(mixed $value, mixed $min, mixed $max, string|callable $message = ''): string + { + static::string($value); + static::integerish($min); + static::integerish($max); + + $length = static::strlen($value); + + if ($length < $min || $length > $max) { + $message = self::resolveMessage($message); + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected a value to contain between %2$s and %3$s characters. Got: %s', + static::valueToString($value), + $min, + $max + )); + } + + return $value; + } + + /** + * Will also pass if $value is a directory, use Assert::file() instead if you need to be sure it is a file. + * + * @param string|callable():string $message + * + * @throws InvalidArgumentException + */ + public static function fileExists(mixed $value, string|callable $message = ''): string + { + static::string($value); + + if (!\file_exists($value)) { + $message = self::resolveMessage($message); + static::reportInvalidArgument(\sprintf( + $message ?: 'The path %s does not exist.', + static::valueToString($value) + )); + } + + return $value; + } + + /** + * @param string|callable():string $message + * + * @throws InvalidArgumentException + */ + public static function file(mixed $value, string|callable $message = ''): string + { + static::string($value); + + if (!\is_file($value)) { + $message = self::resolveMessage($message); + static::reportInvalidArgument(\sprintf( + $message ?: 'The path %s is not a file.', + static::valueToString($value) + )); + } + + return $value; + } + + /** + * @param string|callable():string $message + * + * @throws InvalidArgumentException + */ + public static function directory(mixed $value, string|callable $message = ''): string + { + static::string($value); + + if (!\is_dir($value)) { + $message = self::resolveMessage($message); + static::reportInvalidArgument(\sprintf( + $message ?: 'The path %s is not a directory.', + static::valueToString($value) + )); + } + + return $value; + } + + /** + * @param string|callable():string $message + * + * @throws InvalidArgumentException + */ + public static function readable(mixed $value, string|callable $message = ''): string + { + static::string($value); + + if (!\is_readable($value)) { + $message = self::resolveMessage($message); + static::reportInvalidArgument(\sprintf( + $message ?: 'The path %s is not readable.', + static::valueToString($value) + )); + } + + return $value; + } + + /** + * @param string|callable():string $message + * + * @throws InvalidArgumentException + */ + public static function writable(mixed $value, string|callable $message = ''): string + { + static::string($value); + + if (!\is_writable($value)) { + $message = self::resolveMessage($message); + static::reportInvalidArgument(\sprintf( + $message ?: 'The path %s is not writable.', + static::valueToString($value) + )); + } + + return $value; + } + + /** + * @psalm-assert class-string $value + * + * @param string|callable():string $message + * + * @return class-string + * + * @throws InvalidArgumentException + */ + public static function classExists(mixed $value, string|callable $message = ''): string + { + static::string($value); + + if (!\class_exists($value)) { + $message = self::resolveMessage($message); + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected an existing class name. Got: %s', + static::valueToString($value) + )); + } + + return $value; + } + + /** + * @psalm-pure + * + * @template ExpectedType of object + * + * @psalm-assert class-string $value + * + * @param class-string $class + * @param string|callable():string $message + * + * @return class-string + * + * @throws InvalidArgumentException + */ + public static function subclassOf(mixed $value, mixed $class, string|callable $message = ''): string + { + static::string($value); + static::string($class); + + if (!\is_subclass_of($value, $class)) { + $message = self::resolveMessage($message); + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected a sub-class of %2$s. Got: %s', + static::valueToString($value), + static::valueToString($class) + )); + } + + return $value; + } + + /** + * @psalm-assert class-string $value + * + * @param string|callable():string $message + * + * @return class-string + * + * @throws InvalidArgumentException + */ + public static function interfaceExists(mixed $value, string|callable $message = ''): string + { + static::string($value); + + if (!\interface_exists($value)) { + $message = self::resolveMessage($message); + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected an existing interface name. got %s', + static::valueToString($value) + )); + } + + return $value; + } + + /** + * @psalm-pure + * + * @template ExpectedType of object + * + * @psalm-assert class-string|ExpectedType $value + * + * @param class-string $interface + * @param string|callable():string $message + * + * @return class-string|ExpectedType + * + * @throws InvalidArgumentException + */ + public static function implementsInterface(mixed $value, mixed $interface, string|callable $message = ''): object|string + { + static::objectish($value); + + $implements = \class_implements($value); + + static::isArray($implements); + + if (!\in_array($interface, $implements, true)) { + $message = self::resolveMessage($message); + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected an implementation of %2$s. Got: %s', + static::valueToString($value), + static::valueToString($interface) + )); + } + + return $value; + } + + /** + * @psalm-pure + * + * @param string|object $classOrObject + * @param string|callable():string $message + * + * @throws InvalidArgumentException + */ + public static function propertyExists(mixed $classOrObject, mixed $property, string|callable $message = ''): object|string + { + static::objectish($classOrObject); + + if (!\property_exists($classOrObject, $property)) { + $message = self::resolveMessage($message); + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected the property %s to exist.', + static::valueToString($property) + )); + } + + return $classOrObject; + } + + /** + * @psalm-pure + * + * @template T as class-string|object + * @param T $classOrObject + * @param string|callable():string $message + * + * @return T + * @throws InvalidArgumentException + */ + public static function propertyNotExists(mixed $classOrObject, mixed $property, string|callable $message = ''): mixed + { + if (!(\is_string($classOrObject) || \is_object($classOrObject)) || \property_exists($classOrObject, $property)) { + $message = self::resolveMessage($message); + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected the property %s to not exist.', + static::valueToString($property) + )); + } + + return $classOrObject; + } + + /** + * @psalm-pure + * + * @template T as class-string|object + * @param T $classOrObject + * @param string|callable():string $message + * + * @return T + * @throws InvalidArgumentException + */ + public static function methodExists(mixed $classOrObject, mixed $method, string|callable $message = ''): object|string + { + static::objectish($classOrObject); + + if (!\method_exists($classOrObject, $method)) { + $message = self::resolveMessage($message); + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected the method %s to exist.', + static::valueToString($method) + )); + } + + return $classOrObject; + } + + /** + * @psalm-pure + * + * @template T as class-string|object + * @param T $classOrObject + * @param string|callable():string $message + * + * @return T + * @throws InvalidArgumentException + */ + public static function methodNotExists(mixed $classOrObject, mixed $method, string|callable $message = ''): mixed + { + static::objectish($classOrObject); + + if (\method_exists($classOrObject, $method)) { + $message = self::resolveMessage($message); + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected the method %s to not exist.', + static::valueToString($method) + )); + } + + return $classOrObject; + } + + /** + * @psalm-pure + * + * @param string|int $key + * @param string|callable():string $message + * + * @throws InvalidArgumentException + */ + public static function keyExists(mixed $array, string|int $key, string|callable $message = ''): array + { + static::isArray($array, $message); + + if (!(isset($array[$key]) || \array_key_exists($key, $array))) { + $message = self::resolveMessage($message); + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected the key %s to exist.', + static::valueToString($key) + )); + } + + return $array; + } + + /** + * @psalm-pure + * + * @param string|int $key + * @param string|callable():string $message + * + * @throws InvalidArgumentException + */ + public static function keyNotExists(mixed $array, string|int $key, string|callable $message = ''): array + { + static::isArray($array, $message); + + if (isset($array[$key]) || \array_key_exists($key, $array)) { + $message = self::resolveMessage($message); + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected the key %s to not exist.', + static::valueToString($key) + )); + } + + return $array; + } + + /** + * Checks if a value is a valid array key (int or string). + * + * @psalm-pure + * + * @psalm-assert array-key $value + * + * @param string|callable():string $message + * + * @throws InvalidArgumentException + */ + public static function validArrayKey(mixed $value, string|callable $message = ''): string|int + { + if (!(\is_int($value) || \is_string($value))) { + $message = self::resolveMessage($message); + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected string or integer. Got: %s', + static::typeToString($value) + )); + } + + return $value; + } + + /** + * @param string|callable():string $message + * + * @throws InvalidArgumentException + */ + public static function count(mixed $array, mixed $number, string|callable $message = ''): array|Countable + { + static::isCountable($array); + static::integerish($number); + + static::eq( + \count($array), + $number, + fn () => static::resolveMessage($message) ?: \sprintf( + $message ?: 'Expected an array to contain %d elements. Got: %d.', + $number, + \count($array) + ) + ); + + return $array; + } + + /** + * @param string|callable():string $message + * + * @throws InvalidArgumentException + */ + public static function minCount(mixed $array, mixed $min, string|callable $message = ''): array|Countable + { + static::isCountable($array); + static::integerish($min); + + if (\count($array) < $min) { + $message = self::resolveMessage($message); + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected an array to contain at least %2$d elements. Got: %d', + \count($array), + $min + )); + } + + return $array; + } + + /** + * @param string|callable():string $message + * + * @throws InvalidArgumentException + */ + public static function maxCount(mixed $array, mixed $max, string|callable $message = ''): array|Countable + { + static::isCountable($array); + static::integerish($max); + + if (\count($array) > $max) { + $message = self::resolveMessage($message); + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected an array to contain at most %2$d elements. Got: %d', + \count($array), + $max + )); + } + + return $array; + } + + /** + * @param string|callable():string $message + * + * @throws InvalidArgumentException + */ + public static function countBetween(mixed $array, mixed $min, mixed $max, string|callable $message = ''): array|Countable + { + static::isCountable($array); + static::integerish($min); + static::integerish($max); + + $count = \count($array); + + if ($count < $min || $count > $max) { + $message = self::resolveMessage($message); + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected an array to contain between %2$d and %3$d elements. Got: %d', + $count, + $min, + $max + )); + } + + return $array; + } + + /** + * @psalm-pure + * + * @psalm-assert list $array + * + * @param string|callable():string $message + * + * @return list + * + * @throws InvalidArgumentException + */ + public static function isList(mixed $array, string|callable $message = ''): array + { + if (!\is_array($array) || !\array_is_list($array)) { + $message = self::resolveMessage($message); + static::reportInvalidArgument( + $message ?: 'Expected list - non-associative array.' + ); + } + + return $array; + } + + /** + * @psalm-pure + * + * @psalm-assert non-empty-list $array + * + * @param string|callable():string $message + * + * @return non-empty-list + * + * @throws InvalidArgumentException + */ + public static function isNonEmptyList(mixed $array, string|callable $message = ''): array + { + static::isList($array, $message); + static::notEmpty($array, $message); + + return $array; + } + + /** + * @psalm-pure + * + * @template T + * + * @psalm-assert array $array + * + * @param mixed|array $array + * @param string|callable():string $message + * + * @return array + * + * @throws InvalidArgumentException + */ + public static function isMap(mixed $array, string|callable $message = ''): array + { + static::isArray($array, $message); + + if (\count($array) > 0 && \array_is_list($array)) { + $message = self::resolveMessage($message); + static::reportInvalidArgument( + $message ?: 'Expected map - associative array with string keys.' + ); + } + + return $array; + } + + /** + * @param callable $callable + * @param string|callable():string $message + * + * @return Closure|callable-string + * + * @throws InvalidArgumentException + */ + public static function isStatic(mixed $callable, string|callable $message = ''): Closure|string + { + static::isCallable($callable, $message); + + $callable = static::callableToClosure($callable); + + $reflection = new ReflectionFunction($callable); + + if (!$reflection->isStatic()) { + $message = self::resolveMessage($message); + static::reportInvalidArgument( + $message ?: 'Closure is not static.' + ); + } + + return $callable; + } + + /** + * @param callable $callable + * @param string|callable():string $message + * + * @return Closure|callable-string + * + * @throws InvalidArgumentException + */ + public static function notStatic(mixed $callable, string|callable $message = ''): Closure|string + { + static::isCallable($callable, $message); + + $callable = static::callableToClosure($callable); + + $reflection = new ReflectionFunction($callable); + + if ($reflection->isStatic()) { + $message = self::resolveMessage($message); + static::reportInvalidArgument( + $message ?: 'Closure is not static.' + ); + } + + return $callable; + } + + /** + * @psalm-pure + * + * @template T + * + * @psalm-assert array $array + * @psalm-assert !empty $array + * + * @param array $array + * @param string|callable():string $message + * + * @return non-empty-array + * + * @throws InvalidArgumentException + */ + public static function isNonEmptyMap(mixed $array, string|callable $message = ''): array + { + static::isMap($array, $message); + static::notEmpty($array, $message); + + return $array; + } + + /** + * @psalm-pure + * + * @param string|callable():string $message + * + * @throws InvalidArgumentException + */ + public static function uuid(mixed $value, string|callable $message = ''): string + { + static::string($value, $message); + + $uuid = '[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}'; + + // URN form as specified by RFC 9562, e.g. "urn:uuid:ff6f8cb0-...". + if (\str_starts_with($value, 'urn:uuid:') && \preg_match('/^urn:uuid:'.$uuid.'$/D', $value)) { + return $value; + } + + // "uuid:" prefix, optionally combined with the curly-braced form. + if (\str_starts_with($value, 'uuid:') && \preg_match('/^uuid:(?:'.$uuid.'|\{'.$uuid.'\})$/D', $value)) { + return $value; + } + + // Curly-braced form; the braces must be a matching pair. + if (\str_starts_with($value, '{') && \str_ends_with($value, '}') && \preg_match('/^\{'.$uuid.'\}$/D', $value)) { + return $value; + } + + // Plain form, including the nil UUID with all 128 bits set to zero. + if (\preg_match('/^'.$uuid.'$/D', $value)) { + return $value; + } + + $message = self::resolveMessage($message); + static::reportInvalidArgument(\sprintf( + $message ?: 'Value %s is not a valid UUID.', + static::valueToString($value) + )); + } + + /** + * @template T as callable + * + * @param T $expression + * @param string|callable():string $message + * @param class-string $class + * + * @return T + * + * @throws InvalidArgumentException + */ + public static function throws(mixed $expression, string $class = Throwable::class, string|callable $message = ''): callable + { + static::string($class); + static::isCallable($expression); + + $actual = 'none'; + + try { + $expression(); + } catch (Throwable $e) { + $actual = \get_class($e); + if ($e instanceof $class) { + return $expression; + } + } + + $message = self::resolveMessage($message); + + static::reportInvalidArgument($message ?: \sprintf( + 'Expected to throw "%s", got "%s"', + $class, + $actual + )); + } + + /** + * @psalm-pure + * + * @return Closure|callable-string + */ + protected static function callableToClosure(callable $callable): Closure|string + { + if (\is_string($callable) && \function_exists($callable)) { + return $callable; + } + + if ($callable instanceof Closure) { + return $callable; + } + + return $callable(...); + } + + /** + * @psalm-pure + */ + protected static function valueToString(mixed $value): string + { + if (null === $value) { + return 'null'; + } + + if (true === $value) { + return 'true'; + } + + if (false === $value) { + return 'false'; + } + + if (\is_array($value)) { + return 'array'; + } + + if (\is_object($value)) { + if (\method_exists($value, '__toString')) { + return \get_class($value).': '.self::valueToString($value->__toString()); + } + + if ($value instanceof DateTime || $value instanceof DateTimeImmutable) { + return \get_class($value).': '.self::valueToString($value->format('c')); + } + + if (\enum_exists(\get_class($value))) { + return \get_class($value).'::'.$value->name; + } + + return \get_class($value); + } + + if (\is_resource($value)) { + return 'resource'; + } + + if (\is_string($value)) { + return '"'.$value.'"'; + } + + return (string) $value; + } + + /** + * @psalm-pure + */ + protected static function typeToString(mixed $value): string + { + return \is_object($value) ? \get_class($value) : \gettype($value); + } + + protected static function strlen(string $value): int + { + if (!\function_exists('mb_detect_encoding')) { + return \strlen($value); + } + + if (false === $encoding = \mb_detect_encoding($value)) { + return \strlen($value); + } + + return \mb_strlen($value, $encoding); + } + + /** + * @psalm-pure this method is not supposed to perform side effects + * + * @throws InvalidArgumentException + */ + protected static function reportInvalidArgument(string $message): never + { + throw new InvalidArgumentException($message); + } + + /** + * @psalm-pure + * + * @param string|callable():string $message + */ + protected static function resolveMessage(string|callable $message): string + { + return \is_callable($message) ? $message() : $message; + } + + private function __construct() + { + } +} diff --git a/tests/AssertTest.php b/tests/AssertTest.php new file mode 100644 index 0000000..f0ec064 --- /dev/null +++ b/tests/AssertTest.php @@ -0,0 +1,1129 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Webmozart\Assert\Tests; + +use ArrayIterator; +use ArrayObject; +use DateTime; +use DateTimeImmutable; +use Error; +use Exception; +use LogicException; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\TestCase; +use RuntimeException; +use SimpleXMLElement; +use stdClass; +use Webmozart\Assert\Assert; + +/** + * @since 1.0 + * + * @author Bernhard Schussek + */ +class AssertTest extends TestCase +{ + private const SKIP_MIXIN_ASSERTION_TESTS = [ + 'isInitialized', + ]; + + private const SKIP_CUSTOM_MESSAGE_TESTS = [ + 'isAOf', + 'isAnyOf', + 'isNotA', + 'startsWithLetter', + ]; + + public static function getResource() + { + static $resource; + + return $resource ??= tmpfile(); + } + + public static function getTests(): array + { + $nanList = ['key' => 2, NAN]; + unset($nanList['key']); + + $normalList = ['foo' => 'b', 3]; + unset($normalList['foo']); + + $a = new ToStringClass('testString'); + $b = new ToStringClass('testString'); + $c = new ToStringClass('otherString'); + + return [ + ['string', ['value'], true], + ['string', [''], true], + ['string', [1234], false], + ['stringNotEmpty', ['value'], true], + ['stringNotEmpty', ['0'], true], + ['stringNotEmpty', [''], false], + ['stringNotEmpty', [1234], false], + ['integer', [123], true], + ['integer', ['123'], false], + ['integer', [1.0], false], + ['integer', [1.23], false], + ['integerish', [1.0], true], + ['integerish', [1.23], false], + ['integerish', [123], true], + ['integerish', ['123'], true], + ['positiveInteger', [123], true], + ['positiveInteger', [1], true], + ['positiveInteger', [-123], false], + ['positiveInteger', [0], false], + ['positiveInteger', [0.0], false], + ['positiveInteger', ['123'], false], + ['positiveInteger', ['-123'], false], + ['positiveInteger', ['0'], false], + ['positiveInteger', [1.0], false], + ['positiveInteger', [1.23], false], + ['notNegativeInteger', [123], true], + ['notNegativeInteger', [1], true], + ['notNegativeInteger', [-123], false], + ['notNegativeInteger', [0], true], + ['notNegativeInteger', [0.0], false], + ['notNegativeInteger', ['123'], false], + ['notNegativeInteger', ['-123'], false], + ['notNegativeInteger', ['0'], false], + ['notNegativeInteger', [1.0], false], + ['notNegativeInteger', [1.23], false], + ['negativeInteger', [123], false], + ['negativeInteger', [1], false], + ['negativeInteger', [-123], true], + ['negativeInteger', [0], false], + ['negativeInteger', [0.0], false], + ['negativeInteger', ['123'], false], + ['negativeInteger', ['-123'], false], + ['negativeInteger', ['0'], false], + ['negativeInteger', [1.0], false], + ['negativeInteger', [1.23], false], + ['float', [1.0], true], + ['float', [1.23], true], + ['float', [123], false], + ['float', ['123'], false], + ['numeric', [1.0], true], + ['numeric', [1.23], true], + ['numeric', [123], true], + ['numeric', ['123'], true], + ['numeric', ['foo'], false], + ['natural', [0], true], + ['natural', [1], true], + ['natural', [-1], false], + ['natural', ['1'], false], + ['natural', [1.0], false], + ['natural', [1.23], false], + ['boolean', [true], true], + ['boolean', [false], true], + ['boolean', [1], false], + ['boolean', ['1'], false], + ['scalar', ['1'], true], + ['scalar', [123], true], + ['scalar', [true], true], + ['scalar', [null], false], + ['scalar', [[]], false], + ['scalar', [new stdClass()], false], + ['object', [new stdClass()], true], + ['object', [new RuntimeException()], true], + ['object', [null], false], + ['object', [true], false], + ['object', [1], false], + ['object', [[]], false], + ['objectish', [new stdClass()], true], + ['objectish', [new RuntimeException()], true], + ['objectish', [null], false], + ['objectish', [true], false], + ['objectish', [1], false], + ['objectish', [[]], false], + ['resource', [self::getResource()], true], + ['resource', [self::getResource(), 'stream'], true], + ['resource', [self::getResource(), 'other'], false], + ['resource', [1], false], + ['isInitialized', [new class { + public mixed $a = null; + }, 'a'], true], + ['isInitialized', [new class { + public mixed $a; + }, 'a'], false], + ['isInitialized', [new class { + public mixed $a; + public mixed $b = true; + }, 'a'], false], + ['isInitialized', [new class { + public mixed $a; + public mixed $b = true; + }, 'b'], true], + ['isCallable', ['strlen'], true], + ['isCallable', [[self::class, 'getTests']], true], + ['isCallable', [function () {}], true], + ['isCallable', [1234], false], + ['isCallable', ['foobar'], false], + ['isArray', [[]], true], + ['isArray', [[1, 2, 3]], true], + ['isArray', [new ArrayIterator([])], false], + ['isArray', [123], false], + ['isArray', [new stdClass()], false], + ['isArrayAccessible', [[]], true], + ['isArrayAccessible', [[1, 2, 3]], true], + ['isArrayAccessible', [new ArrayObject([])], true], + ['isArrayAccessible', [123], false], + ['isArrayAccessible', [new stdClass()], false], + ['isCountable', [[]], true], + ['isCountable', [[1, 2]], true], + ['isCountable', [new ArrayIterator([])], true], + ['isCountable', [new stdClass()], false], + ['isCountable', [new SimpleXMLElement('bar')], true], + ['isCountable', ['abcd'], false], + ['isCountable', [123], false], + ['isIterable', [[]], true], + ['isIterable', [[1, 2, 3]], true], + ['isIterable', [new ArrayIterator([])], true], + ['isIterable', [123], false], + ['isIterable', [new stdClass()], false], + ['isInstanceOf', [new stdClass(), 'stdClass'], true], + ['isInstanceOf', [new Exception(), 'stdClass'], false], + ['isInstanceOf', [123, 'stdClass'], false], + ['isInstanceOf', [[], 'stdClass'], false], + ['isInstanceOf', [null, 'stdClass'], false], + ['notInstanceOf', [new stdClass(), 'stdClass'], false], + ['notInstanceOf', [new Exception(), 'stdClass'], true], + ['notInstanceOf', [123, 'stdClass'], false], + ['notInstanceOf', [[], 'stdClass'], false], + ['notInstanceOf', [null, 'stdClass'], false], + ['isInstanceOfAny', [new ArrayIterator(), ['Iterator', 'ArrayAccess']], true], + ['isInstanceOfAny', [new Exception(), ['Exception', 'Countable']], true], + ['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], + ['isAOf', [123, 'Iterator'], false], + ['isAOf', [[], 'Iterator'], false], + ['isAnyOf', ['ArrayIterator', ['Iterator', 'ArrayAccess']], true], + ['isAnyOf', ['ArrayIterator', [123]], false], + ['isAnyOf', ['Exception', ['Exception', 'Countable']], true], + ['isAnyOf', ['Exception', ['ArrayAccess', 'Countable']], false], + ['isAnyOf', [123, ['stdClass']], false], + ['isAnyOf', [[], ['stdClass']], false], + ['isNotA', ['stdClass', 'stdClass'], false], + ['isNotA', ['stdClass', 123], false], + ['isNotA', [[], 'Iterator'], false], + ['true', [true], true], + ['true', [false], false], + ['true', [1], false], + ['true', [null], false], + ['false', [false], true], + ['false', [true], false], + ['false', [1], false], + ['false', [0], false], + ['false', [null], false], + ['notFalse', [false], false], + ['notFalse', [true], true], + ['notFalse', [1], true], + ['notFalse', [0], true], + ['notFalse', [null], true], + ['null', [null], true], + ['null', [false], false], + ['null', [0], false], + ['notNull', [false], true], + ['notNull', [0], true], + ['notNull', [null], false], + ['isEmpty', [null], true], + ['isEmpty', [false], true], + ['isEmpty', [0], true], + ['isEmpty', [''], true], + ['isEmpty', [1], false], + ['isEmpty', ['a'], false], + ['notEmpty', [1], true], + ['notEmpty', ['a'], true], + ['notEmpty', [null], false], + ['notEmpty', [false], false], + ['notEmpty', [0], false], + ['notEmpty', [''], false], + ['eq', [1, 1], true], + ['eq', [1, '1'], true], + ['eq', [1, true], true], + ['eq', [1, 0], false], + ['eq', [$a, $a], true], + ['eq', [$a, $b], true], + ['eq', [$a, $c], false], + ['notEq', [1, 0], true], + ['notEq', [1, 1], false], + ['notEq', [1, '1'], false], + ['notEq', [1, true], false], + ['same', [1, 1], true], + ['same', [1, '1'], false], + ['same', [1, true], false], + ['same', [1, 0], false], + ['same', [$a, $b], false], + ['same', [$a, $a], true], + ['notSame', [1, 0], true], + ['notSame', [1, 1], false], + ['notSame', [1, '1'], true], + ['notSame', [1, true], true], + ['notSame', [$a, $b], true], + ['notSame', [$a, $a], false], + ['greaterThan', [1, 0], true], + ['greaterThan', [0, 0], false], + ['greaterThanEq', [2, 1], true], + ['greaterThanEq', [1, 1], true], + ['greaterThanEq', [0, 1], false], + ['lessThan', [0, 1], true], + ['lessThan', [1, 1], false], + ['lessThanEq', [0, 1], true], + ['lessThanEq', [1, 1], true], + ['lessThanEq', [2, 1], false], + ['range', [1, 1, 2], true], + ['range', [2, 1, 2], true], + ['range', [0, 1, 2], false], + ['range', [3, 1, 2], false], + ['oneOf', [1, [1, 2, 3]], true], + ['oneOf', [1, ['1', '2', '3']], false], + ['notOneOf', [1, [1, 2, 3]], false], + ['notOneOf', [1, ['1', '2', '3']], true], + ['inArray', [1, [1, 2, 3]], true], + ['inArray', [1, ['1', '2', '3']], false], + ['inArray', [$a, [$a]], true], + ['inArray', [$a, [(string) $a]], false], + ['inArray', [$a, [$b]], false], + ['inArray', [$a, [$c]], false], + ['notInArray', [1, [1, 2, 3]], false], + ['notInArray', [1, ['1', '2', '3']], true], + ['notInArray', [$a, [$a]], false], + ['notInArray', [$a, [$b]], true], + ['notInArray', [$a, [$c]], true], + ['contains', ['abcd', 'ab'], true], + ['contains', ['abcd', 'bc'], true], + ['contains', ['abcd', 'cd'], true], + ['contains', ['abcd', 'de'], false], + ['contains', ['', 'de'], false], + ['contains', ['äþçð', 'äþ'], true], + ['contains', ['äþçð', 'þç'], true], + ['contains', ['äþçð', 'çð'], true], + ['contains', ['äþçð', 'ðé'], false], + ['contains', ['', 'ðé'], false], + ['contains', ['あいうえ', 'あい'], true], + ['contains', ['あいうえ', 'いう'], true], + ['contains', ['あいうえ', 'うえ'], true], + ['contains', ['あいうえ', 'えお'], false], + ['contains', ['', 'えお'], false], + ['contains', ['😄😑☹️', '😄'], true], + ['contains', ['😄😑☹️', '😑'], true], + ['contains', ['😄😑☹️', '☹️'], true], + ['contains', ['😄😑☹️', '😄☹️'], false], + ['contains', ['', '😑'], false], + ['notContains', ['abcd', 'ab'], false], + ['notContains', ['abcd', 'bc'], false], + ['notContains', ['abcd', 'cd'], false], + ['notContains', ['abcd', 'de'], true], + ['notContains', ['', 'de'], true], + ['notContains', ['äþçð', 'äþ'], false], + ['notContains', ['äþçð', 'þç'], false], + ['notContains', ['äþçð', 'çð'], false], + ['notContains', ['äþçð', 'ðé'], true], + ['notContains', ['', 'ðé'], true], + ['notContains', ['あいうえ', 'あい'], false], + ['notContains', ['あいうえ', 'いう'], false], + ['notContains', ['あいうえ', 'うえ'], false], + ['notContains', ['あいうえ', 'えお'], true], + ['notContains', ['', 'えお'], true], + ['notContains', ['😄😑☹️', '😄'], false], + ['notContains', ['😄😑☹️', '😑'], false], + ['notContains', ['😄😑☹️', '☹️'], false], + ['notContains', ['😄😑☹️', '😄☹️'], true], + ['notContains', ['', '😑'], true], + ['notWhitespaceOnly', ['abc'], true], + ['notWhitespaceOnly', ['123'], true], + ['notWhitespaceOnly', [' abc '], true], + ['notWhitespaceOnly', ['a b c'], true], + ['notWhitespaceOnly', [''], false], + ['notWhitespaceOnly', [' '], false], + ['notWhitespaceOnly', ["\t"], false], + ['notWhitespaceOnly', ["\n"], false], + ['notWhitespaceOnly', ["\r"], false], + ['notWhitespaceOnly', ["\r\n\t "], false], + ['startsWith', ['abcd', 'ab'], true], + ['startsWith', ['abcd', 'bc'], false], + ['startsWith', ['', 'bc'], false], + ['startsWith', ['äþçð', 'äþ'], true], + ['startsWith', ['äþçð', 'þç'], false], + ['startsWith', ['', 'þç'], false], + ['startsWith', ['あいうえ', 'あい'], true], + ['startsWith', ['あいうえ', 'いう'], false], + ['startsWith', ['', 'いう'], false], + ['startsWith', ['😄😑☹️', '😄'], true], + ['startsWith', ['😄😑☹️', '😑'], false], + ['startsWith', ['', '😑'], false], + ['startsWithLetter', ['abcd'], true], + ['startsWithLetter', [[66]], false], + ['startsWithLetter', ['a'], true], + ['startsWithLetter', ['a1'], true], + ['startsWithLetter', ['1abcd'], false], + ['startsWithLetter', ['1'], false], + ['startsWithLetter', [''], false], + ['startsWithLetter', [null], false], + ['startsWithLetter', [66], false], + ['notStartsWith', ['abcd', 'ab'], false], + ['notStartsWith', ['abcd', 'bc'], true], + ['notStartsWith', ['', 'bc'], true], + ['notStartsWith', ['äþçð', 'äþ'], false], + ['notStartsWith', ['äþçð', 'þç'], true], + ['notStartsWith', ['', 'þç'], true], + ['notStartsWith', ['あいうえ', 'あい'], false], + ['notStartsWith', ['あいうえ', 'いう'], true], + ['notStartsWith', ['', 'いう'], true], + ['notStartsWith', ['😄😑☹️', '😄'], false], + ['notStartsWith', ['😄😑☹️', '😑'], true], + ['notStartsWith', ['', '😑'], true], + ['endsWith', ['abcd', 'cd'], true], + ['endsWith', ['abcd', 'bc'], false], + ['endsWith', ['', 'bc'], false], + ['endsWith', ['äþçð', 'çð'], true], + ['endsWith', ['äþçð', 'þç'], false], + ['endsWith', ['', 'þç'], false], + ['endsWith', ['あいうえ', 'うえ'], true], + ['endsWith', ['あいうえ', 'いう'], false], + ['endsWith', ['', 'いう'], false], + ['endsWith', ['😄😑☹️', '☹️'], true], + ['endsWith', ['😄😑☹️', '😑'], false], + ['endsWith', ['', '😑'], false], + ['notEndsWith', ['abcd', 'cd'], false], + ['notEndsWith', ['abcd', 'bc'], true], + ['notEndsWith', ['', 'bc'], true], + ['notEndsWith', ['äþçð', 'çð'], false], + ['notEndsWith', ['äþçð', 'þç'], true], + ['notEndsWith', ['', 'þç'], true], + ['notEndsWith', ['あいうえ', 'うえ'], false], + ['notEndsWith', ['あいうえ', 'いう'], true], + ['notEndsWith', ['', 'いう'], true], + ['notEndsWith', ['😄😑☹️', '☹️'], false], + ['notEndsWith', ['😄😑☹️', '😑'], true], + ['notEndsWith', ['', '😑'], true], + ['regex', ['abcd', '~^ab~'], true], + ['regex', ['abcd', '~^bc~'], false], + ['regex', ['', '~^bc~'], false], + ['notRegex', ['abcd', '{^ab}'], false], + ['notRegex', ['abcd', '{^bc}'], true], + ['notRegex', ['', '{^bc}'], true], + ['unicodeLetters', ['abcd'], true], + ['unicodeLetters', ['ᴁڅਘธブ乶'], true], + ['unicodeLetters', ['ȁȄ'], true], + ['unicodeLetters', ['ȁ1Ȅ'], false], + ['unicodeLetters', ['©'], false], + ['unicodeLetters', ['🙁'], false], + ['unicodeLetters', [''], false], + ['alpha', ['abcd'], true], + ['alpha', ['ab1cd'], false], + ['alpha', [''], false], + ['alpha', [66], false], + ['alpha', [[]], false], + ['digits', ['1234'], true], + ['digits', ['12a34'], false], + ['digits', [''], false], + ['digits', [1234], false], + ['alnum', ['ab12'], true], + ['alnum', ['ab12$'], false], + ['alnum', [''], false], + ['alnum', [1234], false], + ['lower', ['abcd'], true], + ['lower', ['abCd'], false], + ['lower', ['ab_d'], false], + ['lower', [''], false], + ['lower', [1234], false], + ['upper', ['ABCD'], true], + ['upper', ['ABcD'], false], + ['upper', ['AB_D'], false], + ['upper', [''], false], + ['upper', [1234], false], + ['length', ['abcd', 4], true], + ['length', ['abc', 4], false], + ['length', ['abcde', 4], false], + ['length', ['äbcd', 4], true, true], + ['length', ['äbc', 4], false, true], + ['length', ['äbcde', 4], false, true], + ['length', ['あbcd', 4], true, true], // 'HIRAGANA LETTER A' (U+3042) + ['length', ['あbc', 4], false, true], + ['length', ['あbcde', 4], false, true], + ['minLength', ['abcd', 4], true], + ['minLength', ['abcde', 4], true], + ['minLength', ['abc', 4], false], + ['minLength', ['äbcd', 4], true, true], + ['minLength', ['äbcde', 4], true, true], + ['minLength', ['äbc', 4], false, true], + ['minLength', ['あbcd', 4], true, true], + ['minLength', ['あbcde', 4], true, true], + ['minLength', ['あbc', 4], false, true], + ['maxLength', ['abcd', 4], true], + ['maxLength', ['abc', 4], true], + ['maxLength', ['abcde', 4], false], + ['maxLength', ['äbcd', 4], true, true], + ['maxLength', ['äbc', 4], true, true], + ['maxLength', ['äbcde', 4], false, true], + ['maxLength', ['あbcd', 4], true, true], + ['maxLength', ['あbc', 4], true, true], + ['maxLength', ['あbcde', 4], false, true], + ['lengthBetween', ['abcd', 3, 5], true], + ['lengthBetween', ['abc', 3, 5], true], + ['lengthBetween', ['abcde', 3, 5], true], + ['lengthBetween', ['ab', 3, 5], false], + ['lengthBetween', ['abcdef', 3, 5], false], + ['lengthBetween', ['äbcd', 3, 5], true, true], + ['lengthBetween', ['äbc', 3, 5], true, true], + ['lengthBetween', ['äbcde', 3, 5], true, true], + ['lengthBetween', ['äb', 3, 5], false, true], + ['lengthBetween', ['äbcdef', 3, 5], false, true], + ['lengthBetween', ['あbcd', 3, 5], true, true], + ['lengthBetween', ['あbc', 3, 5], true, true], + ['lengthBetween', ['あbcde', 3, 5], true, true], + ['lengthBetween', ['あb', 3, 5], false, true], + ['lengthBetween', ['あbcdef', 3, 5], false, true], + ['fileExists', [__FILE__], true], + ['fileExists', [__DIR__], true], + ['fileExists', [__DIR__.'/foobar'], false], + ['file', [__FILE__], true], + ['file', [__DIR__], false], + ['file', [__DIR__.'/foobar'], false], + ['directory', [__DIR__], true], + ['directory', [__FILE__], false], + ['directory', [__DIR__.'/foobar'], false], + // no tests for readable()/writable() for now + ['classExists', [__CLASS__], true], + ['classExists', [__NAMESPACE__.'\Foobar'], false], + ['subclassOf', [__CLASS__, 'PHPUnit\Framework\TestCase'], true], + ['subclassOf', [__CLASS__, 'stdClass'], false], + ['interfaceExists', ['\Countable'], true], + ['interfaceExists', [__CLASS__], false], + ['implementsInterface', ['ArrayIterator', 'Traversable'], true], + ['implementsInterface', [__CLASS__, 'Traversable'], false], + ['implementsInterface', [new DateTimeImmutable(), 'DateTimeInterface'], true], + ['implementsInterface', [new DateTimeImmutable(), 'Traversable'], false], + ['implementsInterface', [new ArrayIterator([]), 'DateTimeInterface'], false], + ['implementsInterface', [new ArrayIterator([]), 'Traversable'], true], + ['propertyExists', [(object) ['property' => 0], 'property'], true], + ['propertyExists', [(object) ['property' => null], 'property'], true], + ['propertyExists', [(object) ['property' => null], 'foo'], false], + ['propertyNotExists', [(object) ['property' => 0], 'property'], false], + ['propertyNotExists', [(object) ['property' => null], 'property'], false], + ['methodExists', ['RuntimeException', 'getMessage'], true], + ['methodExists', [new RuntimeException(), 'getMessage'], true], + ['methodExists', ['stdClass', 'getMessage'], false], + ['methodExists', [new stdClass(), 'getMessage'], false], + ['methodNotExists', ['RuntimeException', 'getMessage'], false], + ['methodNotExists', [new RuntimeException(), 'getMessage'], false], + ['methodNotExists', ['stdClass', 'getMessage'], true], + ['methodNotExists', [new stdClass(), 'getMessage'], true], + ['keyExists', [['key' => 0], 'key'], true], + ['keyExists', [['key' => null], 'key'], true], + ['keyExists', [['key' => null], 'foo'], false], + ['keyNotExists', [['key' => 0], 'key'], false], + ['keyNotExists', [['key' => null], 'key'], false], + ['keyNotExists', [['key' => null], 'foo'], true], + ['validArrayKey', ['abcd'], true], + ['validArrayKey', [1], true], + ['validArrayKey', [false], false], + ['validArrayKey', [true], false], + ['validArrayKey', [new stdClass()], false], + ['validArrayKey', [new ToStringClass('testString')], false], + ['validArrayKey', [self::getResource()], false], + ['count', [[0, 1, 2], 3], true], + ['count', [[0, 1, 2], 2], false], + ['minCount', [[0], 2], false], + ['minCount', [[0, 1], 2], true], + ['minCount', [[0, 1, 2], 2], true], + ['maxCount', [[0, 1, 2], 2], false], + ['maxCount', [[0, 1], 2], true], + ['maxCount', [[0], 2], true], + ['countBetween', [[0, 1, 2], 4, 5], false], + ['countBetween', [[0, 1, 2], 3, 5], true], + ['countBetween', [[0, 1, 2], 1, 2], false], + ['countBetween', [[0, 1, 2], 2, 5], true], + ['countBetween', [[0, 1, 2], 2, 3], true], + ['isList', [[1, 2, 3]], true], + ['isList', [[]], true], + ['isList', [[0 => 1, 2 => 3]], false], + ['isList', [['key' => 1, 'foo' => 2]], false], + ['isList', [true], false], + ['isList', [false], false], + ['isList', [[true]], true], + ['isList', [[false]], true], + ['isList', [[[1], [2]]], true], + ['isList', [[['foo' => 'bar'], ['baz' => 'tab']]], true], + ['isList', [$nanList], true], + ['isList', [$normalList], true], + ['isNonEmptyList', [[1, 2, 3]], true], + ['isNonEmptyList', [[]], false], + ['isNonEmptyList', [[0 => 1, 2 => 3]], false], + ['isNonEmptyList', [['key' => 1, 'foo' => 2]], false], + ['isNonEmptyList', [true], false], + ['isNonEmptyList', [false], false], + ['isNonEmptyList', [[true]], true], + ['isNonEmptyList', [[false]], true], + ['isNonEmptyList', [[[1], [2]]], true], + ['isNonEmptyList', [[['foo' => 'bar'], ['baz' => 'tab']]], true], + ['isMap', [['key' => 1, 'foo' => 2]], true], + ['isMap', [[0 => 1, 2 => 3]], true], + ['isMap', [[]], true], + ['isMap', [[1, 2, 3]], false], + ['isNonEmptyMap', [['key' => 1, 'foo' => 2]], true], + ['isNonEmptyMap', [[0 => 1, 2 => 3]], true], + ['isNonEmptyMap', [[]], false], + ['isNonEmptyMap', [[1, 2, 3]], false], + ['uuid', ['00000000-0000-0000-0000-000000000000'], true], + ['uuid', ['urn:uuid:ff6f8cb0-c57d-21e1-9b21-0800200c9a66'], true], + ['uuid', ['uuid:{ff6f8cb0-c57d-21e1-9b21-0800200c9a66}'], true], + ['uuid', ['{ff6f8cb0-c57d-21e1-9b21-0800200c9a66}'], true], + ['uuid', ['ff6f8cb0-c57d-21e1-9b21-0800200c9a66'], true], + ['uuid', ['ff6f8cb0-c57d-11e1-9b21-0800200c9a66'], true], + ['uuid', ['ff6f8cb0-c57d-31e1-9b21-0800200c9a66'], true], + ['uuid', ['ff6f8cb0-c57d-41e1-9b21-0800200c9a66'], true], + ['uuid', ['ff6f8cb0-c57d-51e1-9b21-0800200c9a66'], true], + ['uuid', ['FF6F8CB0-C57D-11E1-9B21-0800200C9A66'], true], + ['uuid', ["urn:ff6f8cb0-c57d-21e1-9b21-0800200c9a66\n"], false], + ['uuid', ["ff6f8cb0-c57d-21e1-9b21-0800200c9a66\n"], false], + ['uuid', ["FF6F8CB0-C57D-11E1-9B21-0800200C9A66\n"], false], + ['uuid', ['zf6f8cb0-c57d-11e1-9b21-0800200c9a66'], false], + ['uuid', ['af6f8cb0c57d11e19b210800200c9a66'], false], + ['uuid', ['ff6f8cb0-c57da-51e1-9b21-0800200c9a66'], false], + ['uuid', ['af6f8cb-c57d-11e1-9b21-0800200c9a66'], false], + ['uuid', ['3f6f8cb0-c57d-11e1-9b21-0800200c9a6'], false], + ['uuid', ['f{}f6f8cb0-c57d-21e1-9b21-0800200c9a66'], false], + ['uuid', ['ff6f8cb0-c57d-21e1-9b21-08002urn:00c9a66'], false], + ['uuid', ['urn:ff6f8cb0-c57d-21e1-9b21-0800200c9a66uuid:'], false], + ['uuid', ['urn:ff6f8cb0-c57d-21e1-9b21-0800200c9a66'], false], + ['uuid', ['urn:uuid:{ff6f8cb0-c57d-21e1-9b21-0800200c9a66}'], false], + ['uuid', ['{}{}ff6f8cb0-c57d-21e1-9b21-0800200c9a66{}{}'], false], + ['uuid', ['{ff6f8cb0-c57d-21e1-9b21-0800200c9a66'], false], + ['uuid', ['ff6f8cb0-c57d-21e1-9b21-0800200c9a66}'], false], + ['throws', [function () { throw new LogicException('test'); }, 'LogicException'], true], + ['throws', [function () { throw new LogicException('test'); }, 'IllogicException'], false], + ['throws', [function () { throw new Exception('test'); }], true], + ['throws', [function () { trigger_error('test'); }, 'Throwable'], false, false], + ['throws', [function () { trigger_error('test'); }, 'Unthrowable'], false, false], + ['throws', [function () { throw new Error(); }, 'Throwable'], true, true], + ['ip', ['192.168.0.1'], true], + ['ip', ['255.255.255.255'], true], + ['ip', ['0.0.0.0'], true], + ['ip', ['2001:0db8:0000:0042:0000:8a2e:0370:7334'], true], + ['ip', ['::ffff:192.0.2.1'], true], + ['ip', ['::1'], true], + ['ip', ['::'], true], + ['ip', ['foo'], false], + ['ip', [123], false], + ['ip', [[]], false], + ['ip', [null], false], + ['ip', [false], false], + ['ipv4', ['192.168.0.1'], true], + ['ipv4', ['255.255.255.255'], true], + ['ipv4', ['0.0.0.0'], true], + ['ipv4', ['2001:0db8:0000:0042:0000:8a2e:0370:7334'], false], + ['ipv4', ['::ffff:192.0.2.1'], false], + ['ipv4', ['::1'], false], + ['ipv4', ['::'], false], + ['ipv4', ['foo'], false], + ['ipv4', [123], false], + ['ipv4', [[]], false], + ['ipv4', [null], false], + ['ipv4', [false], false], + ['ipv6', ['192.168.0.1'], false], + ['ipv6', ['255.255.255.255'], false], + ['ipv6', ['0.0.0.0'], false], + ['ipv6', ['2001:0db8:0000:0042:0000:8a2e:0370:7334'], true], + ['ipv6', ['::ffff:192.0.2.1'], true], + ['ipv6', ['::1'], true], + ['ipv6', ['::'], true], + ['ipv6', ['foo'], false], + ['ipv6', [123], false], + ['ipv6', [[]], false], + ['ipv6', [null], false], + ['ipv6', [false], false], + ['email', ['foo'], false], + ['email', [123], false], + ['email', ['foo.com'], false], + ['email', ['foo@bar.com'], true], + ['email', ['föö@bar.com'], true], + ['uniqueValues', [['qwerty', 'qwerty']], false], + ['uniqueValues', [['asdfg', 'qwerty']], true], + ['uniqueValues', [[123, '123']], false], + ['uniqueValues', [[$a, $a]], false], + ['uniqueValues', [[$a, $b]], false], + ['uniqueValues', [[$a, $c]], true], + ['isStatic', [static function () {}], true], + ['isStatic', [function () {}], false], + ['notStatic', [static function () {}], false], + ['notStatic', [function () {}], true], + ]; + } + + public static function getMethods(): array + { + $methods = []; + + foreach (self::getTests() as $params) { + $methods[$params[0]] = [$params[0]]; + } + + return array_values($methods); + } + + #[DataProvider('getTests')] + public function testAssert(string $method, array $args, bool $success, bool $multibyte = false): void + { + if ($multibyte && !function_exists('mb_strlen')) { + $this->markTestSkipped('The function mb_strlen() is not available'); + } + + if (!$success) { + $this->expectException('\InvalidArgumentException'); + } + + $result = call_user_func_array(['Webmozart\Assert\Assert', $method], $args); + $this->assertSame($args[array_key_first($args)], $result); + } + + #[DataProvider('getTests')] + public function testCustomMessage(string $method, array $args, bool $success, bool $multibyte = false): void + { + if (in_array($method, self::SKIP_CUSTOM_MESSAGE_TESTS)) { + $this->markTestSkipped("The method $method could have specific message handling instead of custom message."); + } + + if ($args === []) { + $this->addToAssertionCount(1); + + return; + } + + if ($multibyte && !function_exists('mb_strlen')) { + $this->markTestSkipped('The function mb_strlen() is not available'); + } + + if (!$success) { + $this->expectException('\InvalidArgumentException'); + $this->expectExceptionMessage('Custom error message'); + } + + $args['message'] = 'Custom error message'; + $result = Assert::$method(...$args); + + $this->assertSame($args[array_key_first($args)], $result); + } + + #[DataProvider('getTests')] + public function testLazyMessageCallbackCalled(string $method, array $args, bool $success, bool $multibyte = false): void + { + if ($args === []) { + $this->addToAssertionCount(1); + + return; + } + + if ($multibyte && !function_exists('mb_strlen')) { + $this->markTestSkipped('The function mb_strlen() is not available'); + } + + if (!$success) { + $this->expectException('\InvalidArgumentException'); + } + + $called = 0; + + $args['message'] = function () use (&$called) { + ++$called; + + return 'Custom error message number'; + }; + + Assert::$method(...$args); + + $expectedCalled = $success ? 0 : 1; + $this->assertSame($expectedCalled, $called, sprintf('The lazy message callback should be called exactly %d times.', $expectedCalled)); + } + + #[DataProvider('getTests')] + public function testNullOr(string $method, array $args, bool $success, bool $multibyte = false): void + { + if (in_array($method, self::SKIP_MIXIN_ASSERTION_TESTS)) { + $this->markTestSkipped("The method $method does not have nullOr Mixin."); + } + + if (in_array($method, ['null', 'notNull'])) { + $this->markTestSkipped('Meaningless test of '.$method); + } + + if ($multibyte && !function_exists('mb_strlen')) { + $this->markTestSkipped('The function mb_strlen() is not available'); + } + + if (!$success && null !== reset($args)) { + $this->expectException('\InvalidArgumentException'); + } + + $result = call_user_func_array(['Webmozart\Assert\Assert', 'nullOr'.ucfirst($method)], $args); + $this->assertSame($args[array_key_first($args)], $result); + } + + #[DataProvider('getMethods')] + public function testNullOrAcceptsNull(string $method): void + { + if (in_array($method, self::SKIP_MIXIN_ASSERTION_TESTS)) { + $this->markTestSkipped("The method $method does not have nullOr Mixin."); + } + + if (in_array($method, ['null', 'notNull'])) { + $this->markTestSkipped('Meaningless test of '.$method); + } + + $result = call_user_func(['Webmozart\Assert\Assert', 'nullOr'.ucfirst($method)], null, '', ''); + $this->assertNull($result); + } + + #[DataProvider('getTests')] + public function testAllArray(string $method, array $args, bool $success, bool $multibyte = false): void + { + if (in_array($method, self::SKIP_MIXIN_ASSERTION_TESTS)) { + $this->markTestSkipped("The method $method does not have all Mixin."); + } + + if ($multibyte && !function_exists('mb_strlen')) { + $this->markTestSkipped('The function mb_strlen() is not available'); + } + + if (!$success) { + $this->expectException('\InvalidArgumentException'); + } + + $arg = array_shift($args); + array_unshift($args, [$arg]); + + $result = call_user_func_array(['Webmozart\Assert\Assert', 'all'.ucfirst($method)], $args); + $this->assertSame($args[array_key_first($args)], $result); + } + + #[DataProvider('getTests')] + public function testAllNullOrArray(string $method, array $args, bool $success, bool $multibyte = false): void + { + if (in_array($method, self::SKIP_MIXIN_ASSERTION_TESTS)) { + $this->markTestSkipped("The method $method does not have allNullOr Mixin."); + } + + if (in_array($method, ['null', 'notNull'])) { + $this->markTestSkipped('Meaningless test of '.$method); + } + + if ($multibyte && !function_exists('mb_strlen')) { + $this->markTestSkipped('The function mb_strlen() is not available'); + } + + $arg = array_shift($args); + + if ($arg === null) { + $this->addToAssertionCount(1); + + return; + } + + if (!$success) { + $this->expectException('\InvalidArgumentException'); + } + + array_unshift($args, [$arg, null]); + + $result = call_user_func_array(['Webmozart\Assert\Assert', 'allNullOr'.ucfirst($method)], $args); + $this->assertSame($args[array_key_first($args)], $result); + } + + #[DataProvider('getTests')] + public function testAllTraversable(string $method, array $args, bool $success, bool $multibyte = false): void + { + if (in_array($method, self::SKIP_MIXIN_ASSERTION_TESTS)) { + $this->markTestSkipped("The method $method does not have all Mixin."); + } + + if ($multibyte && !function_exists('mb_strlen')) { + $this->markTestSkipped('The function mb_strlen() is not available'); + } + + if (!$success) { + $this->expectException('\InvalidArgumentException'); + } + + $arg = array_shift($args); + array_unshift($args, new ArrayIterator([$arg])); + + $result = call_user_func_array(['Webmozart\Assert\Assert', 'all'.ucfirst($method)], $args); + $this->assertSame($args[array_key_first($args)], $result); + } + + public static function getStringConversions(): array + { + return [ + ['integer', ['foobar'], 'Expected an integer. Got: string'], + ['string', [1], 'Expected a string. Got: integer'], + ['string', [true], 'Expected a string. Got: boolean'], + ['string', [null], 'Expected a string. Got: NULL'], + ['string', [[]], 'Expected a string. Got: array'], + ['string', [new stdClass()], 'Expected a string. Got: stdClass'], + ['string', [self::getResource()], 'Expected a string. Got: resource'], + + ['eq', ['1', '2'], 'Expected a value equal to "2". Got: "1"'], + ['eq', [new ToStringClass('XXX'), new ToStringClass('YYY')], 'Expected a value equal to Webmozart\Assert\Tests\ToStringClass: "YYY". Got: Webmozart\Assert\Tests\ToStringClass: "XXX"'], + ['eq', [1, 2], 'Expected a value equal to 2. Got: 1'], + ['eq', [true, false], 'Expected a value equal to false. Got: true'], + ['eq', [true, null], 'Expected a value equal to null. Got: true'], + ['eq', [null, true], 'Expected a value equal to true. Got: null'], + ['eq', [[1], [2]], 'Expected a value equal to array. Got: array'], + ['eq', [new ArrayIterator([]), new stdClass()], 'Expected a value equal to stdClass. Got: ArrayIterator'], + ['eq', [1, self::getResource()], 'Expected a value equal to resource. Got: 1'], + + ['lessThan', [new DateTime('2020-01-01 00:00:00+00:00'), new DateTime('1999-01-01 00:00:00+00:00')], 'Expected a value less than DateTime: "1999-01-01T00:00:00+00:00". Got: DateTime: "2020-01-01T00:00:00+00:00"'], + ]; + } + + #[DataProvider('getStringConversions')] + public function testConvertValuesToStrings(string $method, array $args, string $exceptionMessage): void + { + $this->expectException('\InvalidArgumentException'); + $this->expectExceptionMessage($exceptionMessage); + + call_user_func_array(['Webmozart\Assert\Assert', $method], $args); + } + + public static function getInvalidInstanceOfCases(): iterable + { + yield [ + [null, 'stdClass'], + 'Expected an instance of stdClass. Got: NULL', + ]; + + yield [ + [123, 'stdClass'], + 'Expected an instance of stdClass. Got: integer', + ]; + + yield [ + [[], 'Iterator'], + 'Expected an instance of Iterator. Got: array', + ]; + + yield [ + [new stdClass(), 'Iterator'], + 'Expected an instance of Iterator. Got: stdClass', + ]; + } + + #[DataProvider('getInvalidInstanceOfCases')] + public function testInstanceOfExceptionMessages(array $args, string $exceptionMessage): void + { + $this->expectException('\InvalidArgumentException'); + $this->expectExceptionMessage($exceptionMessage); + + call_user_func_array(['Webmozart\Assert\Assert', 'isInstanceOf'], $args); + } + + public static function getInvalidIsAOfCases(): iterable + { + yield [ + ['stdClass', 123], + 'Expected class as a string. Got: integer', + ]; + + yield [ + ['Iterator', 'ArrayIterator'], + 'Expected an instance of this class or to this class among its parents "ArrayIterator". Got: "Iterator"', + ]; + + yield [ + [123, 'Iterator'], + 'Expected an instance of this class or to this class among its parents "Iterator". Got: 123', + ]; + + yield [ + [[], 'Iterator'], + 'Expected an instance of this class or to this class among its parents "Iterator". Got: array', + ]; + + yield [ + [new stdClass(), 'Iterator'], + 'Expected an instance of this class or to this class among its parents "Iterator". Got: stdClass', + ]; + } + + #[DataProvider('getInvalidIsAOfCases')] + public function testIsAOfExceptionMessages(array $args, string $exceptionMessage): void + { + $this->expectException('\InvalidArgumentException'); + $this->expectExceptionMessage($exceptionMessage); + + call_user_func_array(['Webmozart\Assert\Assert', 'isAOf'], $args); + } + + public function testResourceOfTypeCustomMessage(): void + { + $this->expectException('\InvalidArgumentException'); + $this->expectExceptionMessage('I want a resource of type curl. Got: NULL'); + + Assert::resource(null, 'curl', 'I want a resource of type %2$s. Got: %s'); + } + + public function testEnumAssertionErrorMessage(): void + { + $this->expectException('\InvalidArgumentException'); + $this->expectExceptionMessage('Expected null. Got: Webmozart\Assert\Tests\DummyEnum::CaseName'); + + Assert::null(DummyEnum::CaseName, 'Expected null. Got: %s'); + } + + #[DataProvider('getMethodsThatUseOtherMethods')] + public function testMessageIsPassedToInternalCalls(string $method, array $args, string $exceptionMessage): void + { + $this->expectException('\InvalidArgumentException'); + $this->expectExceptionMessage($exceptionMessage); + + call_user_func_array(['Webmozart\Assert\Assert', $method], $args); + } + + public static function getMethodsThatUseOtherMethods(): array + { + return [ + [ + 'method' => 'positiveInteger', + 'args' => ['not-integer', 'Value must be a positive integer. Got: %s'], + 'exceptionMessage' => 'Value must be a positive integer. Got: string', + ], + [ + 'method' => 'notNegativeInteger', + 'args' => ['not-integer', 'Value must be a non-negative integer. Got: %s'], + 'exceptionMessage' => 'Value must be a non-negative integer. Got: string', + ], + [ + 'method' => 'negativeInteger', + 'args' => ['not-integer', 'Value must be a negative integer. Got: %s'], + 'exceptionMessage' => 'Value must be a negative integer. Got: string', + ], + [ + 'method' => 'ip', + 'args' => [127001, 'Value must be a valid IP. Got: %s'], + 'exceptionMessage' => 'Value must be a valid IP. Got: integer', + ], + [ + 'method' => 'ipv4', + 'args' => [127001, 'Value must be a valid IPv4. Got: %s'], + 'exceptionMessage' => 'Value must be a valid IPv4. Got: integer', + ], + [ + 'method' => 'ipv6', + 'args' => [127001, 'Value must be a valid IPv6. Got: %s'], + 'exceptionMessage' => 'Value must be a valid IPv6. Got: integer', + ], + [ + 'method' => 'email', + 'args' => [111111, 'Value must be a valid email. Got: %s'], + 'exceptionMessage' => 'Value must be a valid email. Got: integer', + ], + [ + 'method' => 'unicodeLetters', + 'args' => [111, 'Value must be a string with valid unicode characters. Got: %s'], + 'exceptionMessage' => 'Value must be a string with valid unicode characters. Got: integer', + ], + [ + 'method' => 'alpha', + 'args' => [111, 'Value must be a string with only alphabetic characters. Got: %s'], + 'exceptionMessage' => 'Value must be a string with only alphabetic characters. Got: integer', + ], + [ + 'method' => 'digits', + 'args' => [111, 'Value must be a string with only digits. Got: %s'], + 'exceptionMessage' => 'Value must be a string with only digits. Got: integer', + ], + [ + 'method' => 'alnum', + 'args' => [111, 'Value must be a string with only alphanumeric characters. Got: %s'], + 'exceptionMessage' => 'Value must be a string with only alphanumeric characters. Got: integer', + ], + [ + 'method' => 'lower', + 'args' => [111, 'Value must be a string with only lowercase characters. Got: %s'], + 'exceptionMessage' => 'Value must be a string with only lowercase characters. Got: integer', + ], + [ + 'method' => 'upper', + 'args' => [111, 'Value must be a string with only uppercase characters. Got: %s'], + 'exceptionMessage' => 'Value must be a string with only uppercase characters. Got: integer', + ], + [ + 'method' => 'keyExists', + 'args' => [111, 'test', 'Value must be an array with key test. Got: %s'], + 'exceptionMessage' => 'Value must be an array with key test. Got: integer', + ], + [ + 'method' => 'keyNotExists', + 'args' => [111, 'test', 'Value must be an array without key test. Got: %s'], + 'exceptionMessage' => 'Value must be an array without key test. Got: integer', + ], + [ + 'method' => 'isInstanceOf', + 'args' => [111, 'stdClass', 'Value must be an instance of stdClass. Got: %s'], + 'exceptionMessage' => 'Value must be an instance of stdClass. Got: integer', + ], + [ + 'method' => 'notInstanceOf', + 'args' => [111, 'stdClass', 'Value must be an instance of stdClass. Got: %s'], + 'exceptionMessage' => 'Value must be an instance of stdClass. Got: integer', + ], + [ + 'method' => 'isInstanceOfAny', + 'args' => [111, ['stdClass'], 'Value must be an instance of stdClass. Got: %s'], + 'exceptionMessage' => 'Value must be an instance of stdClass. Got: integer', + ], + [ + 'method' => 'isNotA', + 'args' => [111, 'stdClass', 'Value must be an instance of stdClass. Got: %s'], + 'exceptionMessage' => 'Value must be an instance of stdClass. Got: integer', + ], + [ + 'method' => 'isAnyOf', + 'args' => [111, ['stdClass'], 'Value must be an instance of stdClass. Got: %s'], + 'exceptionMessage' => 'Value must be an instance of stdClass. Got: integer', + ], + ]; + } +} + +/** + * @ignore + */ +class ToStringClass +{ + /** + * @var string + */ + private $value; + + public function __construct(string $value) + { + $this->value = $value; + } + + public function __toString(): string + { + return $this->value; + } +}