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 <alganet@gmail.com>
This commit is contained in:
Henrique Moody 2025-12-17 13:38:58 +01:00
commit 7f66bcea10
No known key found for this signature in database
GPG key ID: 221E9281655813A6
48 changed files with 201 additions and 224 deletions

View file

@ -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

View file

@ -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",

View file

@ -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.

View file

@ -9,12 +9,12 @@ declare(strict_types=1);
namespace Respect\Validation\Message\Placeholder;
final class Listed
final readonly class Listed
{
/** @param array<int, mixed> $values */
public function __construct(
public readonly array $values,
public readonly string $lastGlue
public array $values,
public string $lastGlue
) {
}
}

View file

@ -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
) {
}

View file

@ -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];
}
}

View file

@ -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);
}

View file

@ -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
) {
}

View file

@ -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
) {
}

View file

@ -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,
) {
}
}

View file

@ -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<string, string> $messages */
public function __construct(
private readonly array $messages
private array $messages
) {
}

View file

@ -22,25 +22,25 @@ use function strrchr;
use function substr;
use function ucfirst;
final class Result
final readonly class Result
{
/** @var array<Result> */
public readonly array $children;
public array $children;
public readonly string $id;
public string $id;
/** @param array<string, mixed> $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<string, mixed> $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<string, mixed> $parameters
* @param array<Result>|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)
);
}
}

View file

@ -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) {

View file

@ -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;
}
}

View file

@ -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<string> */
private readonly array $charset;
private array $charset;
public function __construct(string $charset, string ...$charsets)
{

View file

@ -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
) {
}

View file

@ -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)) {

View file

@ -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(

View file

@ -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)) {

View file

@ -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);

View file

@ -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)) {

View file

@ -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
) {
}

View file

@ -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
) {
}

View file

@ -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
) {
}

View file

@ -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
) {
}

View file

@ -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);
}

View file

@ -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
) {
}

View file

@ -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

View file

@ -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
) {
}

View file

@ -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
) {
}

View file

@ -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

View file

@ -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<KeyRelated> */
private readonly array $rules;
private array $rules;
/** @var array<int|string> */
private readonly array $allKeys;
private array $allKeys;
/** @var array<int|string> */
private readonly array $mandatoryKeys;
private array $mandatoryKeys;
public function __construct(Rule $rule, Rule ...$rules)
{

View file

@ -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(

View file

@ -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()
) {
}

View file

@ -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
) {
}

View file

@ -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
) {
}

View file

@ -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
) {
}

View file

@ -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(

View file

@ -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
) {
}

View file

@ -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)
{

View file

@ -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
) {
}

View file

@ -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);

View file

@ -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

View file

@ -9,13 +9,13 @@ declare(strict_types=1);
namespace Respect\Validation\Transformers;
final class RuleSpec
final readonly class RuleSpec
{
/** @param array<mixed> $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,
) {
}
}

View file

@ -19,6 +19,8 @@ parameters:
path: tests/library/Message/TestingStringifier.php
- message: '/Parameter #1 \$messages of class .+\\ArrayTranslator constructor expects array<string, string>, array<string, int> 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:

View file

@ -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);
};
}

View file

@ -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;

View file

@ -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;