From 7f66bcea1069d58744dbac9fab2f2519f175471e Mon Sep 17 00:00:00 2001 From: Henrique Moody Date: Wed, 17 Dec 2025 13:38:58 +0100 Subject: [PATCH] Bump PHP support from 8.1 to 8.5 We want to release version 3.0 as fresh as possible, without having to maintain backward compatibility with the previous versions. Because that version will be on for some time, we decided it will be best to support only PHP version 8.5 or higher. Acked-by: Alexandre Gomes Gaigalas --- .github/workflows/continuous-integration.yml | 9 +- composer.json | 6 +- docs/01-installation.md | 4 +- library/Message/Placeholder/Listed.php | 6 +- library/Message/Placeholder/Quoted.php | 4 +- library/Message/StandardFormatter.php | 12 +- library/Message/StandardStringifier.php | 6 +- .../Message/Stringifier/ListedStringifier.php | 4 +- .../Message/Stringifier/QuotedStringifier.php | 4 +- library/Message/Template.php | 8 +- .../Message/Translator/ArrayTranslator.php | 4 +- library/Result.php | 152 ++++++++---------- library/Rules/Base.php | 6 +- library/Rules/Call.php | 10 +- library/Rules/Charset.php | 4 +- library/Rules/Contains.php | 6 +- library/Rules/CountryCode.php | 6 +- library/Rules/CreditCard.php | 4 +- library/Rules/CurrencyCode.php | 6 +- library/Rules/Date.php | 4 +- library/Rules/DateTimeDiff.php | 10 +- library/Rules/Decimal.php | 4 +- library/Rules/EndsWith.php | 6 +- library/Rules/Equals.php | 4 +- library/Rules/Extension.php | 4 +- library/Rules/Factor.php | 7 +- library/Rules/Identical.php | 4 +- library/Rules/Image.php | 8 +- library/Rules/In.php | 6 +- library/Rules/Instance.php | 4 +- library/Rules/Ip.php | 6 +- library/Rules/KeySet.php | 8 +- library/Rules/LanguageCode.php | 4 +- library/Rules/Mimetype.php | 6 +- library/Rules/Multiple.php | 4 +- library/Rules/PropertyExists.php | 4 +- library/Rules/Regex.php | 4 +- library/Rules/Sorted.php | 4 +- library/Rules/StartsWith.php | 6 +- library/Rules/SubdivisionCode.php | 6 +- library/Rules/Subset.php | 4 +- library/Rules/Time.php | 4 +- library/Rules/When.php | 15 +- library/Transformers/RuleSpec.php | 8 +- phpstan.neon.dist | 2 + tests/Pest.php | 14 +- .../unit/Helpers/CanValidateDateTimeTest.php | 2 - .../unit/Helpers/CanValidateUndefinedTest.php | 2 - 48 files changed, 201 insertions(+), 224 deletions(-) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 6d022f5f..d15a2ae7 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -19,10 +19,7 @@ jobs: strategy: matrix: php-version: - - "8.1" - - "8.2" - - "8.3" - - "8.4" + - "8.5" steps: - name: Checkout @@ -61,7 +58,7 @@ jobs: - name: Install PHP uses: shivammathur/setup-php@v2 with: - php-version: 8.1 + php-version: 8.5 coverage: pcov - name: Install Localisation Files @@ -93,7 +90,7 @@ jobs: - name: Install PHP uses: shivammathur/setup-php@v2 with: - php-version: 8.4 + php-version: 8.5 coverage: none - name: Install dependencies diff --git a/composer.json b/composer.json index 97fe8471..c6e89c95 100644 --- a/composer.json +++ b/composer.json @@ -20,7 +20,7 @@ } }, "require": { - "php": ">=8.1", + "php": ">=8.5", "respect/stringifier": "^2.0.0", "symfony/polyfill-mbstring": "^1.33" }, @@ -30,12 +30,12 @@ "malukenho/docheader": "^1.0", "mikey179/vfsstream": "^1.6", "nette/php-generator": "^4.1", - "pestphp/pest": "^2.36", + "pestphp/pest": "^4.2", "phpstan/extension-installer": "^1.4", "phpstan/phpstan": "^2.0", "phpstan/phpstan-deprecation-rules": "^2.0", "phpstan/phpstan-phpunit": "^2.0", - "phpunit/phpunit": "^10.5", + "phpunit/phpunit": "^12.5", "psr/http-message": "^1.0 || ^2.0", "ramsey/uuid": "^4", "respect/coding-standard": "^4.0", diff --git a/docs/01-installation.md b/docs/01-installation.md index c0d1ada0..958b091c 100644 --- a/docs/01-installation.md +++ b/docs/01-installation.md @@ -4,7 +4,7 @@ Package is available on [Packagist](http://packagist.org/packages/respect/valida you can install it using [Composer](http://getcomposer.org). ```shell -composer require respect/validation +composer require respect/validation:^3.0 ``` -Works on PHP 8.1 or above. +Works on PHP 8.5 or above. diff --git a/library/Message/Placeholder/Listed.php b/library/Message/Placeholder/Listed.php index 22e68396..80ad024a 100644 --- a/library/Message/Placeholder/Listed.php +++ b/library/Message/Placeholder/Listed.php @@ -9,12 +9,12 @@ declare(strict_types=1); namespace Respect\Validation\Message\Placeholder; -final class Listed +final readonly class Listed { /** @param array $values */ public function __construct( - public readonly array $values, - public readonly string $lastGlue + public array $values, + public string $lastGlue ) { } } diff --git a/library/Message/Placeholder/Quoted.php b/library/Message/Placeholder/Quoted.php index 381d922f..6c8e570c 100644 --- a/library/Message/Placeholder/Quoted.php +++ b/library/Message/Placeholder/Quoted.php @@ -9,10 +9,10 @@ declare(strict_types=1); namespace Respect\Validation\Message\Placeholder; -final class Quoted +final readonly class Quoted { public function __construct( - private readonly string $value + private string $value ) { } diff --git a/library/Message/StandardFormatter.php b/library/Message/StandardFormatter.php index 1ce6b2b4..9bd59b27 100644 --- a/library/Message/StandardFormatter.php +++ b/library/Message/StandardFormatter.php @@ -28,10 +28,10 @@ use function str_repeat; use const PHP_EOL; -final class StandardFormatter implements Formatter +final readonly class StandardFormatter implements Formatter { public function __construct( - private readonly Renderer $renderer = new StandardRenderer(), + private Renderer $renderer = new StandardRenderer(), ) { } @@ -117,7 +117,7 @@ final class StandardFormatter implements Formatter $messages = []; foreach ($deduplicatedChildren as $child) { - $key = $child->getDeepestPath() ?? $child->id; + $key = $child->getDeepestPath() ?? $child->id ?? 0; $messages[$key] = $this->array( $this->resultWithPath($result, $child), $this->selectTemplates($child, $selectedTemplates), @@ -200,7 +200,7 @@ final class StandardFormatter implements Formatter } foreach ([$result->path, $result->name, $result->id, '__root__'] as $key) { - if (!isset($templates[$key])) { + if ($key === null || !isset($templates[$key])) { continue; } @@ -221,7 +221,7 @@ final class StandardFormatter implements Formatter */ private function isFinalTemplate(Result $result, array $templates): bool { - $keys = [$result->path, $result->name, $result->id]; + $keys = array_filter([$result->path, $result->name, $result->id], static fn($key) => $key !== null); foreach ($keys as $key) { if (isset($templates[$key]) && is_string($templates[$key])) { return true; @@ -249,7 +249,7 @@ final class StandardFormatter implements Formatter private function selectTemplates(Result $result, array $templates): array { foreach ([$result->path, $result->name, $result->id] as $key) { - if (isset($templates[$key]) && is_array($templates[$key])) { + if ($key !== null && isset($templates[$key]) && is_array($templates[$key])) { return $templates[$key]; } } diff --git a/library/Message/StandardStringifier.php b/library/Message/StandardStringifier.php index 3f3d27c4..4df97688 100644 --- a/library/Message/StandardStringifier.php +++ b/library/Message/StandardStringifier.php @@ -34,17 +34,17 @@ use Respect\Stringifier\Stringifiers\ThrowableObjectStringifier; use Respect\Validation\Message\Stringifier\ListedStringifier; use Respect\Validation\Message\Stringifier\QuotedStringifier; -final class StandardStringifier implements Stringifier +final readonly class StandardStringifier implements Stringifier { private const MAXIMUM_DEPTH = 3; private const MAXIMUM_NUMBER_OF_ITEMS = 5; private const MAXIMUM_NUMBER_OF_PROPERTIES = self::MAXIMUM_NUMBER_OF_ITEMS; private const MAXIMUM_LENGTH = 120; - private readonly Stringifier $stringifier; + private Stringifier $stringifier; public function __construct( - private readonly Quoter $quoter = new StandardQuoter(self::MAXIMUM_LENGTH) + private Quoter $quoter = new StandardQuoter(self::MAXIMUM_LENGTH) ) { $this->stringifier = $this->createStringifier($quoter); } diff --git a/library/Message/Stringifier/ListedStringifier.php b/library/Message/Stringifier/ListedStringifier.php index b6cded8c..394fc8c5 100644 --- a/library/Message/Stringifier/ListedStringifier.php +++ b/library/Message/Stringifier/ListedStringifier.php @@ -18,10 +18,10 @@ use function count; use function implode; use function sprintf; -final class ListedStringifier implements Stringifier +final readonly class ListedStringifier implements Stringifier { public function __construct( - private readonly Stringifier $stringifier + private Stringifier $stringifier ) { } diff --git a/library/Message/Stringifier/QuotedStringifier.php b/library/Message/Stringifier/QuotedStringifier.php index 437a8a33..6a5a9564 100644 --- a/library/Message/Stringifier/QuotedStringifier.php +++ b/library/Message/Stringifier/QuotedStringifier.php @@ -13,10 +13,10 @@ use Respect\Stringifier\Quoter; use Respect\Stringifier\Stringifier; use Respect\Validation\Message\Placeholder\Quoted; -final class QuotedStringifier implements Stringifier +final readonly class QuotedStringifier implements Stringifier { public function __construct( - private readonly Quoter $quoter + private Quoter $quoter ) { } diff --git a/library/Message/Template.php b/library/Message/Template.php index a8787963..45bc1572 100644 --- a/library/Message/Template.php +++ b/library/Message/Template.php @@ -13,12 +13,12 @@ use Attribute; use Respect\Validation\Rule; #[Attribute(Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)] -final class Template +final readonly class Template { public function __construct( - public readonly string $default, - public readonly string $inverted, - public readonly string $id = Rule::TEMPLATE_STANDARD, + public string $default, + public string $inverted, + public string $id = Rule::TEMPLATE_STANDARD, ) { } } diff --git a/library/Message/Translator/ArrayTranslator.php b/library/Message/Translator/ArrayTranslator.php index 3e895b40..20c20796 100644 --- a/library/Message/Translator/ArrayTranslator.php +++ b/library/Message/Translator/ArrayTranslator.php @@ -13,11 +13,11 @@ use Respect\Validation\Message\Translator; use function is_string; -final class ArrayTranslator implements Translator +final readonly class ArrayTranslator implements Translator { /** @param array $messages */ public function __construct( - private readonly array $messages + private array $messages ) { } diff --git a/library/Result.php b/library/Result.php index bac85425..be86d36c 100644 --- a/library/Result.php +++ b/library/Result.php @@ -22,25 +22,25 @@ use function strrchr; use function substr; use function ucfirst; -final class Result +final readonly class Result { /** @var array */ - public readonly array $children; + public array $children; - public readonly string $id; + public string $id; /** @param array $parameters */ public function __construct( - public readonly bool $hasPassed, - public readonly mixed $input, - public readonly Rule $rule, - public readonly array $parameters = [], - public readonly string $template = Rule::TEMPLATE_STANDARD, - public readonly bool $hasInvertedMode = false, - public readonly ?string $name = null, - ?string $id = null, - public readonly ?Result $adjacent = null, - public readonly string|int|null $path = null, + public bool $hasPassed, + public mixed $input, + public Rule $rule, + public array $parameters = [], + public string $template = Rule::TEMPLATE_STANDARD, + public bool $hasInvertedMode = false, + public string|null $name = null, + string|null $id = null, + public Result|null $adjacent = null, + public string|int|null $path = null, Result ...$children, ) { $this->id = $id ?? lcfirst(substr((string) strrchr($rule::class, '\\'), 1)); @@ -52,7 +52,7 @@ final class Result mixed $input, Rule $rule, array $parameters = [], - string $template = Rule::TEMPLATE_STANDARD + string $template = Rule::TEMPLATE_STANDARD, ): self { return new self(false, $input, $rule, $parameters, $template); } @@ -62,7 +62,7 @@ final class Result mixed $input, Rule $rule, array $parameters = [], - string $template = Rule::TEMPLATE_STANDARD + string $template = Rule::TEMPLATE_STANDARD, ): self { return new self(true, $input, $rule, $parameters, $template); } @@ -74,7 +74,7 @@ final class Result Rule $rule, Result $adjacent, array $parameters = [], - string $template = Rule::TEMPLATE_STANDARD + string $template = Rule::TEMPLATE_STANDARD, ): Result { if ($adjacent->allowsAdjacent()) { return (new Result($adjacent->hasPassed, $input, $rule, $parameters, $template, id: $adjacent->id)) @@ -84,7 +84,7 @@ final class Result $childrenAsAdjacent = array_map( static fn(Result $child) => self::fromAdjacent($input, $prefix, $rule, $child, $parameters, $template), - $adjacent->children + $adjacent->children, ); return $adjacent->withInput($input)->withChildren(...$childrenAsAdjacent); @@ -92,31 +92,32 @@ final class Result public function withTemplate(string $template): self { - return $this->clone(template: $template); + return clone($this, ['template' => $template]); } /** @param array $parameters */ public function withExtraParameters(array $parameters): self { - return $this->clone(parameters: $parameters + $this->parameters); + // phpcs:ignore SlevomatCodingStandard.PHP.UselessParentheses + return clone($this, ['parameters' => $parameters + $this->parameters]); } public function withId(string $id): self { - return $this->clone(id: $id); + return clone($this, ['id' => $id]); } public function withIdFrom(Rule $rule): self { - return $this->clone(id: lcfirst(substr((string) strrchr($rule::class, '\\'), 1))); + return clone($this, ['id' => lcfirst(substr((string) strrchr($rule::class, '\\'), 1))]); } public function withPath(string|int $path): self { - return $this->clone( - adjacent: $this->adjacent?->withPath($path), - path: $this->path === null ? $path : $path . '.' . $this->path, - ); + return clone($this, [ + 'adjacent' => $this->adjacent?->withPath($path), + 'path' => $this->path === null ? $path : $path . '.' . $this->path, + ]); } public function withDeepestPath(): self @@ -126,13 +127,13 @@ final class Result return $this; } - return $this->clone( - adjacent: $this->adjacent?->withPath($path), - path: $path, - ); + return clone($this, [ + 'adjacent' => $this->adjacent?->withPath($path), + 'path' => $path, + ]); } - public function getDeepestPath(): ?string + public function getDeepestPath(): string|null { if ($this->path === null) { return null; @@ -152,24 +153,25 @@ final class Result return $this; } - return $this->clone(id: $prefix . ucfirst($this->id)); + // phpcs:ignore SlevomatCodingStandard.PHP.UselessParentheses + return clone($this, ['id' => $prefix . ucfirst($this->id)]); } public function withChildren(Result ...$children): self { - return $this->clone(children: $children); + return clone($this, ['children' => $children]); } public function withName(string $name): self { - return $this->clone( - name: $this->name ?? $name, - adjacent: $this->adjacent?->withName($name), - children: array_map( - static fn (Result $child) => $child->path === null ? $child->withName($child->name ?? $name) : $child, - $this->children + return clone($this, [ + 'name' => $this->name ?? $name, + 'adjacent' => $this->adjacent?->withName($name), + 'children' => array_map( + static fn(Result $child) => $child->path === null ? $child->withName($child->name ?? $name) : $child, + $this->children, ), - ); + ]); } public function withNameFrom(Rule $rule): self @@ -185,37 +187,40 @@ final class Result { $currentInput = $this->input; - return $this->clone( - input: $input, - children: array_map( - static fn (Result $child) => $child->input === $currentInput ? $child->withInput($input) : $child, - $this->children + return clone($this, [ + 'input' => $input, + 'children' => array_map( + static fn(Result $child) => $child->input === $currentInput ? $child->withInput($input) : $child, + $this->children, ), - ); + ]); } public function withAdjacent(Result $adjacent): self { - return $this->clone(adjacent: $adjacent); + return clone($this, ['adjacent' => $adjacent]); } public function withToggledValidation(): self { - return $this->clone( - hasPassed: !$this->hasPassed, - adjacent: $this->adjacent?->withToggledValidation(), - children: array_map(static fn (Result $child) => $child->withToggledValidation(), $this->children), - ); + return clone($this, [ + 'hasPassed' => !$this->hasPassed, + 'adjacent' => $this->adjacent?->withToggledValidation(), + 'children' => array_map(static fn(Result $child) => $child->withToggledValidation(), $this->children), + ]); } public function withToggledModeAndValidation(): self { - return $this->clone( - hasPassed: !$this->hasPassed, - mode: !$this->hasInvertedMode, - adjacent: $this->adjacent?->withToggledModeAndValidation(), - children: array_map(static fn (Result $child) => $child->withToggledModeAndValidation(), $this->children), - ); + return clone($this, [ + 'hasPassed' => !$this->hasPassed, + 'hasInvertedMode' => !$this->hasInvertedMode, + 'adjacent' => $this->adjacent?->withToggledModeAndValidation(), + 'children' => array_map( + static fn(Result $child) => $child->withToggledModeAndValidation(), + $this->children, + ), + ]); } public function hasCustomTemplate(): bool @@ -231,40 +236,9 @@ final class Result $childrenThatAllowAdjacent = array_filter( $this->children, - static fn (Result $child) => $child->allowsAdjacent() + static fn(Result $child) => $child->allowsAdjacent(), ); return count($childrenThatAllowAdjacent) === 1; } - - /** - * @param array $parameters - * @param array|null $children - */ - private function clone( - ?bool $hasPassed = null, - mixed $input = null, - ?array $parameters = null, - ?string $template = null, - ?bool $mode = null, - ?string $name = null, - ?string $id = null, - ?Result $adjacent = null, - string|int|null $path = null, - ?array $children = null - ): self { - return new self( - $hasPassed ?? $this->hasPassed, - $input ?? $this->input, - $this->rule, - $parameters ?? $this->parameters, - $template ?? $this->template, - $mode ?? $this->hasInvertedMode, - $name ?? $this->name, - $id ?? $this->id, - $adjacent ?? $this->adjacent, - $path ?? $this->path, - ...($children ?? $this->children) - ); - } } diff --git a/library/Rules/Base.php b/library/Rules/Base.php index 6cb372e0..0955bd62 100644 --- a/library/Rules/Base.php +++ b/library/Rules/Base.php @@ -24,11 +24,11 @@ use function preg_match; '{{name}} must be a number in base {{base|raw}}', '{{name}} must not be a number in base {{base|raw}}', )] -final class Base implements Rule +final readonly class Base implements Rule { public function __construct( - private readonly int $base, - private readonly string $chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' + private int $base, + private string $chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' ) { $max = mb_strlen($this->chars); if ($base > $max) { diff --git a/library/Rules/Call.php b/library/Rules/Call.php index ff3a677a..53ca9773 100644 --- a/library/Rules/Call.php +++ b/library/Rules/Call.php @@ -46,11 +46,13 @@ final class Call implements Rule }); try { - return $this->rule->evaluate(call_user_func($this->callable, $input)); + $result = $this->rule->evaluate(call_user_func($this->callable, $input)); } catch (Throwable) { - restore_error_handler(); - - return Result::failed($input, $this, ['callable' => $this->callable]); + $result = Result::failed($input, $this, ['callable' => $this->callable]); } + + restore_error_handler(); + + return $result; } } diff --git a/library/Rules/Charset.php b/library/Rules/Charset.php index 752c0854..6df388f8 100644 --- a/library/Rules/Charset.php +++ b/library/Rules/Charset.php @@ -28,10 +28,10 @@ use function mb_list_encodings; '{{name}} must only contain characters from the {{charset|raw}} charset', '{{name}} must not contain any characters from the {{charset|raw}} charset', )] -final class Charset implements Rule +final readonly class Charset implements Rule { /** @var non-empty-array */ - private readonly array $charset; + private array $charset; public function __construct(string $charset, string ...$charsets) { diff --git a/library/Rules/Contains.php b/library/Rules/Contains.php index ec23eb07..d03350a4 100644 --- a/library/Rules/Contains.php +++ b/library/Rules/Contains.php @@ -25,11 +25,11 @@ use function mb_strpos; '{{name}} must contain {{containsValue}}', '{{name}} must not contain {{containsValue}}', )] -final class Contains implements Rule +final readonly class Contains implements Rule { public function __construct( - private readonly mixed $containsValue, - private readonly bool $identical = false + private mixed $containsValue, + private bool $identical = false ) { } diff --git a/library/Rules/CountryCode.php b/library/Rules/CountryCode.php index a56e49b0..6c795fee 100644 --- a/library/Rules/CountryCode.php +++ b/library/Rules/CountryCode.php @@ -26,13 +26,13 @@ use function is_string; '{{name}} must be a valid country code', '{{name}} must not be a valid country code', )] -final class CountryCode implements Rule +final readonly class CountryCode implements Rule { - private readonly Countries $countries; + private Countries $countries; /** @param "alpha-2"|"alpha-3"|"numeric" $set */ public function __construct( - private readonly string $set = 'alpha-2', + private string $set = 'alpha-2', ?Countries $countries = null ) { if (!class_exists(Countries::class)) { diff --git a/library/Rules/CreditCard.php b/library/Rules/CreditCard.php index da870770..e379b4df 100644 --- a/library/Rules/CreditCard.php +++ b/library/Rules/CreditCard.php @@ -31,7 +31,7 @@ use function preg_replace; '{{name}} must not be a valid {{brand|raw}} credit card number', self::TEMPLATE_BRANDED, )] -final class CreditCard implements Rule +final readonly class CreditCard implements Rule { public const TEMPLATE_BRANDED = '__branded__'; public const ANY = 'Any'; @@ -55,7 +55,7 @@ final class CreditCard implements Rule ]; public function __construct( - private readonly string $brand = self::ANY + private string $brand = self::ANY ) { if (!isset(self::BRAND_REGEX_LIST[$brand])) { throw new InvalidRuleConstructorException( diff --git a/library/Rules/CurrencyCode.php b/library/Rules/CurrencyCode.php index 774c128d..9b04a0cc 100644 --- a/library/Rules/CurrencyCode.php +++ b/library/Rules/CurrencyCode.php @@ -25,13 +25,13 @@ use function in_array; '{{name}} must be a valid currency code', '{{name}} must not be a valid currency code', )] -final class CurrencyCode implements Rule +final readonly class CurrencyCode implements Rule { - private readonly Currencies $currencies; + private Currencies $currencies; /** @param "alpha-3"|"numeric" $set */ public function __construct( - private readonly string $set = 'alpha-3', + private string $set = 'alpha-3', ?Currencies $currencies = null ) { if (!class_exists(Currencies::class)) { diff --git a/library/Rules/Date.php b/library/Rules/Date.php index 8f19d7c0..5718dcc7 100644 --- a/library/Rules/Date.php +++ b/library/Rules/Date.php @@ -26,12 +26,12 @@ use function strtotime; '{{name}} must be a valid date in the format {{sample}}', '{{name}} must not be a valid date in the format {{sample}}', )] -final class Date implements Rule +final readonly class Date implements Rule { use CanValidateDateTime; public function __construct( - private readonly string $format = 'Y-m-d' + private string $format = 'Y-m-d' ) { if (!preg_match('/^[djSFmMnYy\W]+$/', $format)) { throw new InvalidRuleConstructorException('"%s" is not a valid date format', $format); diff --git a/library/Rules/DateTimeDiff.php b/library/Rules/DateTimeDiff.php index a9247e11..6696c6ec 100644 --- a/library/Rules/DateTimeDiff.php +++ b/library/Rules/DateTimeDiff.php @@ -43,7 +43,7 @@ use function ucfirst; 'For comparison with {{now|raw}}, {{name}} must not be a valid datetime in the format {{sample|raw}}', self::TEMPLATE_WRONG_FORMAT )] -final class DateTimeDiff implements Rule +final readonly class DateTimeDiff implements Rule { use CanValidateDateTime; @@ -53,10 +53,10 @@ final class DateTimeDiff implements Rule /** @param "years"|"months"|"days"|"hours"|"minutes"|"seconds"|"microseconds" $type */ public function __construct( - private readonly string $type, - private readonly Rule $rule, - private readonly ?string $format = null, - private readonly ?DateTimeImmutable $now = null, + private string $type, + private Rule $rule, + private ?string $format = null, + private ?DateTimeImmutable $now = null, ) { $availableTypes = ['years', 'months', 'days', 'hours', 'minutes', 'seconds', 'microseconds']; if (!in_array($this->type, $availableTypes, true)) { diff --git a/library/Rules/Decimal.php b/library/Rules/Decimal.php index 5080302c..d1d2b6f0 100644 --- a/library/Rules/Decimal.php +++ b/library/Rules/Decimal.php @@ -25,10 +25,10 @@ use function var_export; '{{name}} must have {{decimals}} decimals', '{{name}} must not have {{decimals}} decimals', )] -final class Decimal implements Rule +final readonly class Decimal implements Rule { public function __construct( - private readonly int $decimals + private int $decimals ) { } diff --git a/library/Rules/EndsWith.php b/library/Rules/EndsWith.php index 7c72adf1..68273d6d 100644 --- a/library/Rules/EndsWith.php +++ b/library/Rules/EndsWith.php @@ -25,11 +25,11 @@ use function mb_strrpos; '{{name}} must end with {{endValue}}', '{{name}} must not end with {{endValue}}', )] -final class EndsWith implements Rule +final readonly class EndsWith implements Rule { public function __construct( - private readonly mixed $endValue, - private readonly bool $identical = false + private mixed $endValue, + private bool $identical = false ) { } diff --git a/library/Rules/Equals.php b/library/Rules/Equals.php index 63f8a749..08fc476a 100644 --- a/library/Rules/Equals.php +++ b/library/Rules/Equals.php @@ -21,10 +21,10 @@ use function is_scalar; '{{name}} must be equal to {{compareTo}}', '{{name}} must not be equal to {{compareTo}}', )] -final class Equals implements Rule +final readonly class Equals implements Rule { public function __construct( - private readonly mixed $compareTo + private mixed $compareTo ) { } diff --git a/library/Rules/Extension.php b/library/Rules/Extension.php index f9a7f725..dbb5ae99 100644 --- a/library/Rules/Extension.php +++ b/library/Rules/Extension.php @@ -25,10 +25,10 @@ use const PATHINFO_EXTENSION; '{{name}} must have {{extension}} extension', '{{name}} must not have {{extension}} extension', )] -final class Extension implements Rule +final readonly class Extension implements Rule { public function __construct( - private readonly string $extension + private string $extension ) { } diff --git a/library/Rules/Factor.php b/library/Rules/Factor.php index 389a8f6d..0c9bb2f2 100644 --- a/library/Rules/Factor.php +++ b/library/Rules/Factor.php @@ -17,16 +17,17 @@ use Respect\Validation\Rule; use function abs; use function is_integer; use function is_numeric; +use function preg_match; #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] #[Template( '{{name}} must be a factor of {{dividend|raw}}', '{{name}} must not be a factor of {{dividend|raw}}', )] -final class Factor implements Rule +final readonly class Factor implements Rule { public function __construct( - private readonly int $dividend + private int $dividend ) { } @@ -40,7 +41,7 @@ final class Factor implements Rule } // Factors must be integers that are not zero. - if (!is_numeric($input) || (int) $input != $input || $input == 0) { + if (!is_numeric($input) || preg_match('/^-?\d+$/', (string) $input) === 0 || $input == 0) { return Result::failed($input, $this, $parameters); } diff --git a/library/Rules/Identical.php b/library/Rules/Identical.php index 57be8c81..c5a47b19 100644 --- a/library/Rules/Identical.php +++ b/library/Rules/Identical.php @@ -19,10 +19,10 @@ use Respect\Validation\Rule; '{{name}} must be identical to {{compareTo}}', '{{name}} must not be identical to {{compareTo}}', )] -final class Identical implements Rule +final readonly class Identical implements Rule { public function __construct( - private readonly mixed $compareTo + private mixed $compareTo ) { } diff --git a/library/Rules/Image.php b/library/Rules/Image.php index d4daf7b3..45f8b2e6 100644 --- a/library/Rules/Image.php +++ b/library/Rules/Image.php @@ -28,11 +28,9 @@ use const FILEINFO_MIME_TYPE; )] final class Image extends Simple { - private readonly finfo $fileInfo; - - public function __construct(?finfo $fileInfo = null) - { - $this->fileInfo = $fileInfo ?: new finfo(FILEINFO_MIME_TYPE); + public function __construct( + private finfo $fileInfo = new finfo(FILEINFO_MIME_TYPE) + ) { } public function isValid(mixed $input): bool diff --git a/library/Rules/In.php b/library/Rules/In.php index ac9ba016..bbfdd895 100644 --- a/library/Rules/In.php +++ b/library/Rules/In.php @@ -24,11 +24,11 @@ use function mb_strpos; '{{name}} must be in {{haystack}}', '{{name}} must not be in {{haystack}}', )] -final class In implements Rule +final readonly class In implements Rule { public function __construct( - private readonly mixed $haystack, - private readonly bool $compareIdentical = false + private mixed $haystack, + private bool $compareIdentical = false ) { } diff --git a/library/Rules/Instance.php b/library/Rules/Instance.php index 7e2051e5..8c41b40b 100644 --- a/library/Rules/Instance.php +++ b/library/Rules/Instance.php @@ -19,11 +19,11 @@ use Respect\Validation\Rule; '{{name}} must be an instance of {{class|quote}}', '{{name}} must not be an instance of {{class|quote}}', )] -final class Instance implements Rule +final readonly class Instance implements Rule { /** @param class-string $class */ public function __construct( - private readonly string $class + private string $class ) { } diff --git a/library/Rules/Ip.php b/library/Rules/Ip.php index 66b1b707..a5948b31 100644 --- a/library/Rules/Ip.php +++ b/library/Rules/Ip.php @@ -23,12 +23,14 @@ use function is_string; use function long2ip; use function mb_strpos; use function mb_substr_count; +use function min; use function sprintf; use function str_repeat; use function str_replace; use function strtr; use const FILTER_VALIDATE_IP; +use const PHP_INT_MAX; #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] #[Template( @@ -89,7 +91,7 @@ final class Ip implements Rule } if ($this->startAddress && $this->mask) { - return $this->startAddress . '/' . long2ip((int) $this->mask); + return $this->startAddress . '/' . long2ip((int) min($this->mask, PHP_INT_MAX)); } return null; @@ -160,7 +162,7 @@ final class Ip implements Rule throw new InvalidRuleConstructorException('Invalid network mask'); } - $this->mask = sprintf('%032b', ip2long((string) long2ip(~(2 ** (32 - (int) $parts[1]) - 1)))); + $this->mask = sprintf('%032b', ip2long(long2ip(~(2 ** (32 - (int) $parts[1]) - 1)))); } private function verifyAddress(string $address): bool diff --git a/library/Rules/KeySet.php b/library/Rules/KeySet.php index 009b3a40..8bb4c7f7 100644 --- a/library/Rules/KeySet.php +++ b/library/Rules/KeySet.php @@ -45,7 +45,7 @@ use function array_slice; '{{name}} contains no missing keys', self::TEMPLATE_MISSING_KEYS )] -final class KeySet implements Rule +final readonly class KeySet implements Rule { public const TEMPLATE_BOTH = '__both__'; public const TEMPLATE_EXTRA_KEYS = '__extra_keys__'; @@ -54,13 +54,13 @@ final class KeySet implements Rule private const MAX_DIFF_KEYS = 10; /** @var array */ - private readonly array $rules; + private array $rules; /** @var array */ - private readonly array $allKeys; + private array $allKeys; /** @var array */ - private readonly array $mandatoryKeys; + private array $mandatoryKeys; public function __construct(Rule $rule, Rule ...$rules) { diff --git a/library/Rules/LanguageCode.php b/library/Rules/LanguageCode.php index 76635265..70071d26 100644 --- a/library/Rules/LanguageCode.php +++ b/library/Rules/LanguageCode.php @@ -27,9 +27,9 @@ use function is_string; '{{name}} must be a valid language code', '{{name}} must not be a valid language code', )] -final class LanguageCode implements Rule +final readonly class LanguageCode implements Rule { - private readonly Languages $languages; + private Languages $languages; /** @param "alpha-2"|"alpha-3" $set */ public function __construct( diff --git a/library/Rules/Mimetype.php b/library/Rules/Mimetype.php index 09e39fd6..2478665c 100644 --- a/library/Rules/Mimetype.php +++ b/library/Rules/Mimetype.php @@ -26,11 +26,11 @@ use const FILEINFO_MIME_TYPE; '{{name}} must have the {{mimetype}} MIME type', '{{name}} must not have the {{mimetype}} MIME type', )] -final class Mimetype implements Rule +final readonly class Mimetype implements Rule { public function __construct( - private readonly string $mimetype, - private readonly finfo $fileInfo = new finfo() + private string $mimetype, + private finfo $fileInfo = new finfo() ) { } diff --git a/library/Rules/Multiple.php b/library/Rules/Multiple.php index 6e18bc57..cdb61b17 100644 --- a/library/Rules/Multiple.php +++ b/library/Rules/Multiple.php @@ -19,10 +19,10 @@ use Respect\Validation\Rule; '{{name}} must be a multiple of {{multipleOf}}', '{{name}} must not be a multiple of {{multipleOf}}', )] -final class Multiple implements Rule +final readonly class Multiple implements Rule { public function __construct( - private readonly int $multipleOf + private int $multipleOf ) { } diff --git a/library/Rules/PropertyExists.php b/library/Rules/PropertyExists.php index 51356de4..00f6d0d2 100644 --- a/library/Rules/PropertyExists.php +++ b/library/Rules/PropertyExists.php @@ -22,10 +22,10 @@ use function is_object; '{{name}} must be present', '{{name}} must not be present', )] -final class PropertyExists implements Rule +final readonly class PropertyExists implements Rule { public function __construct( - private readonly string $propertyName + private string $propertyName ) { } diff --git a/library/Rules/Regex.php b/library/Rules/Regex.php index 9b9d4355..8248fb43 100644 --- a/library/Rules/Regex.php +++ b/library/Rules/Regex.php @@ -22,10 +22,10 @@ use function preg_match; '{{name}} must match the pattern {{regex|quote}}', '{{name}} must not match the pattern {{regex|quote}}', )] -final class Regex implements Rule +final readonly class Regex implements Rule { public function __construct( - private readonly string $regex + private string $regex ) { } diff --git a/library/Rules/Sorted.php b/library/Rules/Sorted.php index ff6ba938..d7e5a338 100644 --- a/library/Rules/Sorted.php +++ b/library/Rules/Sorted.php @@ -32,7 +32,7 @@ use function str_split; '{{name}} must not be sorted in descending order', self::TEMPLATE_DESCENDING, )] -final class Sorted implements Rule +final readonly class Sorted implements Rule { public const TEMPLATE_ASCENDING = '__ascending__'; public const TEMPLATE_DESCENDING = '__descending__'; @@ -41,7 +41,7 @@ final class Sorted implements Rule public const DESCENDING = 'DESC'; public function __construct( - private readonly string $direction + private string $direction ) { if ($direction !== self::ASCENDING && $direction !== self::DESCENDING) { throw new InvalidRuleConstructorException( diff --git a/library/Rules/StartsWith.php b/library/Rules/StartsWith.php index fc34cd60..0e04d543 100644 --- a/library/Rules/StartsWith.php +++ b/library/Rules/StartsWith.php @@ -25,11 +25,11 @@ use function reset; '{{name}} must start with {{startValue}}', '{{name}} must not start with {{startValue}}', )] -final class StartsWith implements Rule +final readonly class StartsWith implements Rule { public function __construct( - private readonly mixed $startValue, - private readonly bool $identical = false + private mixed $startValue, + private bool $identical = false ) { } diff --git a/library/Rules/SubdivisionCode.php b/library/Rules/SubdivisionCode.php index 54fc67e0..8735e9d2 100644 --- a/library/Rules/SubdivisionCode.php +++ b/library/Rules/SubdivisionCode.php @@ -26,13 +26,13 @@ use function class_exists; '{{name}} must be a subdivision code of {{countryName|trans}}', '{{name}} must not be a subdivision code of {{countryName|trans}}', )] -final class SubdivisionCode implements Rule +final readonly class SubdivisionCode implements Rule { use CanValidateUndefined; - private readonly Countries\Country $country; + private Countries\Country $country; - private readonly Subdivisions $subdivisions; + private Subdivisions $subdivisions; public function __construct(string $countryCode, ?Countries $countries = null, ?Subdivisions $subdivisions = null) { diff --git a/library/Rules/Subset.php b/library/Rules/Subset.php index 12cab880..8a04e701 100644 --- a/library/Rules/Subset.php +++ b/library/Rules/Subset.php @@ -22,11 +22,11 @@ use function is_array; '{{name}} must be subset of {{superset}}', '{{name}} must not be subset of {{superset}}', )] -final class Subset implements Rule +final readonly class Subset implements Rule { /** @param mixed[] $superset */ public function __construct( - private readonly array $superset + private array $superset ) { } diff --git a/library/Rules/Time.php b/library/Rules/Time.php index 13a2b829..4fea1488 100644 --- a/library/Rules/Time.php +++ b/library/Rules/Time.php @@ -26,12 +26,12 @@ use function strtotime; '{{name}} must be a valid time in the format {{sample}}', '{{name}} must not be a valid time in the format {{sample}}', )] -final class Time implements Rule +final readonly class Time implements Rule { use CanValidateDateTime; public function __construct( - private readonly string $format = 'H:i:s' + private string $format = 'H:i:s' ) { if (!preg_match('/^[gGhHisuvaA\W]+$/', $format)) { throw new InvalidRuleConstructorException('"%s" is not a valid date format', $format); diff --git a/library/Rules/When.php b/library/Rules/When.php index 0d833ae2..3b153ff2 100644 --- a/library/Rules/When.php +++ b/library/Rules/When.php @@ -14,20 +14,13 @@ use Respect\Validation\Result; use Respect\Validation\Rule; #[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)] -final class When implements Rule +final readonly class When implements Rule { - private readonly Rule $else; - public function __construct( - private readonly Rule $when, - private readonly Rule $then, - ?Rule $else = null + private Rule $when, + private Rule $then, + private Rule $else = new Templated(new AlwaysInvalid(), AlwaysInvalid::TEMPLATE_SIMPLE) ) { - if ($else === null) { - $else = new Templated(new AlwaysInvalid(), AlwaysInvalid::TEMPLATE_SIMPLE); - } - - $this->else = $else; } public function evaluate(mixed $input): Result diff --git a/library/Transformers/RuleSpec.php b/library/Transformers/RuleSpec.php index 94ba06e5..ce7a8232 100644 --- a/library/Transformers/RuleSpec.php +++ b/library/Transformers/RuleSpec.php @@ -9,13 +9,13 @@ declare(strict_types=1); namespace Respect\Validation\Transformers; -final class RuleSpec +final readonly class RuleSpec { /** @param array $arguments */ public function __construct( - public readonly string $name, - public readonly array $arguments = [], - public readonly ?RuleSpec $wrapper = null, + public string $name, + public array $arguments = [], + public ?RuleSpec $wrapper = null, ) { } } diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 1a112b7e..d3a2d532 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -19,6 +19,8 @@ parameters: path: tests/library/Message/TestingStringifier.php - message: '/Parameter #1 \$messages of class .+\\ArrayTranslator constructor expects array, array given./' path: tests/unit/Message/Translator/ArrayTranslatorTest.php + - message: '/Access to an undefined property PHPUnit\\Framework\\TestCase/' + path: tests/feature/Rules/SizeTest.php level: 8 treatPhpDocTypesAsCertain: false paths: diff --git a/tests/Pest.php b/tests/Pest.php index 890da2f6..90a02754 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -70,7 +70,14 @@ function expectDeprecation(Closure $callback, string $error): Closure return true; }); - $callback->call($this); + try { + $callback->call($this); + } catch (Throwable $throwable) { + restore_error_handler(); + + throw $throwable; + } + restore_error_handler(); expect($lastError)->toBe($error); }; @@ -93,8 +100,13 @@ function expectMessageAndDeprecation(Closure $callback, string $message, string test()->expectException(ValidationException::class); } catch (ValidationException $e) { expect($e->getMessage())->toBe($message, 'Validation message does not match'); + } catch (Throwable $throwable) { + restore_error_handler(); + + throw $throwable; } restore_error_handler(); + expect($lastError)->toBe($error); }; } diff --git a/tests/unit/Helpers/CanValidateDateTimeTest.php b/tests/unit/Helpers/CanValidateDateTimeTest.php index 689a3fa9..7d81617a 100644 --- a/tests/unit/Helpers/CanValidateDateTimeTest.php +++ b/tests/unit/Helpers/CanValidateDateTimeTest.php @@ -9,14 +9,12 @@ declare(strict_types=1); namespace Respect\Validation\Helpers; -use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\Attributes\Test; use Respect\Validation\Test\TestCase; #[Group('helper')] -#[CoversClass(CanValidateDateTime::class)] final class CanValidateDateTimeTest extends TestCase { use CanValidateDateTime; diff --git a/tests/unit/Helpers/CanValidateUndefinedTest.php b/tests/unit/Helpers/CanValidateUndefinedTest.php index 53b87ee2..b025b0c0 100644 --- a/tests/unit/Helpers/CanValidateUndefinedTest.php +++ b/tests/unit/Helpers/CanValidateUndefinedTest.php @@ -9,14 +9,12 @@ declare(strict_types=1); namespace Respect\Validation\Helpers; -use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\Attributes\Test; use Respect\Validation\Test\TestCase; #[Group('helper')] -#[CoversClass(CanValidateUndefined::class)] final class CanValidateUndefinedTest extends TestCase { use CanValidateUndefined;