diff --git a/Attribute/HasNamedArguments.php b/Attribute/HasNamedArguments.php index cc1662943..564d42282 100644 --- a/Attribute/HasNamedArguments.php +++ b/Attribute/HasNamedArguments.php @@ -11,6 +11,11 @@ namespace Symfony\Component\Validator\Attribute; +/** + * Hints the loader that some constraint options are required. + * + * @see https://symfony.com/doc/current/validation/custom_constraint.html + */ #[\Attribute(\Attribute::TARGET_METHOD)] final class HasNamedArguments { diff --git a/CHANGELOG.md b/CHANGELOG.md index 5660c2f4e..e8146d2a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,112 @@ CHANGELOG ========= +7.3 +--- + + * Add the `filenameCharset` and `filenameCountUnit` options to the `File` constraint + * Deprecate defining custom constraints not supporting named arguments + + Before: + + ```php + use Symfony\Component\Validator\Constraint; + + class CustomConstraint extends Constraint + { + public function __construct(array $options) + { + // ... + } + } + ``` + + After: + + ```php + use Symfony\Component\Validator\Attribute\HasNamedArguments; + use Symfony\Component\Validator\Constraint; + + class CustomConstraint extends Constraint + { + #[HasNamedArguments] + public function __construct($option1, $option2, $groups, $payload) + { + // ... + } + } + ``` + * Deprecate passing an array of options to the constructors of the constraint classes, pass each option as a dedicated argument instead + + Before: + + ```php + new NotNull([ + 'groups' => ['foo', 'bar'], + 'message' => 'a custom constraint violation message', + ]) + ``` + + After: + + ```php + new NotNull( + groups: ['foo', 'bar'], + message: 'a custom constraint violation message', + ) + ``` + * Add support for ratio checks for SVG files to the `Image` constraint + * Add support for the `otherwise` option in the `When` constraint + * Add support for multiple fields containing nested constraints in `Composite` constraints + * Add the `stopOnFirstError` option to the `Unique` constraint to validate all elements + * Add support for closures in the `When` constraint + +7.2 +--- + + * `IbanValidator` accepts IBANs containing non-breaking and narrow non-breaking spaces + * Make `PasswordStrengthValidator::estimateStrength()` public + * Add the `Yaml` constraint for validating YAML content + * Add `errorPath` to Unique constraint + * Add the `format` option to the `Ulid` constraint to allow accepting different ULID formats + * Add the `WordCount` constraint + * Add the `Week` constraint + * Add `CompoundConstraintTestCase` to ease testing Compound Constraints + * Add context variable to `WhenValidator` + * Add `format` parameter to `DateTime` constraint violation message + +7.1 +--- + + * Deprecate not passing a value for the `requireTld` option to the `Url` constraint (the default value will become `true` in 8.0) + * Add the calculated strength to violations in `PasswordStrengthValidator` + * Add support for `Stringable` values when using the `Cidr`, `CssColor`, `ExpressionSyntax` and `PasswordStrength` constraints + * Add `MacAddress` constraint + * Add `*_NO_PUBLIC`, `*_ONLY_PRIVATE` and `*_ONLY_RESERVED` versions to `Ip` constraint + * Possibility to use all `Ip` constraint versions for `Cidr` constraint + * Add `list` and `associative_array` types to `Type` constraint + * Add the `Charset` constraint + * Add the `requireTld` option to the `Url` constraint + * Deprecate `Bic::INVALID_BANK_CODE_ERROR` + +7.0 +--- + + * Add methods `getConstraint()`, `getCause()` and `__toString()` to `ConstraintViolationInterface` + * Add method `__toString()` to `ConstraintViolationListInterface` + * Add method `disableTranslation()` to `ConstraintViolationBuilderInterface` + * Remove static property `$errorNames` from all constraints, use const `ERROR_NAMES` instead + * Remove static property `$versions` from the `Ip` constraint, use the `VERSIONS` constant instead + * Remove `VALIDATION_MODE_LOOSE` from `Email` constraint, use `VALIDATION_MODE_HTML5` instead + * Remove constraint `ExpressionLanguageSyntax`, use `ExpressionSyntax` instead + * Remove Doctrine annotations support in favor of native attributes + * Remove the annotation reader parameter from the constructor signature of `AnnotationLoader` + * Remove `ValidatorBuilder::setDoctrineAnnotationReader()` + * Remove `ValidatorBuilder::addDefaultDoctrineAnnotationReader()` + * Remove `ValidatorBuilder::enableAnnotationMapping()`, use `ValidatorBuilder::enableAttributeMapping()` instead + * Remove `ValidatorBuilder::disableAnnotationMapping()`, use `ValidatorBuilder::disableAttributeMapping()` instead + * Remove `AnnotationLoader`, use `AttributeLoader` instead + 6.4 --- diff --git a/Command/DebugCommand.php b/Command/DebugCommand.php index c2adfc028..70b508d09 100644 --- a/Command/DebugCommand.php +++ b/Command/DebugCommand.php @@ -38,19 +38,13 @@ #[AsCommand(name: 'debug:validator', description: 'Display validation constraints for classes')] class DebugCommand extends Command { - private MetadataFactoryInterface $validator; - - public function __construct(MetadataFactoryInterface $validator) - { + public function __construct( + private MetadataFactoryInterface $validator, + ) { parent::__construct(); - - $this->validator = $validator; } - /** - * @return void - */ - protected function configure() + protected function configure(): void { $this ->addArgument('class', InputArgument::REQUIRED, 'A fully qualified class name or a path') diff --git a/Constraint.php b/Constraint.php index 2a01a1d7c..5fd8ce84c 100644 --- a/Constraint.php +++ b/Constraint.php @@ -51,24 +51,17 @@ abstract class Constraint */ protected const ERROR_NAMES = []; - /** - * @deprecated since Symfony 6.1, use protected const ERROR_NAMES instead - */ - protected static $errorNames = []; - /** * Domain-specific data attached to a constraint. - * - * @var mixed */ - public $payload; + public mixed $payload; /** * The groups that the constraint belongs to. * * @var string[] */ - public $groups; + public ?array $groups = null; /** * Returns the name of the given error code. @@ -81,13 +74,7 @@ public static function getErrorName(string $errorCode): string return static::ERROR_NAMES[$errorCode]; } - if (!isset(static::$errorNames[$errorCode])) { - throw new InvalidArgumentException(\sprintf('The error code "%s" does not exist for constraint of type "%s".', $errorCode, static::class)); - } - - trigger_deprecation('symfony/validator', '6.1', 'The "%s::$errorNames" property is deprecated, use protected const ERROR_NAMES instead.', static::class); - - return static::$errorNames[$errorCode]; + throw new InvalidArgumentException(\sprintf('The error code "%s" does not exist for constraint of type "%s".', $errorCode, static::class)); } /** @@ -134,12 +121,15 @@ public function __construct(mixed $options = null, ?array $groups = null, mixed } } + /** + * @return array + */ protected function normalizeOptions(mixed $options): array { $normalizedOptions = []; $defaultOption = $this->getDefaultOption(); $invalidOptions = []; - $missingOptions = array_flip((array) $this->getRequiredOptions()); + $missingOptions = array_flip($this->getRequiredOptions()); $knownOptions = get_class_vars(static::class); if (\is_array($options) && isset($options['value']) && !property_exists($this, 'value')) { @@ -194,11 +184,9 @@ protected function normalizeOptions(mixed $options): array * this method will be called at most once per constraint instance and * option name. * - * @return void - * * @throws InvalidOptionsException If an invalid option name is given */ - public function __set(string $option, mixed $value) + public function __set(string $option, mixed $value): void { if ('groups' === $option) { $this->groups = (array) $value; @@ -236,16 +224,14 @@ public function __isset(string $option): bool /** * Adds the given group if this constraint is in the Default group. - * - * @return void */ - public function addImplicitGroupName(string $group) + public function addImplicitGroupName(string $group): void { if (null === $this->groups && \array_key_exists('groups', (array) $this)) { throw new \LogicException(\sprintf('"%s::$groups" is set to null. Did you forget to call "%s::__construct()"?', static::class, self::class)); } - if (\in_array(self::DEFAULT_GROUP, $this->groups) && !\in_array($group, $this->groups)) { + if (\in_array(self::DEFAULT_GROUP, $this->groups) && !\in_array($group, $this->groups, true)) { $this->groups[] = $group; } } @@ -255,11 +241,9 @@ public function addImplicitGroupName(string $group) * * Override this method to define a default option. * - * @return string|null - * * @see __construct() */ - public function getDefaultOption() + public function getDefaultOption(): ?string { return null; } @@ -273,7 +257,7 @@ public function getDefaultOption() * * @see __construct() */ - public function getRequiredOptions() + public function getRequiredOptions(): array { return []; } @@ -284,10 +268,8 @@ public function getRequiredOptions() * By default, this is the fully qualified name of the constraint class * suffixed with "Validator". You can override this method to change that * behavior. - * - * @return string */ - public function validatedBy() + public function validatedBy(): string { return static::class.'Validator'; } @@ -296,12 +278,9 @@ public function validatedBy() * Returns whether the constraint can be put onto classes, properties or * both. * - * This method should return one or more of the constants - * Constraint::CLASS_CONSTRAINT and Constraint::PROPERTY_CONSTRAINT. - * - * @return string|string[] One or more constant values + * @return self::CLASS_CONSTRAINT|self::PROPERTY_CONSTRAINT|array */ - public function getTargets() + public function getTargets(): string|array { return self::PROPERTY_CONSTRAINT; } diff --git a/ConstraintValidator.php b/ConstraintValidator.php index 60808258c..cb71c5dc7 100644 --- a/ConstraintValidator.php +++ b/ConstraintValidator.php @@ -31,15 +31,9 @@ abstract class ConstraintValidator implements ConstraintValidatorInterface */ public const OBJECT_TO_STRING = 2; - /** - * @var ExecutionContextInterface - */ - protected $context; + protected ExecutionContextInterface $context; - /** - * @return void - */ - public function initialize(ExecutionContextInterface $context) + public function initialize(ExecutionContextInterface $context): void { $this->context = $context; } diff --git a/ConstraintValidatorFactory.php b/ConstraintValidatorFactory.php index 778e202a8..67b254db6 100644 --- a/ConstraintValidatorFactory.php +++ b/ConstraintValidatorFactory.php @@ -24,11 +24,9 @@ */ class ConstraintValidatorFactory implements ConstraintValidatorFactoryInterface { - protected $validators = []; - - public function __construct(array $validators = []) - { - $this->validators = $validators; + public function __construct( + protected array $validators = [], + ) { } public function getInstance(Constraint $constraint): ConstraintValidatorInterface diff --git a/ConstraintViolation.php b/ConstraintViolation.php index 5129ccf95..380ba4c84 100644 --- a/ConstraintViolation.php +++ b/ConstraintViolation.php @@ -18,17 +18,6 @@ */ class ConstraintViolation implements ConstraintViolationInterface { - private string|\Stringable $message; - private ?string $messageTemplate; - private array $parameters; - private ?int $plural; - private mixed $root; - private ?string $propertyPath; - private mixed $invalidValue; - private ?Constraint $constraint; - private ?string $code; - private mixed $cause; - /** * Creates a new constraint violation. * @@ -49,18 +38,18 @@ class ConstraintViolation implements ConstraintViolationInterface * caused the violation * @param mixed $cause The cause of the violation */ - public function __construct(string|\Stringable $message, ?string $messageTemplate, array $parameters, mixed $root, ?string $propertyPath, mixed $invalidValue, ?int $plural = null, ?string $code = null, ?Constraint $constraint = null, mixed $cause = null) - { - $this->message = $message; - $this->messageTemplate = $messageTemplate; - $this->parameters = $parameters; - $this->plural = $plural; - $this->root = $root; - $this->propertyPath = $propertyPath; - $this->invalidValue = $invalidValue; - $this->constraint = $constraint; - $this->code = $code; - $this->cause = $cause; + public function __construct( + private string|\Stringable $message, + private ?string $messageTemplate, + private array $parameters, + private mixed $root, + private ?string $propertyPath, + private mixed $invalidValue, + private ?int $plural = null, + private ?string $code = null, + private ?Constraint $constraint = null, + private mixed $cause = null, + ) { } public function __toString(): string diff --git a/ConstraintViolationInterface.php b/ConstraintViolationInterface.php index 6eb279740..a8b6227a2 100644 --- a/ConstraintViolationInterface.php +++ b/ConstraintViolationInterface.php @@ -30,10 +30,6 @@ * element is still the person, but the property path is "address.street". * * @author Bernhard Schussek - * - * @method Constraint|null getConstraint() Returns the constraint whose validation caused the violation. Not implementing it is deprecated since Symfony 6.3. - * @method mixed getCause() Returns the cause of the violation. Not implementing it is deprecated since Symfony 6.2. - * @method string __toString() Converts the violation into a string for debugging purposes. Not implementing it is deprecated since Symfony 6.1. */ interface ConstraintViolationInterface { @@ -113,4 +109,19 @@ public function getInvalidValue(): mixed; * Returns a machine-digestible error code for the violation. */ public function getCode(): ?string; + + /** + * Returns the constraint whose validation caused the violation. + */ + public function getConstraint(): ?Constraint; + + /** + * Returns the cause of the violation. + */ + public function getCause(): mixed; + + /** + * Converts the violation into a string for debugging purposes. + */ + public function __toString(): string; } diff --git a/ConstraintViolationList.php b/ConstraintViolationList.php index dc2a6df43..a67b53389 100644 --- a/ConstraintViolationList.php +++ b/ConstraintViolationList.php @@ -58,18 +58,12 @@ public function __toString(): string return $string; } - /** - * @return void - */ - public function add(ConstraintViolationInterface $violation) + public function add(ConstraintViolationInterface $violation): void { $this->violations[] = $violation; } - /** - * @return void - */ - public function addAll(ConstraintViolationListInterface $otherList) + public function addAll(ConstraintViolationListInterface $otherList): void { foreach ($otherList as $violation) { $this->violations[] = $violation; @@ -90,18 +84,12 @@ public function has(int $offset): bool return isset($this->violations[$offset]); } - /** - * @return void - */ - public function set(int $offset, ConstraintViolationInterface $violation) + public function set(int $offset, ConstraintViolationInterface $violation): void { $this->violations[$offset] = $violation; } - /** - * @return void - */ - public function remove(int $offset) + public function remove(int $offset): void { unset($this->violations[$offset]); } diff --git a/ConstraintViolationListInterface.php b/ConstraintViolationListInterface.php index 1fdbf0bc3..d3be9f07e 100644 --- a/ConstraintViolationListInterface.php +++ b/ConstraintViolationListInterface.php @@ -20,24 +20,18 @@ * * @extends \ArrayAccess * @extends \Traversable - * - * @method string __toString() Converts the violation into a string for debugging purposes. Not implementing it is deprecated since Symfony 6.1. */ interface ConstraintViolationListInterface extends \Traversable, \Countable, \ArrayAccess { /** * Adds a constraint violation to this list. - * - * @return void */ - public function add(ConstraintViolationInterface $violation); + public function add(ConstraintViolationInterface $violation): void; /** * Merges an existing violation list into this list. - * - * @return void */ - public function addAll(self $otherList); + public function addAll(self $otherList): void; /** * Returns the violation at a given offset. @@ -59,17 +53,18 @@ public function has(int $offset): bool; * Sets a violation at a given offset. * * @param int $offset The violation offset - * - * @return void */ - public function set(int $offset, ConstraintViolationInterface $violation); + public function set(int $offset, ConstraintViolationInterface $violation): void; /** * Removes a violation at a given offset. * * @param int $offset The offset to remove - * - * @return void */ - public function remove(int $offset); + public function remove(int $offset): void; + + /** + * Converts the violation into a string for debugging purposes. + */ + public function __toString(): string; } diff --git a/Constraints/AbstractComparison.php b/Constraints/AbstractComparison.php index ffba05511..3830da789 100644 --- a/Constraints/AbstractComparison.php +++ b/Constraints/AbstractComparison.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\PropertyAccess\PropertyAccess; +use Symfony\Component\Validator\Attribute\HasNamedArguments; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Exception\ConstraintDefinitionException; use Symfony\Component\Validator\Exception\LogicException; @@ -24,15 +25,24 @@ */ abstract class AbstractComparison extends Constraint { - public $message; - public $value; - public $propertyPath; + public string $message; + public mixed $value = null; + public ?string $propertyPath = null; - public function __construct(mixed $value = null, ?string $propertyPath = null, ?string $message = null, ?array $groups = null, mixed $payload = null, array $options = []) + #[HasNamedArguments] + public function __construct(mixed $value = null, ?string $propertyPath = null, ?string $message = null, ?array $groups = null, mixed $payload = null, ?array $options = null) { if (\is_array($value)) { - $options = array_merge($value, $options); + trigger_deprecation('symfony/validator', '7.3', 'Passing an array of options to configure the "%s" constraint is deprecated, use named arguments instead.', static::class); + + $options = array_merge($value, $options ?? []); } elseif (null !== $value) { + if (\is_array($options)) { + trigger_deprecation('symfony/validator', '7.3', 'Passing an array of options to configure the "%s" constraint is deprecated, use named arguments instead.', static::class); + } else { + $options = []; + } + $options['value'] = $value; } diff --git a/Constraints/AbstractComparisonValidator.php b/Constraints/AbstractComparisonValidator.php index b7284fdb9..c8663aa25 100644 --- a/Constraints/AbstractComparisonValidator.php +++ b/Constraints/AbstractComparisonValidator.php @@ -28,17 +28,11 @@ */ abstract class AbstractComparisonValidator extends ConstraintValidator { - private ?PropertyAccessorInterface $propertyAccessor; - - public function __construct(?PropertyAccessorInterface $propertyAccessor = null) + public function __construct(private ?PropertyAccessorInterface $propertyAccessor = null) { - $this->propertyAccessor = $propertyAccessor; } - /** - * @return void - */ - public function validate(mixed $value, Constraint $constraint) + public function validate(mixed $value, Constraint $constraint): void { if (!$constraint instanceof AbstractComparison) { throw new UnexpectedTypeException($constraint, AbstractComparison::class); diff --git a/Constraints/All.php b/Constraints/All.php index 0888084d7..92ded329b 100644 --- a/Constraints/All.php +++ b/Constraints/All.php @@ -11,19 +11,31 @@ namespace Symfony\Component\Validator\Constraints; +use Symfony\Component\Validator\Attribute\HasNamedArguments; +use Symfony\Component\Validator\Constraint; + /** - * @Annotation - * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) + * When applied to an array (or Traversable object), this constraint allows you to apply + * a collection of constraints to each element of the array. * * @author Bernhard Schussek */ #[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] class All extends Composite { - public $constraints = []; + public array|Constraint $constraints = []; + /** + * @param array|array|Constraint|null $constraints + * @param string[]|null $groups + */ + #[HasNamedArguments] public function __construct(mixed $constraints = null, ?array $groups = null, mixed $payload = null) { + if (\is_array($constraints) && !array_is_list($constraints)) { + trigger_deprecation('symfony/validator', '7.3', 'Passing an array of options to configure the "%s" constraint is deprecated, use named arguments instead.', static::class); + } + parent::__construct($constraints ?? [], $groups, $payload); } diff --git a/Constraints/AllValidator.php b/Constraints/AllValidator.php index 15896f29d..10ec0266f 100644 --- a/Constraints/AllValidator.php +++ b/Constraints/AllValidator.php @@ -21,10 +21,7 @@ */ class AllValidator extends ConstraintValidator { - /** - * @return void - */ - public function validate(mixed $value, Constraint $constraint) + public function validate(mixed $value, Constraint $constraint): void { if (!$constraint instanceof All) { throw new UnexpectedTypeException($constraint, All::class); diff --git a/Constraints/AtLeastOneOf.php b/Constraints/AtLeastOneOf.php index 4e8647397..20d55f458 100644 --- a/Constraints/AtLeastOneOf.php +++ b/Constraints/AtLeastOneOf.php @@ -11,9 +11,11 @@ namespace Symfony\Component\Validator\Constraints; +use Symfony\Component\Validator\Attribute\HasNamedArguments; +use Symfony\Component\Validator\Constraint; + /** - * @Annotation - * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) + * Checks that at least one of the given constraint is satisfied. * * @author Przemysław Bogusz */ @@ -26,18 +28,25 @@ class AtLeastOneOf extends Composite self::AT_LEAST_ONE_OF_ERROR => 'AT_LEAST_ONE_OF_ERROR', ]; + public array|Constraint $constraints = []; + public string $message = 'This value should satisfy at least one of the following constraints:'; + public string $messageCollection = 'Each element of this collection should satisfy its own set of constraints.'; + public bool $includeInternalMessages = true; + /** - * @deprecated since Symfony 6.1, use const ERROR_NAMES instead + * @param array|array|null $constraints An array of validation constraints + * @param string[]|null $groups + * @param string|null $message Intro of the failure message that will be followed by the failed constraint(s) message(s) + * @param string|null $messageCollection Failure message for All and Collection inner constraints + * @param bool|null $includeInternalMessages Whether to include inner constraint messages (defaults to true) */ - protected static $errorNames = self::ERROR_NAMES; - - public $constraints = []; - public $message = 'This value should satisfy at least one of the following constraints:'; - public $messageCollection = 'Each element of this collection should satisfy its own set of constraints.'; - public $includeInternalMessages = true; - + #[HasNamedArguments] public function __construct(mixed $constraints = null, ?array $groups = null, mixed $payload = null, ?string $message = null, ?string $messageCollection = null, ?bool $includeInternalMessages = null) { + if (\is_array($constraints) && !array_is_list($constraints)) { + trigger_deprecation('symfony/validator', '7.3', 'Passing an array of options to configure the "%s" constraint is deprecated, use named arguments instead.', static::class); + } + parent::__construct($constraints ?? [], $groups, $payload); $this->message = $message ?? $this->message; diff --git a/Constraints/AtLeastOneOfValidator.php b/Constraints/AtLeastOneOfValidator.php index 5348527c7..e34e39ab8 100644 --- a/Constraints/AtLeastOneOfValidator.php +++ b/Constraints/AtLeastOneOfValidator.php @@ -20,10 +20,7 @@ */ class AtLeastOneOfValidator extends ConstraintValidator { - /** - * @return void - */ - public function validate(mixed $value, Constraint $constraint) + public function validate(mixed $value, Constraint $constraint): void { if (!$constraint instanceof AtLeastOneOf) { throw new UnexpectedTypeException($constraint, AtLeastOneOf::class); diff --git a/Constraints/Bic.php b/Constraints/Bic.php index dfd54f7a2..5390d5556 100644 --- a/Constraints/Bic.php +++ b/Constraints/Bic.php @@ -13,21 +13,35 @@ use Symfony\Component\Intl\Countries; use Symfony\Component\PropertyAccess\PropertyAccess; +use Symfony\Component\Validator\Attribute\HasNamedArguments; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Exception\ConstraintDefinitionException; +use Symfony\Component\Validator\Exception\InvalidArgumentException; use Symfony\Component\Validator\Exception\LogicException; /** - * @Annotation - * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) + * Ensures that the value is valid against the BIC format. + * + * @see https://en.wikipedia.org/wiki/ISO_9362 * * @author Michael Hirschler */ #[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] class Bic extends Constraint { + public const VALIDATION_MODE_STRICT = 'strict'; + public const VALIDATION_MODE_CASE_INSENSITIVE = 'case-insensitive'; + + public const VALIDATION_MODES = [ + self::VALIDATION_MODE_STRICT, + self::VALIDATION_MODE_CASE_INSENSITIVE, + ]; + public const INVALID_LENGTH_ERROR = '66dad313-af0b-4214-8566-6c799be9789c'; public const INVALID_CHARACTERS_ERROR = 'f424c529-7add-4417-8f2d-4b656e4833e2'; + /** + * @deprecated since Symfony 7.1, to be removed in 8.0 + */ public const INVALID_BANK_CODE_ERROR = '00559357-6170-4f29-aebd-d19330aa19cf'; public const INVALID_COUNTRY_CODE_ERROR = '1ce76f8d-3c1f-451c-9e62-fe9c3ed486ae'; public const INVALID_CASE_ERROR = '11884038-3312-4ae5-9d04-699f782130c7'; @@ -41,21 +55,46 @@ class Bic extends Constraint self::INVALID_CASE_ERROR => 'INVALID_CASE_ERROR', ]; + public string $message = 'This is not a valid Business Identifier Code (BIC).'; + public string $ibanMessage = 'This Business Identifier Code (BIC) is not associated with IBAN {{ iban }}.'; + public ?string $iban = null; + public ?string $ibanPropertyPath = null; /** - * @deprecated since Symfony 6.1, use const ERROR_NAMES instead + * @var self::VALIDATION_MODE_* */ - protected static $errorNames = self::ERROR_NAMES; - - public $message = 'This is not a valid Business Identifier Code (BIC).'; - public $ibanMessage = 'This Business Identifier Code (BIC) is not associated with IBAN {{ iban }}.'; - public $iban; - public $ibanPropertyPath; + public string $mode = self::VALIDATION_MODE_STRICT; - public function __construct(?array $options = null, ?string $message = null, ?string $iban = null, ?string $ibanPropertyPath = null, ?string $ibanMessage = null, ?array $groups = null, mixed $payload = null) - { + /** + * @param array|null $options + * @param string|null $iban An IBAN value to validate that its country code is the same as the BIC's one + * @param string|null $ibanPropertyPath Property path to the IBAN value when validating objects + * @param string[]|null $groups + * @param self::VALIDATION_MODE_*|null $mode The mode used to validate the BIC; pass null to use the default mode (strict) + */ + #[HasNamedArguments] + public function __construct( + ?array $options = null, + ?string $message = null, + ?string $iban = null, + ?string $ibanPropertyPath = null, + ?string $ibanMessage = null, + ?array $groups = null, + mixed $payload = null, + ?string $mode = null, + ) { if (!class_exists(Countries::class)) { throw new LogicException('The Intl component is required to use the Bic constraint. Try running "composer require symfony/intl".'); } + if (\is_array($options) && \array_key_exists('mode', $options) && !\in_array($options['mode'], self::VALIDATION_MODES, true)) { + throw new InvalidArgumentException('The "mode" parameter value is not valid.'); + } + if (null !== $mode && !\in_array($mode, self::VALIDATION_MODES, true)) { + throw new InvalidArgumentException('The "mode" parameter value is not valid.'); + } + + if (\is_array($options)) { + trigger_deprecation('symfony/validator', '7.3', 'Passing an array of options to configure the "%s" constraint is deprecated, use named arguments instead.', static::class); + } parent::__construct($options, $groups, $payload); @@ -63,6 +102,7 @@ public function __construct(?array $options = null, ?string $message = null, ?st $this->ibanMessage = $ibanMessage ?? $this->ibanMessage; $this->iban = $iban ?? $this->iban; $this->ibanPropertyPath = $ibanPropertyPath ?? $this->ibanPropertyPath; + $this->mode = $mode ?? $this->mode; if (null !== $this->iban && null !== $this->ibanPropertyPath) { throw new ConstraintDefinitionException('The "iban" and "ibanPropertyPath" options of the Iban constraint cannot be used at the same time.'); diff --git a/Constraints/BicValidator.php b/Constraints/BicValidator.php index d038b8cb8..55b6bbbc0 100644 --- a/Constraints/BicValidator.php +++ b/Constraints/BicValidator.php @@ -57,17 +57,11 @@ class BicValidator extends ConstraintValidator 'EA' => 'ES', // Ceuta and Melilla ]; - private ?PropertyAccessor $propertyAccessor; - - public function __construct(?PropertyAccessor $propertyAccessor = null) + public function __construct(private ?PropertyAccessor $propertyAccessor = null) { - $this->propertyAccessor = $propertyAccessor; } - /** - * @return void - */ - public function validate(mixed $value, Constraint $constraint) + public function validate(mixed $value, Constraint $constraint): void { if (!$constraint instanceof Bic) { throw new UnexpectedTypeException($constraint, Bic::class); @@ -104,6 +98,9 @@ public function validate(mixed $value, Constraint $constraint) } $bicCountryCode = substr($canonicalize, 4, 2); + if (Bic::VALIDATION_MODE_CASE_INSENSITIVE === $constraint->mode) { + $bicCountryCode = strtoupper($bicCountryCode); + } if (!isset(self::BIC_COUNTRY_TO_IBAN_COUNTRY_MAP[$bicCountryCode]) && !Countries::exists($bicCountryCode)) { $this->context->buildViolation($constraint->message) ->setParameter('{{ value }}', $this->formatValue($value)) @@ -113,8 +110,8 @@ public function validate(mixed $value, Constraint $constraint) return; } - // should contain uppercase characters only - if (strtoupper($canonicalize) !== $canonicalize) { + // should contain uppercase characters only in strict mode + if (Bic::VALIDATION_MODE_STRICT === $constraint->mode && strtoupper($canonicalize) !== $canonicalize) { $this->context->buildViolation($constraint->message) ->setParameter('{{ value }}', $this->formatValue($value)) ->setCode(Bic::INVALID_CASE_ERROR) diff --git a/Constraints/Blank.php b/Constraints/Blank.php index 00a4e65d9..72fbae57a 100644 --- a/Constraints/Blank.php +++ b/Constraints/Blank.php @@ -11,11 +11,11 @@ namespace Symfony\Component\Validator\Constraints; +use Symfony\Component\Validator\Attribute\HasNamedArguments; use Symfony\Component\Validator\Constraint; /** - * @Annotation - * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) + * Validates that a value is blank, i.e. an empty string or null. * * @author Bernhard Schussek */ @@ -28,15 +28,19 @@ class Blank extends Constraint self::NOT_BLANK_ERROR => 'NOT_BLANK_ERROR', ]; + public string $message = 'This value should be blank.'; + /** - * @deprecated since Symfony 6.1, use const ERROR_NAMES instead + * @param array|null $options + * @param string[]|null $groups */ - protected static $errorNames = self::ERROR_NAMES; - - public $message = 'This value should be blank.'; - + #[HasNamedArguments] public function __construct(?array $options = null, ?string $message = null, ?array $groups = null, mixed $payload = null) { + if (\is_array($options)) { + trigger_deprecation('symfony/validator', '7.3', 'Passing an array of options to configure the "%s" constraint is deprecated, use named arguments instead.', static::class); + } + parent::__construct($options ?? [], $groups, $payload); $this->message = $message ?? $this->message; diff --git a/Constraints/BlankValidator.php b/Constraints/BlankValidator.php index 2551d5e24..ec4b7e445 100644 --- a/Constraints/BlankValidator.php +++ b/Constraints/BlankValidator.php @@ -20,10 +20,7 @@ */ class BlankValidator extends ConstraintValidator { - /** - * @return void - */ - public function validate(mixed $value, Constraint $constraint) + public function validate(mixed $value, Constraint $constraint): void { if (!$constraint instanceof Blank) { throw new UnexpectedTypeException($constraint, Blank::class); diff --git a/Constraints/Callback.php b/Constraints/Callback.php index 5cff153d6..44b51ac2a 100644 --- a/Constraints/Callback.php +++ b/Constraints/Callback.php @@ -11,11 +11,11 @@ namespace Symfony\Component\Validator\Constraints; +use Symfony\Component\Validator\Attribute\HasNamedArguments; use Symfony\Component\Validator\Constraint; /** - * @Annotation - * @Target({"CLASS", "PROPERTY", "METHOD", "ANNOTATION"}) + * Defines custom validation rules through arbitrary callback methods. * * @author Bernhard Schussek */ @@ -27,17 +27,32 @@ class Callback extends Constraint */ public $callback; - public function __construct(array|string|callable|null $callback = null, ?array $groups = null, mixed $payload = null, array $options = []) + /** + * @param string|string[]|callable|array|null $callback The callback definition + * @param string[]|null $groups + */ + #[HasNamedArguments] + public function __construct(array|string|callable|null $callback = null, ?array $groups = null, mixed $payload = null, ?array $options = null) { - // Invocation through annotations with an array parameter only + // Invocation through attributes with an array parameter only if (\is_array($callback) && 1 === \count($callback) && isset($callback['value'])) { + trigger_deprecation('symfony/validator', '7.3', 'Passing an array of options to configure the "%s" constraint is deprecated, use named arguments instead.', static::class); + $callback = $callback['value']; } if (!\is_array($callback) || (!isset($callback['callback']) && !isset($callback['groups']) && !isset($callback['payload']))) { + if (\is_array($options)) { + trigger_deprecation('symfony/validator', '7.3', 'Passing an array of options to configure the "%s" constraint is deprecated, use named arguments instead.', static::class); + } else { + $options = []; + } + $options['callback'] = $callback; } else { - $options = array_merge($callback, $options); + trigger_deprecation('symfony/validator', '7.3', 'Passing an array of options to configure the "%s" constraint is deprecated, use named arguments instead.', static::class); + + $options = array_merge($callback, $options ?? []); } parent::__construct($options, $groups, $payload); diff --git a/Constraints/CallbackValidator.php b/Constraints/CallbackValidator.php index e1936ad96..d2ae8e32d 100644 --- a/Constraints/CallbackValidator.php +++ b/Constraints/CallbackValidator.php @@ -23,10 +23,7 @@ */ class CallbackValidator extends ConstraintValidator { - /** - * @return void - */ - public function validate(mixed $object, Constraint $constraint) + public function validate(mixed $object, Constraint $constraint): void { if (!$constraint instanceof Callback) { throw new UnexpectedTypeException($constraint, Callback::class); diff --git a/Constraints/CardScheme.php b/Constraints/CardScheme.php index 39eb1cd59..81de342f5 100644 --- a/Constraints/CardScheme.php +++ b/Constraints/CardScheme.php @@ -11,13 +11,11 @@ namespace Symfony\Component\Validator\Constraints; +use Symfony\Component\Validator\Attribute\HasNamedArguments; use Symfony\Component\Validator\Constraint; /** - * Metadata for the CardSchemeValidator. - * - * @Annotation - * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) + * Validates a credit card number for a given credit card company. * * @author Tim Nagel * @author Bernhard Schussek @@ -46,20 +44,31 @@ class CardScheme extends Constraint self::INVALID_FORMAT_ERROR => 'INVALID_FORMAT_ERROR', ]; + public string $message = 'Unsupported card type or invalid card number.'; + public array|string|null $schemes = null; + /** - * @deprecated since Symfony 6.1, use const ERROR_NAMES instead + * @param non-empty-string|non-empty-string[]|array|null $schemes Name(s) of the number scheme(s) used to validate the credit card number + * @param string[]|null $groups + * @param array|null $options */ - protected static $errorNames = self::ERROR_NAMES; - - public $message = 'Unsupported card type or invalid card number.'; - public $schemes; - - public function __construct(array|string|null $schemes, ?string $message = null, ?array $groups = null, mixed $payload = null, array $options = []) + #[HasNamedArguments] + public function __construct(array|string|null $schemes, ?string $message = null, ?array $groups = null, mixed $payload = null, ?array $options = null) { if (\is_array($schemes) && \is_string(key($schemes))) { - $options = array_merge($schemes, $options); - } elseif (null !== $schemes) { - $options['value'] = $schemes; + trigger_deprecation('symfony/validator', '7.3', 'Passing an array of options to configure the "%s" constraint is deprecated, use named arguments instead.', static::class); + + $options = array_merge($schemes, $options ?? []); + } else { + if (\is_array($options)) { + trigger_deprecation('symfony/validator', '7.3', 'Passing an array of options to configure the "%s" constraint is deprecated, use named arguments instead.', static::class); + } else { + $options = []; + } + + if (null !== $schemes) { + $options['value'] = $schemes; + } } parent::__construct($options, $groups, $payload); diff --git a/Constraints/CardSchemeValidator.php b/Constraints/CardSchemeValidator.php index 22de0d8db..af26f4e4b 100644 --- a/Constraints/CardSchemeValidator.php +++ b/Constraints/CardSchemeValidator.php @@ -26,7 +26,7 @@ */ class CardSchemeValidator extends ConstraintValidator { - protected $schemes = [ + protected array $schemes = [ // American Express card numbers start with 34 or 37 and have 15 digits. CardScheme::AMEX => [ '/^3[47][0-9]{13}$/D', @@ -93,10 +93,8 @@ class CardSchemeValidator extends ConstraintValidator /** * Validates a creditcard belongs to a specified scheme. - * - * @return void */ - public function validate(mixed $value, Constraint $constraint) + public function validate(mixed $value, Constraint $constraint): void { if (!$constraint instanceof CardScheme) { throw new UnexpectedTypeException($constraint, CardScheme::class); diff --git a/Constraints/Cascade.php b/Constraints/Cascade.php index 5a0fce110..7d90cfcf7 100644 --- a/Constraints/Cascade.php +++ b/Constraints/Cascade.php @@ -11,12 +11,12 @@ namespace Symfony\Component\Validator\Constraints; +use Symfony\Component\Validator\Attribute\HasNamedArguments; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Exception\ConstraintDefinitionException; /** - * @Annotation - * @Target({"CLASS"}) + * Validates a whole class, including nested objects in properties. * * @author Jules Pietri */ @@ -25,12 +25,23 @@ class Cascade extends Constraint { public array $exclude = []; + /** + * @param non-empty-string[]|non-empty-string|array|null $exclude Properties excluded from validation + * @param array|null $options + */ + #[HasNamedArguments] public function __construct(array|string|null $exclude = null, ?array $options = null) { if (\is_array($exclude) && !array_is_list($exclude)) { + trigger_deprecation('symfony/validator', '7.3', 'Passing an array of options to configure the "%s" constraint is deprecated, use named arguments instead.', static::class); + $options = array_merge($exclude, $options ?? []); $options['exclude'] = array_flip((array) ($options['exclude'] ?? [])); } else { + if (\is_array($options)) { + trigger_deprecation('symfony/validator', '7.3', 'Passing an array of options to configure the "%s" constraint is deprecated, use named arguments instead.', static::class); + } + $this->exclude = array_flip((array) $exclude); } diff --git a/Constraints/Charset.php b/Constraints/Charset.php new file mode 100644 index 000000000..aa22c503a --- /dev/null +++ b/Constraints/Charset.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Constraints; + +use Symfony\Component\Validator\Attribute\HasNamedArguments; +use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\Exception\ConstraintDefinitionException; + +/** + * @author Alexandre Daubois + */ +#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] +final class Charset extends Constraint +{ + public const BAD_ENCODING_ERROR = '94c5e58b-f892-4e25-8fd6-9d89c80bfe81'; + + protected const ERROR_NAMES = [ + self::BAD_ENCODING_ERROR => 'BAD_ENCODING_ERROR', + ]; + + #[HasNamedArguments] + public function __construct( + public array|string $encodings = [], + public string $message = 'The detected character encoding is invalid ({{ detected }}). Allowed encodings are {{ encodings }}.', + ?array $groups = null, + mixed $payload = null, + ) { + parent::__construct(null, $groups, $payload); + + if ([] === $this->encodings) { + throw new ConstraintDefinitionException(\sprintf('The "%s" constraint requires at least one encoding.', static::class)); + } + } +} diff --git a/Constraints/CharsetValidator.php b/Constraints/CharsetValidator.php new file mode 100644 index 000000000..63259a45d --- /dev/null +++ b/Constraints/CharsetValidator.php @@ -0,0 +1,46 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Constraints; + +use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\ConstraintValidator; +use Symfony\Component\Validator\Exception\UnexpectedTypeException; +use Symfony\Component\Validator\Exception\UnexpectedValueException; + +/** + * @author Alexandre Daubois + */ +final class CharsetValidator extends ConstraintValidator +{ + public function validate(mixed $value, Constraint $constraint): void + { + if (!$constraint instanceof Charset) { + throw new UnexpectedTypeException($constraint, Charset::class); + } + + if (null === $value) { + return; + } + + if (!\is_string($value) && !$value instanceof \Stringable) { + throw new UnexpectedValueException($value, 'string'); + } + + if (!\in_array(mb_detect_encoding($value, $constraint->encodings, true), (array) $constraint->encodings, true)) { + $this->context->buildViolation($constraint->message) + ->setParameter('{{ detected }}', mb_detect_encoding($value, strict: true)) + ->setParameter('{{ encodings }}', implode(', ', (array) $constraint->encodings)) + ->setCode(Charset::BAD_ENCODING_ERROR) + ->addViolation(); + } + } +} diff --git a/Constraints/Choice.php b/Constraints/Choice.php index 7345e2640..1435a762b 100644 --- a/Constraints/Choice.php +++ b/Constraints/Choice.php @@ -11,11 +11,11 @@ namespace Symfony\Component\Validator\Constraints; +use Symfony\Component\Validator\Attribute\HasNamedArguments; use Symfony\Component\Validator\Constraint; /** - * @Annotation - * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) + * Validates that a value is one of a given set of valid choices. * * @author Bernhard Schussek */ @@ -32,22 +32,17 @@ class Choice extends Constraint self::TOO_MANY_ERROR => 'TOO_MANY_ERROR', ]; - /** - * @deprecated since Symfony 6.1, use const ERROR_NAMES instead - */ - protected static $errorNames = self::ERROR_NAMES; - - public $choices; + public ?array $choices = null; /** @var callable|string|null */ public $callback; - public $multiple = false; - public $strict = true; - public $min; - public $max; - public $message = 'The value you selected is not a valid choice.'; - public $multipleMessage = 'One or more of the given values is invalid.'; - public $minMessage = 'You must select at least {{ limit }} choice.|You must select at least {{ limit }} choices.'; - public $maxMessage = 'You must select at most {{ limit }} choice.|You must select at most {{ limit }} choices.'; + public bool $multiple = false; + public bool $strict = true; + public ?int $min = null; + public ?int $max = null; + public string $message = 'The value you selected is not a valid choice.'; + public string $multipleMessage = 'One or more of the given values is invalid.'; + public string $minMessage = 'You must select at least {{ limit }} choice.|You must select at least {{ limit }} choices.'; + public string $maxMessage = 'You must select at most {{ limit }} choice.|You must select at most {{ limit }} choices.'; public bool $match = true; public function getDefaultOption(): ?string @@ -55,6 +50,17 @@ public function getDefaultOption(): ?string return 'choices'; } + /** + * @param array|null $choices An array of choices (required unless a callback is specified) + * @param callable|string|null $callback Callback method to use instead of the choice option to get the choices + * @param bool|null $multiple Whether to expect the value to be an array of valid choices (defaults to false) + * @param bool|null $strict This option defaults to true and should not be used + * @param int<0, max>|null $min Minimum of valid choices if multiple values are expected + * @param positive-int|null $max Maximum of valid choices if multiple values are expected + * @param string[]|null $groups + * @param bool|null $match Whether to validate the values are part of the choices or not (defaults to true) + */ + #[HasNamedArguments] public function __construct( string|array $options = [], ?array $choices = null, @@ -74,7 +80,10 @@ public function __construct( if (\is_array($options) && $options && array_is_list($options)) { $choices ??= $options; $options = []; + } elseif (\is_array($options) && [] !== $options) { + trigger_deprecation('symfony/validator', '7.3', 'Passing an array of options to configure the "%s" constraint is deprecated, use named arguments instead.', static::class); } + if (null !== $choices) { $options['value'] = $choices; } diff --git a/Constraints/ChoiceValidator.php b/Constraints/ChoiceValidator.php index d5c2b8c2b..916c0732a 100644 --- a/Constraints/ChoiceValidator.php +++ b/Constraints/ChoiceValidator.php @@ -27,10 +27,7 @@ */ class ChoiceValidator extends ConstraintValidator { - /** - * @return void - */ - public function validate(mixed $value, Constraint $constraint) + public function validate(mixed $value, Constraint $constraint): void { if (!$constraint instanceof Choice) { throw new UnexpectedTypeException($constraint, Choice::class); @@ -86,7 +83,7 @@ public function validate(mixed $value, Constraint $constraint) if (null !== $constraint->min && $count < $constraint->min) { $this->context->buildViolation($constraint->minMessage) ->setParameter('{{ limit }}', $constraint->min) - ->setPlural((int) $constraint->min) + ->setPlural($constraint->min) ->setCode(Choice::TOO_FEW_ERROR) ->addViolation(); @@ -96,11 +93,9 @@ public function validate(mixed $value, Constraint $constraint) if (null !== $constraint->max && $count > $constraint->max) { $this->context->buildViolation($constraint->maxMessage) ->setParameter('{{ limit }}', $constraint->max) - ->setPlural((int) $constraint->max) + ->setPlural($constraint->max) ->setCode(Choice::TOO_MANY_ERROR) ->addViolation(); - - return; } } elseif ($constraint->match xor \in_array($value, $choices, true)) { $this->context->buildViolation($constraint->message) diff --git a/Constraints/Cidr.php b/Constraints/Cidr.php index 92563d84d..a6e470177 100644 --- a/Constraints/Cidr.php +++ b/Constraints/Cidr.php @@ -11,17 +11,19 @@ namespace Symfony\Component\Validator\Constraints; +use Symfony\Component\Validator\Attribute\HasNamedArguments; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Exception\ConstraintDefinitionException; +use Symfony\Component\Validator\Exception\InvalidArgumentException; /** * Validates that a value is a valid CIDR notation. * - * @Annotation - * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) + * @see https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing * * @author Sorin Pop * @author Calin Bolea + * @author Ninos Ego */ #[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] class Cidr extends Constraint @@ -35,26 +37,45 @@ class Cidr extends Constraint ]; private const NET_MAXES = [ - Ip::ALL => 128, Ip::V4 => 32, Ip::V6 => 128, - ]; + Ip::ALL => 128, + + Ip::V4_NO_PUBLIC => 32, + Ip::V6_NO_PUBLIC => 128, + Ip::ALL_NO_PUBLIC => 128, + + Ip::V4_NO_PRIVATE => 32, + Ip::V6_NO_PRIVATE => 128, + Ip::ALL_NO_PRIVATE => 128, - /** - * @deprecated since Symfony 6.1, use const ERROR_NAMES instead - */ - protected static $errorNames = self::ERROR_NAMES; + Ip::V4_NO_RESERVED => 32, + Ip::V6_NO_RESERVED => 128, + Ip::ALL_NO_RESERVED => 128, - public $version = Ip::ALL; + Ip::V4_ONLY_PUBLIC => 32, + Ip::V6_ONLY_PUBLIC => 128, + Ip::ALL_ONLY_PUBLIC => 128, - public $message = 'This value is not a valid CIDR notation.'; + Ip::V4_ONLY_PRIVATE => 32, + Ip::V6_ONLY_PRIVATE => 128, + Ip::ALL_ONLY_PRIVATE => 128, - public $netmaskRangeViolationMessage = 'The value of the netmask should be between {{ min }} and {{ max }}.'; + Ip::V4_ONLY_RESERVED => 32, + Ip::V6_ONLY_RESERVED => 128, + Ip::ALL_ONLY_RESERVED => 128, + ]; - public $netmaskMin = 0; + public string $version = Ip::ALL; + public string $message = 'This value is not a valid CIDR notation.'; + public string $netmaskRangeViolationMessage = 'The value of the netmask should be between {{ min }} and {{ max }}.'; + public int $netmaskMin = 0; + public int $netmaskMax; - public $netmaskMax; + /** @var callable|null */ + public $normalizer; + #[HasNamedArguments] public function __construct( ?array $options = null, ?string $version = null, @@ -63,7 +84,12 @@ public function __construct( ?string $message = null, ?array $groups = null, $payload = null, + ?callable $normalizer = null, ) { + if (\is_array($options)) { + trigger_deprecation('symfony/validator', '7.3', 'Passing an array of options to configure the "%s" constraint is deprecated, use named arguments instead.', static::class); + } + $this->version = $version ?? $options['version'] ?? $this->version; if (!\array_key_exists($this->version, self::NET_MAXES)) { @@ -73,6 +99,7 @@ public function __construct( $this->netmaskMin = $netmaskMin ?? $options['netmaskMin'] ?? $this->netmaskMin; $this->netmaskMax = $netmaskMax ?? $options['netmaskMax'] ?? self::NET_MAXES[$this->version]; $this->message = $message ?? $this->message; + $this->normalizer = $normalizer ?? $this->normalizer; unset($options['netmaskMin'], $options['netmaskMax'], $options['version']); @@ -80,6 +107,10 @@ public function __construct( throw new ConstraintDefinitionException(\sprintf('The netmask range must be between 0 and %d.', self::NET_MAXES[$this->version])); } + if (null !== $this->normalizer && !\is_callable($this->normalizer)) { + throw new InvalidArgumentException(\sprintf('The "normalizer" option must be a valid callable ("%s" given).', get_debug_type($this->normalizer))); + } + parent::__construct($options, $groups, $payload); } } diff --git a/Constraints/CidrValidator.php b/Constraints/CidrValidator.php index c90ebcfae..f9dc5c233 100644 --- a/Constraints/CidrValidator.php +++ b/Constraints/CidrValidator.php @@ -16,6 +16,13 @@ use Symfony\Component\Validator\Exception\UnexpectedTypeException; use Symfony\Component\Validator\Exception\UnexpectedValueException; +/** + * Validates whether a value is a CIDR notation. + * + * @author Sorin Pop + * @author Calin Bolea + * @author Ninos Ego + */ class CidrValidator extends ConstraintValidator { public function validate($value, Constraint $constraint): void @@ -28,10 +35,16 @@ public function validate($value, Constraint $constraint): void return; } - if (!\is_string($value)) { + if (!\is_string($value) && !$value instanceof \Stringable) { throw new UnexpectedValueException($value, 'string'); } + $value = (string) $value; + + if (null !== $constraint->normalizer) { + $value = ($constraint->normalizer)($value); + } + $cidrParts = explode('/', $value, 2); if (!isset($cidrParts[1]) @@ -49,14 +62,7 @@ public function validate($value, Constraint $constraint): void $ipAddress = $cidrParts[0]; $netmask = (int) $cidrParts[1]; - $validV4 = Ip::V6 !== $constraint->version - && filter_var($ipAddress, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV4) - && $netmask <= 32; - - $validV6 = Ip::V4 !== $constraint->version - && filter_var($ipAddress, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV6); - - if (!$validV4 && !$validV6) { + if (!IpValidator::checkIP($ipAddress, $constraint->version)) { $this->context ->buildViolation($constraint->message) ->setCode(Cidr::INVALID_CIDR_ERROR) @@ -65,11 +71,17 @@ public function validate($value, Constraint $constraint): void return; } - if ($netmask < $constraint->netmaskMin || $netmask > $constraint->netmaskMax) { + $netmaskMax = $constraint->netmaskMax; + + if (filter_var($ipAddress, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV4) && $netmaskMax > 32) { + $netmaskMax = 32; + } + + if ($netmask < $constraint->netmaskMin || $netmask > $netmaskMax) { $this->context ->buildViolation($constraint->netmaskRangeViolationMessage) ->setParameter('{{ min }}', $constraint->netmaskMin) - ->setParameter('{{ max }}', $constraint->netmaskMax) + ->setParameter('{{ max }}', $netmaskMax) ->setCode(Cidr::OUT_OF_RANGE_ERROR) ->addViolation(); } diff --git a/Constraints/Collection.php b/Constraints/Collection.php index edad4fa0b..b59caa89d 100644 --- a/Constraints/Collection.php +++ b/Constraints/Collection.php @@ -11,12 +11,11 @@ namespace Symfony\Component\Validator\Constraints; +use Symfony\Component\Validator\Attribute\HasNamedArguments; use Symfony\Component\Validator\Constraint; -use Symfony\Component\Validator\Exception\ConstraintDefinitionException; /** - * @Annotation - * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) + * Validates a collection with constraints defined for specific keys. * * @author Bernhard Schussek */ @@ -31,24 +30,34 @@ class Collection extends Composite self::NO_SUCH_FIELD_ERROR => 'NO_SUCH_FIELD_ERROR', ]; + public array $fields = []; + public bool $allowExtraFields = false; + public bool $allowMissingFields = false; + public string $extraFieldsMessage = 'This field was not expected.'; + public string $missingFieldsMessage = 'This field is missing.'; + /** - * @deprecated since Symfony 6.1, use const ERROR_NAMES instead + * @param array|array|null $fields An associative array defining keys in the collection and their constraints + * @param string[]|null $groups + * @param bool|null $allowExtraFields Whether to allow additional keys not declared in the configured fields (defaults to false) + * @param bool|null $allowMissingFields Whether to allow the collection to lack some fields declared in the configured fields (defaults to false) */ - protected static $errorNames = self::ERROR_NAMES; - - public $fields = []; - public $allowExtraFields = false; - public $allowMissingFields = false; - public $extraFieldsMessage = 'This field was not expected.'; - public $missingFieldsMessage = 'This field is missing.'; - + #[HasNamedArguments] public function __construct(mixed $fields = null, ?array $groups = null, mixed $payload = null, ?bool $allowExtraFields = null, ?bool $allowMissingFields = null, ?string $extraFieldsMessage = null, ?string $missingFieldsMessage = null) { + $options = $fields; + if (self::isFieldsOption($fields)) { - $fields = ['fields' => $fields]; + $options = []; + + if (null !== $fields) { + $options['fields'] = $fields; + } + } else { + trigger_deprecation('symfony/validator', '7.3', 'Passing an array of options to configure the "%s" constraint is deprecated, use named arguments instead.', static::class); } - parent::__construct($fields, $groups, $payload); + parent::__construct($options, $groups, $payload); $this->allowExtraFields = $allowExtraFields ?? $this->allowExtraFields; $this->allowMissingFields = $allowMissingFields ?? $this->allowMissingFields; @@ -56,17 +65,10 @@ public function __construct(mixed $fields = null, ?array $groups = null, mixed $ $this->missingFieldsMessage = $missingFieldsMessage ?? $this->missingFieldsMessage; } - /** - * @return void - */ - protected function initializeNestedConstraints() + protected function initializeNestedConstraints(): void { parent::initializeNestedConstraints(); - if (!\is_array($this->fields)) { - throw new ConstraintDefinitionException(\sprintf('The option "fields" is expected to be an array in constraint "%s".', __CLASS__)); - } - foreach ($this->fields as $fieldName => $field) { // the XmlFileLoader and YamlFileLoader pass the field Optional // and Required constraint as an array with exactly one element @@ -92,6 +94,10 @@ protected function getCompositeOption(): string private static function isFieldsOption($options): bool { + if (null === $options) { + return true; + } + if (!\is_array($options)) { return false; } diff --git a/Constraints/CollectionValidator.php b/Constraints/CollectionValidator.php index 141b50fb3..7d5b20bf1 100644 --- a/Constraints/CollectionValidator.php +++ b/Constraints/CollectionValidator.php @@ -21,10 +21,7 @@ */ class CollectionValidator extends ConstraintValidator { - /** - * @return void - */ - public function validate(mixed $value, Constraint $constraint) + public function validate(mixed $value, Constraint $constraint): void { if (!$constraint instanceof Collection) { throw new UnexpectedTypeException($constraint, Collection::class); diff --git a/Constraints/Composite.php b/Constraints/Composite.php index 824c3a174..1710d9a49 100644 --- a/Constraints/Composite.php +++ b/Constraints/Composite.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Validator\Constraints; +use Symfony\Component\Validator\Attribute\HasNamedArguments; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Exception\ConstraintDefinitionException; @@ -49,86 +50,88 @@ abstract class Composite extends Constraint * cached. When constraints are loaded from the cache, no more group * checks need to be done. */ + #[HasNamedArguments] public function __construct(mixed $options = null, ?array $groups = null, mixed $payload = null) { parent::__construct($options, $groups, $payload); $this->initializeNestedConstraints(); - /** @var Constraint[] $nestedConstraints */ - $compositeOption = $this->getCompositeOption(); - $nestedConstraints = $this->$compositeOption; + foreach ((array) $this->getCompositeOption() as $option) { + /** @var Constraint[] $nestedConstraints */ + $nestedConstraints = $this->$option; - if (!\is_array($nestedConstraints)) { - $nestedConstraints = [$nestedConstraints]; - } - - foreach ($nestedConstraints as $constraint) { - if (!$constraint instanceof Constraint) { - if (\is_object($constraint)) { - $constraint = $constraint::class; - } - - throw new ConstraintDefinitionException(\sprintf('The value "%s" is not an instance of Constraint in constraint "%s".', $constraint, static::class)); + if (!\is_array($nestedConstraints)) { + $nestedConstraints = [$nestedConstraints]; } - if ($constraint instanceof Valid) { - throw new ConstraintDefinitionException(\sprintf('The constraint Valid cannot be nested inside constraint "%s". You can only declare the Valid constraint directly on a field or method.', static::class)); - } - } + foreach ($nestedConstraints as $constraint) { + if (!$constraint instanceof Constraint) { + if (\is_object($constraint)) { + $constraint = get_debug_type($constraint); + } - if (!isset(((array) $this)['groups'])) { - $mergedGroups = []; + throw new ConstraintDefinitionException(\sprintf('The value "%s" is not an instance of Constraint in constraint "%s".', $constraint, get_debug_type($this))); + } - foreach ($nestedConstraints as $constraint) { - foreach ($constraint->groups as $group) { - $mergedGroups[$group] = true; + if ($constraint instanceof Valid) { + throw new ConstraintDefinitionException(\sprintf('The constraint Valid cannot be nested inside constraint "%s". You can only declare the Valid constraint directly on a field or method.', get_debug_type($this))); } } - // prevent empty composite constraint to have empty groups - $this->groups = array_keys($mergedGroups) ?: [self::DEFAULT_GROUP]; - $this->$compositeOption = $nestedConstraints; + if (!isset(((array) $this)['groups'])) { + $mergedGroups = []; - return; - } + foreach ($nestedConstraints as $constraint) { + foreach ($constraint->groups as $group) { + $mergedGroups[$group] = true; + } + } - foreach ($nestedConstraints as $constraint) { - if (isset(((array) $constraint)['groups'])) { - $excessGroups = array_diff($constraint->groups, $this->groups); + // prevent empty composite constraint to have empty groups + $this->groups = array_keys($mergedGroups) ?: [self::DEFAULT_GROUP]; + $this->$option = $nestedConstraints; - if (\count($excessGroups) > 0) { - throw new ConstraintDefinitionException(\sprintf('The group(s) "%s" passed to the constraint "%s" should also be passed to its containing constraint "%s".', implode('", "', $excessGroups), get_debug_type($constraint), static::class)); + continue; + } + + foreach ($nestedConstraints as $constraint) { + if (isset(((array) $constraint)['groups'])) { + $excessGroups = array_diff($constraint->groups, $this->groups); + + if (\count($excessGroups) > 0) { + throw new ConstraintDefinitionException(\sprintf('The group(s) "%s" passed to the constraint "%s" should also be passed to its containing constraint "%s".', implode('", "', $excessGroups), get_debug_type($constraint), get_debug_type($this))); + } + } else { + $constraint->groups = $this->groups; } - } else { - $constraint->groups = $this->groups; } - } - $this->$compositeOption = $nestedConstraints; + $this->$option = $nestedConstraints; + } } /** * Implicit group names are forwarded to nested constraints. - * - * @return void */ - public function addImplicitGroupName(string $group) + public function addImplicitGroupName(string $group): void { parent::addImplicitGroupName($group); - /** @var Constraint[] $nestedConstraints */ - $nestedConstraints = $this->{$this->getCompositeOption()}; + foreach ((array) $this->getCompositeOption() as $option) { + /* @var Constraint[] $nestedConstraints */ + $nestedConstraints = (array) $this->$option; - foreach ($nestedConstraints as $constraint) { - $constraint->addImplicitGroupName($group); + foreach ($nestedConstraints as $constraint) { + $constraint->addImplicitGroupName($group); + } } } /** * Returns the name of the property that contains the nested constraints. */ - abstract protected function getCompositeOption(): string; + abstract protected function getCompositeOption(): array|string; /** * @internal Used by metadata @@ -137,8 +140,12 @@ abstract protected function getCompositeOption(): string; */ public function getNestedConstraints(): array { - /** @var Constraint[] $nestedConstraints */ - return $this->{$this->getCompositeOption()}; + $constraints = []; + foreach ((array) $this->getCompositeOption() as $option) { + $constraints = array_merge($constraints, (array) $this->$option); + } + + return $constraints; } /** @@ -148,10 +155,8 @@ public function getNestedConstraints(): array * constraints passed to the constructor. * * @see Collection::initializeNestedConstraints() - * - * @return void */ - protected function initializeNestedConstraints() + protected function initializeNestedConstraints(): void { } } diff --git a/Constraints/Compound.php b/Constraints/Compound.php index aa48dbe5e..261871533 100644 --- a/Constraints/Compound.php +++ b/Constraints/Compound.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Validator\Constraints; +use Symfony\Component\Validator\Attribute\HasNamedArguments; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Exception\ConstraintDefinitionException; @@ -22,9 +23,10 @@ abstract class Compound extends Composite { /** @var Constraint[] */ - public $constraints = []; + public array $constraints = []; - public function __construct(mixed $options = null) + #[HasNamedArguments] + public function __construct(mixed $options = null, ?array $groups = null, mixed $payload = null) { if (isset($options[$this->getCompositeOption()])) { throw new ConstraintDefinitionException(\sprintf('You can\'t redefine the "%s" option. Use the "%s::getConstraints()" method instead.', $this->getCompositeOption(), __CLASS__)); @@ -32,7 +34,7 @@ public function __construct(mixed $options = null) $this->constraints = $this->getConstraints($this->normalizeOptions($options)); - parent::__construct($options); + parent::__construct($options, $groups, $payload); } final protected function getCompositeOption(): string @@ -46,6 +48,8 @@ final public function validatedBy(): string } /** + * @param array $options + * * @return Constraint[] */ abstract protected function getConstraints(array $options): array; diff --git a/Constraints/CompoundValidator.php b/Constraints/CompoundValidator.php index 8f9c713c7..11ce4855b 100644 --- a/Constraints/CompoundValidator.php +++ b/Constraints/CompoundValidator.php @@ -20,10 +20,7 @@ */ class CompoundValidator extends ConstraintValidator { - /** - * @return void - */ - public function validate(mixed $value, Constraint $constraint) + public function validate(mixed $value, Constraint $constraint): void { if (!$constraint instanceof Compound) { throw new UnexpectedTypeException($constraint, Compound::class); diff --git a/Constraints/Count.php b/Constraints/Count.php index 42cf6c196..108872904 100644 --- a/Constraints/Count.php +++ b/Constraints/Count.php @@ -11,12 +11,12 @@ namespace Symfony\Component\Validator\Constraints; +use Symfony\Component\Validator\Attribute\HasNamedArguments; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Exception\MissingOptionsException; /** - * @Annotation - * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) + * Validates a collection's element count. * * @author Bernhard Schussek */ @@ -35,19 +35,23 @@ class Count extends Constraint self::NOT_DIVISIBLE_BY_ERROR => 'NOT_DIVISIBLE_BY_ERROR', ]; + public string $minMessage = 'This collection should contain {{ limit }} element or more.|This collection should contain {{ limit }} elements or more.'; + public string $maxMessage = 'This collection should contain {{ limit }} element or less.|This collection should contain {{ limit }} elements or less.'; + public string $exactMessage = 'This collection should contain exactly {{ limit }} element.|This collection should contain exactly {{ limit }} elements.'; + public string $divisibleByMessage = 'The number of elements in this collection should be a multiple of {{ compared_value }}.'; + public ?int $min = null; + public ?int $max = null; + public ?int $divisibleBy = null; + /** - * @deprecated since Symfony 6.1, use const ERROR_NAMES instead + * @param int<0, max>|array|null $exactly The exact expected number of elements + * @param int<0, max>|null $min Minimum expected number of elements + * @param int<0, max>|null $max Maximum expected number of elements + * @param positive-int|null $divisibleBy The number the collection count should be divisible by + * @param string[]|null $groups + * @param array|null $options */ - protected static $errorNames = self::ERROR_NAMES; - - public $minMessage = 'This collection should contain {{ limit }} element or more.|This collection should contain {{ limit }} elements or more.'; - public $maxMessage = 'This collection should contain {{ limit }} element or less.|This collection should contain {{ limit }} elements or less.'; - public $exactMessage = 'This collection should contain exactly {{ limit }} element.|This collection should contain exactly {{ limit }} elements.'; - public $divisibleByMessage = 'The number of elements in this collection should be a multiple of {{ compared_value }}.'; - public $min; - public $max; - public $divisibleBy; - + #[HasNamedArguments] public function __construct( int|array|null $exactly = null, ?int $min = null, @@ -59,11 +63,17 @@ public function __construct( ?string $divisibleByMessage = null, ?array $groups = null, mixed $payload = null, - array $options = [], + ?array $options = null, ) { if (\is_array($exactly)) { - $options = array_merge($exactly, $options); + trigger_deprecation('symfony/validator', '7.3', 'Passing an array of options to configure the "%s" constraint is deprecated, use named arguments instead.', static::class); + + $options = array_merge($exactly, $options ?? []); $exactly = $options['value'] ?? null; + } elseif (\is_array($options)) { + trigger_deprecation('symfony/validator', '7.3', 'Passing an array of options to configure the "%s" constraint is deprecated, use named arguments instead.', static::class); + } else { + $options = []; } $min ??= $options['min'] ?? null; diff --git a/Constraints/CountValidator.php b/Constraints/CountValidator.php index 3c5602353..40d889fe9 100644 --- a/Constraints/CountValidator.php +++ b/Constraints/CountValidator.php @@ -21,10 +21,7 @@ */ class CountValidator extends ConstraintValidator { - /** - * @return void - */ - public function validate(mixed $value, Constraint $constraint) + public function validate(mixed $value, Constraint $constraint): void { if (!$constraint instanceof Count) { throw new UnexpectedTypeException($constraint, Count::class); @@ -47,7 +44,7 @@ public function validate(mixed $value, Constraint $constraint) ->setParameter('{{ count }}', $count) ->setParameter('{{ limit }}', $constraint->max) ->setInvalidValue($value) - ->setPlural((int) $constraint->max) + ->setPlural($constraint->max) ->setCode($exactlyOptionEnabled ? Count::NOT_EQUAL_COUNT_ERROR : Count::TOO_MANY_ERROR) ->addViolation(); @@ -61,7 +58,7 @@ public function validate(mixed $value, Constraint $constraint) ->setParameter('{{ count }}', $count) ->setParameter('{{ limit }}', $constraint->min) ->setInvalidValue($value) - ->setPlural((int) $constraint->min) + ->setPlural($constraint->min) ->setCode($exactlyOptionEnabled ? Count::NOT_EQUAL_COUNT_ERROR : Count::TOO_FEW_ERROR) ->addViolation(); @@ -73,10 +70,10 @@ public function validate(mixed $value, Constraint $constraint) ->getValidator() ->inContext($this->context) ->validate($count, [ - new DivisibleBy([ - 'value' => $constraint->divisibleBy, - 'message' => $constraint->divisibleByMessage, - ]), + new DivisibleBy( + value: $constraint->divisibleBy, + message: $constraint->divisibleByMessage, + ), ], $this->context->getGroup()); } } diff --git a/Constraints/Country.php b/Constraints/Country.php index 4ab5d67a8..135f996dd 100644 --- a/Constraints/Country.php +++ b/Constraints/Country.php @@ -12,12 +12,14 @@ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\Intl\Countries; +use Symfony\Component\Validator\Attribute\HasNamedArguments; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Exception\LogicException; /** - * @Annotation - * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) + * Validates a value is a valid ISO 3166-1 alpha-2 country code. + * + * @see https://en.wikipedia.org/wiki/ISO_3166-1#Current_codes * * @author Bernhard Schussek */ @@ -30,14 +32,17 @@ class Country extends Constraint self::NO_SUCH_COUNTRY_ERROR => 'NO_SUCH_COUNTRY_ERROR', ]; + public string $message = 'This value is not a valid country.'; + public bool $alpha3 = false; + /** - * @deprecated since Symfony 6.1, use const ERROR_NAMES instead + * @param array|null $options + * @param bool|null $alpha3 Whether to check for alpha-3 codes instead of alpha-2 (defaults to false) + * @param string[]|null $groups + * + * @see https://en.wikipedia.org/wiki/ISO_3166-1_alpha-3#Current_codes */ - protected static $errorNames = self::ERROR_NAMES; - - public $message = 'This value is not a valid country.'; - public $alpha3 = false; - + #[HasNamedArguments] public function __construct( ?array $options = null, ?string $message = null, @@ -49,6 +54,10 @@ public function __construct( throw new LogicException('The Intl component is required to use the Country constraint. Try running "composer require symfony/intl".'); } + if (\is_array($options)) { + trigger_deprecation('symfony/validator', '7.3', 'Passing an array of options to configure the "%s" constraint is deprecated, use named arguments instead.', static::class); + } + parent::__construct($options, $groups, $payload); $this->message = $message ?? $this->message; diff --git a/Constraints/CountryValidator.php b/Constraints/CountryValidator.php index 54c8da0f9..067896922 100644 --- a/Constraints/CountryValidator.php +++ b/Constraints/CountryValidator.php @@ -24,10 +24,7 @@ */ class CountryValidator extends ConstraintValidator { - /** - * @return void - */ - public function validate(mixed $value, Constraint $constraint) + public function validate(mixed $value, Constraint $constraint): void { if (!$constraint instanceof Country) { throw new UnexpectedTypeException($constraint, Country::class); diff --git a/Constraints/CssColor.php b/Constraints/CssColor.php index e8f0aaefd..793a4a576 100644 --- a/Constraints/CssColor.php +++ b/Constraints/CssColor.php @@ -11,12 +11,12 @@ namespace Symfony\Component\Validator\Constraints; +use Symfony\Component\Validator\Attribute\HasNamedArguments; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Exception\InvalidArgumentException; /** - * @Annotation - * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) + * Validates that a value is a valid CSS color. * * @author Mathieu Santostefano */ @@ -41,11 +41,6 @@ class CssColor extends Constraint self::INVALID_FORMAT_ERROR => 'INVALID_FORMAT_ERROR', ]; - /** - * @deprecated since Symfony 6.1, use const ERROR_NAMES instead - */ - protected static $errorNames = self::ERROR_NAMES; - /** * @var string[] */ @@ -64,19 +59,24 @@ class CssColor extends Constraint self::HSLA, ]; - public $message = 'This value is not a valid CSS color.'; - public $formats; + public string $message = 'This value is not a valid CSS color.'; + public array|string $formats; /** - * @param array|string $formats The types of CSS colors allowed (e.g. hexadecimal only, RGB and HSL only, etc.). + * @param non-empty-string[]|non-empty-string|array $formats The types of CSS colors allowed ({@see https://symfony.com/doc/current/reference/constraints/CssColor.html#formats}) + * @param string[]|null $groups + * @param array|null $options */ - public function __construct($formats = [], ?string $message = null, ?array $groups = null, $payload = null, ?array $options = null) + #[HasNamedArguments] + public function __construct(array|string $formats = [], ?string $message = null, ?array $groups = null, $payload = null, ?array $options = null) { $validationModesAsString = implode(', ', self::$validationModes); if (!$formats) { $options['value'] = self::$validationModes; } elseif (\is_array($formats) && \is_string(key($formats))) { + trigger_deprecation('symfony/validator', '7.3', 'Passing an array of options to configure the "%s" constraint is deprecated, use named arguments instead.', static::class); + $options = array_merge($formats, $options ?? []); } elseif (\is_array($formats)) { if ([] === array_intersect(self::$validationModes, $formats)) { @@ -85,7 +85,7 @@ public function __construct($formats = [], ?string $message = null, ?array $grou $options['value'] = $formats; } elseif (\is_string($formats)) { - if (!\in_array($formats, self::$validationModes)) { + if (!\in_array($formats, self::$validationModes, true)) { throw new InvalidArgumentException(\sprintf('The "formats" parameter value is not valid. It must contain one or more of the following values: "%s".', $validationModesAsString)); } diff --git a/Constraints/CssColorValidator.php b/Constraints/CssColorValidator.php index 78563a92c..8ba2d2950 100644 --- a/Constraints/CssColorValidator.php +++ b/Constraints/CssColorValidator.php @@ -62,7 +62,7 @@ public function validate($value, Constraint $constraint): void return; } - if (!\is_string($value)) { + if (!\is_string($value) && !$value instanceof \Stringable) { throw new UnexpectedValueException($value, 'string'); } @@ -76,7 +76,7 @@ public function validate($value, Constraint $constraint): void } $this->context->buildViolation($constraint->message) - ->setParameter('{{ value }}', $this->formatValue($value)) + ->setParameter('{{ value }}', $this->formatValue((string) $value)) ->setCode(CssColor::INVALID_FORMAT_ERROR) ->addViolation(); } diff --git a/Constraints/Currency.php b/Constraints/Currency.php index facf11100..c8f6417b3 100644 --- a/Constraints/Currency.php +++ b/Constraints/Currency.php @@ -12,12 +12,14 @@ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\Intl\Currencies; +use Symfony\Component\Validator\Attribute\HasNamedArguments; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Exception\LogicException; /** - * @Annotation - * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) + * Validates that a value is a valid 3-letter ISO 4217 currency name. + * + * @see https://en.wikipedia.org/wiki/ISO_4217 * * @author Miha Vrhovnik * @author Bernhard Schussek @@ -31,19 +33,23 @@ class Currency extends Constraint self::NO_SUCH_CURRENCY_ERROR => 'NO_SUCH_CURRENCY_ERROR', ]; + public string $message = 'This value is not a valid currency.'; + /** - * @deprecated since Symfony 6.1, use const ERROR_NAMES instead + * @param array|null $options + * @param string[]|null $groups */ - protected static $errorNames = self::ERROR_NAMES; - - public $message = 'This value is not a valid currency.'; - + #[HasNamedArguments] public function __construct(?array $options = null, ?string $message = null, ?array $groups = null, mixed $payload = null) { if (!class_exists(Currencies::class)) { throw new LogicException('The Intl component is required to use the Currency constraint. Try running "composer require symfony/intl".'); } + if (\is_array($options)) { + trigger_deprecation('symfony/validator', '7.3', 'Passing an array of options to configure the "%s" constraint is deprecated, use named arguments instead.', static::class); + } + parent::__construct($options, $groups, $payload); $this->message = $message ?? $this->message; diff --git a/Constraints/CurrencyValidator.php b/Constraints/CurrencyValidator.php index a50ea62ab..ef662802f 100644 --- a/Constraints/CurrencyValidator.php +++ b/Constraints/CurrencyValidator.php @@ -25,10 +25,7 @@ */ class CurrencyValidator extends ConstraintValidator { - /** - * @return void - */ - public function validate(mixed $value, Constraint $constraint) + public function validate(mixed $value, Constraint $constraint): void { if (!$constraint instanceof Currency) { throw new UnexpectedTypeException($constraint, Currency::class); diff --git a/Constraints/Date.php b/Constraints/Date.php index bccb59ee6..adb48474f 100644 --- a/Constraints/Date.php +++ b/Constraints/Date.php @@ -11,11 +11,13 @@ namespace Symfony\Component\Validator\Constraints; +use Symfony\Component\Validator\Attribute\HasNamedArguments; use Symfony\Component\Validator\Constraint; /** - * @Annotation - * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) + * Validates that a value is a valid date, i.e. its string representation follows the Y-m-d format. + * + * @see https://www.php.net/manual/en/datetime.format.php * * @author Bernhard Schussek */ @@ -30,15 +32,19 @@ class Date extends Constraint self::INVALID_DATE_ERROR => 'INVALID_DATE_ERROR', ]; + public string $message = 'This value is not a valid date.'; + /** - * @deprecated since Symfony 6.1, use const ERROR_NAMES instead + * @param array|null $options + * @param string[]|null $groups */ - protected static $errorNames = self::ERROR_NAMES; - - public $message = 'This value is not a valid date.'; - + #[HasNamedArguments] public function __construct(?array $options = null, ?string $message = null, ?array $groups = null, mixed $payload = null) { + if (\is_array($options)) { + trigger_deprecation('symfony/validator', '7.3', 'Passing an array of options to configure the "%s" constraint is deprecated, use named arguments instead.', static::class); + } + parent::__construct($options, $groups, $payload); $this->message = $message ?? $this->message; diff --git a/Constraints/DateTime.php b/Constraints/DateTime.php index 45648f0ba..6b287be75 100644 --- a/Constraints/DateTime.php +++ b/Constraints/DateTime.php @@ -11,11 +11,13 @@ namespace Symfony\Component\Validator\Constraints; +use Symfony\Component\Validator\Attribute\HasNamedArguments; use Symfony\Component\Validator\Constraint; /** - * @Annotation - * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) + * Validates that a value is a valid "datetime" according to a given format. + * + * @see https://www.php.net/manual/en/datetime.format.php * * @author Bernhard Schussek */ @@ -32,19 +34,28 @@ class DateTime extends Constraint self::INVALID_TIME_ERROR => 'INVALID_TIME_ERROR', ]; + public string $format = 'Y-m-d H:i:s'; + public string $message = 'This value is not a valid datetime.'; + /** - * @deprecated since Symfony 6.1, use const ERROR_NAMES instead + * @param non-empty-string|array|null $format The datetime format to match (defaults to 'Y-m-d H:i:s') + * @param string[]|null $groups + * @param array|null $options */ - protected static $errorNames = self::ERROR_NAMES; - - public $format = 'Y-m-d H:i:s'; - public $message = 'This value is not a valid datetime.'; - - public function __construct(string|array|null $format = null, ?string $message = null, ?array $groups = null, mixed $payload = null, array $options = []) + #[HasNamedArguments] + public function __construct(string|array|null $format = null, ?string $message = null, ?array $groups = null, mixed $payload = null, ?array $options = null) { if (\is_array($format)) { - $options = array_merge($format, $options); + trigger_deprecation('symfony/validator', '7.3', 'Passing an array of options to configure the "%s" constraint is deprecated, use named arguments instead.', static::class); + + $options = array_merge($format, $options ?? []); } elseif (null !== $format) { + if (\is_array($options)) { + trigger_deprecation('symfony/validator', '7.3', 'Passing an array of options to configure the "%s" constraint is deprecated, use named arguments instead.', static::class); + } else { + $options = []; + } + $options['value'] = $format; } diff --git a/Constraints/DateTimeValidator.php b/Constraints/DateTimeValidator.php index c88732d4d..f5765cbf6 100644 --- a/Constraints/DateTimeValidator.php +++ b/Constraints/DateTimeValidator.php @@ -21,10 +21,7 @@ */ class DateTimeValidator extends DateValidator { - /** - * @return void - */ - public function validate(mixed $value, Constraint $constraint) + public function validate(mixed $value, Constraint $constraint): void { if (!$constraint instanceof DateTime) { throw new UnexpectedTypeException($constraint, DateTime::class); @@ -47,6 +44,7 @@ public function validate(mixed $value, Constraint $constraint) if (0 < $errors['error_count']) { $this->context->buildViolation($constraint->message) ->setParameter('{{ value }}', $this->formatValue($value)) + ->setParameter('{{ format }}', $this->formatValue($constraint->format)) ->setCode(DateTime::INVALID_FORMAT_ERROR) ->addViolation(); @@ -61,16 +59,19 @@ public function validate(mixed $value, Constraint $constraint) if ('The parsed date was invalid' === $warning) { $this->context->buildViolation($constraint->message) ->setParameter('{{ value }}', $this->formatValue($value)) + ->setParameter('{{ format }}', $this->formatValue($constraint->format)) ->setCode(DateTime::INVALID_DATE_ERROR) ->addViolation(); } elseif ('The parsed time was invalid' === $warning) { $this->context->buildViolation($constraint->message) ->setParameter('{{ value }}', $this->formatValue($value)) + ->setParameter('{{ format }}', $this->formatValue($constraint->format)) ->setCode(DateTime::INVALID_TIME_ERROR) ->addViolation(); } else { $this->context->buildViolation($constraint->message) ->setParameter('{{ value }}', $this->formatValue($value)) + ->setParameter('{{ format }}', $this->formatValue($constraint->format)) ->setCode(DateTime::INVALID_FORMAT_ERROR) ->addViolation(); } diff --git a/Constraints/DateValidator.php b/Constraints/DateValidator.php index 65dc9648a..f71c5b376 100644 --- a/Constraints/DateValidator.php +++ b/Constraints/DateValidator.php @@ -33,10 +33,7 @@ public static function checkDate(int $year, int $month, int $day): bool return checkdate($month, $day, $year); } - /** - * @return void - */ - public function validate(mixed $value, Constraint $constraint) + public function validate(mixed $value, Constraint $constraint): void { if (!$constraint instanceof Date) { throw new UnexpectedTypeException($constraint, Date::class); diff --git a/Constraints/DisableAutoMapping.php b/Constraints/DisableAutoMapping.php index 636801ac9..7cbea8b38 100644 --- a/Constraints/DisableAutoMapping.php +++ b/Constraints/DisableAutoMapping.php @@ -11,29 +11,36 @@ namespace Symfony\Component\Validator\Constraints; +use Symfony\Component\Validator\Attribute\HasNamedArguments; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Exception\ConstraintDefinitionException; /** * Disables auto mapping. * - * Using the annotations on a property has higher precedence than using it on a class, + * Using the attribute on a property has higher precedence than using it on a class, * which has higher precedence than any configuration that might be defined outside the class. * - * @Annotation - * * @author Kévin Dunglas */ #[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::TARGET_CLASS)] class DisableAutoMapping extends Constraint { - public function __construct(?array $options = null) + /** + * @param array|null $options + */ + #[HasNamedArguments] + public function __construct(?array $options = null, mixed $payload = null) { + if (\is_array($options)) { + trigger_deprecation('symfony/validator', '7.3', 'Passing an array of options to configure the "%s" constraint is deprecated, use named arguments instead.', static::class); + } + if (\is_array($options) && \array_key_exists('groups', $options)) { throw new ConstraintDefinitionException(\sprintf('The option "groups" is not supported by the constraint "%s".', __CLASS__)); } - parent::__construct($options); + parent::__construct($options, null, $payload); } public function getTargets(): string|array diff --git a/Constraints/DivisibleBy.php b/Constraints/DivisibleBy.php index 90164aab2..e56ddfb7c 100644 --- a/Constraints/DivisibleBy.php +++ b/Constraints/DivisibleBy.php @@ -12,8 +12,7 @@ namespace Symfony\Component\Validator\Constraints; /** - * @Annotation - * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) + * Validates that a value is divisible by another value. * * @author Colin O'Dell */ @@ -26,10 +25,5 @@ class DivisibleBy extends AbstractComparison self::NOT_DIVISIBLE_BY => 'NOT_DIVISIBLE_BY', ]; - /** - * @deprecated since Symfony 6.1, use const ERROR_NAMES instead - */ - protected static $errorNames = self::ERROR_NAMES; - - public $message = 'This value should be a multiple of {{ compared_value }}.'; + public string $message = 'This value should be a multiple of {{ compared_value }}.'; } diff --git a/Constraints/Email.php b/Constraints/Email.php index 995713c40..4a66986b2 100644 --- a/Constraints/Email.php +++ b/Constraints/Email.php @@ -12,13 +12,13 @@ namespace Symfony\Component\Validator\Constraints; use Egulias\EmailValidator\EmailValidator as StrictEmailValidator; +use Symfony\Component\Validator\Attribute\HasNamedArguments; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Exception\InvalidArgumentException; use Symfony\Component\Validator\Exception\LogicException; /** - * @Annotation - * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) + * Validates that a value is a valid email address. * * @author Bernhard Schussek */ @@ -28,10 +28,6 @@ class Email extends Constraint public const VALIDATION_MODE_HTML5_ALLOW_NO_TLD = 'html5-allow-no-tld'; public const VALIDATION_MODE_HTML5 = 'html5'; public const VALIDATION_MODE_STRICT = 'strict'; - /** - * @deprecated since Symfony 6.2, use VALIDATION_MODE_HTML5 instead - */ - public const VALIDATION_MODE_LOOSE = 'loose'; public const INVALID_FORMAT_ERROR = 'bd79c0ab-ddba-46cc-a703-a7a4b08de310'; @@ -39,23 +35,23 @@ class Email extends Constraint self::VALIDATION_MODE_HTML5_ALLOW_NO_TLD, self::VALIDATION_MODE_HTML5, self::VALIDATION_MODE_STRICT, - self::VALIDATION_MODE_LOOSE, ]; protected const ERROR_NAMES = [ self::INVALID_FORMAT_ERROR => 'INVALID_FORMAT_ERROR', ]; - /** - * @deprecated since Symfony 6.1, use const ERROR_NAMES instead - */ - protected static $errorNames = self::ERROR_NAMES; - - public $message = 'This value is not a valid email address.'; - public $mode; + public string $message = 'This value is not a valid email address.'; + public ?string $mode = null; /** @var callable|null */ public $normalizer; + /** + * @param array|null $options + * @param self::VALIDATION_MODE_*|null $mode The pattern used to validate the email address; pass null to use the default mode configured for the EmailValidator + * @param string[]|null $groups + */ + #[HasNamedArguments] public function __construct( ?array $options = null, ?string $message = null, @@ -72,16 +68,16 @@ public function __construct( throw new InvalidArgumentException('The "mode" parameter value is not valid.'); } + if (\is_array($options)) { + trigger_deprecation('symfony/validator', '7.3', 'Passing an array of options to configure the "%s" constraint is deprecated, use named arguments instead.', static::class); + } + parent::__construct($options, $groups, $payload); $this->message = $message ?? $this->message; $this->mode = $mode ?? $this->mode; $this->normalizer = $normalizer ?? $this->normalizer; - if (self::VALIDATION_MODE_LOOSE === $this->mode) { - trigger_deprecation('symfony/validator', '6.2', 'The "%s" mode is deprecated. It will be removed in 7.0 and the default mode will be changed to "%s".', self::VALIDATION_MODE_LOOSE, self::VALIDATION_MODE_HTML5); - } - if (self::VALIDATION_MODE_STRICT === $this->mode && !class_exists(StrictEmailValidator::class)) { throw new LogicException(\sprintf('The "egulias/email-validator" component is required to use the "%s" constraint in strict mode. Try running "composer require egulias/email-validator".', __CLASS__)); } diff --git a/Constraints/EmailValidator.php b/Constraints/EmailValidator.php index 2883d0d55..a70567cc7 100644 --- a/Constraints/EmailValidator.php +++ b/Constraints/EmailValidator.php @@ -28,33 +28,24 @@ class EmailValidator extends ConstraintValidator { private const PATTERN_HTML5_ALLOW_NO_TLD = '/^[a-zA-Z0-9.!#$%&\'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/D'; private const PATTERN_HTML5 = '/^[a-zA-Z0-9.!#$%&\'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+$/D'; - private const PATTERN_LOOSE = '/^.+\@\S+\.\S+$/D'; private const EMAIL_PATTERNS = [ - Email::VALIDATION_MODE_LOOSE => self::PATTERN_LOOSE, Email::VALIDATION_MODE_HTML5 => self::PATTERN_HTML5, Email::VALIDATION_MODE_HTML5_ALLOW_NO_TLD => self::PATTERN_HTML5_ALLOW_NO_TLD, ]; private string $defaultMode; - public function __construct(string $defaultMode = Email::VALIDATION_MODE_LOOSE) + public function __construct(string $defaultMode = Email::VALIDATION_MODE_HTML5) { if (!\in_array($defaultMode, Email::VALIDATION_MODES, true)) { throw new InvalidArgumentException('The "defaultMode" parameter value is not valid.'); } - if (Email::VALIDATION_MODE_LOOSE === $defaultMode) { - trigger_deprecation('symfony/validator', '6.2', 'The "%s" mode is deprecated. It will be removed in 7.0 and the default mode will be changed to "%s".', Email::VALIDATION_MODE_LOOSE, Email::VALIDATION_MODE_HTML5); - } - $this->defaultMode = $defaultMode; } - /** - * @return void - */ - public function validate(mixed $value, Constraint $constraint) + public function validate(mixed $value, Constraint $constraint): void { if (!$constraint instanceof Email) { throw new UnexpectedTypeException($constraint, Email::class); @@ -97,23 +88,17 @@ public function validate(mixed $value, Constraint $constraint) ->setParameter('{{ value }}', $this->formatValue($value)) ->setCode(Email::INVALID_FORMAT_ERROR) ->addViolation(); - - return; } elseif (!interface_exists(EmailValidation::class) && !$strictValidator->isValid($value, false, true)) { $this->context->buildViolation($constraint->message) ->setParameter('{{ value }}', $this->formatValue($value)) ->setCode(Email::INVALID_FORMAT_ERROR) ->addViolation(); - - return; } } elseif (!preg_match(self::EMAIL_PATTERNS[$constraint->mode], $value)) { $this->context->buildViolation($constraint->message) ->setParameter('{{ value }}', $this->formatValue($value)) ->setCode(Email::INVALID_FORMAT_ERROR) ->addViolation(); - - return; } } } diff --git a/Constraints/EnableAutoMapping.php b/Constraints/EnableAutoMapping.php index c0ece7547..873430677 100644 --- a/Constraints/EnableAutoMapping.php +++ b/Constraints/EnableAutoMapping.php @@ -11,29 +11,36 @@ namespace Symfony\Component\Validator\Constraints; +use Symfony\Component\Validator\Attribute\HasNamedArguments; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Exception\ConstraintDefinitionException; /** * Enables auto mapping. * - * Using the annotations on a property has higher precedence than using it on a class, + * Using the attribute on a property has higher precedence than using it on a class, * which has higher precedence than any configuration that might be defined outside the class. * - * @Annotation - * * @author Kévin Dunglas */ #[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::TARGET_CLASS)] class EnableAutoMapping extends Constraint { - public function __construct(?array $options = null) + /** + * @param array|null $options + */ + #[HasNamedArguments] + public function __construct(?array $options = null, mixed $payload = null) { + if (\is_array($options)) { + trigger_deprecation('symfony/validator', '7.3', 'Passing an array of options to configure the "%s" constraint is deprecated, use named arguments instead.', static::class); + } + if (\is_array($options) && \array_key_exists('groups', $options)) { throw new ConstraintDefinitionException(\sprintf('The option "groups" is not supported by the constraint "%s".', __CLASS__)); } - parent::__construct($options); + parent::__construct($options, null, $payload); } public function getTargets(): string|array diff --git a/Constraints/EqualTo.php b/Constraints/EqualTo.php index 03769ce8a..6ac5b22ca 100644 --- a/Constraints/EqualTo.php +++ b/Constraints/EqualTo.php @@ -12,8 +12,7 @@ namespace Symfony\Component\Validator\Constraints; /** - * @Annotation - * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) + * Validates that a value is equal to another value. * * @author Daniel Holmes * @author Bernhard Schussek @@ -27,10 +26,5 @@ class EqualTo extends AbstractComparison self::NOT_EQUAL_ERROR => 'NOT_EQUAL_ERROR', ]; - /** - * @deprecated since Symfony 6.1, use const ERROR_NAMES instead - */ - protected static $errorNames = self::ERROR_NAMES; - - public $message = 'This value should be equal to {{ compared_value }}.'; + public string $message = 'This value should be equal to {{ compared_value }}.'; } diff --git a/Constraints/Existence.php b/Constraints/Existence.php index a0d6ebd60..72bc1da61 100644 --- a/Constraints/Existence.php +++ b/Constraints/Existence.php @@ -11,12 +11,14 @@ namespace Symfony\Component\Validator\Constraints; +use Symfony\Component\Validator\Constraint; + /** * @author Bernhard Schussek */ abstract class Existence extends Composite { - public $constraints = []; + public array|Constraint $constraints = []; public function getDefaultOption(): ?string { diff --git a/Constraints/Expression.php b/Constraints/Expression.php index e784ce9e8..f40577d7b 100644 --- a/Constraints/Expression.php +++ b/Constraints/Expression.php @@ -13,12 +13,14 @@ use Symfony\Component\ExpressionLanguage\Expression as ExpressionObject; use Symfony\Component\ExpressionLanguage\ExpressionLanguage; +use Symfony\Component\Validator\Attribute\HasNamedArguments; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Exception\LogicException; /** - * @Annotation - * @Target({"CLASS", "PROPERTY", "METHOD", "ANNOTATION"}) + * Validates a value using an expression from the Expression Language component. + * + * @see https://symfony.com/doc/current/components/expression_language.html * * @author Fabien Potencier * @author Bernhard Schussek @@ -32,23 +34,26 @@ class Expression extends Constraint self::EXPRESSION_FAILED_ERROR => 'EXPRESSION_FAILED_ERROR', ]; - /** - * @deprecated since Symfony 6.1, use const ERROR_NAMES instead - */ - protected static $errorNames = self::ERROR_NAMES; - - public $message = 'This value is not valid.'; - public $expression; - public $values = []; + public string $message = 'This value is not valid.'; + public string|ExpressionObject|null $expression = null; + public array $values = []; public bool $negate = true; + /** + * @param string|ExpressionObject|array|null $expression The expression to evaluate + * @param array|null $values The values of the custom variables used in the expression (defaults to an empty array) + * @param string[]|null $groups + * @param array|null $options + * @param bool|null $negate When set to true, if the expression returns true, the validation will pass (defaults to true) + */ + #[HasNamedArguments] public function __construct( string|ExpressionObject|array|null $expression, ?string $message = null, ?array $values = null, ?array $groups = null, mixed $payload = null, - array $options = [], + ?array $options = null, ?bool $negate = null, ) { if (!class_exists(ExpressionLanguage::class)) { @@ -56,9 +61,19 @@ public function __construct( } if (\is_array($expression)) { - $options = array_merge($expression, $options); - } elseif (null !== $expression) { - $options['value'] = $expression; + trigger_deprecation('symfony/validator', '7.3', 'Passing an array of options to configure the "%s" constraint is deprecated, use named arguments instead.', static::class); + + $options = array_merge($expression, $options ?? []); + } else { + if (\is_array($options)) { + trigger_deprecation('symfony/validator', '7.3', 'Passing an array of options to configure the "%s" constraint is deprecated, use named arguments instead.', static::class); + } else { + $options = []; + } + + if (null !== $expression) { + $options['value'] = $expression; + } } parent::__construct($options, $groups, $payload); diff --git a/Constraints/ExpressionLanguageSyntax.php b/Constraints/ExpressionLanguageSyntax.php deleted file mode 100644 index 4f9bbe256..000000000 --- a/Constraints/ExpressionLanguageSyntax.php +++ /dev/null @@ -1,57 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Validator\Constraints; - -use Symfony\Component\Validator\Constraint; - -trigger_deprecation('symfony/validator', '6.1', 'The "%s" constraint is deprecated since symfony 6.1, use "ExpressionSyntax" instead.', ExpressionLanguageSyntax::class); - -/** - * @Annotation - * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) - * - * @author Andrey Sevastianov - * - * @deprecated since symfony 6.1, use ExpressionSyntax instead - */ -#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] -class ExpressionLanguageSyntax extends Constraint -{ - public const EXPRESSION_LANGUAGE_SYNTAX_ERROR = '1766a3f3-ff03-40eb-b053-ab7aa23d988a'; - - protected const ERROR_NAMES = [ - self::EXPRESSION_LANGUAGE_SYNTAX_ERROR => 'EXPRESSION_LANGUAGE_SYNTAX_ERROR', - ]; - - /** - * @deprecated since Symfony 6.1, use const ERROR_NAMES instead - */ - protected static $errorNames = self::ERROR_NAMES; - - public $message = 'This value should be a valid expression.'; - public $service; - public $allowedVariables; - - public function __construct(?array $options = null, ?string $message = null, ?string $service = null, ?array $allowedVariables = null, ?array $groups = null, mixed $payload = null) - { - parent::__construct($options, $groups, $payload); - - $this->message = $message ?? $this->message; - $this->service = $service ?? $this->service; - $this->allowedVariables = $allowedVariables ?? $this->allowedVariables; - } - - public function validatedBy(): string - { - return $this->service ?? static::class.'Validator'; - } -} diff --git a/Constraints/ExpressionLanguageSyntaxValidator.php b/Constraints/ExpressionLanguageSyntaxValidator.php deleted file mode 100644 index 3290e1c05..000000000 --- a/Constraints/ExpressionLanguageSyntaxValidator.php +++ /dev/null @@ -1,63 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Validator\Constraints; - -use Symfony\Component\ExpressionLanguage\ExpressionLanguage; -use Symfony\Component\ExpressionLanguage\SyntaxError; -use Symfony\Component\Validator\Constraint; -use Symfony\Component\Validator\ConstraintValidator; -use Symfony\Component\Validator\Exception\UnexpectedTypeException; -use Symfony\Component\Validator\Exception\UnexpectedValueException; - -trigger_deprecation('symfony/validator', '6.1', 'The "%s" constraint is deprecated since symfony 6.1, use "ExpressionSyntaxValidator" instead.', ExpressionLanguageSyntaxValidator::class); - -/** - * @author Andrey Sevastianov - * - * @deprecated since symfony 6.1, use ExpressionSyntaxValidator instead - */ -class ExpressionLanguageSyntaxValidator extends ConstraintValidator -{ - private ?ExpressionLanguage $expressionLanguage; - - public function __construct(?ExpressionLanguage $expressionLanguage = null) - { - if (!class_exists(ExpressionLanguage::class)) { - throw new \LogicException(\sprintf('The "%s" class requires the "ExpressionLanguage" component. Try running "composer require symfony/expression-language".', self::class)); - } - - $this->expressionLanguage = $expressionLanguage; - } - - public function validate(mixed $expression, Constraint $constraint): void - { - if (!$constraint instanceof ExpressionLanguageSyntax) { - throw new UnexpectedTypeException($constraint, ExpressionLanguageSyntax::class); - } - - if (!\is_string($expression)) { - throw new UnexpectedValueException($expression, 'string'); - } - - $this->expressionLanguage ??= new ExpressionLanguage(); - - try { - $this->expressionLanguage->lint($expression, $constraint->allowedVariables); - } catch (SyntaxError $exception) { - $this->context->buildViolation($constraint->message) - ->setParameter('{{ syntax_error }}', $this->formatValue($exception->getMessage())) - ->setInvalidValue((string) $expression) - ->setCode(ExpressionLanguageSyntax::EXPRESSION_LANGUAGE_SYNTAX_ERROR) - ->addViolation(); - } - } -} diff --git a/Constraints/ExpressionSyntax.php b/Constraints/ExpressionSyntax.php index 2d2eb18a1..5a0a09de1 100644 --- a/Constraints/ExpressionSyntax.php +++ b/Constraints/ExpressionSyntax.php @@ -11,11 +11,11 @@ namespace Symfony\Component\Validator\Constraints; +use Symfony\Component\Validator\Attribute\HasNamedArguments; use Symfony\Component\Validator\Constraint; /** - * @Annotation - * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) + * Validates that a value is valid as an ExpressionLanguage expression. * * @author Andrey Sevastianov */ @@ -28,12 +28,23 @@ class ExpressionSyntax extends Constraint self::EXPRESSION_SYNTAX_ERROR => 'EXPRESSION_SYNTAX_ERROR', ]; - public $message = 'This value should be a valid expression.'; - public $service; - public $allowedVariables; - + public string $message = 'This value should be a valid expression.'; + public ?string $service = null; + public ?array $allowedVariables = null; + + /** + * @param array|null $options + * @param non-empty-string|null $service The service used to validate the constraint instead of the default one + * @param string[]|null $allowedVariables Restrict the available variables in the expression to these values (defaults to null that allows any variable) + * @param string[]|null $groups + */ + #[HasNamedArguments] public function __construct(?array $options = null, ?string $message = null, ?string $service = null, ?array $allowedVariables = null, ?array $groups = null, mixed $payload = null) { + if (\is_array($options)) { + trigger_deprecation('symfony/validator', '7.3', 'Passing an array of options to configure the "%s" constraint is deprecated, use named arguments instead.', static::class); + } + parent::__construct($options, $groups, $payload); $this->message = $message ?? $this->message; diff --git a/Constraints/ExpressionSyntaxValidator.php b/Constraints/ExpressionSyntaxValidator.php index 36db35c63..ff63652c4 100644 --- a/Constraints/ExpressionSyntaxValidator.php +++ b/Constraints/ExpressionSyntaxValidator.php @@ -23,11 +23,9 @@ */ class ExpressionSyntaxValidator extends ConstraintValidator { - private ?ExpressionLanguage $expressionLanguage; - - public function __construct(?ExpressionLanguage $expressionLanguage = null) - { - $this->expressionLanguage = $expressionLanguage; + public function __construct( + private ?ExpressionLanguage $expressionLanguage = null, + ) { } public function validate(mixed $expression, Constraint $constraint): void @@ -40,7 +38,7 @@ public function validate(mixed $expression, Constraint $constraint): void return; } - if (!\is_string($expression)) { + if (!\is_string($expression) && !$expression instanceof \Stringable) { throw new UnexpectedValueException($expression, 'string'); } diff --git a/Constraints/ExpressionValidator.php b/Constraints/ExpressionValidator.php index 9eddbc439..17ee72499 100644 --- a/Constraints/ExpressionValidator.php +++ b/Constraints/ExpressionValidator.php @@ -31,10 +31,7 @@ public function __construct(?ExpressionLanguage $expressionLanguage = null) } } - /** - * @return void - */ - public function validate(mixed $value, Constraint $constraint) + public function validate(mixed $value, Constraint $constraint): void { if (!$constraint instanceof Expression) { throw new UnexpectedTypeException($constraint, Expression::class); diff --git a/Constraints/File.php b/Constraints/File.php index 88712a7a2..7d93a2084 100644 --- a/Constraints/File.php +++ b/Constraints/File.php @@ -11,12 +11,17 @@ namespace Symfony\Component\Validator\Constraints; +use Symfony\Component\Validator\Attribute\HasNamedArguments; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Exception\ConstraintDefinitionException; +use Symfony\Component\Validator\Exception\InvalidArgumentException; /** - * @Annotation - * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) + * Validates that a value is a valid "file". + * + * A file can be one of the following: + * - A string (or object with a __toString() method) path to an existing file; + * - A valid {@see \Symfony\Component\HttpFoundation\File\File File} object (including objects of {@see \Symfony\Component\HttpFoundation\File\UploadedFile UploadedFile} class). * * @property int $maxSize * @@ -34,6 +39,17 @@ class File extends Constraint public const INVALID_MIME_TYPE_ERROR = '744f00bc-4389-4c74-92de-9a43cde55534'; public const INVALID_EXTENSION_ERROR = 'c8c7315c-6186-4719-8b71-5659e16bdcb7'; public const FILENAME_TOO_LONG = 'e5706483-91a8-49d8-9a59-5e81a3c634a8'; + public const FILENAME_INVALID_CHARACTERS = '04ee58e1-42b4-45c7-8423-8a4a145fedd9'; + + public const FILENAME_COUNT_BYTES = 'bytes'; + public const FILENAME_COUNT_CODEPOINTS = 'codepoints'; + public const FILENAME_COUNT_GRAPHEMES = 'graphemes'; + + private const FILENAME_VALID_COUNT_UNITS = [ + self::FILENAME_COUNT_BYTES, + self::FILENAME_COUNT_CODEPOINTS, + self::FILENAME_COUNT_GRAPHEMES, + ]; protected const ERROR_NAMES = [ self::NOT_FOUND_ERROR => 'NOT_FOUND_ERROR', @@ -43,39 +59,58 @@ class File extends Constraint self::INVALID_MIME_TYPE_ERROR => 'INVALID_MIME_TYPE_ERROR', self::INVALID_EXTENSION_ERROR => 'INVALID_EXTENSION_ERROR', self::FILENAME_TOO_LONG => 'FILENAME_TOO_LONG', + self::FILENAME_INVALID_CHARACTERS => 'FILENAME_INVALID_CHARACTERS', ]; - /** - * @deprecated since Symfony 6.1, use const ERROR_NAMES instead - */ - protected static $errorNames = self::ERROR_NAMES; - - public $binaryFormat; - public $mimeTypes = []; + public ?bool $binaryFormat = null; + public array|string $mimeTypes = []; public ?int $filenameMaxLength = null; - public array|string|null $extensions = []; - public $notFoundMessage = 'The file could not be found.'; - public $notReadableMessage = 'The file is not readable.'; - public $maxSizeMessage = 'The file is too large ({{ size }} {{ suffix }}). Allowed maximum size is {{ limit }} {{ suffix }}.'; - public $mimeTypesMessage = 'The mime type of the file is invalid ({{ type }}). Allowed mime types are {{ types }}.'; + public array|string $extensions = []; + public ?string $filenameCharset = null; + /** @var self::FILENAME_COUNT_* */ + public string $filenameCountUnit = self::FILENAME_COUNT_BYTES; + + public string $notFoundMessage = 'The file could not be found.'; + public string $notReadableMessage = 'The file is not readable.'; + public string $maxSizeMessage = 'The file is too large ({{ size }} {{ suffix }}). Allowed maximum size is {{ limit }} {{ suffix }}.'; + public string $mimeTypesMessage = 'The mime type of the file is invalid ({{ type }}). Allowed mime types are {{ types }}.'; public string $extensionsMessage = 'The extension of the file is invalid ({{ extension }}). Allowed extensions are {{ extensions }}.'; - public $disallowEmptyMessage = 'An empty file is not allowed.'; - public $filenameTooLongMessage = 'The filename is too long. It should have {{ filename_max_length }} character or less.|The filename is too long. It should have {{ filename_max_length }} characters or less.'; + public string $disallowEmptyMessage = 'An empty file is not allowed.'; + public string $filenameTooLongMessage = 'The filename is too long. It should have {{ filename_max_length }} character or less.|The filename is too long. It should have {{ filename_max_length }} characters or less.'; + public string $filenameCharsetMessage = 'This filename does not match the expected charset.'; - public $uploadIniSizeErrorMessage = 'The file is too large. Allowed maximum size is {{ limit }} {{ suffix }}.'; - public $uploadFormSizeErrorMessage = 'The file is too large.'; - public $uploadPartialErrorMessage = 'The file was only partially uploaded.'; - public $uploadNoFileErrorMessage = 'No file was uploaded.'; - public $uploadNoTmpDirErrorMessage = 'No temporary folder was configured in php.ini.'; - public $uploadCantWriteErrorMessage = 'Cannot write temporary file to disk.'; - public $uploadExtensionErrorMessage = 'A PHP extension caused the upload to fail.'; - public $uploadErrorMessage = 'The file could not be uploaded.'; + public string $uploadIniSizeErrorMessage = 'The file is too large. Allowed maximum size is {{ limit }} {{ suffix }}.'; + public string $uploadFormSizeErrorMessage = 'The file is too large.'; + public string $uploadPartialErrorMessage = 'The file was only partially uploaded.'; + public string $uploadNoFileErrorMessage = 'No file was uploaded.'; + public string $uploadNoTmpDirErrorMessage = 'No temporary folder was configured in php.ini.'; + public string $uploadCantWriteErrorMessage = 'Cannot write temporary file to disk.'; + public string $uploadExtensionErrorMessage = 'A PHP extension caused the upload to fail.'; + public string $uploadErrorMessage = 'The file could not be uploaded.'; - protected $maxSize; + protected int|string|null $maxSize = null; /** - * @param array|string $extensions + * @param array|null $options + * @param positive-int|string|null $maxSize The max size of the underlying file + * @param bool|null $binaryFormat Pass true to use binary-prefixed units (KiB, MiB, etc.) or false to use SI-prefixed units (kB, MB) in displayed messages. Pass null to guess the format from the maxSize option. (defaults to null) + * @param string[]|string|null $mimeTypes Acceptable media type(s). Prefer the extensions option that also enforce the file's extension consistency. + * @param positive-int|null $filenameMaxLength Maximum length of the file name + * @param string|null $disallowEmptyMessage Enable empty upload validation with this message in case of error + * @param string|null $uploadIniSizeErrorMessage Message if the file size exceeds the max size configured in php.ini + * @param string|null $uploadFormSizeErrorMessage Message if the file size exceeds the max size configured in the HTML input field + * @param string|null $uploadPartialErrorMessage Message if the file is only partially uploaded + * @param string|null $uploadNoTmpDirErrorMessage Message if there is no upload_tmp_dir in php.ini + * @param string|null $uploadCantWriteErrorMessage Message if the uploaded file can not be stored in the temporary directory + * @param string|null $uploadErrorMessage Message if an unknown error occurred on upload + * @param string[]|null $groups + * @param array|string|null $extensions A list of valid extensions to check. Related media types are also enforced ({@see https://symfony.com/doc/current/reference/constraints/File.html#extensions}) + * @param string|null $filenameCharset The charset to be used when computing filename length (defaults to null) + * @param self::FILENAME_COUNT_*|null $filenameCountUnit The character count unit used for checking the filename length (defaults to {@see File::FILENAME_COUNT_BYTES}) + * + * @see https://www.iana.org/assignments/media-types/media-types.xhtml Existing media types */ + #[HasNamedArguments] public function __construct( ?array $options = null, int|string|null $maxSize = null, @@ -99,16 +134,24 @@ public function __construct( ?string $uploadErrorMessage = null, ?array $groups = null, mixed $payload = null, - array|string|null $extensions = null, ?string $extensionsMessage = null, + ?string $filenameCharset = null, + ?string $filenameCountUnit = null, + ?string $filenameCharsetMessage = null, ) { + if (\is_array($options)) { + trigger_deprecation('symfony/validator', '7.3', 'Passing an array of options to configure the "%s" constraint is deprecated, use named arguments instead.', static::class); + } + parent::__construct($options, $groups, $payload); $this->maxSize = $maxSize ?? $this->maxSize; $this->binaryFormat = $binaryFormat ?? $this->binaryFormat; $this->mimeTypes = $mimeTypes ?? $this->mimeTypes; $this->filenameMaxLength = $filenameMaxLength ?? $this->filenameMaxLength; + $this->filenameCharset = $filenameCharset ?? $this->filenameCharset; + $this->filenameCountUnit = $filenameCountUnit ?? $this->filenameCountUnit; $this->extensions = $extensions ?? $this->extensions; $this->notFoundMessage = $notFoundMessage ?? $this->notFoundMessage; $this->notReadableMessage = $notReadableMessage ?? $this->notReadableMessage; @@ -117,6 +160,7 @@ public function __construct( $this->extensionsMessage = $extensionsMessage ?? $this->extensionsMessage; $this->disallowEmptyMessage = $disallowEmptyMessage ?? $this->disallowEmptyMessage; $this->filenameTooLongMessage = $filenameTooLongMessage ?? $this->filenameTooLongMessage; + $this->filenameCharsetMessage = $filenameCharsetMessage ?? $this->filenameCharsetMessage; $this->uploadIniSizeErrorMessage = $uploadIniSizeErrorMessage ?? $this->uploadIniSizeErrorMessage; $this->uploadFormSizeErrorMessage = $uploadFormSizeErrorMessage ?? $this->uploadFormSizeErrorMessage; $this->uploadPartialErrorMessage = $uploadPartialErrorMessage ?? $this->uploadPartialErrorMessage; @@ -129,12 +173,13 @@ public function __construct( if (null !== $this->maxSize) { $this->normalizeBinaryFormat($this->maxSize); } + + if (!\in_array($this->filenameCountUnit, self::FILENAME_VALID_COUNT_UNITS, true)) { + throw new InvalidArgumentException(\sprintf('The "filenameCountUnit" option must be one of the "%s::FILENAME_COUNT_*" constants ("%s" given).', __CLASS__, $this->filenameCountUnit)); + } } - /** - * @return void - */ - public function __set(string $option, mixed $value) + public function __set(string $option, mixed $value): void { if ('maxSize' === $option) { $this->normalizeBinaryFormat($value); diff --git a/Constraints/FileValidator.php b/Constraints/FileValidator.php index 6346ad098..2b8e33494 100644 --- a/Constraints/FileValidator.php +++ b/Constraints/FileValidator.php @@ -38,10 +38,7 @@ class FileValidator extends ConstraintValidator self::MIB_BYTES => 'MiB', ]; - /** - * @return void - */ - public function validate(mixed $value, Constraint $constraint) + public function validate(mixed $value, Constraint $constraint): void { if (!$constraint instanceof File) { throw new UnexpectedTypeException($constraint, File::class); @@ -140,10 +137,36 @@ public function validate(mixed $value, Constraint $constraint) return; } - $sizeInBytes = filesize($path); $basename = $value instanceof UploadedFile ? $value->getClientOriginalName() : basename($path); + $filenameCharset = $constraint->filenameCharset ?? (File::FILENAME_COUNT_BYTES !== $constraint->filenameCountUnit ? 'UTF-8' : null); + + if ($invalidFilenameCharset = null !== $filenameCharset) { + try { + $invalidFilenameCharset = !@mb_check_encoding($basename, $constraint->filenameCharset); + } catch (\ValueError $e) { + if (!str_starts_with($e->getMessage(), 'mb_check_encoding(): Argument #2 ($encoding) must be a valid encoding')) { + throw $e; + } + } + } + + $filenameLength = $invalidFilenameCharset ? 0 : match ($constraint->filenameCountUnit) { + File::FILENAME_COUNT_BYTES => \strlen($basename), + File::FILENAME_COUNT_CODEPOINTS => mb_strlen($basename, $filenameCharset), + File::FILENAME_COUNT_GRAPHEMES => grapheme_strlen($basename), + }; + + if ($invalidFilenameCharset || false === ($filenameLength ?? false)) { + $this->context->buildViolation($constraint->filenameCharsetMessage) + ->setParameter('{{ name }}', $this->formatValue($basename)) + ->setParameter('{{ charset }}', $filenameCharset) + ->setCode(File::FILENAME_INVALID_CHARACTERS) + ->addViolation(); + + return; + } - if ($constraint->filenameMaxLength && $constraint->filenameMaxLength < $filenameLength = \strlen($basename)) { + if ($constraint->filenameMaxLength && $constraint->filenameMaxLength < $filenameLength) { $this->context->buildViolation($constraint->filenameTooLongMessage) ->setParameter('{{ filename_max_length }}', $this->formatValue($constraint->filenameMaxLength)) ->setCode(File::FILENAME_TOO_LONG) @@ -153,7 +176,7 @@ public function validate(mixed $value, Constraint $constraint) return; } - if (0 === $sizeInBytes) { + if (!$sizeInBytes = filesize($path)) { $this->context->buildViolation($constraint->disallowEmptyMessage) ->setParameter('{{ file }}', $this->formatValue($path)) ->setParameter('{{ name }}', $this->formatValue($basename)) diff --git a/Constraints/GreaterThan.php b/Constraints/GreaterThan.php index ce56f1ac1..7ce6cf04b 100644 --- a/Constraints/GreaterThan.php +++ b/Constraints/GreaterThan.php @@ -12,8 +12,7 @@ namespace Symfony\Component\Validator\Constraints; /** - * @Annotation - * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) + * Validates that a value is greater than another value. * * @author Daniel Holmes * @author Bernhard Schussek @@ -27,10 +26,5 @@ class GreaterThan extends AbstractComparison self::TOO_LOW_ERROR => 'TOO_LOW_ERROR', ]; - /** - * @deprecated since Symfony 6.1, use const ERROR_NAMES instead - */ - protected static $errorNames = self::ERROR_NAMES; - - public $message = 'This value should be greater than {{ compared_value }}.'; + public string $message = 'This value should be greater than {{ compared_value }}.'; } diff --git a/Constraints/GreaterThanOrEqual.php b/Constraints/GreaterThanOrEqual.php index c962f7964..103ca9f53 100644 --- a/Constraints/GreaterThanOrEqual.php +++ b/Constraints/GreaterThanOrEqual.php @@ -12,8 +12,7 @@ namespace Symfony\Component\Validator\Constraints; /** - * @Annotation - * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) + * Validates that a value is greater than or equal to another value. * * @author Daniel Holmes * @author Bernhard Schussek @@ -27,10 +26,5 @@ class GreaterThanOrEqual extends AbstractComparison self::TOO_LOW_ERROR => 'TOO_LOW_ERROR', ]; - /** - * @deprecated since Symfony 6.1, use const ERROR_NAMES instead - */ - protected static $errorNames = self::ERROR_NAMES; - - public $message = 'This value should be greater than or equal to {{ compared_value }}.'; + public string $message = 'This value should be greater than or equal to {{ compared_value }}.'; } diff --git a/Constraints/GroupSequence.php b/Constraints/GroupSequence.php index 8c91b4de2..e3e4f47f9 100644 --- a/Constraints/GroupSequence.php +++ b/Constraints/GroupSequence.php @@ -11,6 +11,8 @@ namespace Symfony\Component\Validator\Constraints; +use Symfony\Component\Validator\Attribute\HasNamedArguments; + /** * A sequence of validation groups. * @@ -44,9 +46,6 @@ * * $validator->validate($address, null, "Address") * - * @Annotation - * @Target({"CLASS", "ANNOTATION"}) - * * @author Bernhard Schussek */ #[\Attribute(\Attribute::TARGET_CLASS)] @@ -57,7 +56,7 @@ class GroupSequence * * @var array */ - public $groups; + public array $groups; /** * The group in which cascaded objects are validated when validating @@ -70,16 +69,15 @@ class GroupSequence * "Default" group. When validating that class in the "Default" group, the * group sequence is used instead, but still the "Default" group should be * cascaded to other objects. - * - * @var string|GroupSequence */ - public $cascadedGroup; + public string|GroupSequence $cascadedGroup; /** * Creates a new group sequence. * * @param array $groups The groups in the sequence */ + #[HasNamedArguments] public function __construct(array $groups) { $this->groups = $groups['value'] ?? $groups; diff --git a/Constraints/GroupSequenceProvider.php b/Constraints/GroupSequenceProvider.php index 842bfd468..b72dd0c4c 100644 --- a/Constraints/GroupSequenceProvider.php +++ b/Constraints/GroupSequenceProvider.php @@ -11,16 +11,11 @@ namespace Symfony\Component\Validator\Constraints; -use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor; use Symfony\Component\Validator\Attribute\HasNamedArguments; /** * Attribute to define a group sequence provider. * - * @Annotation - * @NamedArgumentConstructor - * @Target({"CLASS", "ANNOTATION"}) - * * @author Bernhard Schussek */ #[\Attribute(\Attribute::TARGET_CLASS)] diff --git a/Constraints/Hostname.php b/Constraints/Hostname.php index 79e1c87c2..ca9bc3a32 100644 --- a/Constraints/Hostname.php +++ b/Constraints/Hostname.php @@ -11,11 +11,11 @@ namespace Symfony\Component\Validator\Constraints; +use Symfony\Component\Validator\Attribute\HasNamedArguments; use Symfony\Component\Validator\Constraint; /** - * @Annotation - * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) + * Validates that a value is a valid host name. * * @author Dmitrii Poddubnyi */ @@ -28,14 +28,15 @@ class Hostname extends Constraint self::INVALID_HOSTNAME_ERROR => 'INVALID_HOSTNAME_ERROR', ]; + public string $message = 'This value is not a valid hostname.'; + public bool $requireTld = true; + /** - * @deprecated since Symfony 6.1, use const ERROR_NAMES instead + * @param array|null $options + * @param bool|null $requireTld Whether to require the hostname to include its top-level domain (defaults to true) + * @param string[]|null $groups */ - protected static $errorNames = self::ERROR_NAMES; - - public $message = 'This value is not a valid hostname.'; - public $requireTld = true; - + #[HasNamedArguments] public function __construct( ?array $options = null, ?string $message = null, @@ -43,6 +44,10 @@ public function __construct( ?array $groups = null, mixed $payload = null, ) { + if (\is_array($options)) { + trigger_deprecation('symfony/validator', '7.3', 'Passing an array of options to configure the "%s" constraint is deprecated, use named arguments instead.', static::class); + } + parent::__construct($options, $groups, $payload); $this->message = $message ?? $this->message; diff --git a/Constraints/HostnameValidator.php b/Constraints/HostnameValidator.php index 8b0fa60e2..605a74f33 100644 --- a/Constraints/HostnameValidator.php +++ b/Constraints/HostnameValidator.php @@ -31,10 +31,7 @@ class HostnameValidator extends ConstraintValidator 'test', ]; - /** - * @return void - */ - public function validate(mixed $value, Constraint $constraint) + public function validate(mixed $value, Constraint $constraint): void { if (!$constraint instanceof Hostname) { throw new UnexpectedTypeException($constraint, Hostname::class); diff --git a/Constraints/Iban.php b/Constraints/Iban.php index 16f557f12..459fb5fb0 100644 --- a/Constraints/Iban.php +++ b/Constraints/Iban.php @@ -11,11 +11,13 @@ namespace Symfony\Component\Validator\Constraints; +use Symfony\Component\Validator\Attribute\HasNamedArguments; use Symfony\Component\Validator\Constraint; /** - * @Annotation - * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) + * Validates that a value is a valid bank account number according to the IBAN format. + * + * @see https://en.wikipedia.org/wiki/International_Bank_Account_Number * * @author Manuel Reinhard * @author Michael Schummel @@ -38,15 +40,19 @@ class Iban extends Constraint self::NOT_SUPPORTED_COUNTRY_CODE_ERROR => 'NOT_SUPPORTED_COUNTRY_CODE_ERROR', ]; + public string $message = 'This is not a valid International Bank Account Number (IBAN).'; + /** - * @deprecated since Symfony 6.1, use const ERROR_NAMES instead + * @param array|null $options + * @param string[]|null $groups */ - protected static $errorNames = self::ERROR_NAMES; - - public $message = 'This is not a valid International Bank Account Number (IBAN).'; - + #[HasNamedArguments] public function __construct(?array $options = null, ?string $message = null, ?array $groups = null, mixed $payload = null) { + if (\is_array($options)) { + trigger_deprecation('symfony/validator', '7.3', 'Passing an array of options to configure the "%s" constraint is deprecated, use named arguments instead.', static::class); + } + parent::__construct($options, $groups, $payload); $this->message = $message ?? $this->message; diff --git a/Constraints/IbanValidator.php b/Constraints/IbanValidator.php index 13d91315b..866dcec1f 100644 --- a/Constraints/IbanValidator.php +++ b/Constraints/IbanValidator.php @@ -167,10 +167,7 @@ class IbanValidator extends ConstraintValidator 'YT' => 'FR\d{2}\d{5}\d{5}[\dA-Z]{11}\d{2}', // France ]; - /** - * @return void - */ - public function validate(mixed $value, Constraint $constraint) + public function validate(mixed $value, Constraint $constraint): void { if (!$constraint instanceof Iban) { throw new UnexpectedTypeException($constraint, Iban::class); @@ -186,8 +183,8 @@ public function validate(mixed $value, Constraint $constraint) $value = (string) $value; - // Remove spaces and convert to uppercase - $canonicalized = str_replace(' ', '', strtoupper($value)); + // Remove spaces (regular, non-breaking, and narrow non-breaking) and convert to uppercase + $canonicalized = str_replace([' ', "\xc2\xa0", "\xe2\x80\xaf"], '', strtoupper($value)); // The IBAN must contain only digits and characters... if (!ctype_alnum($canonicalized)) { diff --git a/Constraints/IdenticalTo.php b/Constraints/IdenticalTo.php index 50ec5e129..e06cb5356 100644 --- a/Constraints/IdenticalTo.php +++ b/Constraints/IdenticalTo.php @@ -12,8 +12,7 @@ namespace Symfony\Component\Validator\Constraints; /** - * @Annotation - * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) + * Validates that a value is identical to another value. * * @author Daniel Holmes * @author Bernhard Schussek @@ -27,10 +26,5 @@ class IdenticalTo extends AbstractComparison self::NOT_IDENTICAL_ERROR => 'NOT_IDENTICAL_ERROR', ]; - /** - * @deprecated since Symfony 6.1, use const ERROR_NAMES instead - */ - protected static $errorNames = self::ERROR_NAMES; - - public $message = 'This value should be identical to {{ compared_value_type }} {{ compared_value }}.'; + public string $message = 'This value should be identical to {{ compared_value_type }} {{ compared_value }}.'; } diff --git a/Constraints/Image.php b/Constraints/Image.php index 8fafa5044..d9b7c8822 100644 --- a/Constraints/Image.php +++ b/Constraints/Image.php @@ -11,9 +11,10 @@ namespace Symfony\Component\Validator\Constraints; +use Symfony\Component\Validator\Attribute\HasNamedArguments; + /** - * @Annotation - * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) + * Validates that a file (or a path to a file) is a valid image. * * @author Benjamin Dulau * @author Bernhard Schussek @@ -59,41 +60,67 @@ class Image extends File self::CORRUPTED_IMAGE_ERROR => 'CORRUPTED_IMAGE_ERROR', ]; - /** - * @deprecated since Symfony 6.1, use const ERROR_NAMES instead - */ - protected static $errorNames = self::ERROR_NAMES; - - public $mimeTypes; - public $minWidth; - public $maxWidth; - public $maxHeight; - public $minHeight; - public $maxRatio; - public $minRatio; - public $minPixels; - public $maxPixels; - public $allowSquare = true; - public $allowLandscape = true; - public $allowPortrait = true; - public $detectCorrupted = false; + public array|string $mimeTypes = []; + public ?int $minWidth = null; + public ?int $maxWidth = null; + public ?int $maxHeight = null; + public ?int $minHeight = null; + public int|float|null $maxRatio = null; + public int|float|null $minRatio = null; + public int|float|null $minPixels = null; + public int|float|null $maxPixels = null; + public bool $allowSquare = true; + public bool $allowLandscape = true; + public bool $allowPortrait = true; + public bool $detectCorrupted = false; // The constant for a wrong MIME type is taken from the parent class. - public $mimeTypesMessage = 'This file is not a valid image.'; - public $sizeNotDetectedMessage = 'The size of the image could not be detected.'; - public $maxWidthMessage = 'The image width is too big ({{ width }}px). Allowed maximum width is {{ max_width }}px.'; - public $minWidthMessage = 'The image width is too small ({{ width }}px). Minimum width expected is {{ min_width }}px.'; - public $maxHeightMessage = 'The image height is too big ({{ height }}px). Allowed maximum height is {{ max_height }}px.'; - public $minHeightMessage = 'The image height is too small ({{ height }}px). Minimum height expected is {{ min_height }}px.'; - public $minPixelsMessage = 'The image has too few pixels ({{ pixels }} pixels). Minimum amount expected is {{ min_pixels }} pixels.'; - public $maxPixelsMessage = 'The image has too many pixels ({{ pixels }} pixels). Maximum amount expected is {{ max_pixels }} pixels.'; - public $maxRatioMessage = 'The image ratio is too big ({{ ratio }}). Allowed maximum ratio is {{ max_ratio }}.'; - public $minRatioMessage = 'The image ratio is too small ({{ ratio }}). Minimum ratio expected is {{ min_ratio }}.'; - public $allowSquareMessage = 'The image is square ({{ width }}x{{ height }}px). Square images are not allowed.'; - public $allowLandscapeMessage = 'The image is landscape oriented ({{ width }}x{{ height }}px). Landscape oriented images are not allowed.'; - public $allowPortraitMessage = 'The image is portrait oriented ({{ width }}x{{ height }}px). Portrait oriented images are not allowed.'; - public $corruptedMessage = 'The image file is corrupted.'; + public string $mimeTypesMessage = 'This file is not a valid image.'; + public string $sizeNotDetectedMessage = 'The size of the image could not be detected.'; + public string $maxWidthMessage = 'The image width is too big ({{ width }}px). Allowed maximum width is {{ max_width }}px.'; + public string $minWidthMessage = 'The image width is too small ({{ width }}px). Minimum width expected is {{ min_width }}px.'; + public string $maxHeightMessage = 'The image height is too big ({{ height }}px). Allowed maximum height is {{ max_height }}px.'; + public string $minHeightMessage = 'The image height is too small ({{ height }}px). Minimum height expected is {{ min_height }}px.'; + public string $minPixelsMessage = 'The image has too few pixels ({{ pixels }} pixels). Minimum amount expected is {{ min_pixels }} pixels.'; + public string $maxPixelsMessage = 'The image has too many pixels ({{ pixels }} pixels). Maximum amount expected is {{ max_pixels }} pixels.'; + public string $maxRatioMessage = 'The image ratio is too big ({{ ratio }}). Allowed maximum ratio is {{ max_ratio }}.'; + public string $minRatioMessage = 'The image ratio is too small ({{ ratio }}). Minimum ratio expected is {{ min_ratio }}.'; + public string $allowSquareMessage = 'The image is square ({{ width }}x{{ height }}px). Square images are not allowed.'; + public string $allowLandscapeMessage = 'The image is landscape oriented ({{ width }}x{{ height }}px). Landscape oriented images are not allowed.'; + public string $allowPortraitMessage = 'The image is portrait oriented ({{ width }}x{{ height }}px). Portrait oriented images are not allowed.'; + public string $corruptedMessage = 'The image file is corrupted.'; + /** + * @param array|null $options + * @param positive-int|string|null $maxSize The max size of the underlying file + * @param bool|null $binaryFormat Pass true to use binary-prefixed units (KiB, MiB, etc.) or false to use SI-prefixed units (kB, MB) in displayed messages. Pass null to guess the format from the maxSize option. (defaults to null) + * @param non-empty-string[]|null $mimeTypes Acceptable media types + * @param positive-int|null $filenameMaxLength Maximum length of the file name + * @param string|null $disallowEmptyMessage Enable empty upload validation with this message in case of error + * @param string|null $uploadIniSizeErrorMessage Message if the file size exceeds the max size configured in php.ini + * @param string|null $uploadFormSizeErrorMessage Message if the file size exceeds the max size configured in the HTML input field + * @param string|null $uploadPartialErrorMessage Message if the file is only partially uploaded + * @param string|null $uploadNoTmpDirErrorMessage Message if there is no upload_tmp_dir in php.ini + * @param string|null $uploadCantWriteErrorMessage Message if the uploaded file can not be stored in the temporary directory + * @param string|null $uploadErrorMessage Message if an unknown error occurred on upload + * @param string[]|null $groups + * @param int<0, int>|null $minWidth Minimum image width + * @param positive-int|null $maxWidth Maximum image width + * @param positive-int|null $maxHeight Maximum image height + * @param int<0, int>|null $minHeight Minimum image weight + * @param positive-int|float|null $maxRatio Maximum image ratio + * @param int<0, max>|float|null $minRatio Minimum image ratio + * @param int<0, max>|float|null $minPixels Minimum amount of pixels + * @param positive-int|float|null $maxPixels Maximum amount of pixels + * @param bool|null $allowSquare Whether to allow a square image (defaults to true) + * @param bool|null $allowLandscape Whether to allow a landscape image (defaults to true) + * @param bool|null $allowPortrait Whether to allow a portrait image (defaults to true) + * @param bool|null $detectCorrupted Whether to validate the image is not corrupted (defaults to false) + * @param string|null $sizeNotDetectedMessage Message if the system can not determine image size and there is a size constraint to validate + * + * @see https://www.iana.org/assignments/media-types/media-types.xhtml Existing media types + */ + #[HasNamedArguments] public function __construct( ?array $options = null, int|string|null $maxSize = null, @@ -143,6 +170,9 @@ public function __construct( mixed $payload = null, array|string|null $extensions = null, ?string $extensionsMessage = null, + ?string $filenameCharset = null, + ?string $filenameCountUnit = null, + ?string $filenameCharsetMessage = null, ) { parent::__construct( $options, @@ -168,6 +198,9 @@ public function __construct( $payload, $extensions, $extensionsMessage, + $filenameCharset, + $filenameCountUnit, + $filenameCharsetMessage, ); $this->minWidth = $minWidth ?? $this->minWidth; @@ -196,7 +229,7 @@ public function __construct( $this->allowPortraitMessage = $allowPortraitMessage ?? $this->allowPortraitMessage; $this->corruptedMessage = $corruptedMessage ?? $this->corruptedMessage; - if (null === $this->mimeTypes && [] === $this->extensions) { + if ([] === $this->mimeTypes && [] === $this->extensions) { $this->mimeTypes = 'image/*'; } diff --git a/Constraints/ImageValidator.php b/Constraints/ImageValidator.php index 0c2fd318a..9075a5b23 100644 --- a/Constraints/ImageValidator.php +++ b/Constraints/ImageValidator.php @@ -11,6 +11,8 @@ namespace Symfony\Component\Validator\Constraints; +use Symfony\Component\HttpFoundation\File\File; +use Symfony\Component\Mime\MimeTypes; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Exception\ConstraintDefinitionException; use Symfony\Component\Validator\Exception\LogicException; @@ -25,10 +27,7 @@ */ class ImageValidator extends FileValidator { - /** - * @return void - */ - public function validate(mixed $value, Constraint $constraint) + public function validate(mixed $value, Constraint $constraint): void { if (!$constraint instanceof Image) { throw new UnexpectedTypeException($constraint, Image::class); @@ -53,9 +52,15 @@ public function validate(mixed $value, Constraint $constraint) return; } - $size = @getimagesize($value); + $isSvg = $this->isSvg($value); + + if ($isSvg) { + $size = $this->getSvgSize($value); + } else { + $size = @getimagesize($value); + } - if (empty($size) || (0 === $size[0]) || (0 === $size[1])) { + if (!$size || (0 === $size[0]) || (0 === $size[1])) { $this->context->buildViolation($constraint->sizeNotDetectedMessage) ->setCode(Image::SIZE_NOT_DETECTED_ERROR) ->addViolation(); @@ -66,7 +71,7 @@ public function validate(mixed $value, Constraint $constraint) $width = $size[0]; $height = $size[1]; - if ($constraint->minWidth) { + if (!$isSvg && $constraint->minWidth) { if (!ctype_digit((string) $constraint->minWidth)) { throw new ConstraintDefinitionException(\sprintf('"%s" is not a valid minimum width.', $constraint->minWidth)); } @@ -82,7 +87,7 @@ public function validate(mixed $value, Constraint $constraint) } } - if ($constraint->maxWidth) { + if (!$isSvg && $constraint->maxWidth) { if (!ctype_digit((string) $constraint->maxWidth)) { throw new ConstraintDefinitionException(\sprintf('"%s" is not a valid maximum width.', $constraint->maxWidth)); } @@ -98,7 +103,7 @@ public function validate(mixed $value, Constraint $constraint) } } - if ($constraint->minHeight) { + if (!$isSvg && $constraint->minHeight) { if (!ctype_digit((string) $constraint->minHeight)) { throw new ConstraintDefinitionException(\sprintf('"%s" is not a valid minimum height.', $constraint->minHeight)); } @@ -114,7 +119,7 @@ public function validate(mixed $value, Constraint $constraint) } } - if ($constraint->maxHeight) { + if (!$isSvg && $constraint->maxHeight) { if (!ctype_digit((string) $constraint->maxHeight)) { throw new ConstraintDefinitionException(\sprintf('"%s" is not a valid maximum height.', $constraint->maxHeight)); } @@ -130,7 +135,7 @@ public function validate(mixed $value, Constraint $constraint) $pixels = $width * $height; - if (null !== $constraint->minPixels) { + if (!$isSvg && null !== $constraint->minPixels) { if (!ctype_digit((string) $constraint->minPixels)) { throw new ConstraintDefinitionException(\sprintf('"%s" is not a valid minimum amount of pixels.', $constraint->minPixels)); } @@ -146,7 +151,7 @@ public function validate(mixed $value, Constraint $constraint) } } - if (null !== $constraint->maxPixels) { + if (!$isSvg && null !== $constraint->maxPixels) { if (!ctype_digit((string) $constraint->maxPixels)) { throw new ConstraintDefinitionException(\sprintf('"%s" is not a valid maximum amount of pixels.', $constraint->maxPixels)); } @@ -232,4 +237,52 @@ public function validate(mixed $value, Constraint $constraint) } } } + + private function isSvg(mixed $value): bool + { + if ($value instanceof File) { + $mime = $value->getMimeType(); + } elseif (class_exists(MimeTypes::class)) { + $mime = MimeTypes::getDefault()->guessMimeType($value); + } elseif (!class_exists(File::class)) { + return false; + } else { + $mime = (new File($value))->getMimeType(); + } + + return 'image/svg+xml' === $mime; + } + + /** + * @return array{int, int}|null index 0 and 1 contains respectively the width and the height of the image, null if size can't be found + */ + private function getSvgSize(mixed $value): ?array + { + if ($value instanceof File) { + $content = $value->getContent(); + } elseif (!class_exists(File::class)) { + return null; + } else { + $content = (new File($value))->getContent(); + } + + if (1 === preg_match('/]+width="(?[0-9]+)"[^<>]*>/', $content, $widthMatches)) { + $width = (int) $widthMatches['width']; + } + + if (1 === preg_match('/]+height="(?[0-9]+)"[^<>]*>/', $content, $heightMatches)) { + $height = (int) $heightMatches['height']; + } + + if (1 === preg_match('/]+viewBox="-?[0-9]+ -?[0-9]+ (?-?[0-9]+) (?-?[0-9]+)"[^<>]*>/', $content, $viewBoxMatches)) { + $width ??= (int) $viewBoxMatches['width']; + $height ??= (int) $viewBoxMatches['height']; + } + + if (isset($width) && isset($height)) { + return [$width, $height]; + } + + return null; + } } diff --git a/Constraints/Ip.php b/Constraints/Ip.php index 0ff522107..4db552a76 100644 --- a/Constraints/Ip.php +++ b/Constraints/Ip.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Validator\Constraints; +use Symfony\Component\Validator\Attribute\HasNamedArguments; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Exception\ConstraintDefinitionException; use Symfony\Component\Validator\Exception\InvalidArgumentException; @@ -18,11 +19,9 @@ /** * Validates that a value is a valid IP address. * - * @Annotation - * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) - * * @author Bernhard Schussek * @author Joseph Bielawski + * @author Ninos Ego */ #[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] class Ip extends Constraint @@ -31,21 +30,42 @@ class Ip extends Constraint public const V6 = '6'; public const ALL = 'all'; + // adds inverse FILTER_FLAG_NO_RES_RANGE and FILTER_FLAG_NO_PRIV_RANGE flags (skip both) + public const V4_NO_PUBLIC = '4_no_public'; + public const V6_NO_PUBLIC = '6_no_public'; + public const ALL_NO_PUBLIC = 'all_no_public'; + // adds FILTER_FLAG_NO_PRIV_RANGE flag (skip private ranges) - public const V4_NO_PRIV = '4_no_priv'; - public const V6_NO_PRIV = '6_no_priv'; - public const ALL_NO_PRIV = 'all_no_priv'; + public const V4_NO_PRIVATE = '4_no_priv'; + public const V4_NO_PRIV = self::V4_NO_PRIVATE; // BC: Alias + public const V6_NO_PRIVATE = '6_no_priv'; + public const V6_NO_PRIV = self::V6_NO_PRIVATE; // BC: Alias + public const ALL_NO_PRIVATE = 'all_no_priv'; + public const ALL_NO_PRIV = self::ALL_NO_PRIVATE; // BC: Alias // adds FILTER_FLAG_NO_RES_RANGE flag (skip reserved ranges) - public const V4_NO_RES = '4_no_res'; - public const V6_NO_RES = '6_no_res'; - public const ALL_NO_RES = 'all_no_res'; + public const V4_NO_RESERVED = '4_no_res'; + public const V4_NO_RES = self::V4_NO_RESERVED; // BC: Alias + public const V6_NO_RESERVED = '6_no_res'; + public const V6_NO_RES = self::V6_NO_RESERVED; // BC: Alias + public const ALL_NO_RESERVED = 'all_no_res'; + public const ALL_NO_RES = self::ALL_NO_RESERVED; // BC: Alias // adds FILTER_FLAG_NO_PRIV_RANGE and FILTER_FLAG_NO_RES_RANGE flags (skip both) public const V4_ONLY_PUBLIC = '4_public'; public const V6_ONLY_PUBLIC = '6_public'; public const ALL_ONLY_PUBLIC = 'all_public'; + // adds inverse FILTER_FLAG_NO_PRIV_RANGE + public const V4_ONLY_PRIVATE = '4_private'; + public const V6_ONLY_PRIVATE = '6_private'; + public const ALL_ONLY_PRIVATE = 'all_private'; + + // adds inverse FILTER_FLAG_NO_RES_RANGE + public const V4_ONLY_RESERVED = '4_reserved'; + public const V6_ONLY_RESERVED = '6_reserved'; + public const ALL_ONLY_RESERVED = 'all_reserved'; + public const INVALID_IP_ERROR = 'b1b427ae-9f6f-41b0-aa9b-84511fbb3c5b'; protected const VERSIONS = [ @@ -53,40 +73,46 @@ class Ip extends Constraint self::V6, self::ALL, - self::V4_NO_PRIV, - self::V6_NO_PRIV, - self::ALL_NO_PRIV, + self::V4_NO_PUBLIC, + self::V6_NO_PUBLIC, + self::ALL_NO_PUBLIC, + + self::V4_NO_PRIVATE, + self::V6_NO_PRIVATE, + self::ALL_NO_PRIVATE, - self::V4_NO_RES, - self::V6_NO_RES, - self::ALL_NO_RES, + self::V4_NO_RESERVED, + self::V6_NO_RESERVED, + self::ALL_NO_RESERVED, self::V4_ONLY_PUBLIC, self::V6_ONLY_PUBLIC, self::ALL_ONLY_PUBLIC, + + self::V4_ONLY_PRIVATE, + self::V6_ONLY_PRIVATE, + self::ALL_ONLY_PRIVATE, + + self::V4_ONLY_RESERVED, + self::V6_ONLY_RESERVED, + self::ALL_ONLY_RESERVED, ]; protected const ERROR_NAMES = [ self::INVALID_IP_ERROR => 'INVALID_IP_ERROR', ]; - /** - * @deprecated since Symfony 6.1, use const VERSIONS instead - */ - protected static $versions = self::VERSIONS; - - /** - * @deprecated since Symfony 6.1, use const ERROR_NAMES instead - */ - protected static $errorNames = self::ERROR_NAMES; - - public $version = self::V4; - - public $message = 'This is not a valid IP address.'; - + public string $version = self::V4; + public string $message = 'This is not a valid IP address.'; /** @var callable|null */ public $normalizer; + /** + * @param array|null $options + * @param self::V4*|self::V6*|self::ALL*|null $version The IP version to validate (defaults to {@see self::V4}) + * @param string[]|null $groups + */ + #[HasNamedArguments] public function __construct( ?array $options = null, ?string $version = null, @@ -95,14 +121,18 @@ public function __construct( ?array $groups = null, mixed $payload = null, ) { + if (\is_array($options)) { + trigger_deprecation('symfony/validator', '7.3', 'Passing an array of options to configure the "%s" constraint is deprecated, use named arguments instead.', static::class); + } + parent::__construct($options, $groups, $payload); $this->version = $version ?? $this->version; $this->message = $message ?? $this->message; $this->normalizer = $normalizer ?? $this->normalizer; - if (!\in_array($this->version, self::$versions)) { - throw new ConstraintDefinitionException(\sprintf('The option "version" must be one of "%s".', implode('", "', self::$versions))); + if (!\in_array($this->version, static::VERSIONS, true)) { + throw new ConstraintDefinitionException(\sprintf('The option "version" must be one of "%s".', implode('", "', static::VERSIONS))); } if (null !== $this->normalizer && !\is_callable($this->normalizer)) { diff --git a/Constraints/IpValidator.php b/Constraints/IpValidator.php index 2f71a8804..e2bf0d29a 100644 --- a/Constraints/IpValidator.php +++ b/Constraints/IpValidator.php @@ -21,13 +21,51 @@ * * @author Bernhard Schussek * @author Joseph Bielawski + * @author Ninos Ego */ class IpValidator extends ConstraintValidator { /** - * @return void + * Checks whether an IP address is valid. + * + * @internal */ - public function validate(mixed $value, Constraint $constraint) + public static function checkIp(string $ip, mixed $version): bool + { + $flag = match ($version) { + Ip::V4, Ip::V4_NO_PUBLIC, Ip::V4_ONLY_PRIVATE, Ip::V4_ONLY_RESERVED => \FILTER_FLAG_IPV4, + Ip::V6, Ip::V6_NO_PUBLIC, Ip::V6_ONLY_PRIVATE, Ip::V6_ONLY_RESERVED => \FILTER_FLAG_IPV6, + Ip::V4_NO_PRIVATE => \FILTER_FLAG_IPV4 | \FILTER_FLAG_NO_PRIV_RANGE, + Ip::V6_NO_PRIVATE => \FILTER_FLAG_IPV6 | \FILTER_FLAG_NO_PRIV_RANGE, + Ip::ALL_NO_PRIVATE => \FILTER_FLAG_NO_PRIV_RANGE, + Ip::V4_NO_RESERVED => \FILTER_FLAG_IPV4 | \FILTER_FLAG_NO_RES_RANGE, + Ip::V6_NO_RESERVED => \FILTER_FLAG_IPV6 | \FILTER_FLAG_NO_RES_RANGE, + Ip::ALL_NO_RESERVED => \FILTER_FLAG_NO_RES_RANGE, + Ip::V4_ONLY_PUBLIC => \FILTER_FLAG_IPV4 | \FILTER_FLAG_NO_PRIV_RANGE | \FILTER_FLAG_NO_RES_RANGE, + Ip::V6_ONLY_PUBLIC => \FILTER_FLAG_IPV6 | \FILTER_FLAG_NO_PRIV_RANGE | \FILTER_FLAG_NO_RES_RANGE, + Ip::ALL_ONLY_PUBLIC => \FILTER_FLAG_NO_PRIV_RANGE | \FILTER_FLAG_NO_RES_RANGE, + default => 0, + }; + + if (!filter_var($ip, \FILTER_VALIDATE_IP, $flag)) { + return false; + } + + $inverseFlag = match ($version) { + Ip::V4_NO_PUBLIC, Ip::V6_NO_PUBLIC, Ip::ALL_NO_PUBLIC => \FILTER_FLAG_NO_PRIV_RANGE | \FILTER_FLAG_NO_RES_RANGE, + Ip::V4_ONLY_PRIVATE, Ip::V6_ONLY_PRIVATE, Ip::ALL_ONLY_PRIVATE => \FILTER_FLAG_NO_PRIV_RANGE, + Ip::V4_ONLY_RESERVED, Ip::V6_ONLY_RESERVED, Ip::ALL_ONLY_RESERVED => \FILTER_FLAG_NO_RES_RANGE, + default => 0, + }; + + if ($inverseFlag && filter_var($ip, \FILTER_VALIDATE_IP, $inverseFlag)) { + return false; + } + + return true; + } + + public function validate(mixed $value, Constraint $constraint): void { if (!$constraint instanceof Ip) { throw new UnexpectedTypeException($constraint, Ip::class); @@ -47,22 +85,7 @@ public function validate(mixed $value, Constraint $constraint) $value = ($constraint->normalizer)($value); } - $flag = match ($constraint->version) { - Ip::V4 => \FILTER_FLAG_IPV4, - Ip::V6 => \FILTER_FLAG_IPV6, - Ip::V4_NO_PRIV => \FILTER_FLAG_IPV4 | \FILTER_FLAG_NO_PRIV_RANGE, - Ip::V6_NO_PRIV => \FILTER_FLAG_IPV6 | \FILTER_FLAG_NO_PRIV_RANGE, - Ip::ALL_NO_PRIV => \FILTER_FLAG_NO_PRIV_RANGE, - Ip::V4_NO_RES => \FILTER_FLAG_IPV4 | \FILTER_FLAG_NO_RES_RANGE, - Ip::V6_NO_RES => \FILTER_FLAG_IPV6 | \FILTER_FLAG_NO_RES_RANGE, - Ip::ALL_NO_RES => \FILTER_FLAG_NO_RES_RANGE, - Ip::V4_ONLY_PUBLIC => \FILTER_FLAG_IPV4 | \FILTER_FLAG_NO_PRIV_RANGE | \FILTER_FLAG_NO_RES_RANGE, - Ip::V6_ONLY_PUBLIC => \FILTER_FLAG_IPV6 | \FILTER_FLAG_NO_PRIV_RANGE | \FILTER_FLAG_NO_RES_RANGE, - Ip::ALL_ONLY_PUBLIC => \FILTER_FLAG_NO_PRIV_RANGE | \FILTER_FLAG_NO_RES_RANGE, - default => 0, - }; - - if (!filter_var($value, \FILTER_VALIDATE_IP, $flag)) { + if (!self::checkIp($value, $constraint->version)) { $this->context->buildViolation($constraint->message) ->setParameter('{{ value }}', $this->formatValue($value)) ->setCode(Ip::INVALID_IP_ERROR) diff --git a/Constraints/IsFalse.php b/Constraints/IsFalse.php index ad6f64f1d..bcdadeaf9 100644 --- a/Constraints/IsFalse.php +++ b/Constraints/IsFalse.php @@ -11,11 +11,11 @@ namespace Symfony\Component\Validator\Constraints; +use Symfony\Component\Validator\Attribute\HasNamedArguments; use Symfony\Component\Validator\Constraint; /** - * @Annotation - * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) + * Validates that a value is false. * * @author Bernhard Schussek */ @@ -28,15 +28,19 @@ class IsFalse extends Constraint self::NOT_FALSE_ERROR => 'NOT_FALSE_ERROR', ]; + public string $message = 'This value should be false.'; + /** - * @deprecated since Symfony 6.1, use const ERROR_NAMES instead + * @param array|null $options + * @param string[]|null $groups */ - protected static $errorNames = self::ERROR_NAMES; - - public $message = 'This value should be false.'; - + #[HasNamedArguments] public function __construct(?array $options = null, ?string $message = null, ?array $groups = null, mixed $payload = null) { + if (\is_array($options)) { + trigger_deprecation('symfony/validator', '7.3', 'Passing an array of options to configure the "%s" constraint is deprecated, use named arguments instead.', static::class); + } + parent::__construct($options ?? [], $groups, $payload); $this->message = $message ?? $this->message; diff --git a/Constraints/IsFalseValidator.php b/Constraints/IsFalseValidator.php index 76a24ad78..78c4c5a92 100644 --- a/Constraints/IsFalseValidator.php +++ b/Constraints/IsFalseValidator.php @@ -20,10 +20,7 @@ */ class IsFalseValidator extends ConstraintValidator { - /** - * @return void - */ - public function validate(mixed $value, Constraint $constraint) + public function validate(mixed $value, Constraint $constraint): void { if (!$constraint instanceof IsFalse) { throw new UnexpectedTypeException($constraint, IsFalse::class); diff --git a/Constraints/IsNull.php b/Constraints/IsNull.php index 11c05485e..fa04703ea 100644 --- a/Constraints/IsNull.php +++ b/Constraints/IsNull.php @@ -11,11 +11,11 @@ namespace Symfony\Component\Validator\Constraints; +use Symfony\Component\Validator\Attribute\HasNamedArguments; use Symfony\Component\Validator\Constraint; /** - * @Annotation - * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) + * Validates that a value is exactly equal to null. * * @author Bernhard Schussek */ @@ -28,15 +28,19 @@ class IsNull extends Constraint self::NOT_NULL_ERROR => 'NOT_NULL_ERROR', ]; + public string $message = 'This value should be null.'; + /** - * @deprecated since Symfony 6.1, use const ERROR_NAMES instead + * @param array|null $options + * @param string[]|null $groups */ - protected static $errorNames = self::ERROR_NAMES; - - public $message = 'This value should be null.'; - + #[HasNamedArguments] public function __construct(?array $options = null, ?string $message = null, ?array $groups = null, mixed $payload = null) { + if (\is_array($options)) { + trigger_deprecation('symfony/validator', '7.3', 'Passing an array of options to configure the "%s" constraint is deprecated, use named arguments instead.', static::class); + } + parent::__construct($options ?? [], $groups, $payload); $this->message = $message ?? $this->message; diff --git a/Constraints/IsNullValidator.php b/Constraints/IsNullValidator.php index 628aacf26..c292854bd 100644 --- a/Constraints/IsNullValidator.php +++ b/Constraints/IsNullValidator.php @@ -20,10 +20,7 @@ */ class IsNullValidator extends ConstraintValidator { - /** - * @return void - */ - public function validate(mixed $value, Constraint $constraint) + public function validate(mixed $value, Constraint $constraint): void { if (!$constraint instanceof IsNull) { throw new UnexpectedTypeException($constraint, IsNull::class); diff --git a/Constraints/IsTrue.php b/Constraints/IsTrue.php index db2ac166f..3c0345e77 100644 --- a/Constraints/IsTrue.php +++ b/Constraints/IsTrue.php @@ -11,11 +11,11 @@ namespace Symfony\Component\Validator\Constraints; +use Symfony\Component\Validator\Attribute\HasNamedArguments; use Symfony\Component\Validator\Constraint; /** - * @Annotation - * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) + * Validates that a value is true. * * @author Bernhard Schussek */ @@ -28,15 +28,19 @@ class IsTrue extends Constraint self::NOT_TRUE_ERROR => 'NOT_TRUE_ERROR', ]; + public string $message = 'This value should be true.'; + /** - * @deprecated since Symfony 6.1, use const ERROR_NAMES instead + * @param array|null $options + * @param string[]|null $groups */ - protected static $errorNames = self::ERROR_NAMES; - - public $message = 'This value should be true.'; - + #[HasNamedArguments] public function __construct(?array $options = null, ?string $message = null, ?array $groups = null, mixed $payload = null) { + if (\is_array($options)) { + trigger_deprecation('symfony/validator', '7.3', 'Passing an array of options to configure the "%s" constraint is deprecated, use named arguments instead.', static::class); + } + parent::__construct($options ?? [], $groups, $payload); $this->message = $message ?? $this->message; diff --git a/Constraints/IsTrueValidator.php b/Constraints/IsTrueValidator.php index 644bbf70c..69e411402 100644 --- a/Constraints/IsTrueValidator.php +++ b/Constraints/IsTrueValidator.php @@ -20,10 +20,7 @@ */ class IsTrueValidator extends ConstraintValidator { - /** - * @return void - */ - public function validate(mixed $value, Constraint $constraint) + public function validate(mixed $value, Constraint $constraint): void { if (!$constraint instanceof IsTrue) { throw new UnexpectedTypeException($constraint, IsTrue::class); diff --git a/Constraints/Isbn.php b/Constraints/Isbn.php index e27d6d977..45ca4e4b8 100644 --- a/Constraints/Isbn.php +++ b/Constraints/Isbn.php @@ -11,11 +11,13 @@ namespace Symfony\Component\Validator\Constraints; +use Symfony\Component\Validator\Attribute\HasNamedArguments; use Symfony\Component\Validator\Constraint; /** - * @Annotation - * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) + * Validates that a value is a valid ISBN according to ISBN-10 or ISBN-13 formats. + * + * @see https://en.wikipedia.org/wiki/ISBN * * @author The Whole Life To Learn * @author Manuel Reinhard @@ -41,17 +43,19 @@ class Isbn extends Constraint self::TYPE_NOT_RECOGNIZED_ERROR => 'TYPE_NOT_RECOGNIZED_ERROR', ]; + public string $isbn10Message = 'This value is not a valid ISBN-10.'; + public string $isbn13Message = 'This value is not a valid ISBN-13.'; + public string $bothIsbnMessage = 'This value is neither a valid ISBN-10 nor a valid ISBN-13.'; + public ?string $type = null; + public ?string $message = null; + /** - * @deprecated since Symfony 6.1, use const ERROR_NAMES instead + * @param self::ISBN_*|array|null $type The type of ISBN to validate (i.e. {@see Isbn::ISBN_10}, {@see Isbn::ISBN_13} or null to accept both, defaults to null) + * @param string|null $message If defined, this message has priority over the others + * @param string[]|null $groups + * @param array|null $options */ - protected static $errorNames = self::ERROR_NAMES; - - public $isbn10Message = 'This value is not a valid ISBN-10.'; - public $isbn13Message = 'This value is not a valid ISBN-13.'; - public $bothIsbnMessage = 'This value is neither a valid ISBN-10 nor a valid ISBN-13.'; - public $type; - public $message; - + #[HasNamedArguments] public function __construct( string|array|null $type = null, ?string $message = null, @@ -60,11 +64,19 @@ public function __construct( ?string $bothIsbnMessage = null, ?array $groups = null, mixed $payload = null, - array $options = [], + ?array $options = null, ) { if (\is_array($type)) { - $options = array_merge($type, $options); + trigger_deprecation('symfony/validator', '7.3', 'Passing an array of options to configure the "%s" constraint is deprecated, use named arguments instead.', static::class); + + $options = array_merge($type, $options ?? []); } elseif (null !== $type) { + if (\is_array($options)) { + trigger_deprecation('symfony/validator', '7.3', 'Passing an array of options to configure the "%s" constraint is deprecated, use named arguments instead.', static::class); + } else { + $options = []; + } + $options['value'] = $type; } diff --git a/Constraints/IsbnValidator.php b/Constraints/IsbnValidator.php index 26e9d4a21..de5f4437b 100644 --- a/Constraints/IsbnValidator.php +++ b/Constraints/IsbnValidator.php @@ -27,10 +27,7 @@ */ class IsbnValidator extends ConstraintValidator { - /** - * @return void - */ - public function validate(mixed $value, Constraint $constraint) + public function validate(mixed $value, Constraint $constraint): void { if (!$constraint instanceof Isbn) { throw new UnexpectedTypeException($constraint, Isbn::class); @@ -95,10 +92,7 @@ public function validate(mixed $value, Constraint $constraint) } } - /** - * @return string|bool - */ - protected function validateIsbn10(string $isbn) + protected function validateIsbn10(string $isbn): string|bool { // Choose an algorithm so that ERROR_INVALID_CHARACTERS is preferred // over ERROR_TOO_SHORT/ERROR_TOO_LONG @@ -138,10 +132,7 @@ protected function validateIsbn10(string $isbn) return 0 === $checkSum % 11 ? true : Isbn::CHECKSUM_FAILED_ERROR; } - /** - * @return string|bool - */ - protected function validateIsbn13(string $isbn) + protected function validateIsbn13(string $isbn): string|bool { // Error priority: // 1. ERROR_INVALID_CHARACTERS @@ -175,10 +166,7 @@ protected function validateIsbn13(string $isbn) return 0 === $checkSum % 10 ? true : Isbn::CHECKSUM_FAILED_ERROR; } - /** - * @return string - */ - protected function getMessage(Isbn $constraint, ?string $type = null) + protected function getMessage(Isbn $constraint, ?string $type = null): string { if (null !== $constraint->message) { return $constraint->message; diff --git a/Constraints/Isin.php b/Constraints/Isin.php index 19f964222..7bd9abe2d 100644 --- a/Constraints/Isin.php +++ b/Constraints/Isin.php @@ -11,11 +11,13 @@ namespace Symfony\Component\Validator\Constraints; +use Symfony\Component\Validator\Attribute\HasNamedArguments; use Symfony\Component\Validator\Constraint; /** - * @Annotation - * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) + * Validates that a value is a valid International Securities Identification Number (ISIN). + * + * @see https://en.wikipedia.org/wiki/International_Securities_Identification_Number * * @author Laurent Masforné */ @@ -35,15 +37,19 @@ class Isin extends Constraint self::INVALID_CHECKSUM_ERROR => 'INVALID_CHECKSUM_ERROR', ]; + public string $message = 'This value is not a valid International Securities Identification Number (ISIN).'; + /** - * @deprecated since Symfony 6.1, use const ERROR_NAMES instead + * @param array|null $options + * @param string[]|null $groups */ - protected static $errorNames = self::ERROR_NAMES; - - public $message = 'This value is not a valid International Securities Identification Number (ISIN).'; - + #[HasNamedArguments] public function __construct(?array $options = null, ?string $message = null, ?array $groups = null, mixed $payload = null) { + if (\is_array($options)) { + trigger_deprecation('symfony/validator', '7.3', 'Passing an array of options to configure the "%s" constraint is deprecated, use named arguments instead.', static::class); + } + parent::__construct($options, $groups, $payload); $this->message = $message ?? $this->message; diff --git a/Constraints/IsinValidator.php b/Constraints/IsinValidator.php index 21539cbcb..32b3b93d5 100644 --- a/Constraints/IsinValidator.php +++ b/Constraints/IsinValidator.php @@ -23,10 +23,7 @@ */ class IsinValidator extends ConstraintValidator { - /** - * @return void - */ - public function validate(mixed $value, Constraint $constraint) + public function validate(mixed $value, Constraint $constraint): void { if (!$constraint instanceof Isin) { throw new UnexpectedTypeException($constraint, Isin::class); diff --git a/Constraints/Issn.php b/Constraints/Issn.php index e2c45cb51..048c18f5e 100644 --- a/Constraints/Issn.php +++ b/Constraints/Issn.php @@ -11,11 +11,13 @@ namespace Symfony\Component\Validator\Constraints; +use Symfony\Component\Validator\Attribute\HasNamedArguments; use Symfony\Component\Validator\Constraint; /** - * @Annotation - * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) + * Validates that a value is a valid International Standard Serial Number (ISSN). + * + * @see https://en.wikipedia.org/wiki/ISSN * * @author Antonio J. García Lagar * @author Bernhard Schussek @@ -39,15 +41,17 @@ class Issn extends Constraint self::CHECKSUM_FAILED_ERROR => 'CHECKSUM_FAILED_ERROR', ]; + public string $message = 'This value is not a valid ISSN.'; + public bool $caseSensitive = false; + public bool $requireHyphen = false; + /** - * @deprecated since Symfony 6.1, use const ERROR_NAMES instead + * @param array|null $options + * @param bool|null $caseSensitive Whether to allow the value to end with a lowercase character (defaults to false) + * @param bool|null $requireHyphen Whether to require a hyphenated ISSN value (defaults to false) + * @param string[]|null $groups */ - protected static $errorNames = self::ERROR_NAMES; - - public $message = 'This value is not a valid ISSN.'; - public $caseSensitive = false; - public $requireHyphen = false; - + #[HasNamedArguments] public function __construct( ?array $options = null, ?string $message = null, @@ -56,6 +60,10 @@ public function __construct( ?array $groups = null, mixed $payload = null, ) { + if (\is_array($options)) { + trigger_deprecation('symfony/validator', '7.3', 'Passing an array of options to configure the "%s" constraint is deprecated, use named arguments instead.', static::class); + } + parent::__construct($options, $groups, $payload); $this->message = $message ?? $this->message; diff --git a/Constraints/IssnValidator.php b/Constraints/IssnValidator.php index 1962322b4..f8668c63d 100644 --- a/Constraints/IssnValidator.php +++ b/Constraints/IssnValidator.php @@ -26,10 +26,7 @@ */ class IssnValidator extends ConstraintValidator { - /** - * @return void - */ - public function validate(mixed $value, Constraint $constraint) + public function validate(mixed $value, Constraint $constraint): void { if (!$constraint instanceof Issn) { throw new UnexpectedTypeException($constraint, Issn::class); diff --git a/Constraints/Json.php b/Constraints/Json.php index 05018e7a0..18078a2fe 100644 --- a/Constraints/Json.php +++ b/Constraints/Json.php @@ -11,11 +11,11 @@ namespace Symfony\Component\Validator\Constraints; +use Symfony\Component\Validator\Attribute\HasNamedArguments; use Symfony\Component\Validator\Constraint; /** - * @Annotation - * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) + * Validates that a value has valid JSON syntax. * * @author Imad ZAIRIG */ @@ -28,15 +28,19 @@ class Json extends Constraint self::INVALID_JSON_ERROR => 'INVALID_JSON_ERROR', ]; + public string $message = 'This value should be valid JSON.'; + /** - * @deprecated since Symfony 6.1, use const ERROR_NAMES instead + * @param array|null $options + * @param string[]|null $groups */ - protected static $errorNames = self::ERROR_NAMES; - - public $message = 'This value should be valid JSON.'; - + #[HasNamedArguments] public function __construct(?array $options = null, ?string $message = null, ?array $groups = null, mixed $payload = null) { + if (\is_array($options)) { + trigger_deprecation('symfony/validator', '7.3', 'Passing an array of options to configure the "%s" constraint is deprecated, use named arguments instead.', static::class); + } + parent::__construct($options, $groups, $payload); $this->message = $message ?? $this->message; diff --git a/Constraints/JsonValidator.php b/Constraints/JsonValidator.php index 9134da9dd..e65831ca8 100644 --- a/Constraints/JsonValidator.php +++ b/Constraints/JsonValidator.php @@ -21,10 +21,7 @@ */ class JsonValidator extends ConstraintValidator { - /** - * @return void - */ - public function validate(mixed $value, Constraint $constraint) + public function validate(mixed $value, Constraint $constraint): void { if (!$constraint instanceof Json) { throw new UnexpectedTypeException($constraint, Json::class); diff --git a/Constraints/Language.php b/Constraints/Language.php index b35414471..61ac4644b 100644 --- a/Constraints/Language.php +++ b/Constraints/Language.php @@ -12,12 +12,14 @@ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\Intl\Languages; +use Symfony\Component\Validator\Attribute\HasNamedArguments; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Exception\LogicException; /** - * @Annotation - * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) + * Validates that a value is a valid language Unicode language identifier. + * + * @see https://unicode.org/reports/tr35/#Unicode_language_identifier * * @author Bernhard Schussek */ @@ -30,14 +32,15 @@ class Language extends Constraint self::NO_SUCH_LANGUAGE_ERROR => 'NO_SUCH_LANGUAGE_ERROR', ]; + public string $message = 'This value is not a valid language.'; + public bool $alpha3 = false; + /** - * @deprecated since Symfony 6.1, use const ERROR_NAMES instead + * @param array|null $options + * @param bool|null $alpha3 Pass true to validate the language with three-letter code (ISO 639-2 (2T)) or false with two-letter code (ISO 639-1) (defaults to false) + * @param string[]|null $groups */ - protected static $errorNames = self::ERROR_NAMES; - - public $message = 'This value is not a valid language.'; - public $alpha3 = false; - + #[HasNamedArguments] public function __construct( ?array $options = null, ?string $message = null, @@ -49,6 +52,10 @@ public function __construct( throw new LogicException('The Intl component is required to use the Language constraint. Try running "composer require symfony/intl".'); } + if (\is_array($options)) { + trigger_deprecation('symfony/validator', '7.3', 'Passing an array of options to configure the "%s" constraint is deprecated, use named arguments instead.', static::class); + } + parent::__construct($options, $groups, $payload); $this->message = $message ?? $this->message; diff --git a/Constraints/LanguageValidator.php b/Constraints/LanguageValidator.php index 4706c3356..9f9832006 100644 --- a/Constraints/LanguageValidator.php +++ b/Constraints/LanguageValidator.php @@ -24,10 +24,7 @@ */ class LanguageValidator extends ConstraintValidator { - /** - * @return void - */ - public function validate(mixed $value, Constraint $constraint) + public function validate(mixed $value, Constraint $constraint): void { if (!$constraint instanceof Language) { throw new UnexpectedTypeException($constraint, Language::class); diff --git a/Constraints/Length.php b/Constraints/Length.php index 93e797d30..ce1460c6e 100644 --- a/Constraints/Length.php +++ b/Constraints/Length.php @@ -11,13 +11,13 @@ namespace Symfony\Component\Validator\Constraints; +use Symfony\Component\Validator\Attribute\HasNamedArguments; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Exception\InvalidArgumentException; use Symfony\Component\Validator\Exception\MissingOptionsException; /** - * @Annotation - * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) + * Validates that a given string length is between some minimum and maximum value. * * @author Bernhard Schussek */ @@ -46,26 +46,29 @@ class Length extends Constraint self::COUNT_GRAPHEMES, ]; - /** - * @deprecated since Symfony 6.1, use const ERROR_NAMES instead - */ - protected static $errorNames = self::ERROR_NAMES; - - public $maxMessage = 'This value is too long. It should have {{ limit }} character or less.|This value is too long. It should have {{ limit }} characters or less.'; - public $minMessage = 'This value is too short. It should have {{ limit }} character or more.|This value is too short. It should have {{ limit }} characters or more.'; - public $exactMessage = 'This value should have exactly {{ limit }} character.|This value should have exactly {{ limit }} characters.'; - public $charsetMessage = 'This value does not match the expected {{ charset }} charset.'; - public $max; - public $min; - public $charset = 'UTF-8'; + public string $maxMessage = 'This value is too long. It should have {{ limit }} character or less.|This value is too long. It should have {{ limit }} characters or less.'; + public string $minMessage = 'This value is too short. It should have {{ limit }} character or more.|This value is too short. It should have {{ limit }} characters or more.'; + public string $exactMessage = 'This value should have exactly {{ limit }} character.|This value should have exactly {{ limit }} characters.'; + public string $charsetMessage = 'This value does not match the expected {{ charset }} charset.'; + public ?int $max = null; + public ?int $min = null; + public string $charset = 'UTF-8'; /** @var callable|null */ public $normalizer; /** @var self::COUNT_* */ public string $countUnit = self::COUNT_CODEPOINTS; /** - * @param self::COUNT_*|null $countUnit + * @param positive-int|array|null $exactly The exact expected length + * @param int<0, max>|null $min The minimum expected length + * @param positive-int|null $max The maximum expected length + * @param string|null $charset The charset to be used when computing value's length (defaults to UTF-8) + * @param callable|null $normalizer A callable to normalize value before it is validated + * @param self::COUNT_*|null $countUnit The character count unit for the length check (defaults to {@see Length::COUNT_CODEPOINTS}) + * @param string[]|null $groups + * @param array|null $options */ + #[HasNamedArguments] public function __construct( int|array|null $exactly = null, ?int $min = null, @@ -79,11 +82,17 @@ public function __construct( ?string $charsetMessage = null, ?array $groups = null, mixed $payload = null, - array $options = [], + ?array $options = null, ) { if (\is_array($exactly)) { - $options = array_merge($exactly, $options); + trigger_deprecation('symfony/validator', '7.3', 'Passing an array of options to configure the "%s" constraint is deprecated, use named arguments instead.', static::class); + + $options = array_merge($exactly, $options ?? []); $exactly = $options['value'] ?? null; + } elseif (\is_array($options)) { + trigger_deprecation('symfony/validator', '7.3', 'Passing an array of options to configure the "%s" constraint is deprecated, use named arguments instead.', static::class); + } else { + $options = []; } $min ??= $options['min'] ?? null; @@ -115,7 +124,7 @@ public function __construct( throw new InvalidArgumentException(\sprintf('The "normalizer" option must be a valid callable ("%s" given).', get_debug_type($this->normalizer))); } - if (!\in_array($this->countUnit, self::VALID_COUNT_UNITS)) { + if (!\in_array($this->countUnit, self::VALID_COUNT_UNITS, true)) { throw new InvalidArgumentException(\sprintf('The "countUnit" option must be one of the "%s"::COUNT_* constants ("%s" given).', __CLASS__, $this->countUnit)); } } diff --git a/Constraints/LengthValidator.php b/Constraints/LengthValidator.php index c92fca0d5..985660bc2 100644 --- a/Constraints/LengthValidator.php +++ b/Constraints/LengthValidator.php @@ -21,10 +21,7 @@ */ class LengthValidator extends ConstraintValidator { - /** - * @return void - */ - public function validate(mixed $value, Constraint $constraint) + public function validate(mixed $value, Constraint $constraint): void { if (!$constraint instanceof Length) { throw new UnexpectedTypeException($constraint, Length::class); @@ -79,7 +76,7 @@ public function validate(mixed $value, Constraint $constraint) ->setParameter('{{ limit }}', $constraint->max) ->setParameter('{{ value_length }}', $length) ->setInvalidValue($value) - ->setPlural((int) $constraint->max) + ->setPlural($constraint->max) ->setCode($exactlyOptionEnabled ? Length::NOT_EQUAL_LENGTH_ERROR : Length::TOO_LONG_ERROR) ->addViolation(); @@ -94,7 +91,7 @@ public function validate(mixed $value, Constraint $constraint) ->setParameter('{{ limit }}', $constraint->min) ->setParameter('{{ value_length }}', $length) ->setInvalidValue($value) - ->setPlural((int) $constraint->min) + ->setPlural($constraint->min) ->setCode($exactlyOptionEnabled ? Length::NOT_EQUAL_LENGTH_ERROR : Length::TOO_SHORT_ERROR) ->addViolation(); } diff --git a/Constraints/LessThan.php b/Constraints/LessThan.php index cf4144d6d..d9be15958 100644 --- a/Constraints/LessThan.php +++ b/Constraints/LessThan.php @@ -12,8 +12,7 @@ namespace Symfony\Component\Validator\Constraints; /** - * @Annotation - * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) + * Validates that a value is less than another value. * * @author Daniel Holmes * @author Bernhard Schussek @@ -27,10 +26,5 @@ class LessThan extends AbstractComparison self::TOO_HIGH_ERROR => 'TOO_HIGH_ERROR', ]; - /** - * @deprecated since Symfony 6.1, use const ERROR_NAMES instead - */ - protected static $errorNames = self::ERROR_NAMES; - - public $message = 'This value should be less than {{ compared_value }}.'; + public string $message = 'This value should be less than {{ compared_value }}.'; } diff --git a/Constraints/LessThanOrEqual.php b/Constraints/LessThanOrEqual.php index 84e31abfc..ddeaf2c38 100644 --- a/Constraints/LessThanOrEqual.php +++ b/Constraints/LessThanOrEqual.php @@ -12,8 +12,7 @@ namespace Symfony\Component\Validator\Constraints; /** - * @Annotation - * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) + * Validates that a value is less than or equal to another value. * * @author Daniel Holmes * @author Bernhard Schussek @@ -27,10 +26,5 @@ class LessThanOrEqual extends AbstractComparison self::TOO_HIGH_ERROR => 'TOO_HIGH_ERROR', ]; - /** - * @deprecated since Symfony 6.1, use const ERROR_NAMES instead - */ - protected static $errorNames = self::ERROR_NAMES; - - public $message = 'This value should be less than or equal to {{ compared_value }}.'; + public string $message = 'This value should be less than or equal to {{ compared_value }}.'; } diff --git a/Constraints/Locale.php b/Constraints/Locale.php index 9470f1ac8..0ffe4b0e8 100644 --- a/Constraints/Locale.php +++ b/Constraints/Locale.php @@ -12,12 +12,14 @@ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\Intl\Locales; +use Symfony\Component\Validator\Attribute\HasNamedArguments; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Exception\LogicException; /** - * @Annotation - * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) + * Validates that a value is a valid locale (e.g. fr, fr_FR, etc.). + * + * @see https://unicode-org.github.io/icu/userguide/locale/ * * @author Bernhard Schussek */ @@ -30,14 +32,15 @@ class Locale extends Constraint self::NO_SUCH_LOCALE_ERROR => 'NO_SUCH_LOCALE_ERROR', ]; + public string $message = 'This value is not a valid locale.'; + public bool $canonicalize = true; + /** - * @deprecated since Symfony 6.1, use const ERROR_NAMES instead + * @param array|null $options + * @param bool|null $canonicalize Whether to canonicalize the value before validation (defaults to true) (see {@see https://www.php.net/manual/en/locale.canonicalize.php}) + * @param string[]|null $groups */ - protected static $errorNames = self::ERROR_NAMES; - - public $message = 'This value is not a valid locale.'; - public $canonicalize = true; - + #[HasNamedArguments] public function __construct( ?array $options = null, ?string $message = null, @@ -49,6 +52,10 @@ public function __construct( throw new LogicException('The Intl component is required to use the Locale constraint. Try running "composer require symfony/intl".'); } + if (\is_array($options)) { + trigger_deprecation('symfony/validator', '7.3', 'Passing an array of options to configure the "%s" constraint is deprecated, use named arguments instead.', static::class); + } + parent::__construct($options, $groups, $payload); $this->message = $message ?? $this->message; diff --git a/Constraints/LocaleValidator.php b/Constraints/LocaleValidator.php index 11045ca95..7f1bfe265 100644 --- a/Constraints/LocaleValidator.php +++ b/Constraints/LocaleValidator.php @@ -24,10 +24,7 @@ */ class LocaleValidator extends ConstraintValidator { - /** - * @return void - */ - public function validate(mixed $value, Constraint $constraint) + public function validate(mixed $value, Constraint $constraint): void { if (!$constraint instanceof Locale) { throw new UnexpectedTypeException($constraint, Locale::class); diff --git a/Constraints/Luhn.php b/Constraints/Luhn.php index 3725c3bc5..9421fc3c7 100644 --- a/Constraints/Luhn.php +++ b/Constraints/Luhn.php @@ -11,13 +11,13 @@ namespace Symfony\Component\Validator\Constraints; +use Symfony\Component\Validator\Attribute\HasNamedArguments; use Symfony\Component\Validator\Constraint; /** - * Metadata for the LuhnValidator. + * Validates that a value (typically a credit card number) passes the Luhn algorithm. * - * @Annotation - * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) + * @see https://en.wikipedia.org/wiki/Luhn_algorithm * * @author Tim Nagel * @author Greg Knapp http://gregk.me/2011/php-implementation-of-bank-card-luhn-algorithm/ @@ -34,19 +34,23 @@ class Luhn extends Constraint self::CHECKSUM_FAILED_ERROR => 'CHECKSUM_FAILED_ERROR', ]; + public string $message = 'Invalid card number.'; + /** - * @deprecated since Symfony 6.1, use const ERROR_NAMES instead + * @param array|null $options + * @param string[]|null $groups */ - protected static $errorNames = self::ERROR_NAMES; - - public $message = 'Invalid card number.'; - + #[HasNamedArguments] public function __construct( ?array $options = null, ?string $message = null, ?array $groups = null, mixed $payload = null, ) { + if (\is_array($options)) { + trigger_deprecation('symfony/validator', '7.3', 'Passing an array of options to configure the "%s" constraint is deprecated, use named arguments instead.', static::class); + } + parent::__construct($options, $groups, $payload); $this->message = $message ?? $this->message; diff --git a/Constraints/LuhnValidator.php b/Constraints/LuhnValidator.php index a3f871e33..242b9ad4b 100644 --- a/Constraints/LuhnValidator.php +++ b/Constraints/LuhnValidator.php @@ -30,10 +30,7 @@ */ class LuhnValidator extends ConstraintValidator { - /** - * @return void - */ - public function validate(mixed $value, Constraint $constraint) + public function validate(mixed $value, Constraint $constraint): void { if (!$constraint instanceof Luhn) { throw new UnexpectedTypeException($constraint, Luhn::class); diff --git a/Constraints/MacAddress.php b/Constraints/MacAddress.php new file mode 100644 index 000000000..566495b38 --- /dev/null +++ b/Constraints/MacAddress.php @@ -0,0 +1,85 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Constraints; + +use Symfony\Component\Validator\Attribute\HasNamedArguments; +use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\Exception\ConstraintDefinitionException; + +/** + * Validates that a value is a valid MAC address. + * + * @author Ninos Ego + */ +#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] +class MacAddress extends Constraint +{ + public const ALL = 'all'; + public const ALL_NO_BROADCAST = 'all_no_broadcast'; + public const LOCAL_ALL = 'local_all'; + public const LOCAL_NO_BROADCAST = 'local_no_broadcast'; + public const LOCAL_UNICAST = 'local_unicast'; + public const LOCAL_MULTICAST = 'local_multicast'; + public const LOCAL_MULTICAST_NO_BROADCAST = 'local_multicast_no_broadcast'; + public const UNIVERSAL_ALL = 'universal_all'; + public const UNIVERSAL_UNICAST = 'universal_unicast'; + public const UNIVERSAL_MULTICAST = 'universal_multicast'; + public const UNICAST_ALL = 'unicast_all'; + public const MULTICAST_ALL = 'multicast_all'; + public const MULTICAST_NO_BROADCAST = 'multicast_no_broadcast'; + public const BROADCAST = 'broadcast'; + + public const INVALID_MAC_ERROR = 'a183fbff-6968-43b4-82a2-cc5cf7150036'; + + private const TYPES = [ + self::ALL, + self::ALL_NO_BROADCAST, + self::LOCAL_ALL, + self::LOCAL_NO_BROADCAST, + self::LOCAL_UNICAST, + self::LOCAL_MULTICAST, + self::LOCAL_MULTICAST_NO_BROADCAST, + self::UNIVERSAL_ALL, + self::UNIVERSAL_UNICAST, + self::UNIVERSAL_MULTICAST, + self::UNICAST_ALL, + self::MULTICAST_ALL, + self::MULTICAST_NO_BROADCAST, + self::BROADCAST, + ]; + + protected const ERROR_NAMES = [ + self::INVALID_MAC_ERROR => 'INVALID_MAC_ERROR', + ]; + + public ?\Closure $normalizer; + + /** + * @param self::ALL*|self::LOCAL_*|self::UNIVERSAL_*|self::UNICAST_*|self::MULTICAST_*|self::BROADCAST $type A mac address type to validate (defaults to {@see self::ALL}) + */ + #[HasNamedArguments] + public function __construct( + public string $message = 'This value is not a valid MAC address.', + public string $type = self::ALL, + ?callable $normalizer = null, + ?array $groups = null, + mixed $payload = null, + ) { + parent::__construct(null, $groups, $payload); + + if (!\in_array($this->type, self::TYPES, true)) { + throw new ConstraintDefinitionException(\sprintf('The option "type" must be one of "%s".', implode('", "', self::TYPES))); + } + + $this->normalizer = null !== $normalizer ? $normalizer(...) : null; + } +} diff --git a/Constraints/MacAddressValidator.php b/Constraints/MacAddressValidator.php new file mode 100644 index 000000000..803798128 --- /dev/null +++ b/Constraints/MacAddressValidator.php @@ -0,0 +1,118 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Constraints; + +use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\ConstraintValidator; +use Symfony\Component\Validator\Exception\UnexpectedTypeException; +use Symfony\Component\Validator\Exception\UnexpectedValueException; + +/** + * Validates whether a value is a valid MAC address. + * + * @author Ninos Ego + */ +class MacAddressValidator extends ConstraintValidator +{ + public function validate(mixed $value, Constraint $constraint): void + { + if (!$constraint instanceof MacAddress) { + throw new UnexpectedTypeException($constraint, MacAddress::class); + } + + if (null === $value || '' === $value) { + return; + } + + if (!\is_scalar($value) && !$value instanceof \Stringable) { + throw new UnexpectedValueException($value, 'string'); + } + + $value = (string) $value; + + if (null !== $constraint->normalizer) { + $value = ($constraint->normalizer)($value); + } + + if (!self::checkMac($value, $constraint->type)) { + $this->context->buildViolation($constraint->message) + ->setParameter('{{ value }}', $this->formatValue($value)) + ->setCode(MacAddress::INVALID_MAC_ERROR) + ->addViolation(); + } + } + + /** + * Checks whether a MAC address is valid. + */ + private static function checkMac(string $mac, string $type): bool + { + if (!filter_var($mac, \FILTER_VALIDATE_MAC)) { + return false; + } + + return match ($type) { + MacAddress::ALL => true, + MacAddress::ALL_NO_BROADCAST => !self::isBroadcast($mac), + MacAddress::LOCAL_ALL => self::isLocal($mac), + MacAddress::LOCAL_NO_BROADCAST => self::isLocal($mac) && !self::isBroadcast($mac), + MacAddress::LOCAL_UNICAST => self::isLocal($mac) && self::isUnicast($mac), + MacAddress::LOCAL_MULTICAST => self::isLocal($mac) && !self::isUnicast($mac), + MacAddress::LOCAL_MULTICAST_NO_BROADCAST => self::isLocal($mac) && !self::isUnicast($mac) && !self::isBroadcast($mac), + MacAddress::UNIVERSAL_ALL => !self::isLocal($mac), + MacAddress::UNIVERSAL_UNICAST => !self::isLocal($mac) && self::isUnicast($mac), + MacAddress::UNIVERSAL_MULTICAST => !self::isLocal($mac) && !self::isUnicast($mac), + MacAddress::UNICAST_ALL => self::isUnicast($mac), + MacAddress::MULTICAST_ALL => !self::isUnicast($mac), + MacAddress::MULTICAST_NO_BROADCAST => !self::isUnicast($mac) && !self::isBroadcast($mac), + MacAddress::BROADCAST => self::isBroadcast($mac), + }; + } + + /** + * Checks whether a MAC address is unicast or multicast. + */ + private static function isUnicast(string $mac): bool + { + return match (self::sanitize($mac)[1]) { + '0', '4', '8', 'c', '2', '6', 'a', 'e' => true, + default => false, + }; + } + + /** + * Checks whether a MAC address is local or universal. + */ + private static function isLocal(string $mac): bool + { + return match (self::sanitize($mac)[1]) { + '2', '6', 'a', 'e', '3', '7', 'b', 'f' => true, + default => false, + }; + } + + /** + * Checks whether a MAC address is broadcast. + */ + private static function isBroadcast(string $mac): bool + { + return 'ffffffffffff' === self::sanitize($mac); + } + + /** + * Returns the sanitized MAC address. + */ + private static function sanitize(string $mac): string + { + return strtolower(str_replace([':', '-', '.'], '', $mac)); + } +} diff --git a/Constraints/Negative.php b/Constraints/Negative.php index c13ebcb4a..ff52073ea 100644 --- a/Constraints/Negative.php +++ b/Constraints/Negative.php @@ -12,8 +12,7 @@ namespace Symfony\Component\Validator\Constraints; /** - * @Annotation - * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) + * Validates that a value is a negative number. * * @author Jan Schädlich */ @@ -22,5 +21,5 @@ class Negative extends LessThan { use ZeroComparisonConstraintTrait; - public $message = 'This value should be negative.'; + public string $message = 'This value should be negative.'; } diff --git a/Constraints/NegativeOrZero.php b/Constraints/NegativeOrZero.php index 5be735c31..610af4954 100644 --- a/Constraints/NegativeOrZero.php +++ b/Constraints/NegativeOrZero.php @@ -12,8 +12,7 @@ namespace Symfony\Component\Validator\Constraints; /** - * @Annotation - * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) + * Validates that a value is a negative number or equal to zero. * * @author Jan Schädlich */ @@ -22,5 +21,5 @@ class NegativeOrZero extends LessThanOrEqual { use ZeroComparisonConstraintTrait; - public $message = 'This value should be either negative or zero.'; + public string $message = 'This value should be either negative or zero.'; } diff --git a/Constraints/NoSuspiciousCharacters.php b/Constraints/NoSuspiciousCharacters.php index b64d26865..f0d28dba2 100644 --- a/Constraints/NoSuspiciousCharacters.php +++ b/Constraints/NoSuspiciousCharacters.php @@ -11,12 +11,14 @@ namespace Symfony\Component\Validator\Constraints; +use Symfony\Component\Validator\Attribute\HasNamedArguments; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Exception\LogicException; /** - * @Annotation - * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) + * Validates that the given string does not contain characters used in spoofing security attacks. + * + * @see https://www.php.net/manual/en/class.spoofchecker.php * * @author Mathieu Lechat */ @@ -81,9 +83,14 @@ class NoSuspiciousCharacters extends Constraint public ?array $locales = null; /** - * @param int-mask-of|null $checks - * @param self::RESTRICTION_LEVEL_*|null $restrictionLevel + * @param array|null $options + * @param int-mask-of|null $checks A bitmask of the checks to perform on the string (defaults to all checks) + * @param int-mask-of|null $restrictionLevel Configures the set of acceptable characters for the validated string through a specified "level" (defaults to + * {@see NoSuspiciousCharacters::RESTRICTION_LEVEL_MODERATE} on ICU >= 58, {@see NoSuspiciousCharacters::RESTRICTION_LEVEL_SINGLE_SCRIPT} otherwise) + * @param string[]|null $locales Restrict the string's characters to those normally used with these locales. Pass null to use the default locales configured for the NoSuspiciousCharactersValidator. (defaults to null) + * @param string[]|null $groups */ + #[HasNamedArguments] public function __construct( ?array $options = null, ?string $restrictionLevelMessage = null, @@ -100,6 +107,10 @@ public function __construct( throw new LogicException('The intl extension is required to use the NoSuspiciousCharacters constraint.'); } + if (\is_array($options)) { + trigger_deprecation('symfony/validator', '7.3', 'Passing an array of options to configure the "%s" constraint is deprecated, use named arguments instead.', static::class); + } + parent::__construct($options, $groups, $payload); $this->restrictionLevelMessage = $restrictionLevelMessage ?? $this->restrictionLevelMessage; diff --git a/Constraints/NoSuspiciousCharactersValidator.php b/Constraints/NoSuspiciousCharactersValidator.php index 659de93f9..0b7a78ef9 100644 --- a/Constraints/NoSuspiciousCharactersValidator.php +++ b/Constraints/NoSuspiciousCharactersValidator.php @@ -18,7 +18,7 @@ use Symfony\Component\Validator\Exception\UnexpectedValueException; /** - * @author Mathieu Lechat + * @author Mathieu Lechat */ class NoSuspiciousCharactersValidator extends ConstraintValidator { @@ -56,10 +56,7 @@ public function __construct(private readonly array $defaultLocales = []) { } - /** - * @return void - */ - public function validate(mixed $value, Constraint $constraint) + public function validate(mixed $value, Constraint $constraint): void { if (!$constraint instanceof NoSuspiciousCharacters) { throw new UnexpectedTypeException($constraint, NoSuspiciousCharacters::class); @@ -97,18 +94,22 @@ public function validate(mixed $value, Constraint $constraint) $checker->setChecks($checks); - if (!$checker->isSuspicious($value)) { + if (!$checker->isSuspicious($value, $errorCode)) { return; } foreach (self::CHECK_ERROR as $check => $error) { - if (!($checks & $check)) { - continue; - } + if (\PHP_VERSION_ID < 80204) { + if (!($checks & $check)) { + continue; + } - $checker->setChecks($check); + $checker->setChecks($check); - if (!$checker->isSuspicious($value)) { + if (!$checker->isSuspicious($value)) { + continue; + } + } elseif (!($errorCode & $check)) { continue; } diff --git a/Constraints/NotBlank.php b/Constraints/NotBlank.php index 17ada2770..725e7eede 100644 --- a/Constraints/NotBlank.php +++ b/Constraints/NotBlank.php @@ -11,12 +11,12 @@ namespace Symfony\Component\Validator\Constraints; +use Symfony\Component\Validator\Attribute\HasNamedArguments; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Exception\InvalidArgumentException; /** - * @Annotation - * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) + * Validates that a value is not blank. * * @author Bernhard Schussek * @author Kévin Dunglas @@ -30,18 +30,23 @@ class NotBlank extends Constraint self::IS_BLANK_ERROR => 'IS_BLANK_ERROR', ]; - /** - * @deprecated since Symfony 6.1, use const ERROR_NAMES instead - */ - protected static $errorNames = self::ERROR_NAMES; - - public $message = 'This value should not be blank.'; - public $allowNull = false; + public string $message = 'This value should not be blank.'; + public bool $allowNull = false; /** @var callable|null */ public $normalizer; + /** + * @param array|null $options + * @param bool|null $allowNull Whether to allow null values (defaults to false) + * @param string[]|null $groups + */ + #[HasNamedArguments] public function __construct(?array $options = null, ?string $message = null, ?bool $allowNull = null, ?callable $normalizer = null, ?array $groups = null, mixed $payload = null) { + if (\is_array($options)) { + trigger_deprecation('symfony/validator', '7.3', 'Passing an array of options to configure the "%s" constraint is deprecated, use named arguments instead.', static::class); + } + parent::__construct($options ?? [], $groups, $payload); $this->message = $message ?? $this->message; diff --git a/Constraints/NotBlankValidator.php b/Constraints/NotBlankValidator.php index fa6c794c0..2b21b3f3c 100644 --- a/Constraints/NotBlankValidator.php +++ b/Constraints/NotBlankValidator.php @@ -21,10 +21,7 @@ */ class NotBlankValidator extends ConstraintValidator { - /** - * @return void - */ - public function validate(mixed $value, Constraint $constraint) + public function validate(mixed $value, Constraint $constraint): void { if (!$constraint instanceof NotBlank) { throw new UnexpectedTypeException($constraint, NotBlank::class); @@ -38,7 +35,7 @@ public function validate(mixed $value, Constraint $constraint) $value = ($constraint->normalizer)($value); } - if (false === $value || (empty($value) && '0' != $value)) { + if (false === $value || (!$value && '0' != $value)) { $this->context->buildViolation($constraint->message) ->setParameter('{{ value }}', $this->formatValue($value)) ->setCode(NotBlank::IS_BLANK_ERROR) diff --git a/Constraints/NotCompromisedPassword.php b/Constraints/NotCompromisedPassword.php index f5970a178..ef1e03da9 100644 --- a/Constraints/NotCompromisedPassword.php +++ b/Constraints/NotCompromisedPassword.php @@ -11,14 +11,12 @@ namespace Symfony\Component\Validator\Constraints; +use Symfony\Component\Validator\Attribute\HasNamedArguments; use Symfony\Component\Validator\Constraint; /** * Checks if a password has been leaked in a data breach. * - * @Annotation - * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) - * * @author Kévin Dunglas */ #[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] @@ -30,15 +28,17 @@ class NotCompromisedPassword extends Constraint self::COMPROMISED_PASSWORD_ERROR => 'COMPROMISED_PASSWORD_ERROR', ]; + public string $message = 'This password has been leaked in a data breach, it must not be used. Please use another password.'; + public int $threshold = 1; + public bool $skipOnError = false; + /** - * @deprecated since Symfony 6.1, use const ERROR_NAMES instead + * @param array|null $options + * @param positive-int|null $threshold The number of times the password should have been leaked to consider it is compromised (defaults to 1) + * @param bool|null $skipOnError Whether to ignore HTTP errors while requesting the API and thus consider the password valid (defaults to false) + * @param string[]|null $groups */ - protected static $errorNames = self::ERROR_NAMES; - - public $message = 'This password has been leaked in a data breach, it must not be used. Please use another password.'; - public $threshold = 1; - public $skipOnError = false; - + #[HasNamedArguments] public function __construct( ?array $options = null, ?string $message = null, @@ -47,6 +47,10 @@ public function __construct( ?array $groups = null, mixed $payload = null, ) { + if (\is_array($options)) { + trigger_deprecation('symfony/validator', '7.3', 'Passing an array of options to configure the "%s" constraint is deprecated, use named arguments instead.', static::class); + } + parent::__construct($options, $groups, $payload); $this->message = $message ?? $this->message; diff --git a/Constraints/NotCompromisedPasswordValidator.php b/Constraints/NotCompromisedPasswordValidator.php index 47a9942d8..3b1d25b4c 100644 --- a/Constraints/NotCompromisedPasswordValidator.php +++ b/Constraints/NotCompromisedPasswordValidator.php @@ -50,11 +50,9 @@ public function __construct(?HttpClientInterface $httpClient = null, string $cha } /** - * @return void - * * @throws ExceptionInterface */ - public function validate(mixed $value, Constraint $constraint) + public function validate(mixed $value, Constraint $constraint): void { if (!$constraint instanceof NotCompromisedPassword) { throw new UnexpectedTypeException($constraint, NotCompromisedPassword::class); diff --git a/Constraints/NotEqualTo.php b/Constraints/NotEqualTo.php index 9a5c07b21..02d466566 100644 --- a/Constraints/NotEqualTo.php +++ b/Constraints/NotEqualTo.php @@ -12,8 +12,7 @@ namespace Symfony\Component\Validator\Constraints; /** - * @Annotation - * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) + * Validates that a value is not equal to another value. * * @author Daniel Holmes * @author Bernhard Schussek @@ -27,10 +26,5 @@ class NotEqualTo extends AbstractComparison self::IS_EQUAL_ERROR => 'IS_EQUAL_ERROR', ]; - /** - * @deprecated since Symfony 6.1, use const ERROR_NAMES instead - */ - protected static $errorNames = self::ERROR_NAMES; - - public $message = 'This value should not be equal to {{ compared_value }}.'; + public string $message = 'This value should not be equal to {{ compared_value }}.'; } diff --git a/Constraints/NotIdenticalTo.php b/Constraints/NotIdenticalTo.php index 206c10613..3654bfb03 100644 --- a/Constraints/NotIdenticalTo.php +++ b/Constraints/NotIdenticalTo.php @@ -12,8 +12,7 @@ namespace Symfony\Component\Validator\Constraints; /** - * @Annotation - * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) + * Validates that a value is not identical to another value. * * @author Daniel Holmes * @author Bernhard Schussek @@ -27,10 +26,5 @@ class NotIdenticalTo extends AbstractComparison self::IS_IDENTICAL_ERROR => 'IS_IDENTICAL_ERROR', ]; - /** - * @deprecated since Symfony 6.1, use const ERROR_NAMES instead - */ - protected static $errorNames = self::ERROR_NAMES; - - public $message = 'This value should not be identical to {{ compared_value_type }} {{ compared_value }}.'; + public string $message = 'This value should not be identical to {{ compared_value_type }} {{ compared_value }}.'; } diff --git a/Constraints/NotNull.php b/Constraints/NotNull.php index b8523466c..28596925e 100644 --- a/Constraints/NotNull.php +++ b/Constraints/NotNull.php @@ -11,11 +11,11 @@ namespace Symfony\Component\Validator\Constraints; +use Symfony\Component\Validator\Attribute\HasNamedArguments; use Symfony\Component\Validator\Constraint; /** - * @Annotation - * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) + * Validates that a value is not strictly equal to null. * * @author Bernhard Schussek */ @@ -28,15 +28,19 @@ class NotNull extends Constraint self::IS_NULL_ERROR => 'IS_NULL_ERROR', ]; + public string $message = 'This value should not be null.'; + /** - * @deprecated since Symfony 6.1, use const ERROR_NAMES instead + * @param array|null $options + * @param string[]|null $groups */ - protected static $errorNames = self::ERROR_NAMES; - - public $message = 'This value should not be null.'; - + #[HasNamedArguments] public function __construct(?array $options = null, ?string $message = null, ?array $groups = null, mixed $payload = null) { + if (\is_array($options)) { + trigger_deprecation('symfony/validator', '7.3', 'Passing an array of options to configure the "%s" constraint is deprecated, use named arguments instead.', static::class); + } + parent::__construct($options ?? [], $groups, $payload); $this->message = $message ?? $this->message; diff --git a/Constraints/NotNullValidator.php b/Constraints/NotNullValidator.php index 3f8f95128..0fd773685 100644 --- a/Constraints/NotNullValidator.php +++ b/Constraints/NotNullValidator.php @@ -20,10 +20,7 @@ */ class NotNullValidator extends ConstraintValidator { - /** - * @return void - */ - public function validate(mixed $value, Constraint $constraint) + public function validate(mixed $value, Constraint $constraint): void { if (!$constraint instanceof NotNull) { throw new UnexpectedTypeException($constraint, NotNull::class); diff --git a/Constraints/Optional.php b/Constraints/Optional.php index dab8b4371..df331a676 100644 --- a/Constraints/Optional.php +++ b/Constraints/Optional.php @@ -12,9 +12,6 @@ namespace Symfony\Component\Validator\Constraints; /** - * @Annotation - * @Target({"ANNOTATION"}) - * * @author Bernhard Schussek */ class Optional extends Existence diff --git a/Constraints/PasswordStrength.php b/Constraints/PasswordStrength.php index 090d50d67..3867cfbda 100644 --- a/Constraints/PasswordStrength.php +++ b/Constraints/PasswordStrength.php @@ -11,12 +11,12 @@ namespace Symfony\Component\Validator\Constraints; +use Symfony\Component\Validator\Attribute\HasNamedArguments; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Exception\ConstraintDefinitionException; /** - * @Annotation - * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) + * Validates that the given password has reached a minimum strength. * * @author Florent Morselli */ @@ -39,8 +39,18 @@ final class PasswordStrength extends Constraint public int $minScore; + /** + * @param array|null $options + * @param self::STRENGTH_*|null $minScore The minimum required strength of the password (defaults to {@see PasswordStrength::STRENGTH_MEDIUM}) + * @param string[]|null $groups + */ + #[HasNamedArguments] public function __construct(?array $options = null, ?int $minScore = null, ?array $groups = null, mixed $payload = null, ?string $message = null) { + if (\is_array($options)) { + trigger_deprecation('symfony/validator', '7.3', 'Passing an array of options to configure the "%s" constraint is deprecated, use named arguments instead.', static::class); + } + $options['minScore'] ??= self::STRENGTH_MEDIUM; parent::__construct($options, $groups, $payload); diff --git a/Constraints/PasswordStrengthValidator.php b/Constraints/PasswordStrengthValidator.php index c3d2b7d76..509ea2a42 100644 --- a/Constraints/PasswordStrengthValidator.php +++ b/Constraints/PasswordStrengthValidator.php @@ -36,15 +36,16 @@ public function validate(#[\SensitiveParameter] mixed $value, Constraint $constr return; } - if (!\is_string($value)) { + if (!\is_string($value) && !$value instanceof \Stringable) { throw new UnexpectedValueException($value, 'string'); } $passwordStrengthEstimator = $this->passwordStrengthEstimator ?? self::estimateStrength(...); - $strength = $passwordStrengthEstimator($value); + $strength = $passwordStrengthEstimator((string) $value); if ($strength < $constraint->minScore) { $this->context->buildViolation($constraint->message) ->setCode(PasswordStrength::PASSWORD_STRENGTH_ERROR) + ->setParameter('{{ strength }}', $strength) ->addViolation(); } } @@ -56,7 +57,7 @@ public function validate(#[\SensitiveParameter] mixed $value, Constraint $constr * * @return PasswordStrength::STRENGTH_* */ - private static function estimateStrength(#[\SensitiveParameter] string $password): int + public static function estimateStrength(#[\SensitiveParameter] string $password): int { if (!$length = \strlen($password)) { return PasswordStrength::STRENGTH_VERY_WEAK; diff --git a/Constraints/Positive.php b/Constraints/Positive.php index 951e944c9..9a5aea43f 100644 --- a/Constraints/Positive.php +++ b/Constraints/Positive.php @@ -12,8 +12,7 @@ namespace Symfony\Component\Validator\Constraints; /** - * @Annotation - * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) + * Validates that a value is a positive number. * * @author Jan Schädlich */ @@ -22,5 +21,5 @@ class Positive extends GreaterThan { use ZeroComparisonConstraintTrait; - public $message = 'This value should be positive.'; + public string $message = 'This value should be positive.'; } diff --git a/Constraints/PositiveOrZero.php b/Constraints/PositiveOrZero.php index a7669c610..f1f761bcf 100644 --- a/Constraints/PositiveOrZero.php +++ b/Constraints/PositiveOrZero.php @@ -12,8 +12,7 @@ namespace Symfony\Component\Validator\Constraints; /** - * @Annotation - * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) + * Validates that a value is a positive number or equal to zero. * * @author Jan Schädlich */ @@ -22,5 +21,5 @@ class PositiveOrZero extends GreaterThanOrEqual { use ZeroComparisonConstraintTrait; - public $message = 'This value should be either positive or zero.'; + public string $message = 'This value should be either positive or zero.'; } diff --git a/Constraints/Range.php b/Constraints/Range.php index 48dc39487..aac582430 100644 --- a/Constraints/Range.php +++ b/Constraints/Range.php @@ -12,14 +12,14 @@ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\PropertyAccess\PropertyAccess; +use Symfony\Component\Validator\Attribute\HasNamedArguments; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Exception\ConstraintDefinitionException; use Symfony\Component\Validator\Exception\LogicException; use Symfony\Component\Validator\Exception\MissingOptionsException; /** - * @Annotation - * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) + * Validates that a given number or DateTime object is between some minimum and maximum. * * @author Bernhard Schussek */ @@ -38,21 +38,27 @@ class Range extends Constraint self::TOO_LOW_ERROR => 'TOO_LOW_ERROR', ]; + public string $notInRangeMessage = 'This value should be between {{ min }} and {{ max }}.'; + public string $minMessage = 'This value should be {{ limit }} or more.'; + public string $maxMessage = 'This value should be {{ limit }} or less.'; + public string $invalidMessage = 'This value should be a valid number.'; + public string $invalidDateTimeMessage = 'This value is not a valid datetime.'; + public mixed $min = null; + public ?string $minPropertyPath = null; + public mixed $max = null; + public ?string $maxPropertyPath = null; + /** - * @deprecated since Symfony 6.1, use const ERROR_NAMES instead + * @param array|null $options + * @param string|null $invalidMessage The message if min and max values are numeric but the given value is not + * @param string|null $invalidDateTimeMessage The message if min and max values are PHP datetimes but the given value is not + * @param int|float|non-empty-string|null $min The minimum value, either numeric or a datetime string representation + * @param non-empty-string|null $minPropertyPath Property path to the min value + * @param int|float|non-empty-string|null $max The maximum value, either numeric or a datetime string representation + * @param non-empty-string|null $maxPropertyPath Property path to the max value + * @param string[]|null $groups */ - protected static $errorNames = self::ERROR_NAMES; - - public $notInRangeMessage = 'This value should be between {{ min }} and {{ max }}.'; - public $minMessage = 'This value should be {{ limit }} or more.'; - public $maxMessage = 'This value should be {{ limit }} or less.'; - public $invalidMessage = 'This value should be a valid number.'; - public $invalidDateTimeMessage = 'This value is not a valid datetime.'; - public $min; - public $minPropertyPath; - public $max; - public $maxPropertyPath; - + #[HasNamedArguments] public function __construct( ?array $options = null, ?string $notInRangeMessage = null, @@ -67,6 +73,10 @@ public function __construct( ?array $groups = null, mixed $payload = null, ) { + if (\is_array($options)) { + trigger_deprecation('symfony/validator', '7.3', 'Passing an array of options to configure the "%s" constraint is deprecated, use named arguments instead.', static::class); + } + parent::__construct($options, $groups, $payload); $this->notInRangeMessage = $notInRangeMessage ?? $this->notInRangeMessage; diff --git a/Constraints/RangeValidator.php b/Constraints/RangeValidator.php index cc1109c36..8702ea840 100644 --- a/Constraints/RangeValidator.php +++ b/Constraints/RangeValidator.php @@ -25,17 +25,11 @@ */ class RangeValidator extends ConstraintValidator { - private ?PropertyAccessorInterface $propertyAccessor; - - public function __construct(?PropertyAccessorInterface $propertyAccessor = null) + public function __construct(private ?PropertyAccessorInterface $propertyAccessor = null) { - $this->propertyAccessor = $propertyAccessor; } - /** - * @return void - */ - public function validate(mixed $value, Constraint $constraint) + public function validate(mixed $value, Constraint $constraint): void { if (!$constraint instanceof Range) { throw new UnexpectedTypeException($constraint, Range::class); diff --git a/Constraints/Regex.php b/Constraints/Regex.php index 006e5c507..5c8501fa0 100644 --- a/Constraints/Regex.php +++ b/Constraints/Regex.php @@ -11,12 +11,12 @@ namespace Symfony\Component\Validator\Constraints; +use Symfony\Component\Validator\Attribute\HasNamedArguments; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Exception\InvalidArgumentException; /** - * @Annotation - * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) + * Validates that a value matches a regular expression. * * @author Bernhard Schussek */ @@ -29,18 +29,21 @@ class Regex extends Constraint self::REGEX_FAILED_ERROR => 'REGEX_FAILED_ERROR', ]; - /** - * @deprecated since Symfony 6.1, use const ERROR_NAMES instead - */ - protected static $errorNames = self::ERROR_NAMES; - - public $message = 'This value is not valid.'; - public $pattern; - public $htmlPattern; - public $match = true; + public string $message = 'This value is not valid.'; + public ?string $pattern = null; + public ?string $htmlPattern = null; + public bool $match = true; /** @var callable|null */ public $normalizer; + /** + * @param string|array|null $pattern The regular expression to match + * @param string|null $htmlPattern The pattern to use in the HTML5 pattern attribute + * @param bool|null $match Whether to validate the value matches the configured pattern or not (defaults to true) + * @param string[]|null $groups + * @param array|null $options + */ + #[HasNamedArguments] public function __construct( string|array|null $pattern, ?string $message = null, @@ -49,11 +52,19 @@ public function __construct( ?callable $normalizer = null, ?array $groups = null, mixed $payload = null, - array $options = [], + ?array $options = null, ) { if (\is_array($pattern)) { - $options = array_merge($pattern, $options); + trigger_deprecation('symfony/validator', '7.3', 'Passing an array of options to configure the "%s" constraint is deprecated, use named arguments instead.', static::class); + + $options = array_merge($pattern, $options ?? []); } elseif (null !== $pattern) { + if (\is_array($options)) { + trigger_deprecation('symfony/validator', '7.3', 'Passing an array of options to configure the "%s" constraint is deprecated, use named arguments instead.', static::class); + } else { + $options = []; + } + $options['value'] = $pattern; } @@ -90,9 +101,7 @@ public function getHtmlPattern(): ?string { // If htmlPattern is specified, use it if (null !== $this->htmlPattern) { - return empty($this->htmlPattern) - ? null - : $this->htmlPattern; + return $this->htmlPattern ?: null; } // Quit if delimiters not at very beginning/end (e.g. when options are passed) @@ -121,8 +130,6 @@ public function getHtmlPattern(): ?string $pattern = '^' === $pattern[0] ? substr($pattern, 1) : '.*'.$pattern; // Trim trailing $, otherwise append .* - $pattern = '$' === $pattern[\strlen($pattern) - 1] ? substr($pattern, 0, -1) : $pattern.'.*'; - - return $pattern; + return '$' === $pattern[\strlen($pattern) - 1] ? substr($pattern, 0, -1) : $pattern.'.*'; } } diff --git a/Constraints/RegexValidator.php b/Constraints/RegexValidator.php index 4e9ae9039..5823f5d7c 100644 --- a/Constraints/RegexValidator.php +++ b/Constraints/RegexValidator.php @@ -24,10 +24,7 @@ */ class RegexValidator extends ConstraintValidator { - /** - * @return void - */ - public function validate(mixed $value, Constraint $constraint) + public function validate(mixed $value, Constraint $constraint): void { if (!$constraint instanceof Regex) { throw new UnexpectedTypeException($constraint, Regex::class); diff --git a/Constraints/Required.php b/Constraints/Required.php index bd77a909f..2a1a27f6a 100644 --- a/Constraints/Required.php +++ b/Constraints/Required.php @@ -12,9 +12,6 @@ namespace Symfony\Component\Validator\Constraints; /** - * @Annotation - * @Target({"ANNOTATION"}) - * * @author Bernhard Schussek */ class Required extends Existence diff --git a/Constraints/Sequentially.php b/Constraints/Sequentially.php index 79901f480..6389ebb89 100644 --- a/Constraints/Sequentially.php +++ b/Constraints/Sequentially.php @@ -11,22 +11,31 @@ namespace Symfony\Component\Validator\Constraints; +use Symfony\Component\Validator\Attribute\HasNamedArguments; +use Symfony\Component\Validator\Constraint; + /** * Use this constraint to sequentially validate nested constraints. * Validation for the nested constraints collection will stop at first violation. * - * @Annotation - * @Target({"CLASS", "PROPERTY", "METHOD", "ANNOTATION"}) - * * @author Maxime Steinhausser */ #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] class Sequentially extends Composite { - public $constraints = []; + public array|Constraint $constraints = []; + /** + * @param Constraint[]|array|null $constraints An array of validation constraints + * @param string[]|null $groups + */ + #[HasNamedArguments] public function __construct(mixed $constraints = null, ?array $groups = null, mixed $payload = null) { + if (\is_array($constraints) && !array_is_list($constraints)) { + trigger_deprecation('symfony/validator', '7.3', 'Passing an array of options to configure the "%s" constraint is deprecated, use named arguments instead.', static::class); + } + parent::__construct($constraints ?? [], $groups, $payload); } diff --git a/Constraints/SequentiallyValidator.php b/Constraints/SequentiallyValidator.php index d076f3cfd..f5b4cb243 100644 --- a/Constraints/SequentiallyValidator.php +++ b/Constraints/SequentiallyValidator.php @@ -20,10 +20,7 @@ */ class SequentiallyValidator extends ConstraintValidator { - /** - * @return void - */ - public function validate(mixed $value, Constraint $constraint) + public function validate(mixed $value, Constraint $constraint): void { if (!$constraint instanceof Sequentially) { throw new UnexpectedTypeException($constraint, Sequentially::class); diff --git a/Constraints/Time.php b/Constraints/Time.php index b3adbfd9e..a99702cb2 100644 --- a/Constraints/Time.php +++ b/Constraints/Time.php @@ -11,11 +11,11 @@ namespace Symfony\Component\Validator\Constraints; +use Symfony\Component\Validator\Attribute\HasNamedArguments; use Symfony\Component\Validator\Constraint; /** - * @Annotation - * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) + * Validates that a value is a valid time that follows the H:i:s format. * * @author Bernhard Schussek */ @@ -30,14 +30,15 @@ class Time extends Constraint self::INVALID_TIME_ERROR => 'INVALID_TIME_ERROR', ]; + public bool $withSeconds = true; + public string $message = 'This value is not a valid time.'; + /** - * @deprecated since Symfony 6.1, use const ERROR_NAMES instead + * @param array|null $options + * @param string[]|null $groups + * @param bool|null $withSeconds Whether to allow seconds in the given value (defaults to true) */ - protected static $errorNames = self::ERROR_NAMES; - - public $withSeconds = true; - public $message = 'This value is not a valid time.'; - + #[HasNamedArguments] public function __construct( ?array $options = null, ?string $message = null, @@ -45,6 +46,10 @@ public function __construct( mixed $payload = null, ?bool $withSeconds = null, ) { + if (\is_array($options)) { + trigger_deprecation('symfony/validator', '7.3', 'Passing an array of options to configure the "%s" constraint is deprecated, use named arguments instead.', static::class); + } + parent::__construct($options, $groups, $payload); $this->withSeconds = $withSeconds ?? $this->withSeconds; diff --git a/Constraints/TimeValidator.php b/Constraints/TimeValidator.php index ef422cdf5..42f00c24b 100644 --- a/Constraints/TimeValidator.php +++ b/Constraints/TimeValidator.php @@ -34,10 +34,7 @@ public static function checkTime(int $hour, int $minute, float $second): bool return $hour >= 0 && $hour < 24 && $minute >= 0 && $minute < 60 && $second >= 0 && $second < 60; } - /** - * @return void - */ - public function validate(mixed $value, Constraint $constraint) + public function validate(mixed $value, Constraint $constraint): void { if (!$constraint instanceof Time) { throw new UnexpectedTypeException($constraint, Time::class); diff --git a/Constraints/Timezone.php b/Constraints/Timezone.php index 17d740a49..93b0692ef 100644 --- a/Constraints/Timezone.php +++ b/Constraints/Timezone.php @@ -11,12 +11,12 @@ namespace Symfony\Component\Validator\Constraints; +use Symfony\Component\Validator\Attribute\HasNamedArguments; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Exception\ConstraintDefinitionException; /** - * @Annotation - * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) + * Validates that a value is a valid timezone identifier. * * @author Javier Spagnoletti * @author Hugo Hamon @@ -29,10 +29,10 @@ class Timezone extends Constraint public const TIMEZONE_IDENTIFIER_IN_COUNTRY_ERROR = 'c4a22222-dc92-4fc0-abb0-d95b268c7d0b'; public const TIMEZONE_IDENTIFIER_INTL_ERROR = '45863c26-88dc-41ba-bf53-c73bd1f7e90d'; - public $zone = \DateTimeZone::ALL; - public $countryCode; - public $intlCompatible = false; - public $message = 'This value is not a valid timezone.'; + public int $zone = \DateTimeZone::ALL; + public ?string $countryCode = null; + public bool $intlCompatible = false; + public string $message = 'This value is not a valid timezone.'; protected const ERROR_NAMES = [ self::TIMEZONE_IDENTIFIER_ERROR => 'TIMEZONE_IDENTIFIER_ERROR', @@ -42,10 +42,15 @@ class Timezone extends Constraint ]; /** - * @deprecated since Symfony 6.1, use const ERROR_NAMES instead + * @param int|array|null $zone Restrict valid timezones to this geographical zone (defaults to {@see \DateTimeZone::ALL}) + * @param string|null $countryCode Restrict the valid timezones to this country if the zone option is {@see \DateTimeZone::PER_COUNTRY} + * @param bool|null $intlCompatible Whether to restrict valid timezones to ones available in PHP's intl (defaults to false) + * @param string[]|null $groups + * @param array|null $options + * + * @see \DateTimeZone */ - protected static $errorNames = self::ERROR_NAMES; - + #[HasNamedArguments] public function __construct( int|array|null $zone = null, ?string $message = null, @@ -53,11 +58,19 @@ public function __construct( ?bool $intlCompatible = null, ?array $groups = null, mixed $payload = null, - array $options = [], + ?array $options = null, ) { if (\is_array($zone)) { - $options = array_merge($zone, $options); + trigger_deprecation('symfony/validator', '7.3', 'Passing an array of options to configure the "%s" constraint is deprecated, use named arguments instead.', static::class); + + $options = array_merge($zone, $options ?? []); } elseif (null !== $zone) { + if (\is_array($options)) { + trigger_deprecation('symfony/validator', '7.3', 'Passing an array of options to configure the "%s" constraint is deprecated, use named arguments instead.', static::class); + } else { + $options = []; + } + $options['value'] = $zone; } diff --git a/Constraints/TimezoneValidator.php b/Constraints/TimezoneValidator.php index 409deec7c..c6c7cbbfc 100644 --- a/Constraints/TimezoneValidator.php +++ b/Constraints/TimezoneValidator.php @@ -26,10 +26,7 @@ */ class TimezoneValidator extends ConstraintValidator { - /** - * @return void - */ - public function validate(mixed $value, Constraint $constraint) + public function validate(mixed $value, Constraint $constraint): void { if (!$constraint instanceof Timezone) { throw new UnexpectedTypeException($constraint, Timezone::class); diff --git a/Constraints/Traverse.php b/Constraints/Traverse.php index f4754ff0a..d8546e323 100644 --- a/Constraints/Traverse.php +++ b/Constraints/Traverse.php @@ -11,26 +11,35 @@ namespace Symfony\Component\Validator\Constraints; +use Symfony\Component\Validator\Attribute\HasNamedArguments; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Exception\ConstraintDefinitionException; /** - * @Annotation + * Validates an object that needs to be traversed. * * @author Bernhard Schussek */ #[\Attribute(\Attribute::TARGET_CLASS)] class Traverse extends Constraint { - public $traverse = true; + public bool $traverse = true; - public function __construct(bool|array|null $traverse = null) + /** + * @param bool|array|null $traverse Whether to traverse the given object or not (defaults to true). Pass an associative array to configure the constraint's options (e.g. payload). + */ + #[HasNamedArguments] + public function __construct(bool|array|null $traverse = null, mixed $payload = null) { if (\is_array($traverse) && \array_key_exists('groups', $traverse)) { throw new ConstraintDefinitionException(\sprintf('The option "groups" is not supported by the constraint "%s".', __CLASS__)); } - parent::__construct($traverse); + if (\is_array($traverse)) { + trigger_deprecation('symfony/validator', '7.3', 'Passing an array of options to configure the "%s" constraint is deprecated, use named arguments instead.', static::class); + } + + parent::__construct($traverse, null, $payload); } public function getDefaultOption(): ?string diff --git a/Constraints/Type.php b/Constraints/Type.php index e6f479789..f3fe56dbb 100644 --- a/Constraints/Type.php +++ b/Constraints/Type.php @@ -11,11 +11,11 @@ namespace Symfony\Component\Validator\Constraints; +use Symfony\Component\Validator\Attribute\HasNamedArguments; use Symfony\Component\Validator\Constraint; /** - * @Annotation - * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) + * Validates that a value is of a specific data type. * * @author Bernhard Schussek */ @@ -28,20 +28,31 @@ class Type extends Constraint self::INVALID_TYPE_ERROR => 'INVALID_TYPE_ERROR', ]; + public string $message = 'This value should be of type {{ type }}.'; + public string|array|null $type = null; + /** - * @deprecated since Symfony 6.1, use const ERROR_NAMES instead + * @param string|list|array|null $type The type(s) to enforce on the value + * @param string[]|null $groups + * @param array|null $options */ - protected static $errorNames = self::ERROR_NAMES; - - public $message = 'This value should be of type {{ type }}.'; - public $type; - - public function __construct(string|array|null $type, ?string $message = null, ?array $groups = null, mixed $payload = null, array $options = []) + #[HasNamedArguments] + public function __construct(string|array|null $type, ?string $message = null, ?array $groups = null, mixed $payload = null, ?array $options = null) { if (\is_array($type) && \is_string(key($type))) { - $options = array_merge($type, $options); + trigger_deprecation('symfony/validator', '7.3', 'Passing an array of options to configure the "%s" constraint is deprecated, use named arguments instead.', static::class); + + $options = array_merge($type, $options ?? []); } elseif (null !== $type) { + if (\is_array($options)) { + trigger_deprecation('symfony/validator', '7.3', 'Passing an array of options to configure the "%s" constraint is deprecated, use named arguments instead.', static::class); + } else { + $options = []; + } + $options['value'] = $type; + } elseif (\is_array($options)) { + trigger_deprecation('symfony/validator', '7.3', 'Passing an array of options to configure the "%s" constraint is deprecated, use named arguments instead.', static::class); } parent::__construct($options, $groups, $payload); diff --git a/Constraints/TypeValidator.php b/Constraints/TypeValidator.php index 0b4513ac0..94c0c8639 100644 --- a/Constraints/TypeValidator.php +++ b/Constraints/TypeValidator.php @@ -36,6 +36,8 @@ class TypeValidator extends ConstraintValidator 'string' => 'is_string', 'scalar' => 'is_scalar', 'array' => 'is_array', + 'list' => 'is_array && array_is_list', + 'associative_array' => 'is_array && !array_is_list', 'iterable' => 'is_iterable', 'countable' => 'is_countable', 'callable' => 'is_callable', @@ -55,10 +57,7 @@ class TypeValidator extends ConstraintValidator 'xdigit' => 'ctype_xdigit', ]; - /** - * @return void - */ - public function validate(mixed $value, Constraint $constraint) + public function validate(mixed $value, Constraint $constraint): void { if (!$constraint instanceof Type) { throw new UnexpectedTypeException($constraint, Type::class); @@ -76,6 +75,8 @@ public function validate(mixed $value, Constraint $constraint) 'finite-float' => \is_float($value) && is_finite($value), 'finite-number' => \is_int($value) || \is_float($value) && is_finite($value), 'number' => \is_int($value) || \is_float($value) && !is_nan($value), + 'list' => \is_array($value) && array_is_list($value), + 'associative_array' => \is_array($value) && !array_is_list($value), default => self::VALIDATION_FUNCTIONS[$type]($value), }) { return; diff --git a/Constraints/Ulid.php b/Constraints/Ulid.php index 5b2b825a7..91d395fd2 100644 --- a/Constraints/Ulid.php +++ b/Constraints/Ulid.php @@ -11,10 +11,14 @@ namespace Symfony\Component\Validator\Constraints; +use Symfony\Component\Validator\Attribute\HasNamedArguments; use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\Exception\ConstraintDefinitionException; /** - * @Annotation + * Validates that a value is a valid Universally Unique Lexicographically Sortable Identifier (ULID). + * + * @see https://github.com/ulid/spec * * @author Laurent Clouet */ @@ -23,31 +27,49 @@ class Ulid extends Constraint { public const TOO_SHORT_ERROR = '7b44804e-37d5-4df4-9bdd-b738d4a45bb4'; public const TOO_LONG_ERROR = '9608249f-6da1-4d53-889e-9864b58c4d37'; - public const INVALID_CHARACTERS_ERROR = 'e4155739-5135-4258-9c81-ae7b44b5311e'; public const TOO_LARGE_ERROR = 'df8cfb9a-ce6d-4a69-ae5a-eea7ab6f278b'; + public const INVALID_CHARACTERS_ERROR = 'e4155739-5135-4258-9c81-ae7b44b5311e'; + public const INVALID_FORMAT_ERROR = '34d5cdd7-5aac-4ba0-b9a2-b45e0bab3e2e'; protected const ERROR_NAMES = [ self::TOO_SHORT_ERROR => 'TOO_SHORT_ERROR', self::TOO_LONG_ERROR => 'TOO_LONG_ERROR', - self::INVALID_CHARACTERS_ERROR => 'INVALID_CHARACTERS_ERROR', self::TOO_LARGE_ERROR => 'TOO_LARGE_ERROR', + self::INVALID_CHARACTERS_ERROR => 'INVALID_CHARACTERS_ERROR', + self::INVALID_FORMAT_ERROR => 'INVALID_FORMAT_ERROR', ]; - /** - * @deprecated since Symfony 6.1, use const ERROR_NAMES instead - */ - protected static $errorNames = self::ERROR_NAMES; + public const FORMAT_BASE_32 = 'base32'; + public const FORMAT_BASE_58 = 'base58'; + public const FORMAT_RFC_4122 = 'rfc4122'; - public $message = 'This is not a valid ULID.'; + public string $message = 'This is not a valid ULID.'; + public string $format = self::FORMAT_BASE_32; + /** + * @param array|null $options + * @param string[]|null $groups + * @param self::FORMAT_*|null $format + */ + #[HasNamedArguments] public function __construct( ?array $options = null, ?string $message = null, ?array $groups = null, mixed $payload = null, + ?string $format = null, ) { + if (\is_array($options)) { + trigger_deprecation('symfony/validator', '7.3', 'Passing an array of options to configure the "%s" constraint is deprecated, use named arguments instead.', static::class); + } + parent::__construct($options, $groups, $payload); $this->message = $message ?? $this->message; + $this->format = $format ?? $this->format; + + if (!\in_array($this->format, [self::FORMAT_BASE_32, self::FORMAT_BASE_58, self::FORMAT_RFC_4122], true)) { + throw new ConstraintDefinitionException(\sprintf('The "%s" validation format is not supported.', $format)); + } } } diff --git a/Constraints/UlidValidator.php b/Constraints/UlidValidator.php index ad47f66d4..ae49ad34b 100644 --- a/Constraints/UlidValidator.php +++ b/Constraints/UlidValidator.php @@ -24,10 +24,7 @@ */ class UlidValidator extends ConstraintValidator { - /** - * @return void - */ - public function validate(mixed $value, Constraint $constraint) + public function validate(mixed $value, Constraint $constraint): void { if (!$constraint instanceof Ulid) { throw new UnexpectedTypeException($constraint, Ulid::class); @@ -43,31 +40,58 @@ public function validate(mixed $value, Constraint $constraint) $value = (string) $value; - if (26 !== \strlen($value)) { + [$requiredLength, $requiredCharset] = match ($constraint->format) { + Ulid::FORMAT_BASE_32 => [26, '0123456789ABCDEFGHJKMNPQRSTVWXYZabcdefghjkmnpqrstvwxyz'], + Ulid::FORMAT_BASE_58 => [22, '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'], + Ulid::FORMAT_RFC_4122 => [36, '0123456789ABCDEFabcdef-'], + }; + + if ($requiredLength !== \strlen($value)) { $this->context->buildViolation($constraint->message) - ->setParameter('{{ value }}', $this->formatValue($value)) - ->setCode(26 > \strlen($value) ? Ulid::TOO_SHORT_ERROR : Ulid::TOO_LONG_ERROR) + ->setParameters([ + '{{ value }}' => $this->formatValue($value), + '{{ format }}' => $constraint->format, + ]) + ->setCode($requiredLength > \strlen($value) ? Ulid::TOO_SHORT_ERROR : Ulid::TOO_LONG_ERROR) ->addViolation(); return; } - if (\strlen($value) !== strspn($value, '0123456789ABCDEFGHJKMNPQRSTVWXYZabcdefghjkmnpqrstvwxyz')) { + if (\strlen($value) !== strspn($value, $requiredCharset)) { $this->context->buildViolation($constraint->message) - ->setParameter('{{ value }}', $this->formatValue($value)) + ->setParameters([ + '{{ value }}' => $this->formatValue($value), + '{{ format }}' => $constraint->format, + ]) ->setCode(Ulid::INVALID_CHARACTERS_ERROR) ->addViolation(); return; } - // Largest valid ULID is '7ZZZZZZZZZZZZZZZZZZZZZZZZZ' - // Cf https://github.com/ulid/spec#overflow-errors-when-parsing-base32-strings - if ($value[0] > '7') { - $this->context->buildViolation($constraint->message) - ->setParameter('{{ value }}', $this->formatValue($value)) - ->setCode(Ulid::TOO_LARGE_ERROR) - ->addViolation(); + if (Ulid::FORMAT_BASE_32 === $constraint->format) { + // Largest valid ULID is '7ZZZZZZZZZZZZZZZZZZZZZZZZZ' + // Cf https://github.com/ulid/spec#overflow-errors-when-parsing-base32-strings + if ($value[0] > '7') { + $this->context->buildViolation($constraint->message) + ->setParameters([ + '{{ value }}' => $this->formatValue($value), + '{{ format }}' => $constraint->format, + ]) + ->setCode(Ulid::TOO_LARGE_ERROR) + ->addViolation(); + } + } elseif (Ulid::FORMAT_RFC_4122 === $constraint->format) { + if (!preg_match('/^[^-]{8}-[^-]{4}-[^-]{4}-[^-]{4}-[^-]{12}$/', $value)) { + $this->context->buildViolation($constraint->message) + ->setParameters([ + '{{ value }}' => $this->formatValue($value), + '{{ format }}' => $constraint->format, + ]) + ->setCode(Ulid::INVALID_FORMAT_ERROR) + ->addViolation(); + } } } } diff --git a/Constraints/Unique.php b/Constraints/Unique.php index 4f77d3a7f..1e6503785 100644 --- a/Constraints/Unique.php +++ b/Constraints/Unique.php @@ -11,12 +11,12 @@ namespace Symfony\Component\Validator\Constraints; +use Symfony\Component\Validator\Attribute\HasNamedArguments; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Exception\InvalidArgumentException; /** - * @Annotation - * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) + * Validates that all the elements of the given collection are unique. * * @author Yevgeniy Zholkevskiy */ @@ -26,23 +26,23 @@ class Unique extends Constraint public const IS_NOT_UNIQUE = '7911c98d-b845-4da0-94b7-a8dac36bc55a'; public array|string $fields = []; + public ?string $errorPath = null; + public bool $stopOnFirstError = true; protected const ERROR_NAMES = [ self::IS_NOT_UNIQUE => 'IS_NOT_UNIQUE', ]; - /** - * @deprecated since Symfony 6.1, use const ERROR_NAMES instead - */ - protected static $errorNames = self::ERROR_NAMES; - - public $message = 'This collection should contain only unique elements.'; + public string $message = 'This collection should contain only unique elements.'; /** @var callable|null */ public $normalizer; /** - * @param array|string $fields the combination of fields that must contain unique values or a set of options + * @param array|null $options + * @param string[]|null $groups + * @param string[]|string|null $fields Defines the key or keys in the collection that should be checked for uniqueness (defaults to null, which ensure uniqueness for all keys) */ + #[HasNamedArguments] public function __construct( ?array $options = null, ?string $message = null, @@ -50,12 +50,20 @@ public function __construct( ?array $groups = null, mixed $payload = null, array|string|null $fields = null, + ?string $errorPath = null, + ?bool $stopOnFirstError = null, ) { + if (\is_array($options)) { + trigger_deprecation('symfony/validator', '7.3', 'Passing an array of options to configure the "%s" constraint is deprecated, use named arguments instead.', static::class); + } + parent::__construct($options, $groups, $payload); $this->message = $message ?? $this->message; $this->normalizer = $normalizer ?? $this->normalizer; $this->fields = $fields ?? $this->fields; + $this->errorPath = $errorPath ?? $this->errorPath; + $this->stopOnFirstError = $stopOnFirstError ?? $this->stopOnFirstError; if (null !== $this->normalizer && !\is_callable($this->normalizer)) { throw new InvalidArgumentException(\sprintf('The "normalizer" option must be a valid callable ("%s" given).', get_debug_type($this->normalizer))); diff --git a/Constraints/UniqueValidator.php b/Constraints/UniqueValidator.php index 94d6a8965..bd78cac72 100644 --- a/Constraints/UniqueValidator.php +++ b/Constraints/UniqueValidator.php @@ -21,10 +21,7 @@ */ class UniqueValidator extends ConstraintValidator { - /** - * @return void - */ - public function validate(mixed $value, Constraint $constraint) + public function validate(mixed $value, Constraint $constraint): void { if (!$constraint instanceof Unique) { throw new UnexpectedTypeException($constraint, Unique::class); @@ -42,32 +39,37 @@ public function validate(mixed $value, Constraint $constraint) $collectionElements = []; $normalizer = $this->getNormalizer($constraint); - foreach ($value as $element) { + foreach ($value as $index => $element) { $element = $normalizer($element); if ($fields && !$element = $this->reduceElementKeys($fields, $element)) { continue; } - if (\in_array($element, $collectionElements, true)) { - $this->context->buildViolation($constraint->message) - ->setParameter('{{ value }}', $this->formatValue($element)) - ->setCode(Unique::IS_NOT_UNIQUE) - ->addViolation(); + if (!\in_array($element, $collectionElements, true)) { + $collectionElements[] = $element; + continue; + } + + $violationBuilder = $this->context->buildViolation($constraint->message) + ->setParameter('{{ value }}', $this->formatValue($element)) + ->setCode(Unique::IS_NOT_UNIQUE); + + if (!$constraint->stopOnFirstError || null !== $constraint->errorPath) { + $violationBuilder->atPath("[$index]".(null !== $constraint->errorPath ? ".{$constraint->errorPath}" : '')); + } + + $violationBuilder->addViolation(); + if ($constraint->stopOnFirstError) { return; } - $collectionElements[] = $element; } } private function getNormalizer(Unique $unique): callable { - if (null === $unique->normalizer) { - return static fn ($value) => $value; - } - - return $unique->normalizer; + return $unique->normalizer ?? static fn ($value) => $value; } private function reduceElementKeys(array $fields, array $element): array diff --git a/Constraints/Url.php b/Constraints/Url.php index 0986d8a17..b3e7256a0 100644 --- a/Constraints/Url.php +++ b/Constraints/Url.php @@ -11,12 +11,12 @@ namespace Symfony\Component\Validator\Constraints; +use Symfony\Component\Validator\Attribute\HasNamedArguments; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Exception\InvalidArgumentException; /** - * @Annotation - * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) + * Validates that a value is a valid URL string. * * @author Bernhard Schussek */ @@ -24,22 +24,29 @@ class Url extends Constraint { public const INVALID_URL_ERROR = '57c2f299-1154-4870-89bb-ef3b1f5ad229'; + public const MISSING_TLD_ERROR = '8a5d387f-0716-46b4-844b-67367faf435a'; protected const ERROR_NAMES = [ self::INVALID_URL_ERROR => 'INVALID_URL_ERROR', + self::MISSING_TLD_ERROR => 'MISSING_TLD_ERROR', ]; - /** - * @deprecated since Symfony 6.1, use const ERROR_NAMES instead - */ - protected static $errorNames = self::ERROR_NAMES; - - public $message = 'This value is not a valid URL.'; - public $protocols = ['http', 'https']; - public $relativeProtocol = false; + public string $message = 'This value is not a valid URL.'; + public string $tldMessage = 'This URL is missing a top-level domain.'; + public array $protocols = ['http', 'https']; + public bool $relativeProtocol = false; + public bool $requireTld = false; /** @var callable|null */ public $normalizer; + /** + * @param array|null $options + * @param string[]|null $protocols The protocols considered to be valid for the URL (e.g. http, https, ftp, etc.) (defaults to ['http', 'https'] + * @param bool|null $relativeProtocol Whether to accept URL without the protocol (i.e. //example.com) (defaults to false) + * @param string[]|null $groups + * @param bool|null $requireTld Whether to require the URL to include a top-level domain (defaults to false) + */ + #[HasNamedArguments] public function __construct( ?array $options = null, ?string $message = null, @@ -48,13 +55,25 @@ public function __construct( ?callable $normalizer = null, ?array $groups = null, mixed $payload = null, + ?bool $requireTld = null, + ?string $tldMessage = null, ) { + if (\is_array($options)) { + trigger_deprecation('symfony/validator', '7.3', 'Passing an array of options to configure the "%s" constraint is deprecated, use named arguments instead.', static::class); + } + parent::__construct($options, $groups, $payload); + if (null === ($options['requireTld'] ?? $requireTld)) { + trigger_deprecation('symfony/validator', '7.1', 'Not passing a value for the "requireTld" option to the Url constraint is deprecated. Its default value will change to "true".'); + } + $this->message = $message ?? $this->message; $this->protocols = $protocols ?? $this->protocols; $this->relativeProtocol = $relativeProtocol ?? $this->relativeProtocol; $this->normalizer = $normalizer ?? $this->normalizer; + $this->requireTld = $requireTld ?? $this->requireTld; + $this->tldMessage = $tldMessage ?? $this->tldMessage; if (null !== $this->normalizer && !\is_callable($this->normalizer)) { throw new InvalidArgumentException(\sprintf('The "normalizer" option must be a valid callable ("%s" given).', get_debug_type($this->normalizer))); diff --git a/Constraints/UrlValidator.php b/Constraints/UrlValidator.php index 55a545e8b..dc4c2486a 100644 --- a/Constraints/UrlValidator.php +++ b/Constraints/UrlValidator.php @@ -52,10 +52,7 @@ class UrlValidator extends ConstraintValidator $}ixuD REGEX; - /** - * @return void - */ - public function validate(mixed $value, Constraint $constraint) + public function validate(mixed $value, Constraint $constraint): void { if (!$constraint instanceof Url) { throw new UnexpectedTypeException($constraint, Url::class); @@ -89,5 +86,16 @@ public function validate(mixed $value, Constraint $constraint) return; } + + if ($constraint->requireTld) { + $urlHost = parse_url(https://github.com/$value,%20/PHP_URL_HOST); + // the host of URLs with a TLD must include at least a '.' (but it can't be an IP address like '127.0.0.1') + if (!str_contains($urlHost, '.') || filter_var($urlHost, \FILTER_VALIDATE_IP)) { + $this->context->buildViolation($constraint->tldMessage) + ->setParameter('{{ value }}', $this->formatValue($value)) + ->setCode(Url::MISSING_TLD_ERROR) + ->addViolation(); + } + } } } diff --git a/Constraints/Uuid.php b/Constraints/Uuid.php index 87fb9b322..9c6526457 100644 --- a/Constraints/Uuid.php +++ b/Constraints/Uuid.php @@ -11,11 +11,15 @@ namespace Symfony\Component\Validator\Constraints; +use Symfony\Component\Validator\Attribute\HasNamedArguments; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Exception\InvalidArgumentException; /** - * @Annotation + * Validates that a value is a valid Universally unique identifier (UUID). + * + * @see https://en.wikipedia.org/wiki/Universally_unique_identifier + * @see https://datatracker.ietf.org/doc/html/rfc4122 * * @author Colin O'Dell * @author Bernhard Schussek @@ -40,11 +44,6 @@ class Uuid extends Constraint self::INVALID_VARIANT_ERROR => 'INVALID_VARIANT_ERROR', ]; - /** - * @deprecated since Symfony 6.1, use const ERROR_NAMES instead - */ - protected static $errorNames = self::ERROR_NAMES; - // Possible versions defined by RFC 9562/4122 public const V1_MAC = 1; public const V2_DCE = 2; @@ -74,19 +73,15 @@ class Uuid extends Constraint /** * Message to display when validation fails. - * - * @var string */ - public $message = 'This is not a valid UUID.'; + public string $message = 'This is not a valid UUID.'; /** * Strict mode only allows UUIDs that meet the formal definition and formatting per RFC 9562/4122. * * Set this to `false` to allow legacy formats with different dash positioning or wrapping characters - * - * @var bool */ - public $strict = true; + public bool $strict = true; /** * Array of allowed versions (see version constants above). @@ -95,14 +90,18 @@ class Uuid extends Constraint * * @var int[] */ - public $versions = self::ALL_VERSIONS; + public array $versions = self::ALL_VERSIONS; /** @var callable|null */ public $normalizer; /** - * @param int[]|int|null $versions + * @param array|null $options + * @param self::V*[]|self::V*|null $versions Specific UUID versions (defaults to {@see Uuid::ALL_VERSIONS}) + * @param bool|null $strict Whether to force the value to follow the RFC's input format rules; pass false to allow alternate formats (defaults to true) + * @param string[]|null $groups */ + #[HasNamedArguments] public function __construct( ?array $options = null, ?string $message = null, @@ -112,6 +111,10 @@ public function __construct( ?array $groups = null, mixed $payload = null, ) { + if (\is_array($options)) { + trigger_deprecation('symfony/validator', '7.3', 'Passing an array of options to configure the "%s" constraint is deprecated, use named arguments instead.', static::class); + } + parent::__construct($options, $groups, $payload); $this->message = $message ?? $this->message; diff --git a/Constraints/UuidValidator.php b/Constraints/UuidValidator.php index 120f8a426..b7a553e3f 100644 --- a/Constraints/UuidValidator.php +++ b/Constraints/UuidValidator.php @@ -59,10 +59,7 @@ class UuidValidator extends ConstraintValidator public const LOOSE_MAX_LENGTH = 39; public const LOOSE_FIRST_HYPHEN_POSITION = 4; - /** - * @return void - */ - public function validate(mixed $value, Constraint $constraint) + public function validate(mixed $value, Constraint $constraint): void { if (!$constraint instanceof Uuid) { throw new UnexpectedTypeException($constraint, Uuid::class); diff --git a/Constraints/Valid.php b/Constraints/Valid.php index b58686e20..48deae8ac 100644 --- a/Constraints/Valid.php +++ b/Constraints/Valid.php @@ -11,21 +11,31 @@ namespace Symfony\Component\Validator\Constraints; +use Symfony\Component\Validator\Attribute\HasNamedArguments; use Symfony\Component\Validator\Constraint; /** - * @Annotation - * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) + * Validates an object embedded in an object's property. * * @author Bernhard Schussek */ #[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] class Valid extends Constraint { - public $traverse = true; + public bool $traverse = true; + /** + * @param array|null $options + * @param string[]|null $groups + * @param bool|null $traverse Whether to validate {@see \Traversable} objects (defaults to true) + */ + #[HasNamedArguments] public function __construct(?array $options = null, ?array $groups = null, $payload = null, ?bool $traverse = null) { + if (\is_array($options)) { + trigger_deprecation('symfony/validator', '7.3', 'Passing an array of options to configure the "%s" constraint is deprecated, use named arguments instead.', static::class); + } + parent::__construct($options ?? [], $groups, $payload); $this->traverse = $traverse ?? $this->traverse; @@ -41,10 +51,7 @@ public function __get(string $option): mixed return parent::__get($option); } - /** - * @return void - */ - public function addImplicitGroupName(string $group) + public function addImplicitGroupName(string $group): void { if (null !== $this->groups) { parent::addImplicitGroupName($group); diff --git a/Constraints/ValidValidator.php b/Constraints/ValidValidator.php index 7c960ffee..b1a02d2e9 100644 --- a/Constraints/ValidValidator.php +++ b/Constraints/ValidValidator.php @@ -20,10 +20,7 @@ */ class ValidValidator extends ConstraintValidator { - /** - * @return void - */ - public function validate(mixed $value, Constraint $constraint) + public function validate(mixed $value, Constraint $constraint): void { if (!$constraint instanceof Valid) { throw new UnexpectedTypeException($constraint, Valid::class); diff --git a/Constraints/Week.php b/Constraints/Week.php new file mode 100644 index 000000000..f40f3462a --- /dev/null +++ b/Constraints/Week.php @@ -0,0 +1,70 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Constraints; + +use Symfony\Component\Validator\Attribute\HasNamedArguments; +use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\Exception\ConstraintDefinitionException; + +/** + * @author Alexandre Daubois + */ +#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] +final class Week extends Constraint +{ + public const INVALID_FORMAT_ERROR = '19012dd1-01c8-4ce8-959f-72ad22684f5f'; + public const INVALID_WEEK_NUMBER_ERROR = 'd67ebfc9-45fe-4e4c-a038-5eaa56895ea3'; + public const TOO_LOW_ERROR = '9b506423-77a3-4749-aa34-c822a08be978'; + public const TOO_HIGH_ERROR = '85156377-d1e6-42cd-8f6e-dc43c2ecb72b'; + + protected const ERROR_NAMES = [ + self::INVALID_FORMAT_ERROR => 'INVALID_FORMAT_ERROR', + self::INVALID_WEEK_NUMBER_ERROR => 'INVALID_WEEK_NUMBER_ERROR', + self::TOO_LOW_ERROR => 'TOO_LOW_ERROR', + self::TOO_HIGH_ERROR => 'TOO_HIGH_ERROR', + ]; + + /** + * @param non-empty-string|null $min + * @param non-empty-string|null $max + */ + #[HasNamedArguments] + public function __construct( + public ?string $min = null, + public ?string $max = null, + public string $invalidFormatMessage = 'This value does not represent a valid week in the ISO 8601 format.', + public string $invalidWeekNumberMessage = 'This value is not a valid week.', + public string $tooLowMessage = 'This value should not be before week "{{ min }}".', + public string $tooHighMessage = 'This value should not be after week "{{ max }}".', + ?array $groups = null, + mixed $payload = null, + ) { + parent::__construct(null, $groups, $payload); + + if (null !== $min && !preg_match('/^\d{4}-W(0[1-9]|[1-4][0-9]|5[0-3])$/', $min)) { + throw new ConstraintDefinitionException(\sprintf('The "%s" constraint requires the min week to be in the ISO 8601 format if set.', __CLASS__)); + } + + if (null !== $max && !preg_match('/^\d{4}-W(0[1-9]|[1-4][0-9]|5[0-3])$/', $max)) { + throw new ConstraintDefinitionException(\sprintf('The "%s" constraint requires the max week to be in the ISO 8601 format if set.', __CLASS__)); + } + + if (null !== $min && null !== $max) { + [$minYear, $minWeekNumber] = explode('-W', $min, 2); + [$maxYear, $maxWeekNumber] = explode('-W', $max, 2); + + if ($minYear > $maxYear || ($minYear === $maxYear && $minWeekNumber > $maxWeekNumber)) { + throw new ConstraintDefinitionException(\sprintf('The "%s" constraint requires the min week to be less than or equal to the max week.', __CLASS__)); + } + } + } +} diff --git a/Constraints/WeekValidator.php b/Constraints/WeekValidator.php new file mode 100644 index 000000000..8139b156e --- /dev/null +++ b/Constraints/WeekValidator.php @@ -0,0 +1,82 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Constraints; + +use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\ConstraintValidator; +use Symfony\Component\Validator\Exception\UnexpectedTypeException; +use Symfony\Component\Validator\Exception\UnexpectedValueException; + +/** + * @author Alexandre Daubois + */ +final class WeekValidator extends ConstraintValidator +{ + public function validate(mixed $value, Constraint $constraint): void + { + if (!$constraint instanceof Week) { + throw new UnexpectedTypeException($constraint, Week::class); + } + + if (null === $value) { + return; + } + + if (!\is_string($value) && !$value instanceof \Stringable) { + throw new UnexpectedValueException($value, 'string'); + } + + if (!preg_match('/^\d{4}-W(0[1-9]|[1-4][0-9]|5[0-3])$/D', $value)) { + $this->context->buildViolation($constraint->invalidFormatMessage) + ->setCode(Week::INVALID_FORMAT_ERROR) + ->addViolation(); + + return; + } + + [$year, $weekNumber] = explode('-W', $value, 2); + $weeksInYear = (int) date('W', mktime(0, 0, 0, 12, 28, $year)); + + if ($weekNumber > $weeksInYear) { + $this->context->buildViolation($constraint->invalidWeekNumberMessage) + ->setCode(Week::INVALID_WEEK_NUMBER_ERROR) + ->setParameter('{{ value }}', $value) + ->addViolation(); + + return; + } + + if ($constraint->min) { + [$minYear, $minWeekNumber] = explode('-W', $constraint->min, 2); + if ($year < $minYear || ($year === $minYear && $weekNumber < $minWeekNumber)) { + $this->context->buildViolation($constraint->tooLowMessage) + ->setCode(Week::TOO_LOW_ERROR) + ->setInvalidValue($value) + ->setParameter('{{ min }}', $constraint->min) + ->addViolation(); + + return; + } + } + + if ($constraint->max) { + [$maxYear, $maxWeekNumber] = explode('-W', $constraint->max, 2); + if ($year > $maxYear || ($year === $maxYear && $weekNumber > $maxWeekNumber)) { + $this->context->buildViolation($constraint->tooHighMessage) + ->setCode(Week::TOO_HIGH_ERROR) + ->setInvalidValue($value) + ->setParameter('{{ max }}', $constraint->max) + ->addViolation(); + } + } + } +} diff --git a/Constraints/When.php b/Constraints/When.php index 315028427..74601c1d1 100644 --- a/Constraints/When.php +++ b/Constraints/When.php @@ -13,40 +13,64 @@ use Symfony\Component\ExpressionLanguage\Expression; use Symfony\Component\ExpressionLanguage\ExpressionLanguage; +use Symfony\Component\Validator\Attribute\HasNamedArguments; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Exception\LogicException; /** - * @Annotation - * @Target({"CLASS", "PROPERTY", "METHOD", "ANNOTATION"}) + * Conditionally apply validation constraints based on an expression using the ExpressionLanguage syntax. + * + * @see https://symfony.com/doc/current/components/expression_language.html */ #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] class When extends Composite { - public $expression; - public $constraints = []; - public $values = []; + public string|Expression|\Closure $expression; + public array|Constraint $constraints = []; + public array $values = []; + public array|Constraint $otherwise = []; - public function __construct(string|Expression|array $expression, array|Constraint|null $constraints = null, ?array $values = null, ?array $groups = null, $payload = null, array $options = []) + /** + * @param string|Expression|array|\Closure(object): bool $expression The condition to evaluate, either as a closure or using the ExpressionLanguage syntax + * @param Constraint[]|Constraint|null $constraints One or multiple constraints that are applied if the expression returns true + * @param array|null $values The values of the custom variables used in the expression (defaults to []) + * @param string[]|null $groups + * @param array|null $options + * @param Constraint[]|Constraint $otherwise One or multiple constraints that are applied if the expression returns false + */ + #[HasNamedArguments] + public function __construct(string|Expression|array|\Closure $expression, array|Constraint|null $constraints = null, ?array $values = null, ?array $groups = null, $payload = null, ?array $options = null, array|Constraint $otherwise = []) { if (!class_exists(ExpressionLanguage::class)) { throw new LogicException(\sprintf('The "symfony/expression-language" component is required to use the "%s" constraint. Try running "composer require symfony/expression-language".', __CLASS__)); } if (\is_array($expression)) { - $options = array_merge($expression, $options); + trigger_deprecation('symfony/validator', '7.3', 'Passing an array of options to configure the "%s" constraint is deprecated, use named arguments instead.', static::class); + + $options = array_merge($expression, $options ?? []); } else { - $options['expression'] = $expression; + if (\is_array($options)) { + trigger_deprecation('symfony/validator', '7.3', 'Passing an array of options to configure the "%s" constraint is deprecated, use named arguments instead.', static::class); + } else { + $options = []; + } + $options['expression'] = $expression; if (null !== $constraints) { $options['constraints'] = $constraints; } + $options['otherwise'] = $otherwise; } - if (isset($options['constraints']) && !\is_array($options['constraints'])) { + if (!\is_array($options['constraints'] ?? [])) { $options['constraints'] = [$options['constraints']]; } + if (!\is_array($options['otherwise'] ?? [])) { + $options['otherwise'] = [$options['otherwise']]; + } + if (null !== $groups) { $options['groups'] = $groups; } @@ -70,8 +94,8 @@ public function getTargets(): string|array return [self::CLASS_CONSTRAINT, self::PROPERTY_CONSTRAINT]; } - protected function getCompositeOption(): string + protected function getCompositeOption(): array|string { - return 'constraints'; + return ['constraints', 'otherwise']; } } diff --git a/Constraints/WhenValidator.php b/Constraints/WhenValidator.php index c02a450e8..1ef14469f 100644 --- a/Constraints/WhenValidator.php +++ b/Constraints/WhenValidator.php @@ -33,10 +33,20 @@ public function validate(mixed $value, Constraint $constraint): void $variables = $constraint->values; $variables['value'] = $value; $variables['this'] = $context->getObject(); + $variables['context'] = $context; - if ($this->getExpressionLanguage()->evaluate($constraint->expression, $variables)) { + if ($constraint->expression instanceof \Closure) { + $result = ($constraint->expression)($context->getObject()); + } else { + $result = $this->getExpressionLanguage()->evaluate($constraint->expression, $variables); + } + + if ($result) { $context->getValidator()->inContext($context) ->validate($value, $constraint->constraints); + } elseif ($constraint->otherwise) { + $context->getValidator()->inContext($context) + ->validate($value, $constraint->otherwise); } } diff --git a/Constraints/WordCount.php b/Constraints/WordCount.php new file mode 100644 index 000000000..6b889aa4a --- /dev/null +++ b/Constraints/WordCount.php @@ -0,0 +1,69 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Constraints; + +use Symfony\Component\Validator\Attribute\HasNamedArguments; +use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\Exception\ConstraintDefinitionException; +use Symfony\Component\Validator\Exception\MissingOptionsException; + +/** + * @author Alexandre Daubois + */ +#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] +final class WordCount extends Constraint +{ + public const TOO_SHORT_ERROR = 'cc4925df-b5a6-42dd-87f3-21919f349bf3'; + public const TOO_LONG_ERROR = 'a951a642-f662-4fad-8761-79250eef74cb'; + + protected const ERROR_NAMES = [ + self::TOO_SHORT_ERROR => 'TOO_SHORT_ERROR', + self::TOO_LONG_ERROR => 'TOO_LONG_ERROR', + ]; + + /** + * @param int<0, max>|null $min + * @param positive-int|null $max + */ + #[HasNamedArguments] + public function __construct( + public ?int $min = null, + public ?int $max = null, + public ?string $locale = null, + public string $minMessage = 'This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words.', + public string $maxMessage = 'This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less.', + ?array $groups = null, + mixed $payload = null, + ) { + if (!class_exists(\IntlBreakIterator::class)) { + throw new \RuntimeException(\sprintf('The "%s" constraint requires the "intl" PHP extension.', __CLASS__)); + } + + if (null === $min && null === $max) { + throw new MissingOptionsException(\sprintf('Either option "min" or "max" must be given for constraint "%s".', __CLASS__), ['min', 'max']); + } + + if (null !== $min && $min <= 0) { + throw new ConstraintDefinitionException(\sprintf('The "%s" constraint requires the min word count to be a positive integer if set.', __CLASS__)); + } + + if (null !== $max && $max <= 0) { + throw new ConstraintDefinitionException(\sprintf('The "%s" constraint requires the max word count to be a positive integer if set.', __CLASS__)); + } + + if (null !== $min && null !== $max && $min > $max) { + throw new ConstraintDefinitionException(\sprintf('The "%s" constraint requires the min word count to be less than or equal to the max word count.', __CLASS__)); + } + + parent::__construct(null, $groups, $payload); + } +} diff --git a/Constraints/WordCountValidator.php b/Constraints/WordCountValidator.php new file mode 100644 index 000000000..0fe6e885a --- /dev/null +++ b/Constraints/WordCountValidator.php @@ -0,0 +1,65 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Constraints; + +use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\ConstraintValidator; +use Symfony\Component\Validator\Exception\UnexpectedTypeException; +use Symfony\Component\Validator\Exception\UnexpectedValueException; + +/** + * @author Alexandre Daubois + */ +final class WordCountValidator extends ConstraintValidator +{ + public function validate(mixed $value, Constraint $constraint): void + { + if (!class_exists(\IntlBreakIterator::class)) { + throw new \RuntimeException(\sprintf('The "%s" constraint requires the "intl" PHP extension.', __CLASS__)); + } + + if (!$constraint instanceof WordCount) { + throw new UnexpectedTypeException($constraint, WordCount::class); + } + + if (null === $value || '' === $value) { + return; + } + + if (!\is_string($value) && !$value instanceof \Stringable) { + throw new UnexpectedValueException($value, 'string'); + } + + $iterator = \IntlBreakIterator::createWordInstance($constraint->locale); + $iterator->setText($value); + $words = iterator_to_array($iterator->getPartsIterator()); + + // erase "blank words" and don't count them as words + $wordsCount = \count(array_filter(array_map(trim(...), $words), fn ($word) => '' !== $word)); + + if (null !== $constraint->min && $wordsCount < $constraint->min) { + $this->context->buildViolation($constraint->minMessage) + ->setParameter('{{ count }}', $wordsCount) + ->setParameter('{{ min }}', $constraint->min) + ->setPlural($constraint->min) + ->setInvalidValue($value) + ->addViolation(); + } elseif (null !== $constraint->max && $wordsCount > $constraint->max) { + $this->context->buildViolation($constraint->maxMessage) + ->setParameter('{{ count }}', $wordsCount) + ->setParameter('{{ max }}', $constraint->max) + ->setPlural($constraint->max) + ->setInvalidValue($value) + ->addViolation(); + } + } +} diff --git a/Constraints/Yaml.php b/Constraints/Yaml.php new file mode 100644 index 000000000..99f2092d1 --- /dev/null +++ b/Constraints/Yaml.php @@ -0,0 +1,48 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Constraints; + +use Symfony\Component\Validator\Attribute\HasNamedArguments; +use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\Exception\LogicException; +use Symfony\Component\Yaml\Parser; + +/** + * @author Kev + */ +#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] +class Yaml extends Constraint +{ + public const INVALID_YAML_ERROR = '63313a31-837c-42bb-99eb-542c76aacc48'; + + protected const ERROR_NAMES = [ + self::INVALID_YAML_ERROR => 'INVALID_YAML_ERROR', + ]; + + /** + * @param int-mask-of<\Symfony\Component\Yaml\Yaml::PARSE_*> $flags + * @param string[]|null $groups + */ + #[HasNamedArguments] + public function __construct( + public string $message = 'This value is not valid YAML.', + public int $flags = 0, + ?array $groups = null, + mixed $payload = null, + ) { + if (!class_exists(Parser::class)) { + throw new LogicException('The Yaml component is required to use the Yaml constraint. Try running "composer require symfony/yaml".'); + } + + parent::__construct(null, $groups, $payload); + } +} diff --git a/Constraints/YamlValidator.php b/Constraints/YamlValidator.php new file mode 100644 index 000000000..165e3fad8 --- /dev/null +++ b/Constraints/YamlValidator.php @@ -0,0 +1,65 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Constraints; + +use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\ConstraintValidator; +use Symfony\Component\Validator\Exception\UnexpectedTypeException; +use Symfony\Component\Validator\Exception\UnexpectedValueException; +use Symfony\Component\Yaml\Exception\ParseException; +use Symfony\Component\Yaml\Parser; + +/** + * @author Kev + */ +class YamlValidator extends ConstraintValidator +{ + public function validate(mixed $value, Constraint $constraint): void + { + if (!$constraint instanceof Yaml) { + throw new UnexpectedTypeException($constraint, Yaml::class); + } + + if (null === $value || '' === $value) { + return; + } + + if (!\is_scalar($value) && !$value instanceof \Stringable) { + throw new UnexpectedValueException($value, 'string'); + } + + $value = (string) $value; + + $parser = new Parser(); + + /** @see \Symfony\Component\Yaml\Command\LintCommand::validate() */ + $prevErrorHandler = set_error_handler(static function ($level, $message, $file, $line) use (&$prevErrorHandler, $parser) { + if (\E_USER_DEPRECATED === $level) { + throw new ParseException($message, $parser->getRealCurrentLineNb() + 1); + } + + return $prevErrorHandler ? $prevErrorHandler($level, $message, $file, $line) : false; + }); + + try { + $parser->parse($value, $constraint->flags); + } catch (ParseException $e) { + $this->context->buildViolation($constraint->message) + ->setParameter('{{ error }}', $e->getMessage()) + ->setParameter('{{ line }}', $e->getParsedLine()) + ->setCode(Yaml::INVALID_YAML_ERROR) + ->addViolation(); + } finally { + restore_error_handler(); + } + } +} diff --git a/Constraints/ZeroComparisonConstraintTrait.php b/Constraints/ZeroComparisonConstraintTrait.php index 78fab1f54..d0841adea 100644 --- a/Constraints/ZeroComparisonConstraintTrait.php +++ b/Constraints/ZeroComparisonConstraintTrait.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Validator\Constraints; +use Symfony\Component\Validator\Attribute\HasNamedArguments; use Symfony\Component\Validator\Exception\ConstraintDefinitionException; /** @@ -21,15 +22,18 @@ */ trait ZeroComparisonConstraintTrait { + #[HasNamedArguments] public function __construct(?array $options = null, ?string $message = null, ?array $groups = null, mixed $payload = null) { - $options ??= []; + if (null !== $options) { + trigger_deprecation('symfony/validator', '7.3', 'Passing an array of options to configure the "%s" constraint is deprecated, use named arguments instead.', static::class); + } - if (isset($options['propertyPath'])) { + if (\is_array($options) && isset($options['propertyPath'])) { throw new ConstraintDefinitionException(\sprintf('The "propertyPath" option of the "%s" constraint cannot be set.', static::class)); } - if (isset($options['value'])) { + if (\is_array($options) && isset($options['value'])) { throw new ConstraintDefinitionException(\sprintf('The "value" option of the "%s" constraint cannot be set.', static::class)); } diff --git a/ContainerConstraintValidatorFactory.php b/ContainerConstraintValidatorFactory.php index 89e2ce14e..49e8d5aa3 100644 --- a/ContainerConstraintValidatorFactory.php +++ b/ContainerConstraintValidatorFactory.php @@ -22,12 +22,11 @@ */ class ContainerConstraintValidatorFactory implements ConstraintValidatorFactoryInterface { - private ContainerInterface $container; private array $validators; - public function __construct(ContainerInterface $container) - { - $this->container = $container; + public function __construct( + private ContainerInterface $container, + ) { $this->validators = []; } diff --git a/Context/ExecutionContext.php b/Context/ExecutionContext.php index 8ab1ec4d5..1fd373309 100644 --- a/Context/ExecutionContext.php +++ b/Context/ExecutionContext.php @@ -33,20 +33,10 @@ * * @see ExecutionContextInterface * - * @internal since version 2.5. Code against ExecutionContextInterface instead. + * @internal */ class ExecutionContext implements ExecutionContextInterface { - private ValidatorInterface $validator; - - /** - * The root value of the validated object graph. - */ - private mixed $root; - - private TranslatorInterface $translator; - private ?string $translationDomain; - /** * The violations generated in the current context. */ @@ -110,13 +100,15 @@ class ExecutionContext implements ExecutionContextInterface /** * @internal Called by {@link ExecutionContextFactory}. Should not be used in user code. + * + * @param mixed $root the root value of the validated object graph */ - public function __construct(ValidatorInterface $validator, mixed $root, TranslatorInterface $translator, ?string $translationDomain = null) - { - $this->validator = $validator; - $this->root = $root; - $this->translator = $translator; - $this->translationDomain = $translationDomain; + public function __construct( + private ValidatorInterface $validator, + private mixed $root, + private TranslatorInterface $translator, + private string|false|null $translationDomain = null, + ) { $this->violations = new ConstraintViolationList(); $this->cachedObjectsRefs = new \SplObjectStorage(); } @@ -142,7 +134,9 @@ public function setConstraint(Constraint $constraint): void public function addViolation(string|\Stringable $message, array $parameters = []): void { $this->violations->add(new ConstraintViolation( - $this->translator->trans($message, $parameters, $this->translationDomain), + false === $this->translationDomain ? + strtr($message, $parameters) : + $this->translator->trans($message, $parameters, $this->translationDomain), $message, $parameters, $this->root, diff --git a/Context/ExecutionContextFactory.php b/Context/ExecutionContextFactory.php index 9979059b6..39c2add35 100644 --- a/Context/ExecutionContextFactory.php +++ b/Context/ExecutionContextFactory.php @@ -19,17 +19,14 @@ * * @author Bernhard Schussek * - * @internal version 2.5. Code against ExecutionContextFactoryInterface instead. + * @internal */ class ExecutionContextFactory implements ExecutionContextFactoryInterface { - private TranslatorInterface $translator; - private ?string $translationDomain; - - public function __construct(TranslatorInterface $translator, ?string $translationDomain = null) - { - $this->translator = $translator; - $this->translationDomain = $translationDomain; + public function __construct( + private TranslatorInterface $translator, + private string|false|null $translationDomain = null, + ) { } public function createContext(ValidatorInterface $validator, mixed $root): ExecutionContextInterface diff --git a/Context/ExecutionContextInterface.php b/Context/ExecutionContextInterface.php index fd72a149e..56e39bd6a 100644 --- a/Context/ExecutionContextInterface.php +++ b/Context/ExecutionContextInterface.php @@ -66,10 +66,8 @@ interface ExecutionContextInterface * * @param string|\Stringable $message The error message as a string or a stringable object * @param array $params The parameters substituted in the error message - * - * @return void */ - public function addViolation(string $message, array $params = []); + public function addViolation(string $message, array $params = []): void; /** * Returns a builder for adding a violation with extended information. @@ -97,7 +95,7 @@ public function buildViolation(string $message, array $parameters = []): Constra * { * $validator = $this->context->getValidator(); * - * $violations = $validator->validate($value, new Length(['min' => 3])); + * $violations = $validator->validate($value, new Length(min: 3)); * * if (count($violations) > 0) { * // ... @@ -123,26 +121,20 @@ public function getObject(): ?object; * * @param object|null $object The currently validated object * @param string $propertyPath The property path to the current value - * - * @return void */ - public function setNode(mixed $value, ?object $object, ?MetadataInterface $metadata, string $propertyPath); + public function setNode(mixed $value, ?object $object, ?MetadataInterface $metadata, string $propertyPath): void; /** * Warning: Should not be called by user code, to be used by the validator engine only. * * @param string|null $group The validated group - * - * @return void */ - public function setGroup(?string $group); + public function setGroup(?string $group): void; /** * Warning: Should not be called by user code, to be used by the validator engine only. - * - * @return void */ - public function setConstraint(Constraint $constraint); + public function setConstraint(Constraint $constraint): void; /** * Warning: Should not be called by user code, to be used by the validator engine only. @@ -150,10 +142,8 @@ public function setConstraint(Constraint $constraint); * @param string $cacheKey The hash of the object * @param string $groupHash The group's name or hash, if it is group * sequence - * - * @return void */ - public function markGroupAsValidated(string $cacheKey, string $groupHash); + public function markGroupAsValidated(string $cacheKey, string $groupHash): void; /** * Warning: Should not be called by user code, to be used by the validator engine only. @@ -169,10 +159,8 @@ public function isGroupValidated(string $cacheKey, string $groupHash): bool; * * @param string $cacheKey The hash of the object * @param string $constraintHash The hash of the constraint - * - * @return void */ - public function markConstraintAsValidated(string $cacheKey, string $constraintHash); + public function markConstraintAsValidated(string $cacheKey, string $constraintHash): void; /** * Warning: Should not be called by user code, to be used by the validator engine only. diff --git a/DataCollector/ValidatorDataCollector.php b/DataCollector/ValidatorDataCollector.php index a50b16687..1b5439783 100644 --- a/DataCollector/ValidatorDataCollector.php +++ b/DataCollector/ValidatorDataCollector.php @@ -29,11 +29,9 @@ */ class ValidatorDataCollector extends DataCollector implements LateDataCollectorInterface { - private TraceableValidator $validator; - - public function __construct(TraceableValidator $validator) - { - $this->validator = $validator; + public function __construct( + private TraceableValidator $validator, + ) { $this->reset(); } diff --git a/DependencyInjection/AddAutoMappingConfigurationPass.php b/DependencyInjection/AddAutoMappingConfigurationPass.php index 3fcc84e7d..0a6250696 100644 --- a/DependencyInjection/AddAutoMappingConfigurationPass.php +++ b/DependencyInjection/AddAutoMappingConfigurationPass.php @@ -22,10 +22,7 @@ */ class AddAutoMappingConfigurationPass implements CompilerPassInterface { - /** - * @return void - */ - public function process(ContainerBuilder $container) + public function process(ContainerBuilder $container): void { if (!$container->hasParameter('validator.auto_mapping') || !$container->hasDefinition('validator.builder')) { return; diff --git a/DependencyInjection/AddConstraintValidatorsPass.php b/DependencyInjection/AddConstraintValidatorsPass.php index 4bcae00ef..eb60c94cb 100644 --- a/DependencyInjection/AddConstraintValidatorsPass.php +++ b/DependencyInjection/AddConstraintValidatorsPass.php @@ -22,10 +22,7 @@ */ class AddConstraintValidatorsPass implements CompilerPassInterface { - /** - * @return void - */ - public function process(ContainerBuilder $container) + public function process(ContainerBuilder $container): void { if (!$container->hasDefinition('validator.validator_factory')) { return; diff --git a/DependencyInjection/AddValidatorInitializersPass.php b/DependencyInjection/AddValidatorInitializersPass.php index d53e3c85e..df7385b0c 100644 --- a/DependencyInjection/AddValidatorInitializersPass.php +++ b/DependencyInjection/AddValidatorInitializersPass.php @@ -21,10 +21,7 @@ */ class AddValidatorInitializersPass implements CompilerPassInterface { - /** - * @return void - */ - public function process(ContainerBuilder $container) + public function process(ContainerBuilder $container): void { if (!$container->hasDefinition('validator.builder')) { return; diff --git a/Exception/InvalidOptionsException.php b/Exception/InvalidOptionsException.php index 8a4fba25d..69576760a 100644 --- a/Exception/InvalidOptionsException.php +++ b/Exception/InvalidOptionsException.php @@ -13,19 +13,14 @@ class InvalidOptionsException extends ValidatorException { - private array $options; - - public function __construct(string $message, array $options) - { + public function __construct( + string $message, + private array $options, + ) { parent::__construct($message); - - $this->options = $options; } - /** - * @return array - */ - public function getOptions() + public function getOptions(): array { return $this->options; } diff --git a/Exception/MissingOptionsException.php b/Exception/MissingOptionsException.php index a7eda1457..d40d1c9af 100644 --- a/Exception/MissingOptionsException.php +++ b/Exception/MissingOptionsException.php @@ -13,19 +13,14 @@ class MissingOptionsException extends ValidatorException { - private array $options; - - public function __construct(string $message, array $options) - { + public function __construct( + string $message, + private array $options, + ) { parent::__construct($message); - - $this->options = $options; } - /** - * @return array - */ - public function getOptions() + public function getOptions(): array { return $this->options; } diff --git a/Exception/UnexpectedValueException.php b/Exception/UnexpectedValueException.php index 83a7e7a7d..1a65ebd7d 100644 --- a/Exception/UnexpectedValueException.php +++ b/Exception/UnexpectedValueException.php @@ -16,13 +16,11 @@ */ class UnexpectedValueException extends UnexpectedTypeException { - private string $expectedType; - - public function __construct(mixed $value, string $expectedType) - { + public function __construct( + mixed $value, + private string $expectedType, + ) { parent::__construct($value, $expectedType); - - $this->expectedType = $expectedType; } public function getExpectedType(): string diff --git a/Exception/ValidationFailedException.php b/Exception/ValidationFailedException.php index 2106e0a0b..fa8cc7cbf 100644 --- a/Exception/ValidationFailedException.php +++ b/Exception/ValidationFailedException.php @@ -18,20 +18,14 @@ */ class ValidationFailedException extends RuntimeException { - private ConstraintViolationListInterface $violations; - private mixed $value; - - public function __construct(mixed $value, ConstraintViolationListInterface $violations) - { - $this->violations = $violations; - $this->value = $value; + public function __construct( + private mixed $value, + private ConstraintViolationListInterface $violations, + ) { parent::__construct($violations); } - /** - * @return mixed - */ - public function getValue() + public function getValue(): mixed { return $this->value; } diff --git a/Mapping/ClassMetadata.php b/Mapping/ClassMetadata.php index bf164faac..d812255c4 100644 --- a/Mapping/ClassMetadata.php +++ b/Mapping/ClassMetadata.php @@ -321,10 +321,8 @@ public function addGetterMethodConstraints(string $property, string $method, arr /** * Merges the constraints of the given metadata into this object. - * - * @return void */ - public function mergeConstraints(self $source) + public function mergeConstraints(self $source): void { if ($source->isGroupSequenceProvider()) { $this->setGroupProvider($source->getGroupProvider()); @@ -431,11 +429,9 @@ public function getReflectionClass(): \ReflectionClass /** * Sets whether a group sequence provider should be used. * - * @return void - * * @throws GroupDefinitionException */ - public function setGroupSequenceProvider(bool $active) + public function setGroupSequenceProvider(bool $active): void { if ($this->hasGroupSequence()) { throw new GroupDefinitionException('Defining a group sequence provider is not allowed with a static group sequence.'); diff --git a/Mapping/Factory/LazyLoadingMetadataFactory.php b/Mapping/Factory/LazyLoadingMetadataFactory.php index f977f20df..1404df963 100644 --- a/Mapping/Factory/LazyLoadingMetadataFactory.php +++ b/Mapping/Factory/LazyLoadingMetadataFactory.php @@ -39,20 +39,17 @@ */ class LazyLoadingMetadataFactory implements MetadataFactoryInterface { - protected $loader; - protected $cache; - /** * The loaded metadata, indexed by class name. * * @var ClassMetadata[] */ - protected $loadedClasses = []; + protected array $loadedClasses = []; - public function __construct(?LoaderInterface $loader = null, ?CacheItemPoolInterface $cache = null) - { - $this->loader = $loader; - $this->cache = $cache; + public function __construct( + protected ?LoaderInterface $loader = null, + protected ?CacheItemPoolInterface $cache = null, + ) { } /** diff --git a/Mapping/Factory/MetadataFactoryInterface.php b/Mapping/Factory/MetadataFactoryInterface.php index 8e89d3b7d..b7a86ffc8 100644 --- a/Mapping/Factory/MetadataFactoryInterface.php +++ b/Mapping/Factory/MetadataFactoryInterface.php @@ -15,7 +15,7 @@ use Symfony\Component\Validator\Mapping\MetadataInterface; /** - * Returns {@link \Symfony\Component\Validator\Mapping\MetadataInterface} instances for values. + * Returns {@link MetadataInterface} instances for values. * * @author Bernhard Schussek */ diff --git a/Mapping/Loader/AbstractLoader.php b/Mapping/Loader/AbstractLoader.php index 5c81baa5d..ebff64fd5 100644 --- a/Mapping/Loader/AbstractLoader.php +++ b/Mapping/Loader/AbstractLoader.php @@ -33,7 +33,7 @@ abstract class AbstractLoader implements LoaderInterface */ public const DEFAULT_NAMESPACE = '\\Symfony\\Component\\Validator\\Constraints\\'; - protected $namespaces = []; + protected array $namespaces = []; /** * @var array @@ -49,10 +49,8 @@ abstract class AbstractLoader implements LoaderInterface * $this->addNamespaceAlias('mynamespace', '\\Acme\\Package\\Constraints\\'); * * $constraint = $this->newConstraint('mynamespace:NotNull'); - * - * @return void */ - protected function addNamespaceAlias(string $alias, string $namespace) + protected function addNamespaceAlias(string $alias, string $namespace): void { $this->namespaces[$alias] = $namespace; } @@ -99,9 +97,27 @@ protected function newConstraint(string $name, mixed $options = null): Constrain return new $className($options['value']); } - return new $className(...$options); + if (array_is_list($options)) { + return new $className($options); + } + + try { + return new $className(...$options); + } catch (\Error $e) { + if (str_starts_with($e->getMessage(), 'Unknown named parameter ')) { + return new $className($options); + } + + throw $e; + } + } + + if ($options) { + trigger_deprecation('symfony/validator', '7.3', 'Using constraints not supporting named arguments is deprecated. Try adding the HasNamedArguments attribute to %s.', $className); + + return new $className($options); } - return new $className($options); + return new $className(); } } diff --git a/Mapping/Loader/AnnotationLoader.php b/Mapping/Loader/AnnotationLoader.php deleted file mode 100644 index f90f9ce5e..000000000 --- a/Mapping/Loader/AnnotationLoader.php +++ /dev/null @@ -1,159 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Validator\Mapping\Loader; - -use Doctrine\Common\Annotations\Reader; -use Symfony\Component\Validator\Constraint; -use Symfony\Component\Validator\Constraints\Callback; -use Symfony\Component\Validator\Constraints\GroupSequence; -use Symfony\Component\Validator\Constraints\GroupSequenceProvider; -use Symfony\Component\Validator\Exception\MappingException; -use Symfony\Component\Validator\Mapping\ClassMetadata; - -/** - * Loads validation metadata using a Doctrine annotation {@link Reader} or using PHP 8 attributes. - * - * @deprecated since Symfony 6.4, use {@see AttributeLoader} instead - * - * @author Bernhard Schussek - * @author Alexander M. Turek - */ -class AnnotationLoader implements LoaderInterface -{ - /** - * @deprecated since Symfony 6.4, this property will be removed in 7.0 - * - * @var Reader|null - */ - protected $reader; - - public function __construct(?Reader $reader = null) - { - $this->reader = $reader; - } - - public function loadClassMetadata(ClassMetadata $metadata): bool - { - $reflClass = $metadata->getReflectionClass(); - $className = $reflClass->name; - $success = false; - - foreach ($this->getAnnotations($reflClass) as $constraint) { - if ($constraint instanceof GroupSequence) { - $metadata->setGroupSequence($constraint->groups); - } elseif ($constraint instanceof GroupSequenceProvider) { - $metadata->setGroupProvider($constraint->provider); - $metadata->setGroupSequenceProvider(true); - } elseif ($constraint instanceof Constraint) { - $metadata->addConstraint($constraint); - } - - $success = true; - } - - foreach ($reflClass->getProperties() as $property) { - if ($property->getDeclaringClass()->name === $className) { - foreach ($this->getAnnotations($property) as $constraint) { - if ($constraint instanceof Constraint) { - $metadata->addPropertyConstraint($property->name, $constraint); - } - - $success = true; - } - } - } - - foreach ($reflClass->getMethods() as $method) { - if ($method->getDeclaringClass()->name === $className) { - foreach ($this->getAnnotations($method) as $constraint) { - if ($constraint instanceof Callback) { - $constraint->callback = $method->getName(); - - $metadata->addConstraint($constraint); - } elseif ($constraint instanceof Constraint) { - if (preg_match('/^(get|is|has)(.+)$/i', $method->name, $matches)) { - $metadata->addGetterMethodConstraint(lcfirst($matches[2]), $matches[0], $constraint); - } else { - throw new MappingException(\sprintf('The constraint on "%s::%s()" cannot be added. Constraints can only be added on methods beginning with "get", "is" or "has".', $className, $method->name)); - } - } - - $success = true; - } - } - } - - return $success; - } - - private function getAnnotations(\ReflectionMethod|\ReflectionClass|\ReflectionProperty $reflection): iterable - { - $dedup = []; - - foreach ($reflection->getAttributes(GroupSequence::class) as $attribute) { - $dedup[] = $attribute->newInstance(); - yield $attribute->newInstance(); - } - foreach ($reflection->getAttributes(GroupSequenceProvider::class) as $attribute) { - $dedup[] = $attribute->newInstance(); - yield $attribute->newInstance(); - } - foreach ($reflection->getAttributes(Constraint::class, \ReflectionAttribute::IS_INSTANCEOF) as $attribute) { - $dedup[] = $attribute->newInstance(); - yield $attribute->newInstance(); - } - if (!$this->reader) { - return; - } - - $annotations = []; - - if ($reflection instanceof \ReflectionClass && $annotations = $this->reader->getClassAnnotations($reflection)) { - $this->triggerDeprecationIfAnnotationIsUsed($annotations, \sprintf('Class "%s"', $reflection->getName())); - } - if ($reflection instanceof \ReflectionMethod && $annotations = $this->reader->getMethodAnnotations($reflection)) { - $this->triggerDeprecationIfAnnotationIsUsed($annotations, \sprintf('Method "%s::%s()"', $reflection->getDeclaringClass()->getName(), $reflection->getName())); - } - if ($reflection instanceof \ReflectionProperty && $annotations = $this->reader->getPropertyAnnotations($reflection)) { - $this->triggerDeprecationIfAnnotationIsUsed($annotations, \sprintf('Property "%s::$%s"', $reflection->getDeclaringClass()->getName(), $reflection->getName())); - } - - foreach ($dedup as $annotation) { - if ($annotation instanceof Constraint) { - $annotation->groups; // trigger initialization of the "groups" property - } - } - - foreach ($annotations as $annotation) { - if ($annotation instanceof Constraint) { - $annotation->groups; // trigger initialization of the "groups" property - } - if (!\in_array($annotation, $dedup, false)) { - yield $annotation; - } - } - } - - private function triggerDeprecationIfAnnotationIsUsed(array $annotations, string $messagePrefix): void - { - foreach ($annotations as $annotation) { - if ( - $annotation instanceof Constraint - || $annotation instanceof GroupSequence - || $annotation instanceof GroupSequenceProvider - ) { - trigger_deprecation('symfony/validator', '6.4', \sprintf('%s uses Doctrine Annotations to configure validation constraints, which is deprecated. Use PHP attributes instead.', $messagePrefix)); - break; - } - } - } -} diff --git a/Mapping/Loader/AttributeLoader.php b/Mapping/Loader/AttributeLoader.php index 2a8a75e1e..ddf0b78b7 100644 --- a/Mapping/Loader/AttributeLoader.php +++ b/Mapping/Loader/AttributeLoader.php @@ -11,6 +11,13 @@ namespace Symfony\Component\Validator\Mapping\Loader; +use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\Constraints\Callback; +use Symfony\Component\Validator\Constraints\GroupSequence; +use Symfony\Component\Validator\Constraints\GroupSequenceProvider; +use Symfony\Component\Validator\Exception\MappingException; +use Symfony\Component\Validator\Mapping\ClassMetadata; + /** * Loads validation metadata using PHP attributes. * @@ -18,10 +25,72 @@ * @author Alexander M. Turek * @author Alexandre Daubois */ -class AttributeLoader extends AnnotationLoader +class AttributeLoader implements LoaderInterface { - public function __construct() + public function loadClassMetadata(ClassMetadata $metadata): bool + { + $reflClass = $metadata->getReflectionClass(); + $className = $reflClass->name; + $success = false; + + foreach ($this->getAttributes($reflClass) as $constraint) { + if ($constraint instanceof GroupSequence) { + $metadata->setGroupSequence($constraint->groups); + } elseif ($constraint instanceof GroupSequenceProvider) { + $metadata->setGroupProvider($constraint->provider); + $metadata->setGroupSequenceProvider(true); + } elseif ($constraint instanceof Constraint) { + $metadata->addConstraint($constraint); + } + + $success = true; + } + + foreach ($reflClass->getProperties() as $property) { + if ($property->getDeclaringClass()->name === $className) { + foreach ($this->getAttributes($property) as $constraint) { + if ($constraint instanceof Constraint) { + $metadata->addPropertyConstraint($property->name, $constraint); + } + + $success = true; + } + } + } + + foreach ($reflClass->getMethods() as $method) { + if ($method->getDeclaringClass()->name === $className) { + foreach ($this->getAttributes($method) as $constraint) { + if ($constraint instanceof Callback) { + $constraint->callback = $method->getName(); + + $metadata->addConstraint($constraint); + } elseif ($constraint instanceof Constraint) { + if (preg_match('/^(get|is|has)(.+)$/i', $method->name, $matches)) { + $metadata->addGetterMethodConstraint(lcfirst($matches[2]), $matches[0], $constraint); + } else { + throw new MappingException(\sprintf('The constraint on "%s::%s()" cannot be added. Constraints can only be added on methods beginning with "get", "is" or "has".', $className, $method->name)); + } + } + + $success = true; + } + } + } + + return $success; + } + + private function getAttributes(\ReflectionMethod|\ReflectionClass|\ReflectionProperty $reflection): iterable { - parent::__construct(null); + foreach ($reflection->getAttributes(GroupSequence::class) as $attribute) { + yield $attribute->newInstance(); + } + foreach ($reflection->getAttributes(GroupSequenceProvider::class) as $attribute) { + yield $attribute->newInstance(); + } + foreach ($reflection->getAttributes(Constraint::class, \ReflectionAttribute::IS_INSTANCEOF) as $attribute) { + yield $attribute->newInstance(); + } } } diff --git a/Mapping/Loader/FileLoader.php b/Mapping/Loader/FileLoader.php index f1c82bfbf..ea46ffc8c 100644 --- a/Mapping/Loader/FileLoader.php +++ b/Mapping/Loader/FileLoader.php @@ -23,8 +23,6 @@ */ abstract class FileLoader extends AbstractLoader { - protected $file; - /** * Creates a new loader. * @@ -32,8 +30,9 @@ abstract class FileLoader extends AbstractLoader * * @throws MappingException If the file does not exist or is not readable */ - public function __construct(string $file) - { + public function __construct( + protected string $file, + ) { if (!is_file($file)) { throw new MappingException(\sprintf('The mapping file "%s" does not exist.', $file)); } @@ -45,7 +44,5 @@ public function __construct(string $file) if (!stream_is_local($this->file)) { throw new MappingException(\sprintf('The mapping file "%s" is not a local file.', $file)); } - - $this->file = $file; } } diff --git a/Mapping/Loader/LoaderChain.php b/Mapping/Loader/LoaderChain.php index d6cb4eda7..88510a262 100644 --- a/Mapping/Loader/LoaderChain.php +++ b/Mapping/Loader/LoaderChain.php @@ -25,22 +25,19 @@ */ class LoaderChain implements LoaderInterface { - protected $loaders; - /** * @param LoaderInterface[] $loaders The metadata loaders to use * * @throws MappingException If any of the loaders has an invalid type */ - public function __construct(array $loaders) - { + public function __construct( + protected array $loaders, + ) { foreach ($loaders as $loader) { if (!$loader instanceof LoaderInterface) { throw new MappingException(\sprintf('Class "%s" is expected to implement LoaderInterface.', get_debug_type($loader))); } } - - $this->loaders = $loaders; } public function loadClassMetadata(ClassMetadata $metadata): bool diff --git a/Mapping/Loader/PropertyInfoLoader.php b/Mapping/Loader/PropertyInfoLoader.php index db6ac33ca..57d65696e 100644 --- a/Mapping/Loader/PropertyInfoLoader.php +++ b/Mapping/Loader/PropertyInfoLoader.php @@ -15,6 +15,16 @@ use Symfony\Component\PropertyInfo\PropertyListExtractorInterface; use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface; use Symfony\Component\PropertyInfo\Type as PropertyInfoType; +use Symfony\Component\TypeInfo\Type as TypeInfoType; +use Symfony\Component\TypeInfo\Type\BuiltinType; +use Symfony\Component\TypeInfo\Type\CollectionType; +use Symfony\Component\TypeInfo\Type\CompositeTypeInterface; +use Symfony\Component\TypeInfo\Type\IntersectionType; +use Symfony\Component\TypeInfo\Type\NullableType; +use Symfony\Component\TypeInfo\Type\ObjectType; +use Symfony\Component\TypeInfo\Type\UnionType; +use Symfony\Component\TypeInfo\Type\WrappingTypeInterface; +use Symfony\Component\TypeInfo\TypeIdentifier; use Symfony\Component\Validator\Constraints\All; use Symfony\Component\Validator\Constraints\NotBlank; use Symfony\Component\Validator\Constraints\NotNull; @@ -31,17 +41,12 @@ final class PropertyInfoLoader implements LoaderInterface { use AutoMappingTrait; - private PropertyListExtractorInterface $listExtractor; - private PropertyTypeExtractorInterface $typeExtractor; - private PropertyAccessExtractorInterface $accessExtractor; - private ?string $classValidatorRegexp; - - public function __construct(PropertyListExtractorInterface $listExtractor, PropertyTypeExtractorInterface $typeExtractor, PropertyAccessExtractorInterface $accessExtractor, ?string $classValidatorRegexp = null) - { - $this->listExtractor = $listExtractor; - $this->typeExtractor = $typeExtractor; - $this->accessExtractor = $accessExtractor; - $this->classValidatorRegexp = $classValidatorRegexp; + public function __construct( + private PropertyListExtractorInterface $listExtractor, + private PropertyTypeExtractorInterface $typeExtractor, + private PropertyAccessExtractorInterface $accessExtractor, + private ?string $classValidatorRegexp = null, + ) { } public function loadClassMetadata(ClassMetadata $metadata): bool @@ -62,7 +67,7 @@ public function loadClassMetadata(ClassMetadata $metadata): bool continue; } - $types = $this->typeExtractor->getTypes($className, $property); + $types = $this->getPropertyTypes($className, $property); if (null === $types) { continue; } @@ -100,30 +105,71 @@ public function loadClassMetadata(ClassMetadata $metadata): bool } $loaded = true; - $builtinTypes = []; - $nullable = false; - $scalar = true; - foreach ($types as $type) { - $builtinTypes[] = $type->getBuiltinType(); - - if ($scalar && !\in_array($type->getBuiltinType(), [PropertyInfoType::BUILTIN_TYPE_INT, PropertyInfoType::BUILTIN_TYPE_FLOAT, PropertyInfoType::BUILTIN_TYPE_STRING, PropertyInfoType::BUILTIN_TYPE_BOOL], true)) { - $scalar = false; + + // BC layer for PropertyTypeExtractorInterface::getTypes(). + // Can be removed as soon as PropertyTypeExtractorInterface::getTypes() is removed (8.0). + if (\is_array($types)) { + $builtinTypes = []; + $nullable = false; + $scalar = true; + + foreach ($types as $type) { + $builtinTypes[] = $type->getBuiltinType(); + + if ($scalar && !\in_array($type->getBuiltinType(), ['int', 'float', 'string', 'bool'], true)) { + $scalar = false; + } + + if (!$nullable && $type->isNullable()) { + $nullable = true; + } } - if (!$nullable && $type->isNullable()) { - $nullable = true; + if (!$hasTypeConstraint) { + if (1 === \count($builtinTypes)) { + if ($types[0]->isCollection() && \count($collectionValueType = $types[0]->getCollectionValueTypes()) > 0) { + [$collectionValueType] = $collectionValueType; + $this->handleAllConstraintLegacy($property, $allConstraint, $collectionValueType, $metadata); + } + + $metadata->addPropertyConstraint($property, $this->getTypeConstraintLegacy($builtinTypes[0], $types[0])); + } elseif ($scalar) { + $metadata->addPropertyConstraint($property, new Type(type: 'scalar')); + } } - } - if (!$hasTypeConstraint) { - if (1 === \count($builtinTypes)) { - if ($types[0]->isCollection() && \count($collectionValueType = $types[0]->getCollectionValueTypes()) > 0) { - [$collectionValueType] = $collectionValueType; - $this->handleAllConstraint($property, $allConstraint, $collectionValueType, $metadata); + } else { + if ($hasTypeConstraint) { + continue; + } + + $type = $types; + + // BC layer for type-info < 7.2 + if (!class_exists(NullableType::class)) { + $nullable = false; + + if ($type instanceof UnionType && $type->isNullable()) { + $nullable = true; + $type = $type->asNonNullable(); } + } else { + $nullable = $type->isNullable(); - $metadata->addPropertyConstraint($property, $this->getTypeConstraint($builtinTypes[0], $types[0])); - } elseif ($scalar) { - $metadata->addPropertyConstraint($property, new Type(['type' => 'scalar'])); + if ($type instanceof NullableType) { + $type = $type->getWrappedType(); + } + } + + if ($type instanceof NullableType) { + $type = $type->getWrappedType(); + } + + if ($type instanceof CollectionType) { + $this->handleAllConstraint($property, $allConstraint, $type->getCollectionValueType(), $metadata); + } + + if (null !== $typeConstraint = $this->getTypeConstraint($type)) { + $metadata->addPropertyConstraint($property, $typeConstraint); } } @@ -135,16 +181,120 @@ public function loadClassMetadata(ClassMetadata $metadata): bool return $loaded; } - private function getTypeConstraint(string $builtinType, PropertyInfoType $type): Type + /** + * BC layer for PropertyTypeExtractorInterface::getTypes(). + * Can be removed as soon as PropertyTypeExtractorInterface::getTypes() is removed (8.0). + * + * @return TypeInfoType|list|null + */ + private function getPropertyTypes(string $className, string $property): TypeInfoType|array|null + { + if (class_exists(TypeInfoType::class) && method_exists($this->typeExtractor, 'getType')) { + return $this->typeExtractor->getType($className, $property); + } + + return $this->typeExtractor->getTypes($className, $property); + } + + /** + * BC layer for PropertyTypeExtractorInterface::getTypes(). + * Can be removed as soon as PropertyTypeExtractorInterface::getTypes() is removed (8.0). + */ + private function getTypeConstraintLegacy(string $builtinType, PropertyInfoType $type): Type { if (PropertyInfoType::BUILTIN_TYPE_OBJECT === $builtinType && null !== $className = $type->getClassName()) { - return new Type(['type' => $className]); + return new Type(type: $className); + } + + return new Type(type: $builtinType); + } + + private function getTypeConstraint(TypeInfoType $type): ?Type + { + // BC layer for type-info < 7.2 + if (!interface_exists(CompositeTypeInterface::class)) { + if ($type instanceof UnionType || $type instanceof IntersectionType) { + return ($type->isA(TypeIdentifier::INT) || $type->isA(TypeIdentifier::FLOAT) || $type->isA(TypeIdentifier::STRING) || $type->isA(TypeIdentifier::BOOL)) ? new Type(['type' => 'scalar']) : null; + } + + $baseType = $type->getBaseType(); + + if ($baseType instanceof ObjectType) { + return new Type(type: $baseType->getClassName()); + } + + if (TypeIdentifier::MIXED !== $baseType->getTypeIdentifier()) { + return new Type(type: $baseType->getTypeIdentifier()->value); + } + + return null; + } + + if ($type instanceof CompositeTypeInterface) { + return $type->isIdentifiedBy( + TypeIdentifier::INT, + TypeIdentifier::FLOAT, + TypeIdentifier::STRING, + TypeIdentifier::BOOL, + TypeIdentifier::TRUE, + TypeIdentifier::FALSE, + ) ? new Type(type: 'scalar') : null; + } + + while ($type instanceof WrappingTypeInterface) { + $type = $type->getWrappedType(); + } + + if ($type instanceof ObjectType) { + return new Type(type: $type->getClassName()); + } + + if ($type instanceof BuiltinType && TypeIdentifier::MIXED !== $type->getTypeIdentifier()) { + return new Type(type: $type->getTypeIdentifier()->value); } - return new Type(['type' => $builtinType]); + return null; + } + + private function handleAllConstraint(string $property, ?All $allConstraint, TypeInfoType $type, ClassMetadata $metadata): void + { + $containsTypeConstraint = false; + $containsNotNullConstraint = false; + if (null !== $allConstraint) { + foreach ($allConstraint->constraints as $constraint) { + if ($constraint instanceof Type) { + $containsTypeConstraint = true; + } elseif ($constraint instanceof NotNull) { + $containsNotNullConstraint = true; + } + } + } + + $constraints = []; + if (!$containsNotNullConstraint && !$type->isNullable()) { + $constraints[] = new NotNull(); + } + + if (!$containsTypeConstraint && null !== $typeConstraint = $this->getTypeConstraint($type)) { + $constraints[] = $typeConstraint; + } + + if (!$constraints) { + return; + } + + if (null === $allConstraint) { + $metadata->addPropertyConstraint($property, new All(constraints: $constraints)); + } else { + $allConstraint->constraints = array_merge($allConstraint->constraints, $constraints); + } } - private function handleAllConstraint(string $property, ?All $allConstraint, PropertyInfoType $propertyInfoType, ClassMetadata $metadata): void + /** + * BC layer for PropertyTypeExtractorInterface::getTypes(). + * Can be removed as soon as PropertyTypeExtractorInterface::getTypes() is removed (8.0). + */ + private function handleAllConstraintLegacy(string $property, ?All $allConstraint, PropertyInfoType $propertyInfoType, ClassMetadata $metadata): void { $containsTypeConstraint = false; $containsNotNullConstraint = false; @@ -164,11 +314,11 @@ private function handleAllConstraint(string $property, ?All $allConstraint, Prop } if (!$containsTypeConstraint) { - $constraints[] = $this->getTypeConstraint($propertyInfoType->getBuiltinType(), $propertyInfoType); + $constraints[] = $this->getTypeConstraintLegacy($propertyInfoType->getBuiltinType(), $propertyInfoType); } if (null === $allConstraint) { - $metadata->addPropertyConstraint($property, new All(['constraints' => $constraints])); + $metadata->addPropertyConstraint($property, new All(constraints: $constraints)); } else { $allConstraint->constraints = array_merge($allConstraint->constraints, $constraints); } diff --git a/Mapping/Loader/StaticMethodLoader.php b/Mapping/Loader/StaticMethodLoader.php index cc978ddef..c230c36d3 100644 --- a/Mapping/Loader/StaticMethodLoader.php +++ b/Mapping/Loader/StaticMethodLoader.php @@ -21,16 +21,14 @@ */ class StaticMethodLoader implements LoaderInterface { - protected $methodName; - /** * Creates a new loader. * * @param string $methodName The name of the static method to call */ - public function __construct(string $methodName = 'loadValidatorMetadata') - { - $this->methodName = $methodName; + public function __construct( + protected string $methodName = 'loadValidatorMetadata', + ) { } public function loadClassMetadata(ClassMetadata $metadata): bool diff --git a/Mapping/Loader/XmlFileLoader.php b/Mapping/Loader/XmlFileLoader.php index 1c5f52873..83a14b2e8 100644 --- a/Mapping/Loader/XmlFileLoader.php +++ b/Mapping/Loader/XmlFileLoader.php @@ -26,9 +26,9 @@ class XmlFileLoader extends FileLoader /** * The XML nodes of the mapping file. * - * @var \SimpleXMLElement[]|null + * @var \SimpleXMLElement[] */ - protected $classes; + protected array $classes; public function __construct(string $file) { diff --git a/Mapping/Loader/YamlFileLoader.php b/Mapping/Loader/YamlFileLoader.php index 4fae76bf8..78d1b0ef9 100644 --- a/Mapping/Loader/YamlFileLoader.php +++ b/Mapping/Loader/YamlFileLoader.php @@ -24,12 +24,7 @@ */ class YamlFileLoader extends FileLoader { - /** - * An array of YAML class descriptions. - * - * @var array - */ - protected $classes; + protected array $classes; public function __construct(string $file) { diff --git a/Mapping/MemberMetadata.php b/Mapping/MemberMetadata.php index 1ebc51a1e..6df8add12 100644 --- a/Mapping/MemberMetadata.php +++ b/Mapping/MemberMetadata.php @@ -93,10 +93,7 @@ public function getName(): string return $this->name; } - /** - * @return string - */ - public function getClassName() + public function getClassName(): string { return $this->class; } diff --git a/ObjectInitializerInterface.php b/ObjectInitializerInterface.php index 629a214a0..4d7bd854b 100644 --- a/ObjectInitializerInterface.php +++ b/ObjectInitializerInterface.php @@ -22,8 +22,5 @@ */ interface ObjectInitializerInterface { - /** - * @return void - */ - public function initialize(object $object); + public function initialize(object $object): void; } diff --git a/Test/CompoundConstraintTestCase.php b/Test/CompoundConstraintTestCase.php new file mode 100644 index 000000000..2282645f2 --- /dev/null +++ b/Test/CompoundConstraintTestCase.php @@ -0,0 +1,129 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Test; + +use PHPUnit\Framework\ExpectationFailedException; +use PHPUnit\Framework\TestCase; +use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\Constraints\Compound; +use Symfony\Component\Validator\Constraints\CompoundValidator; +use Symfony\Component\Validator\ConstraintViolationListInterface; +use Symfony\Component\Validator\Context\ExecutionContext; +use Symfony\Component\Validator\Context\ExecutionContextInterface; +use Symfony\Component\Validator\Validation; +use Symfony\Component\Validator\Validator\ValidatorInterface; +use Symfony\Contracts\Translation\TranslatorInterface; + +/** + * A test case to ease testing Compound Constraints. + * + * @author Alexandre Daubois + * + * @template T of Compound + */ +abstract class CompoundConstraintTestCase extends TestCase +{ + protected ValidatorInterface $validator; + protected ?ConstraintViolationListInterface $violationList = null; + protected ExecutionContextInterface $context; + protected mixed $root; + + private mixed $validatedValue; + + protected function setUp(): void + { + $this->root = 'root'; + $this->validator = $this->createValidator(); + $this->context = $this->createContext($this->validator); + } + + protected function validateValue(mixed $value): void + { + $this->validator->inContext($this->context)->validate($this->validatedValue = $value, $this->createCompound()); + } + + protected function createValidator(): ValidatorInterface + { + return Validation::createValidator(); + } + + protected function createContext(?ValidatorInterface $validator = null): ExecutionContextInterface + { + $translator = $this->createMock(TranslatorInterface::class); + $translator->expects($this->any())->method('trans')->willReturnArgument(0); + + return new ExecutionContext($validator ?? $this->createValidator(), $this->root, $translator); + } + + public function assertViolationsRaisedByCompound(Constraint|array $constraints): void + { + if ($constraints instanceof Constraint) { + $constraints = [$constraints]; + } + + $validator = new CompoundValidator(); + $context = $this->createContext(); + $validator->initialize($context); + + $validator->validate($this->validatedValue, new class($constraints) extends Compound { + public function __construct(private array $testedConstraints) + { + parent::__construct(); + } + + protected function getConstraints(array $options): array + { + return $this->testedConstraints; + } + }); + + $expectedViolations = iterator_to_array($context->getViolations()); + + if (!$expectedViolations) { + throw new ExpectationFailedException(\sprintf('Expected at least one violation for constraint(s) "%s", got none raised.', implode(', ', array_map(fn ($constraint) => $constraint::class, $constraints)))); + } + + $failedToAssertViolations = []; + reset($expectedViolations); + foreach ($this->context->getViolations() as $violation) { + if ($violation != current($expectedViolations)) { + $failedToAssertViolations[] = $violation; + } + + next($expectedViolations); + } + + $this->assertSame( + [], + $failedToAssertViolations, + \sprintf('Expected violation(s) for constraint(s) %s to be raised by compound.', + implode(', ', array_map(fn ($violation) => ($violation->getConstraint())::class, $failedToAssertViolations)) + ) + ); + } + + public function assertViolationsCount(int $count): void + { + $this->assertCount($count, $this->context->getViolations()); + } + + protected function assertNoViolation(): void + { + $violationsCount = \count($this->context->getViolations()); + $this->assertSame(0, $violationsCount, \sprintf('No violation expected. Got %d.', $violationsCount)); + } + + /** + * @return T + */ + abstract protected function createCompound(): Compound; +} diff --git a/Test/ConstraintValidatorTestCase.php b/Test/ConstraintValidatorTestCase.php index 573210b62..2fe70eaef 100644 --- a/Test/ConstraintValidatorTestCase.php +++ b/Test/ConstraintValidatorTestCase.php @@ -30,6 +30,7 @@ use Symfony\Component\Validator\Context\ExecutionContext; use Symfony\Component\Validator\Context\ExecutionContextInterface; use Symfony\Component\Validator\Mapping\ClassMetadata; +use Symfony\Component\Validator\Mapping\MetadataInterface; use Symfony\Component\Validator\Mapping\PropertyMetadata; use Symfony\Component\Validator\Validator\ContextualValidatorInterface; use Symfony\Component\Validator\Validator\ValidatorInterface; @@ -44,24 +45,22 @@ */ abstract class ConstraintValidatorTestCase extends TestCase { - /** - * @var ExecutionContextInterface - */ - protected $context; + protected ExecutionContextInterface $context; /** * @var T */ - protected $validator; - - protected $group; - protected $metadata; - protected $object; - protected $value; - protected $root; - protected $propertyPath; - protected $constraint; - protected $defaultTimezone; + protected ConstraintValidatorInterface $validator; + + protected string $group; + protected ?MetadataInterface $metadata; + protected mixed $object; + protected mixed $value; + protected mixed $root; + protected string $propertyPath; + protected Constraint $constraint; + protected ?string $defaultTimezone = null; + private string $defaultLocale; private array $expectedViolations; private int $call; @@ -235,7 +234,7 @@ protected function expectValidateValue(int $i, mixed $value, array $constraints { $contextualValidator = $this->context->getValidator()->inContext($this->context); $contextualValidator->expectValidation($i, null, $value, $group, function ($passedConstraints) use ($constraints) { - if (\is_array($constraints) && !\is_array($passedConstraints)) { + if (!\is_array($passedConstraints)) { $passedConstraints = [$passedConstraints]; } @@ -247,7 +246,7 @@ protected function expectFailingValueValidation(int $i, mixed $value, array $con { $contextualValidator = $this->context->getValidator()->inContext($this->context); $contextualValidator->expectValidation($i, null, $value, $group, function ($passedConstraints) use ($constraints) { - if (\is_array($constraints) && !\is_array($passedConstraints)) { + if (!\is_array($passedConstraints)) { $passedConstraints = [$passedConstraints]; } @@ -289,40 +288,31 @@ protected function buildViolation(string|\Stringable $message): ConstraintViolat } /** - * @return ConstraintValidatorInterface - * - * @psalm-return T + * @return T */ - abstract protected function createValidator(); + abstract protected function createValidator(): ConstraintValidatorInterface; } final class ConstraintViolationAssertion { - private ExecutionContextInterface $context; - - /** - * @var ConstraintViolationAssertion[] - */ - private array $assertions; - - private string $message; private array $parameters = []; private mixed $invalidValue = 'InvalidValue'; private string $propertyPath = 'property.path'; private ?int $plural = null; private ?string $code = null; - private ?Constraint $constraint; private mixed $cause = null; /** + * @param ConstraintViolationAssertion[] $assertions + * * @internal */ - public function __construct(ExecutionContextInterface $context, string $message, ?Constraint $constraint = null, array $assertions = []) - { - $this->context = $context; - $this->message = $message; - $this->constraint = $constraint; - $this->assertions = $assertions; + public function __construct( + private ExecutionContextInterface $context, + private string $message, + private ?Constraint $constraint = null, + private array $assertions = [], + ) { } /** @@ -455,16 +445,15 @@ private function getViolation(): ConstraintViolation */ class AssertingContextualValidator implements ContextualValidatorInterface { - private ExecutionContextInterface $context; private bool $expectNoValidate = false; private int $atPathCalls = -1; private array $expectedAtPath = []; private int $validateCalls = -1; private array $expectedValidate = []; - public function __construct(ExecutionContextInterface $context) - { - $this->context = $context; + public function __construct( + private ExecutionContextInterface $context, + ) { } public function __destruct() diff --git a/Tests/ConstraintTest.php b/Tests/ConstraintTest.php index 3d233c178..80e33c7b7 100644 --- a/Tests/ConstraintTest.php +++ b/Tests/ConstraintTest.php @@ -245,7 +245,7 @@ public function testOptionsWithInvalidInternalPointer() $this->assertEquals('foo', $constraint->property1); } - public function testAnnotationSetUndefinedDefaultOption() + public function testAttributeSetUndefinedDefaultOption() { $this->expectException(ConstraintDefinitionException::class); $this->expectExceptionMessage('No default option is configured for constraint "Symfony\Component\Validator\Tests\Fixtures\ConstraintB".'); diff --git a/Tests/Constraints/AbstractComparisonValidatorTestCase.php b/Tests/Constraints/AbstractComparisonValidatorTestCase.php index 0a92994f6..25fed976c 100644 --- a/Tests/Constraints/AbstractComparisonValidatorTestCase.php +++ b/Tests/Constraints/AbstractComparisonValidatorTestCase.php @@ -11,12 +11,9 @@ namespace Symfony\Component\Validator\Tests\Constraints; -use Symfony\Component\Intl\Util\IntlTestHelper; use Symfony\Component\Validator\Constraint; -use Symfony\Component\Validator\Constraints\AbstractComparison; use Symfony\Component\Validator\Exception\ConstraintDefinitionException; use Symfony\Component\Validator\Test\ConstraintValidatorTestCase; -use Symfony\Component\Validator\Tests\Constraints\Fixtures\TypedDummy; class ComparisonTest_Class { @@ -98,32 +95,6 @@ public function testThrowsConstraintExceptionIfBothValueAndPropertyPath() ]); } - /** - * @dataProvider provideAllValidComparisons - */ - public function testValidComparisonToValue($dirtyValue, $comparisonValue) - { - $constraint = $this->createConstraint(['value' => $comparisonValue]); - - $this->validator->validate($dirtyValue, $constraint); - - $this->assertNoViolation(); - } - - public static function provideAllValidComparisons(): array - { - // The provider runs before setUp(), so we need to manually fix - // the default timezone - $timezone = date_default_timezone_get(); - date_default_timezone_set('UTC'); - - $comparisons = self::addPhp5Dot5Comparisons(static::provideValidComparisons()); - - date_default_timezone_set($timezone); - - return $comparisons; - } - /** * @dataProvider provideValidComparisonsToPropertyPath */ @@ -169,146 +140,8 @@ abstract public static function provideValidComparisons(): array; abstract public static function provideValidComparisonsToPropertyPath(): array; - /** - * @dataProvider provideAllInvalidComparisons - */ - public function testInvalidComparisonToValue($dirtyValue, $dirtyValueAsString, $comparedValue, $comparedValueString, $comparedValueType) - { - // Conversion of dates to string differs between ICU versions - // Make sure we have the correct version loaded - if ($dirtyValue instanceof \DateTimeInterface) { - IntlTestHelper::requireIntl($this, '57.1'); - } - - $constraint = $this->createConstraint(['value' => $comparedValue]); - $constraint->message = 'Constraint Message'; - - $this->validator->validate($dirtyValue, $constraint); - - $this->buildViolation('Constraint Message') - ->setParameter('{{ value }}', $dirtyValueAsString) - ->setParameter('{{ compared_value }}', $comparedValueString) - ->setParameter('{{ compared_value_type }}', $comparedValueType) - ->setCode($this->getErrorCode()) - ->assertRaised(); - } - - public function testInvalidComparisonToPropertyPathAddsPathAsParameter() - { - [$dirtyValue, $dirtyValueAsString, $comparedValue, $comparedValueString, $comparedValueType] = current($this->provideAllInvalidComparisons()); - - $constraint = $this->createConstraint(['propertyPath' => 'value']); - $constraint->message = 'Constraint Message'; - - $object = new ComparisonTest_Class($comparedValue); - - $this->setObject($object); - - $this->validator->validate($dirtyValue, $constraint); - - $this->buildViolation('Constraint Message') - ->setParameter('{{ value }}', $dirtyValueAsString) - ->setParameter('{{ compared_value }}', $comparedValueString) - ->setParameter('{{ compared_value_path }}', 'value') - ->setParameter('{{ compared_value_type }}', $comparedValueType) - ->setCode($this->getErrorCode()) - ->assertRaised(); - } - - /** - * @dataProvider throwsOnInvalidStringDatesProvider - */ - public function testThrowsOnInvalidStringDates(AbstractComparison $constraint, $expectedMessage, $value) - { - $this->expectException(ConstraintDefinitionException::class); - $this->expectExceptionMessage($expectedMessage); - - $this->validator->validate($value, $constraint); - } - - public static function throwsOnInvalidStringDatesProvider(): array - { - $constraint = static::createConstraint([ - 'value' => 'foo', - ]); - - $constraintClass = $constraint::class; - - return [ - [$constraint, \sprintf('The compared value "foo" could not be converted to a "DateTimeImmutable" instance in the "%s" constraint.', $constraintClass), new \DateTimeImmutable()], - [$constraint, \sprintf('The compared value "foo" could not be converted to a "DateTime" instance in the "%s" constraint.', $constraintClass), new \DateTime()], - ]; - } - - /** - * @dataProvider provideComparisonsToNullValueAtPropertyPath - */ - public function testCompareWithNullValueAtPropertyAt($dirtyValue, $dirtyValueAsString, $isValid) - { - $constraint = $this->createConstraint(['propertyPath' => 'value']); - $constraint->message = 'Constraint Message'; - - $object = new ComparisonTest_Class(null); - $this->setObject($object); - - $this->validator->validate($dirtyValue, $constraint); - - if ($isValid) { - $this->assertNoViolation(); - } else { - $this->buildViolation('Constraint Message') - ->setParameter('{{ value }}', $dirtyValueAsString) - ->setParameter('{{ compared_value }}', 'null') - ->setParameter('{{ compared_value_type }}', 'null') - ->setParameter('{{ compared_value_path }}', 'value') - ->setCode($this->getErrorCode()) - ->assertRaised(); - } - } - - /** - * @dataProvider provideComparisonsToNullValueAtPropertyPath - */ - public function testCompareWithUninitializedPropertyAtPropertyPath($dirtyValue, $dirtyValueAsString, $isValid) - { - $this->setObject(new TypedDummy()); - - $this->validator->validate($dirtyValue, $this->createConstraint([ - 'message' => 'Constraint Message', - 'propertyPath' => 'value', - ])); - - if ($isValid) { - $this->assertNoViolation(); - } else { - $this->buildViolation('Constraint Message') - ->setParameter('{{ value }}', $dirtyValueAsString) - ->setParameter('{{ compared_value }}', 'null') - ->setParameter('{{ compared_value_type }}', 'null') - ->setParameter('{{ compared_value_path }}', 'value') - ->setCode($this->getErrorCode()) - ->assertRaised(); - } - } - - public static function provideAllInvalidComparisons(): array - { - // The provider runs before setUp(), so we need to manually fix - // the default timezone - $timezone = date_default_timezone_get(); - date_default_timezone_set('UTC'); - - $comparisons = self::addPhp5Dot5Comparisons(static::provideInvalidComparisons()); - - date_default_timezone_set($timezone); - - return $comparisons; - } - abstract public static function provideInvalidComparisons(): array; - abstract public static function provideComparisonsToNullValueAtPropertyPath(); - abstract protected static function createConstraint(?array $options = null): Constraint; protected function getErrorCode(): ?string diff --git a/Tests/Constraints/AllValidatorTest.php b/Tests/Constraints/AllValidatorTest.php index 65dae6275..ee6a29174 100644 --- a/Tests/Constraints/AllValidatorTest.php +++ b/Tests/Constraints/AllValidatorTest.php @@ -27,7 +27,7 @@ protected function createValidator(): AllValidator public function testNullIsValid() { - $this->validator->validate(null, new All(new Range(['min' => 4]))); + $this->validator->validate(null, new All(new Range(min: 4))); $this->assertNoViolation(); } @@ -35,7 +35,7 @@ public function testNullIsValid() public function testThrowsExceptionIfNotTraversable() { $this->expectException(UnexpectedValueException::class); - $this->validator->validate('foo.barbar', new All(new Range(['min' => 4]))); + $this->validator->validate('foo.barbar', new All(new Range(min: 4))); } /** @@ -43,7 +43,7 @@ public function testThrowsExceptionIfNotTraversable() */ public function testWalkSingleConstraint($array) { - $constraint = new Range(['min' => 4]); + $constraint = new Range(min: 4); $i = 0; @@ -61,7 +61,7 @@ public function testWalkSingleConstraint($array) */ public function testWalkMultipleConstraints($array) { - $constraint1 = new Range(['min' => 4]); + $constraint1 = new Range(min: 4); $constraint2 = new NotNull(); $constraints = [$constraint1, $constraint2]; diff --git a/Tests/Constraints/AtLeastOneOfValidatorTest.php b/Tests/Constraints/AtLeastOneOfValidatorTest.php index 8bda680b2..22b53dd13 100644 --- a/Tests/Constraints/AtLeastOneOfValidatorTest.php +++ b/Tests/Constraints/AtLeastOneOfValidatorTest.php @@ -66,31 +66,31 @@ public static function getValidCombinations() { return [ ['symfony', [ - new Length(['min' => 10]), - new EqualTo(['value' => 'symfony']), + new Length(min: 10), + new EqualTo(value: 'symfony'), ]], [150, [ - new Range(['min' => 10, 'max' => 20]), - new GreaterThanOrEqual(['value' => 100]), + new Range(min: 10, max: 20), + new GreaterThanOrEqual(value: 100), ]], [7, [ - new LessThan(['value' => 5]), - new IdenticalTo(['value' => 7]), + new LessThan(value: 5), + new IdenticalTo(value: 7), ]], [-3, [ - new DivisibleBy(['value' => 4]), + new DivisibleBy(value: 4), new Negative(), ]], ['FOO', [ - new Choice(['choices' => ['bar', 'BAR']]), - new Regex(['pattern' => '/foo/i']), + new Choice(choices: ['bar', 'BAR']), + new Regex(pattern: '/foo/i'), ]], ['fr', [ new Country(), new Language(), ]], [[1, 3, 5], [ - new Count(['min' => 5]), + new Count(min: 5), new Unique(), ]], ]; @@ -101,7 +101,7 @@ public static function getValidCombinations() */ public function testInvalidCombinationsWithDefaultMessage($value, $constraints) { - $atLeastOneOf = new AtLeastOneOf(['constraints' => $constraints]); + $atLeastOneOf = new AtLeastOneOf(constraints: $constraints); $validator = Validation::createValidator(); $message = [$atLeastOneOf->message]; @@ -123,7 +123,11 @@ public function testInvalidCombinationsWithDefaultMessage($value, $constraints) */ public function testInvalidCombinationsWithCustomMessage($value, $constraints) { - $atLeastOneOf = new AtLeastOneOf(['constraints' => $constraints, 'message' => 'foo', 'includeInternalMessages' => false]); + $atLeastOneOf = new AtLeastOneOf( + constraints: $constraints, + message: 'foo', + includeInternalMessages: false, + ); $violations = Validation::createValidator()->validate($value, $atLeastOneOf); @@ -135,31 +139,31 @@ public static function getInvalidCombinations() { return [ ['symphony', [ - new Length(['min' => 10]), - new EqualTo(['value' => 'symfony']), + new Length(min: 10), + new EqualTo(value: 'symfony'), ]], [70, [ - new Range(['min' => 10, 'max' => 20]), - new GreaterThanOrEqual(['value' => 100]), + new Range(min: 10, max: 20), + new GreaterThanOrEqual(value: 100), ]], [8, [ - new LessThan(['value' => 5]), - new IdenticalTo(['value' => 7]), + new LessThan(value: 5), + new IdenticalTo(value: 7), ]], [3, [ - new DivisibleBy(['value' => 4]), + new DivisibleBy(value: 4), new Negative(), ]], ['F_O_O', [ - new Choice(['choices' => ['bar', 'BAR']]), - new Regex(['pattern' => '/foo/i']), + new Choice(choices: ['bar', 'BAR']), + new Regex(pattern: '/foo/i'), ]], ['f_r', [ new Country(), new Language(), ]], [[1, 3, 3], [ - new Count(['min' => 5]), + new Count(min: 5), new Unique(), ]], ]; @@ -169,21 +173,21 @@ public function testGroupsArePropagatedToNestedConstraints() { $validator = Validation::createValidator(); - $violations = $validator->validate(50, new AtLeastOneOf([ - 'constraints' => [ - new Range([ - 'groups' => 'non_default_group', - 'min' => 10, - 'max' => 20, - ]), - new Range([ - 'groups' => 'non_default_group', - 'min' => 30, - 'max' => 40, - ]), + $violations = $validator->validate(50, new AtLeastOneOf( + constraints: [ + new Range( + groups: ['non_default_group'], + min: 10, + max: 20, + ), + new Range( + groups: ['non_default_group'], + min: 30, + max: 40, + ), ], - 'groups' => 'non_default_group', - ]), 'non_default_group'); + groups: ['non_default_group'], + ), ['non_default_group']); $this->assertCount(1, $violations); } @@ -221,9 +225,9 @@ public function testEmbeddedMessageTakenFromFailingConstraint() public function getMetadataFor($classOrObject): MetadataInterface { return (new ClassMetadata(Data::class)) - ->addPropertyConstraint('foo', new NotNull(['message' => 'custom message foo'])) + ->addPropertyConstraint('foo', new NotNull(message: 'custom message foo')) ->addPropertyConstraint('bar', new AtLeastOneOf([ - new NotNull(['message' => 'custom message bar']), + new NotNull(message: 'custom message bar'), ])) ; } @@ -247,20 +251,20 @@ public function testNestedConstraintsAreNotExecutedWhenGroupDoesNotMatch() { $validator = Validation::createValidator(); - $violations = $validator->validate(50, new AtLeastOneOf([ - 'constraints' => [ - new Range([ - 'groups' => 'adult', - 'min' => 18, - 'max' => 55, - ]), - new GreaterThan([ - 'groups' => 'senior', - 'value' => 55, - ]), + $violations = $validator->validate(50, new AtLeastOneOf( + constraints: [ + new Range( + groups: ['adult'], + min: 18, + max: 55, + ), + new GreaterThan( + groups: ['senior'], + value: 55, + ), ], - 'groups' => ['adult', 'senior'], - ]), 'senior'); + groups: ['adult', 'senior'], + ), 'senior'); $this->assertCount(1, $violations); } diff --git a/Tests/Constraints/BicValidatorTest.php b/Tests/Constraints/BicValidatorTest.php index 9661350a1..315cb859e 100644 --- a/Tests/Constraints/BicValidatorTest.php +++ b/Tests/Constraints/BicValidatorTest.php @@ -14,6 +14,7 @@ use Symfony\Component\Validator\Constraints\Bic; use Symfony\Component\Validator\Constraints\BicValidator; use Symfony\Component\Validator\Exception\ConstraintDefinitionException; +use Symfony\Component\Validator\Exception\InvalidArgumentException; use Symfony\Component\Validator\Exception\UnexpectedValueException; use Symfony\Component\Validator\Mapping\ClassMetadata; use Symfony\Component\Validator\Mapping\Loader\AttributeLoader; @@ -43,7 +44,7 @@ public function testEmptyStringIsValid() public function testValidComparisonToPropertyPath() { - $constraint = new Bic(['ibanPropertyPath' => 'value']); + $constraint = new Bic(ibanPropertyPath: 'value'); $object = new BicComparisonTestClass('FR14 2004 1010 0505 0001 3M02 606'); @@ -56,7 +57,7 @@ public function testValidComparisonToPropertyPath() public function testInvalidComparisonToPropertyPath() { - $constraint = new Bic(['ibanPropertyPath' => 'value']); + $constraint = new Bic(ibanPropertyPath: 'value'); $constraint->ibanMessage = 'Constraint Message'; $object = new BicComparisonTestClass('FR14 2004 1010 0505 0001 3M02 606'); @@ -94,14 +95,14 @@ public function testPropertyPathReferencingUninitializedProperty() { $this->setObject(new BicTypedDummy()); - $this->validator->validate('UNCRIT2B912', new Bic(['ibanPropertyPath' => 'iban'])); + $this->validator->validate('UNCRIT2B912', new Bic(ibanPropertyPath: 'iban')); $this->assertNoViolation(); } public function testValidComparisonToValue() { - $constraint = new Bic(['iban' => 'FR14 2004 1010 0505 0001 3M02 606']); + $constraint = new Bic(iban: 'FR14 2004 1010 0505 0001 3M02 606'); $constraint->ibanMessage = 'Constraint Message'; $this->validator->validate('SOGEFRPP', $constraint); @@ -111,7 +112,7 @@ public function testValidComparisonToValue() public function testInvalidComparisonToValue() { - $constraint = new Bic(['iban' => 'FR14 2004 1010 0505 0001 3M02 606']); + $constraint = new Bic(iban: 'FR14 2004 1010 0505 0001 3M02 606'); $constraint->ibanMessage = 'Constraint Message'; $this->validator->validate('UNCRIT2B912', $constraint); @@ -141,7 +142,7 @@ public function testInvalidComparisonToValueFromAttribute() public function testNoViolationOnNullObjectWithPropertyPath() { - $constraint = new Bic(['ibanPropertyPath' => 'propertyPath']); + $constraint = new Bic(ibanPropertyPath: 'propertyPath'); $this->setObject(null); @@ -154,10 +155,10 @@ public function testThrowsConstraintExceptionIfBothValueAndPropertyPath() { $this->expectException(ConstraintDefinitionException::class); $this->expectExceptionMessage('The "iban" and "ibanPropertyPath" options of the Iban constraint cannot be used at the same time'); - new Bic([ - 'iban' => 'value', - 'ibanPropertyPath' => 'propertyPath', - ]); + new Bic( + iban: 'value', + ibanPropertyPath: 'propertyPath', + ); } public function testThrowsConstraintExceptionIfBothValueAndPropertyPathNamed() @@ -170,7 +171,7 @@ public function testThrowsConstraintExceptionIfBothValueAndPropertyPathNamed() public function testInvalidValuePath() { - $constraint = new Bic(['ibanPropertyPath' => 'foo']); + $constraint = new Bic(ibanPropertyPath: 'foo'); $this->expectException(ConstraintDefinitionException::class); $this->expectExceptionMessage(\sprintf('Invalid property path "foo" provided to "%s" constraint', $constraint::class)); @@ -216,9 +217,9 @@ public static function getValidBics() */ public function testInvalidBics($bic, $code) { - $constraint = new Bic([ - 'message' => 'myMessage', - ]); + $constraint = new Bic( + message: 'myMessage', + ); $this->validator->validate($bic, $constraint); @@ -276,7 +277,7 @@ public static function getInvalidBics() */ public function testValidBicSpecialCases(string $bic, string $iban) { - $constraint = new Bic(['iban' => $iban]); + $constraint = new Bic(iban: $iban); $this->validator->validate($bic, $constraint); $this->assertNoViolation(); @@ -311,6 +312,36 @@ public static function getValidBicSpecialCases() yield ['CAIXICBBXXX', 'ES79 2100 0813 6101 2345 6789']; yield ['CAIXEABBXXX', 'ES79 2100 0813 6101 2345 6789']; } + + /** + * @dataProvider getValidBicsWithNormalizerToUpper + */ + public function testValidBicsWithNormalizerToUpper($bic) + { + $this->validator->validate($bic, new Bic(mode: Bic::VALIDATION_MODE_CASE_INSENSITIVE)); + + $this->assertNoViolation(); + } + + public static function getValidBicsWithNormalizerToUpper() + { + return [ + ['ASPKAT2LXXX'], + ['ASPKat2LXXX'], + ['ASPKaT2LXXX'], + ['ASPKAt2LXXX'], + ['aspkat2lxxx'], + ]; + } + + public function testFailOnInvalidMode() + { + $this->expectException(InvalidArgumentException::class); + $this->validator->validate('ASPKAT2LXXX', new Bic(mode: 'invalid')); + + $this->expectException(InvalidArgumentException::class); + $this->validator->validate('ASPKAT2LXXX', new Bic(options: ['mode' => 'invalid'])); + } } class BicComparisonTestClass diff --git a/Tests/Constraints/BlankValidatorTest.php b/Tests/Constraints/BlankValidatorTest.php index 9643c6793..21d3fc83e 100644 --- a/Tests/Constraints/BlankValidatorTest.php +++ b/Tests/Constraints/BlankValidatorTest.php @@ -41,9 +41,9 @@ public function testBlankIsValid() */ public function testInvalidValues($value, $valueAsString) { - $constraint = new Blank([ - 'message' => 'myMessage', - ]); + $constraint = new Blank( + message: 'myMessage', + ); $this->validator->validate($value, $constraint); diff --git a/Tests/Constraints/CallbackValidatorTest.php b/Tests/Constraints/CallbackValidatorTest.php index 084b192b6..7fbcd2714 100644 --- a/Tests/Constraints/CallbackValidatorTest.php +++ b/Tests/Constraints/CallbackValidatorTest.php @@ -74,7 +74,7 @@ public function testSingleMethod() public function testSingleMethodExplicitName() { $object = new CallbackValidatorTest_Object(); - $constraint = new Callback(['callback' => 'validate']); + $constraint = new Callback(callback: 'validate'); $this->validator->validate($object, $constraint); @@ -129,13 +129,11 @@ public function testClosureNullObject() public function testClosureExplicitName() { $object = new CallbackValidatorTest_Object(); - $constraint = new Callback([ - 'callback' => function ($object, ExecutionContextInterface $context) { - $context->addViolation('My message', ['{{ value }}' => 'foobar']); + $constraint = new Callback(callback: function ($object, ExecutionContextInterface $context) { + $context->addViolation('My message', ['{{ value }}' => 'foobar']); - return false; - }, - ]); + return false; + }); $this->validator->validate($object, $constraint); @@ -170,9 +168,7 @@ public function testArrayCallableNullObject() public function testArrayCallableExplicitName() { $object = new CallbackValidatorTest_Object(); - $constraint = new Callback([ - 'callback' => [__CLASS__.'_Class', 'validateCallback'], - ]); + $constraint = new Callback(callback: [__CLASS__.'_Class', 'validateCallback']); $this->validator->validate($object, $constraint); @@ -186,7 +182,7 @@ public function testExpectValidMethods() $this->expectException(ConstraintDefinitionException::class); $object = new CallbackValidatorTest_Object(); - $this->validator->validate($object, new Callback(['callback' => ['foobar']])); + $this->validator->validate($object, new Callback(callback: ['foobar'])); } public function testExpectValidCallbacks() @@ -194,18 +190,18 @@ public function testExpectValidCallbacks() $this->expectException(ConstraintDefinitionException::class); $object = new CallbackValidatorTest_Object(); - $this->validator->validate($object, new Callback(['callback' => ['foo', 'bar']])); + $this->validator->validate($object, new Callback(callback: ['foo', 'bar'])); } public function testConstraintGetTargets() { - $constraint = new Callback([]); + $constraint = new Callback(callback: []); $targets = [Constraint::CLASS_CONSTRAINT, Constraint::PROPERTY_CONSTRAINT]; $this->assertEquals($targets, $constraint->getTargets()); } - // Should succeed. Needed when defining constraints as annotations. + // Should succeed. Needed when defining constraints as attributes. public function testNoConstructorArguments() { $constraint = new Callback(); @@ -213,18 +209,18 @@ public function testNoConstructorArguments() $this->assertSame([Constraint::CLASS_CONSTRAINT, Constraint::PROPERTY_CONSTRAINT], $constraint->getTargets()); } - public function testAnnotationInvocationSingleValued() + public function testAttributeInvocationSingleValued() { - $constraint = new Callback(['value' => 'validateStatic']); + $constraint = new Callback(callback: 'validateStatic'); - $this->assertEquals(new Callback('validateStatic'), $constraint); + $this->assertEquals(new Callback(callback: 'validateStatic'), $constraint); } - public function testAnnotationInvocationMultiValued() + public function testAttributeInvocationMultiValued() { - $constraint = new Callback(['value' => [__CLASS__.'_Class', 'validateCallback']]); + $constraint = new Callback(callback: [__CLASS__.'_Class', 'validateCallback']); - $this->assertEquals(new Callback([__CLASS__.'_Class', 'validateCallback']), $constraint); + $this->assertEquals(new Callback(callback: [__CLASS__.'_Class', 'validateCallback']), $constraint); } public function testPayloadIsPassedToCallback() @@ -235,10 +231,10 @@ public function testPayloadIsPassedToCallback() $payloadCopy = $payload; }; - $constraint = new Callback([ - 'callback' => $callback, - 'payload' => 'Hello world!', - ]); + $constraint = new Callback( + callback: $callback, + payload: 'Hello world!', + ); $this->validator->validate($object, $constraint); $this->assertEquals('Hello world!', $payloadCopy); @@ -246,12 +242,9 @@ public function testPayloadIsPassedToCallback() $constraint = new Callback(callback: $callback, payload: 'Hello world!'); $this->validator->validate($object, $constraint); $this->assertEquals('Hello world!', $payloadCopy); - $payloadCopy = 'Replace me!'; $payloadCopy = 'Replace me!'; - $constraint = new Callback([ - 'callback' => $callback, - ]); + $constraint = new Callback(callback: $callback); $this->validator->validate($object, $constraint); $this->assertNull($payloadCopy); } diff --git a/Tests/Constraints/CardSchemeValidatorTest.php b/Tests/Constraints/CardSchemeValidatorTest.php index 15f4fa634..d70457833 100644 --- a/Tests/Constraints/CardSchemeValidatorTest.php +++ b/Tests/Constraints/CardSchemeValidatorTest.php @@ -24,14 +24,14 @@ protected function createValidator(): CardSchemeValidator public function testNullIsValid() { - $this->validator->validate(null, new CardScheme(['schemes' => []])); + $this->validator->validate(null, new CardScheme(schemes: [])); $this->assertNoViolation(); } public function testEmptyStringIsValid() { - $this->validator->validate('', new CardScheme(['schemes' => []])); + $this->validator->validate('', new CardScheme(schemes: [])); $this->assertNoViolation(); } @@ -41,7 +41,7 @@ public function testEmptyStringIsValid() */ public function testValidNumbers($scheme, $number) { - $this->validator->validate($number, new CardScheme(['schemes' => $scheme])); + $this->validator->validate($number, new CardScheme(schemes: $scheme)); $this->assertNoViolation(); } @@ -51,7 +51,7 @@ public function testValidNumbers($scheme, $number) */ public function testValidNumbersWithNewLine($scheme, $number) { - $this->validator->validate($number."\n", new CardScheme(['schemes' => $scheme, 'message' => 'myMessage'])); + $this->validator->validate($number."\n", new CardScheme(schemes: $scheme, message: 'myMessage')); $this->buildViolation('myMessage') ->setParameter('{{ value }}', '"'.$number."\n\"") @@ -74,10 +74,10 @@ public function testValidNumberWithOrderedArguments() */ public function testInvalidNumbers($scheme, $number, $code) { - $constraint = new CardScheme([ - 'schemes' => $scheme, - 'message' => 'myMessage', - ]); + $constraint = new CardScheme( + schemes: $scheme, + message: 'myMessage', + ); $this->validator->validate($number, $constraint); diff --git a/Tests/Constraints/CascadeTest.php b/Tests/Constraints/CascadeTest.php index 2ef4c9c83..fc4d7ce0f 100644 --- a/Tests/Constraints/CascadeTest.php +++ b/Tests/Constraints/CascadeTest.php @@ -35,6 +35,9 @@ public function testExcludeProperties() self::assertSame(['foo' => 0, 'bar' => 1], $constraint->exclude); } + /** + * @group legacy + */ public function testExcludePropertiesDoctrineStyle() { $constraint = new Cascade(['exclude' => ['foo', 'bar']]); diff --git a/Tests/Constraints/CharsetTest.php b/Tests/Constraints/CharsetTest.php new file mode 100644 index 000000000..1b23a2ea7 --- /dev/null +++ b/Tests/Constraints/CharsetTest.php @@ -0,0 +1,65 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Tests\Constraints; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Validator\Constraints\Charset; +use Symfony\Component\Validator\Exception\ConstraintDefinitionException; +use Symfony\Component\Validator\Mapping\ClassMetadata; +use Symfony\Component\Validator\Mapping\Loader\AttributeLoader; + +class CharsetTest extends TestCase +{ + public function testSingleEncodingCanBeSet() + { + $encoding = new Charset('UTF-8'); + + $this->assertSame('UTF-8', $encoding->encodings); + } + + public function testMultipleEncodingCanBeSet() + { + $encoding = new Charset(['ASCII', 'UTF-8']); + + $this->assertSame(['ASCII', 'UTF-8'], $encoding->encodings); + } + + public function testThrowsOnNoCharset() + { + $this->expectException(ConstraintDefinitionException::class); + $this->expectExceptionMessage('The "Symfony\Component\Validator\Constraints\Charset" constraint requires at least one encoding.'); + + new Charset(); + } + + public function testAttributes() + { + $metadata = new ClassMetadata(CharsetDummy::class); + $loader = new AttributeLoader(); + $this->assertTrue($loader->loadClassMetadata($metadata)); + + [$aConstraint] = $metadata->properties['a']->getConstraints(); + $this->assertSame('UTF-8', $aConstraint->encodings); + + [$bConstraint] = $metadata->properties['b']->getConstraints(); + $this->assertSame(['ASCII', 'UTF-8'], $bConstraint->encodings); + } +} + +class CharsetDummy +{ + #[Charset('UTF-8')] + private string $a; + + #[Charset(['ASCII', 'UTF-8'])] + private string $b; +} diff --git a/Tests/Constraints/CharsetValidatorTest.php b/Tests/Constraints/CharsetValidatorTest.php new file mode 100644 index 000000000..5c2f2c884 --- /dev/null +++ b/Tests/Constraints/CharsetValidatorTest.php @@ -0,0 +1,90 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Tests\Constraints; + +use Symfony\Component\Validator\Constraints\Charset; +use Symfony\Component\Validator\Constraints\CharsetValidator; +use Symfony\Component\Validator\Exception\UnexpectedValueException; +use Symfony\Component\Validator\Test\ConstraintValidatorTestCase; +use Symfony\Component\Validator\Tests\Constraints\Fixtures\StringableValue; + +class CharsetValidatorTest extends ConstraintValidatorTestCase +{ + protected function createValidator(): CharsetValidator + { + return new CharsetValidator(); + } + + /** + * @dataProvider provideValidValues + */ + public function testEncodingIsValid(string|\Stringable $value, array|string $encodings) + { + $this->validator->validate($value, new Charset(encodings: $encodings)); + + $this->assertNoViolation(); + } + + /** + * @dataProvider provideInvalidValues + */ + public function testInvalidValues(string $value, array|string $encodings) + { + $this->validator->validate($value, new Charset(encodings: $encodings)); + + $this->buildViolation('The detected character encoding is invalid ({{ detected }}). Allowed encodings are {{ encodings }}.') + ->setParameter('{{ detected }}', 'UTF-8') + ->setParameter('{{ encodings }}', implode(', ', (array) $encodings)) + ->setCode(Charset::BAD_ENCODING_ERROR) + ->assertRaised(); + } + + /** + * @dataProvider provideInvalidTypes + */ + public function testNonStringValues(mixed $value) + { + $this->expectException(UnexpectedValueException::class); + $this->expectExceptionMessageMatches('/Expected argument of type "string", ".*" given/'); + + $this->validator->validate($value, new Charset(encodings: ['UTF-8'])); + } + + public static function provideValidValues() + { + yield ['my ascii string', ['ASCII']]; + yield ['my ascii string', ['UTF-8']]; + yield ['my ascii string', 'UTF-8']; + yield ['my ascii string', ['ASCII', 'UTF-8']]; + yield ['my ûtf 8', ['ASCII', 'UTF-8']]; + yield ['my ûtf 8', ['UTF-8']]; + yield ['string', ['ISO-8859-1']]; + yield [new StringableValue('my ûtf 8'), ['UTF-8']]; + } + + public static function provideInvalidValues() + { + yield ['my non-Äscîi string', 'ASCII']; + yield ['my non-Äscîi string', ['ASCII']]; + yield ['😊', ['7bit']]; + } + + public static function provideInvalidTypes() + { + yield [true]; + yield [false]; + yield [1]; + yield [1.1]; + yield [[]]; + yield [new \stdClass()]; + } +} diff --git a/Tests/Constraints/ChoiceValidatorTest.php b/Tests/Constraints/ChoiceValidatorTest.php index a78a2bfa5..39affe442 100644 --- a/Tests/Constraints/ChoiceValidatorTest.php +++ b/Tests/Constraints/ChoiceValidatorTest.php @@ -47,22 +47,17 @@ public static function staticCallbackInvalid() public function testExpectArrayIfMultipleIsTrue() { $this->expectException(UnexpectedValueException::class); - $constraint = new Choice([ - 'choices' => ['foo', 'bar'], - 'multiple' => true, - ]); + $constraint = new Choice( + choices: ['foo', 'bar'], + multiple: true, + ); $this->validator->validate('asdf', $constraint); } public function testNullIsValid() { - $this->validator->validate( - null, - new Choice([ - 'choices' => ['foo', 'bar'], - ]) - ); + $this->validator->validate(null, new Choice(choices: ['foo', 'bar'])); $this->assertNoViolation(); } @@ -76,7 +71,7 @@ public function testChoicesOrCallbackExpected() public function testValidCallbackExpected() { $this->expectException(ConstraintDefinitionException::class); - $this->validator->validate('foobar', new Choice(['callback' => 'abcd'])); + $this->validator->validate('foobar', new Choice(callback: 'abcd')); } /** @@ -91,12 +86,28 @@ public function testValidChoiceArray(Choice $constraint) public static function provideConstraintsWithChoicesArray(): iterable { - yield 'Doctrine style' => [new Choice(['choices' => ['foo', 'bar']])]; - yield 'Doctrine default option' => [new Choice(['value' => ['foo', 'bar']])]; yield 'first argument' => [new Choice(['foo', 'bar'])]; yield 'named arguments' => [new Choice(choices: ['foo', 'bar'])]; } + /** + * @group legacy + * + * @dataProvider provideLegacyConstraintsWithChoicesArrayDoctrineStyle + */ + public function testValidChoiceArrayDoctrineStyle(Choice $constraint) + { + $this->validator->validate('bar', $constraint); + + $this->assertNoViolation(); + } + + public static function provideLegacyConstraintsWithChoicesArrayDoctrineStyle(): iterable + { + yield 'Doctrine style' => [new Choice(['choices' => ['foo', 'bar']])]; + yield 'Doctrine default option' => [new Choice(['value' => ['foo', 'bar']])]; + } + /** * @dataProvider provideConstraintsWithCallbackFunction */ @@ -108,15 +119,31 @@ public function testValidChoiceCallbackFunction(Choice $constraint) } public static function provideConstraintsWithCallbackFunction(): iterable + { + yield 'named arguments, namespaced function' => [new Choice(callback: __NAMESPACE__.'\choice_callback')]; + yield 'named arguments, closure' => [new Choice(callback: fn () => ['foo', 'bar'])]; + yield 'named arguments, static method' => [new Choice(callback: [__CLASS__, 'staticCallback'])]; + } + + /** + * @group legacy + * + * @dataProvider provideLegacyConstraintsWithCallbackFunctionDoctrineStyle + */ + public function testValidChoiceCallbackFunctionDoctrineStyle(Choice $constraint) + { + $this->validator->validate('bar', $constraint); + + $this->assertNoViolation(); + } + + public static function provideLegacyConstraintsWithCallbackFunctionDoctrineStyle(): iterable { yield 'doctrine style, namespaced function' => [new Choice(['callback' => __NAMESPACE__.'\choice_callback'])]; yield 'doctrine style, closure' => [new Choice([ 'callback' => fn () => ['foo', 'bar'], ])]; yield 'doctrine style, static method' => [new Choice(['callback' => [__CLASS__, 'staticCallback']])]; - yield 'named arguments, namespaced function' => [new Choice(callback: __NAMESPACE__.'\choice_callback')]; - yield 'named arguments, closure' => [new Choice(callback: fn () => ['foo', 'bar'])]; - yield 'named arguments, static method' => [new Choice(callback: [__CLASS__, 'staticCallback'])]; } public function testValidChoiceCallbackContextMethod() @@ -124,7 +151,7 @@ public function testValidChoiceCallbackContextMethod() // search $this for "staticCallback" $this->setObject($this); - $constraint = new Choice(['callback' => 'staticCallback']); + $constraint = new Choice(callback: 'staticCallback'); $this->validator->validate('bar', $constraint); @@ -139,7 +166,7 @@ public function testInvalidChoiceCallbackContextMethod() // search $this for "staticCallbackInvalid" $this->setObject($this); - $constraint = new Choice(['callback' => 'staticCallbackInvalid']); + $constraint = new Choice(callback: 'staticCallbackInvalid'); $this->validator->validate('bar', $constraint); } @@ -149,41 +176,39 @@ public function testValidChoiceCallbackContextObjectMethod() // search $this for "objectMethodCallback" $this->setObject($this); - $constraint = new Choice(['callback' => 'objectMethodCallback']); + $constraint = new Choice(callback: 'objectMethodCallback'); $this->validator->validate('bar', $constraint); $this->assertNoViolation(); } - /** - * @dataProvider provideConstraintsWithMultipleTrue - */ - public function testMultipleChoices(Choice $constraint) + public function testMultipleChoices() { - $this->validator->validate(['baz', 'bar'], $constraint); + $this->validator->validate(['baz', 'bar'], new Choice( + choices: ['foo', 'bar', 'baz'], + multiple: true, + )); $this->assertNoViolation(); } - public static function provideConstraintsWithMultipleTrue(): iterable + /** + * @group legacy + */ + public function testMultipleChoicesDoctrineStyle() { - yield 'Doctrine style' => [new Choice([ + $this->validator->validate(['baz', 'bar'], new Choice([ 'choices' => ['foo', 'bar', 'baz'], 'multiple' => true, - ])]; - yield 'named arguments' => [new Choice( - choices: ['foo', 'bar', 'baz'], - multiple: true, - )]; + ])); + + $this->assertNoViolation(); } - /** - * @dataProvider provideConstraintsWithMessage - */ - public function testInvalidChoice(Choice $constraint) + public function testInvalidChoice() { - $this->validator->validate('baz', $constraint); + $this->validator->validate('baz', new Choice(choices: ['foo', 'bar'], message: 'myMessage')); $this->buildViolation('myMessage') ->setParameter('{{ value }}', '"baz"') @@ -192,20 +217,28 @@ public function testInvalidChoice(Choice $constraint) ->assertRaised(); } - public static function provideConstraintsWithMessage(): iterable + /** + * @group legacy + */ + public function testInvalidChoiceDoctrineStyle() { - yield 'Doctrine style' => [new Choice(['choices' => ['foo', 'bar'], 'message' => 'myMessage'])]; - yield 'named arguments' => [new Choice(choices: ['foo', 'bar'], message: 'myMessage')]; + $this->validator->validate('baz', new Choice(['choices' => ['foo', 'bar'], 'message' => 'myMessage'])); + + $this->buildViolation('myMessage') + ->setParameter('{{ value }}', '"baz"') + ->setParameter('{{ choices }}', '"foo", "bar"') + ->setCode(Choice::NO_SUCH_CHOICE_ERROR) + ->assertRaised(); } public function testInvalidChoiceEmptyChoices() { - $constraint = new Choice([ + $constraint = new Choice( // May happen when the choices are provided dynamically, e.g. from // the DB or the model - 'choices' => [], - 'message' => 'myMessage', - ]); + choices: [], + message: 'myMessage', + ); $this->validator->validate('baz', $constraint); @@ -216,12 +249,13 @@ public function testInvalidChoiceEmptyChoices() ->assertRaised(); } - /** - * @dataProvider provideConstraintsWithMultipleMessage - */ - public function testInvalidChoiceMultiple(Choice $constraint) + public function testInvalidChoiceMultiple() { - $this->validator->validate(['foo', 'baz'], $constraint); + $this->validator->validate(['foo', 'baz'], new Choice( + choices: ['foo', 'bar'], + multipleMessage: 'myMessage', + multiple: true, + )); $this->buildViolation('myMessage') ->setParameter('{{ value }}', '"baz"') @@ -231,30 +265,37 @@ public function testInvalidChoiceMultiple(Choice $constraint) ->assertRaised(); } - public static function provideConstraintsWithMultipleMessage(): iterable + /** + * @group legacy + */ + public function testInvalidChoiceMultipleDoctrineStyle() { - yield 'Doctrine style' => [new Choice([ + $this->validator->validate(['foo', 'baz'], new Choice([ 'choices' => ['foo', 'bar'], 'multipleMessage' => 'myMessage', 'multiple' => true, - ])]; - yield 'named arguments' => [new Choice( - choices: ['foo', 'bar'], - multipleMessage: 'myMessage', - multiple: true, - )]; + ])); + + $this->buildViolation('myMessage') + ->setParameter('{{ value }}', '"baz"') + ->setParameter('{{ choices }}', '"foo", "bar"') + ->setInvalidValue('baz') + ->setCode(Choice::NO_SUCH_CHOICE_ERROR) + ->assertRaised(); } - /** - * @dataProvider provideConstraintsWithMin - */ - public function testTooFewChoices(Choice $constraint) + public function testTooFewChoices() { $value = ['foo']; $this->setValue($value); - $this->validator->validate($value, $constraint); + $this->validator->validate($value, new Choice( + choices: ['foo', 'bar', 'moo', 'maa'], + multiple: true, + min: 2, + minMessage: 'myMessage', + )); $this->buildViolation('myMessage') ->setParameter('{{ limit }}', 2) @@ -264,32 +305,42 @@ public function testTooFewChoices(Choice $constraint) ->assertRaised(); } - public static function provideConstraintsWithMin(): iterable + /** + * @group legacy + */ + public function testTooFewChoicesDoctrineStyle() { - yield 'Doctrine style' => [new Choice([ + $value = ['foo']; + + $this->setValue($value); + + $this->validator->validate($value, new Choice([ 'choices' => ['foo', 'bar', 'moo', 'maa'], 'multiple' => true, 'min' => 2, 'minMessage' => 'myMessage', - ])]; - yield 'named arguments' => [new Choice( - choices: ['foo', 'bar', 'moo', 'maa'], - multiple: true, - min: 2, - minMessage: 'myMessage', - )]; + ])); + + $this->buildViolation('myMessage') + ->setParameter('{{ limit }}', 2) + ->setInvalidValue($value) + ->setPlural(2) + ->setCode(Choice::TOO_FEW_ERROR) + ->assertRaised(); } - /** - * @dataProvider provideConstraintsWithMax - */ - public function testTooManyChoices(Choice $constraint) + public function testTooManyChoices() { $value = ['foo', 'bar', 'moo']; $this->setValue($value); - $this->validator->validate($value, $constraint); + $this->validator->validate($value, new Choice( + choices: ['foo', 'bar', 'moo', 'maa'], + multiple: true, + max: 2, + maxMessage: 'myMessage', + )); $this->buildViolation('myMessage') ->setParameter('{{ limit }}', 2) @@ -299,27 +350,33 @@ public function testTooManyChoices(Choice $constraint) ->assertRaised(); } - public static function provideConstraintsWithMax(): iterable + /** + * @group legacy + */ + public function testTooManyChoicesDoctrineStyle() { - yield 'Doctrine style' => [new Choice([ + $value = ['foo', 'bar', 'moo']; + + $this->setValue($value); + + $this->validator->validate($value, new Choice([ 'choices' => ['foo', 'bar', 'moo', 'maa'], 'multiple' => true, 'max' => 2, 'maxMessage' => 'myMessage', - ])]; - yield 'named arguments' => [new Choice( - choices: ['foo', 'bar', 'moo', 'maa'], - multiple: true, - max: 2, - maxMessage: 'myMessage', - )]; + ])); + + $this->buildViolation('myMessage') + ->setParameter('{{ limit }}', 2) + ->setInvalidValue($value) + ->setPlural(2) + ->setCode(Choice::TOO_MANY_ERROR) + ->assertRaised(); } public function testStrictAllowsExactValue() { - $constraint = new Choice([ - 'choices' => [1, 2], - ]); + $constraint = new Choice(choices: [1, 2]); $this->validator->validate(2, $constraint); @@ -328,10 +385,10 @@ public function testStrictAllowsExactValue() public function testStrictDisallowsDifferentType() { - $constraint = new Choice([ - 'choices' => [1, 2], - 'message' => 'myMessage', - ]); + $constraint = new Choice( + choices: [1, 2], + message: 'myMessage', + ); $this->validator->validate('2', $constraint); @@ -344,11 +401,11 @@ public function testStrictDisallowsDifferentType() public function testStrictWithMultipleChoices() { - $constraint = new Choice([ - 'choices' => [1, 2, 3], - 'multiple' => true, - 'multipleMessage' => 'myMessage', - ]); + $constraint = new Choice( + choices: [1, 2, 3], + multiple: true, + multipleMessage: 'myMessage', + ); $this->validator->validate([2, '3'], $constraint); @@ -362,10 +419,10 @@ public function testStrictWithMultipleChoices() public function testMatchFalse() { - $this->validator->validate('foo', new Choice([ - 'choices' => ['foo', 'bar'], - 'match' => false, - ])); + $this->validator->validate('foo', new Choice( + choices: ['foo', 'bar'], + match: false, + )); $this->buildViolation('The value you selected is not a valid choice.') ->setParameter('{{ value }}', '"foo"') @@ -376,11 +433,11 @@ public function testMatchFalse() public function testMatchFalseWithMultiple() { - $this->validator->validate(['ccc', 'bar', 'zzz'], new Choice([ - 'choices' => ['foo', 'bar'], - 'multiple' => true, - 'match' => false, - ])); + $this->validator->validate(['ccc', 'bar', 'zzz'], new Choice( + choices: ['foo', 'bar'], + multiple: true, + match: false, + )); $this->buildViolation('One or more of the given values is invalid.') ->setParameter('{{ value }}', '"bar"') diff --git a/Tests/Constraints/CidrTest.php b/Tests/Constraints/CidrTest.php index 3ad25af5a..25059104d 100644 --- a/Tests/Constraints/CidrTest.php +++ b/Tests/Constraints/CidrTest.php @@ -31,7 +31,7 @@ public function testForAll() public function testForV4() { - $cidrConstraint = new Cidr(['version' => Ip::V4]); + $cidrConstraint = new Cidr(version: Ip::V4); self::assertEquals(Ip::V4, $cidrConstraint->version); self::assertEquals(0, $cidrConstraint->netmaskMin); @@ -40,7 +40,7 @@ public function testForV4() public function testForV6() { - $cidrConstraint = new Cidr(['version' => Ip::V6]); + $cidrConstraint = new Cidr(version: Ip::V6); self::assertEquals(Ip::V6, $cidrConstraint->version); self::assertEquals(0, $cidrConstraint->netmaskMin); @@ -49,12 +49,20 @@ public function testForV6() public function testWithInvalidVersion() { - $availableVersions = [Ip::ALL, Ip::V4, Ip::V6]; + $availableVersions = [ + Ip::V4, Ip::V6, Ip::ALL, + Ip::V4_NO_PUBLIC, Ip::V6_NO_PUBLIC, Ip::ALL_NO_PUBLIC, + Ip::V4_NO_PRIVATE, Ip::V6_NO_PRIVATE, Ip::ALL_NO_PRIVATE, + Ip::V4_NO_RESERVED, Ip::V6_NO_RESERVED, Ip::ALL_NO_RESERVED, + Ip::V4_ONLY_PUBLIC, Ip::V6_ONLY_PUBLIC, Ip::ALL_ONLY_PUBLIC, + Ip::V4_ONLY_PRIVATE, Ip::V6_ONLY_PRIVATE, Ip::ALL_ONLY_PRIVATE, + Ip::V4_ONLY_RESERVED, Ip::V6_ONLY_RESERVED, Ip::ALL_ONLY_RESERVED, + ]; self::expectException(ConstraintDefinitionException::class); self::expectExceptionMessage(\sprintf('The option "version" must be one of "%s".', implode('", "', $availableVersions))); - new Cidr(['version' => '8']); + new Cidr(version: '8'); } /** @@ -62,11 +70,11 @@ public function testWithInvalidVersion() */ public function testWithValidMinMaxValues(string $ipVersion, int $netmaskMin, int $netmaskMax) { - $cidrConstraint = new Cidr([ - 'version' => $ipVersion, - 'netmaskMin' => $netmaskMin, - 'netmaskMax' => $netmaskMax, - ]); + $cidrConstraint = new Cidr( + version: $ipVersion, + netmaskMin: $netmaskMin, + netmaskMax: $netmaskMax, + ); self::assertEquals($ipVersion, $cidrConstraint->version); self::assertEquals($netmaskMin, $cidrConstraint->netmaskMin); @@ -83,11 +91,11 @@ public function testWithInvalidMinMaxValues(string $ipVersion, int $netmaskMin, self::expectException(ConstraintDefinitionException::class); self::expectExceptionMessage(\sprintf('The netmask range must be between 0 and %d.', $expectedMax)); - new Cidr([ - 'version' => $ipVersion, - 'netmaskMin' => $netmaskMin, - 'netmaskMax' => $netmaskMax, - ]); + new Cidr( + version: $ipVersion, + netmaskMin: $netmaskMin, + netmaskMax: $netmaskMax, + ); } public static function getInvalidMinMaxValues(): array diff --git a/Tests/Constraints/CidrValidatorTest.php b/Tests/Constraints/CidrValidatorTest.php index d2a617d72..6dfdc4931 100644 --- a/Tests/Constraints/CidrValidatorTest.php +++ b/Tests/Constraints/CidrValidatorTest.php @@ -18,6 +18,7 @@ use Symfony\Component\Validator\Exception\UnexpectedTypeException; use Symfony\Component\Validator\Exception\UnexpectedValueException; use Symfony\Component\Validator\Test\ConstraintValidatorTestCase; +use Symfony\Component\Validator\Tests\Constraints\Fixtures\StringableValue; class CidrValidatorTest extends ConstraintValidatorTestCase { @@ -83,9 +84,9 @@ public function testInvalidIpValue(string $cidr) /** * @dataProvider getValid */ - public function testValidCidr(string $cidr, string $version) + public function testValidCidr(string|\Stringable $cidr, string $version) { - $this->validator->validate($cidr, new Cidr(['version' => $version])); + $this->validator->validate($cidr, new Cidr(version: $version)); $this->assertNoViolation(); } @@ -93,7 +94,7 @@ public function testValidCidr(string $cidr, string $version) /** * @dataProvider getWithInvalidMasksAndIps */ - public function testInvalidIpAddressAndNetmask(string $cidr) + public function testInvalidIpAddressAndNetmask(string|\Stringable $cidr) { $this->validator->validate($cidr, new Cidr()); $this @@ -105,19 +106,19 @@ public function testInvalidIpAddressAndNetmask(string $cidr) /** * @dataProvider getOutOfRangeNetmask */ - public function testOutOfRangeNetmask(string $cidr, ?string $version = null, ?int $min = null, ?int $max = null) + public function testOutOfRangeNetmask(string $cidr, int $maxExpected, ?string $version = null, ?int $min = null, ?int $max = null) { - $cidrConstraint = new Cidr([ - 'version' => $version, - 'netmaskMin' => $min, - 'netmaskMax' => $max, - ]); + $cidrConstraint = new Cidr( + version: $version, + netmaskMin: $min, + netmaskMax: $max, + ); $this->validator->validate($cidr, $cidrConstraint); $this ->buildViolation('The value of the netmask should be between {{ min }} and {{ max }}.') ->setParameter('{{ min }}', $cidrConstraint->netmaskMin) - ->setParameter('{{ max }}', $cidrConstraint->netmaskMax) + ->setParameter('{{ max }}', $maxExpected) ->setCode(Cidr::OUT_OF_RANGE_ERROR) ->assertRaised(); } @@ -127,7 +128,7 @@ public function testOutOfRangeNetmask(string $cidr, ?string $version = null, ?in */ public function testWrongVersion(string $cidr, string $version) { - $this->validator->validate($cidr, new Cidr(['version' => $version])); + $this->validator->validate($cidr, new Cidr(version: $version)); $this ->buildViolation('This value is not a valid CIDR notation.') @@ -195,6 +196,7 @@ public static function getValid(): array ['::255.255.255.255/32', Ip::V6], ['::123.45.67.178/120', Ip::V6], ['::123.45.67.178/120', Ip::ALL], + [new StringableValue('::123.45.67.178/120'), Ip::ALL], ]; } @@ -203,7 +205,6 @@ public static function getWithInvalidNetmask(): array return [ ['192.168.1.0/-1'], ['0.0.0.0/foobar'], - ['10.0.0.0/128'], ['123.45.67.178/aaa'], ['172.16.0.0//'], ['255.255.255.255/1/4'], @@ -221,7 +222,6 @@ public static function getWithInvalidMasksAndIps(): array { return [ ['0.0.0.0/foobar'], - ['10.0.0.0/128'], ['123.45.67.178/aaa'], ['172.16.0.0//'], ['172.16.0.0/a/'], @@ -233,14 +233,16 @@ public static function getWithInvalidMasksAndIps(): array ['::0.0.0/a/'], ['::256.0.0.0/-1aa'], ['::0.256.0.0/1b'], + [new StringableValue('::0.256.0.0/1b')], ]; } public static function getOutOfRangeNetmask(): array { return [ - ['10.0.0.0/24', Ip::V4, 10, 20], - ['2001:0DB8:85A3:0000:0000:8A2E:0370:7334/24', Ip::V6, 10, 20], + ['10.0.0.0/24', 20, Ip::V4, 10, 20], + ['10.0.0.0/128', 32], + ['2001:0DB8:85A3:0000:0000:8A2E:0370:7334/24', 20, Ip::V6, 10, 20], ]; } @@ -253,4 +255,17 @@ public static function getWithWrongVersion(): array ['2001:0db8:85a3:0000:0000:8a2e:0370:7334/13', Ip::V4], ]; } + + public function testDoesNotModifyContextBetweenValidations() + { + $constraint = new Cidr(); + + $this->validator->validate('1.2.3.4/28', $constraint); + + $this->assertNoViolation(); + + $this->validator->validate('::1/64', $constraint); + + $this->assertNoViolation(); + } } diff --git a/Tests/Constraints/CollectionTest.php b/Tests/Constraints/CollectionTest.php index 17f543ffb..c1c32f90a 100644 --- a/Tests/Constraints/CollectionTest.php +++ b/Tests/Constraints/CollectionTest.php @@ -26,14 +26,9 @@ */ class CollectionTest extends TestCase { - public function testRejectInvalidFieldsOption() - { - $this->expectException(ConstraintDefinitionException::class); - new Collection([ - 'fields' => 'foo', - ]); - } - + /** + * @group legacy + */ public function testRejectNonConstraints() { $this->expectException(InvalidOptionsException::class); @@ -68,18 +63,14 @@ public function testRejectValidConstraintWithinRequired() public function testAcceptOptionalConstraintAsOneElementArray() { - $collection1 = new Collection([ - 'fields' => [ - 'alternate_email' => [ - new Optional(new Email()), - ], + $collection1 = new Collection(fields: [ + 'alternate_email' => [ + new Optional(new Email()), ], ]); - $collection2 = new Collection([ - 'fields' => [ - 'alternate_email' => new Optional(new Email()), - ], + $collection2 = new Collection(fields: [ + 'alternate_email' => new Optional(new Email()), ]); $this->assertEquals($collection1, $collection2); @@ -87,18 +78,14 @@ public function testAcceptOptionalConstraintAsOneElementArray() public function testAcceptRequiredConstraintAsOneElementArray() { - $collection1 = new Collection([ - 'fields' => [ - 'alternate_email' => [ - new Required(new Email()), - ], + $collection1 = new Collection(fields: [ + 'alternate_email' => [ + new Required(new Email()), ], ]); - $collection2 = new Collection([ - 'fields' => [ - 'alternate_email' => new Required(new Email()), - ], + $collection2 = new Collection(fields: [ + 'alternate_email' => new Required(new Email()), ]); $this->assertEquals($collection1, $collection2); @@ -116,6 +103,9 @@ public function testConstraintHasDefaultGroupWithOptionalValues() $this->assertEquals(['Default'], $constraint->fields['bar']->groups); } + /** + * @group legacy + */ public function testOnlySomeKeysAreKnowOptions() { $constraint = new Collection([ @@ -134,15 +124,15 @@ public function testOnlySomeKeysAreKnowOptions() public function testAllKeysAreKnowOptions() { - $constraint = new Collection([ - 'fields' => [ + $constraint = new Collection( + fields: [ 'fields' => [new Required()], 'properties' => [new Required()], 'catalog' => [new Optional()], ], - 'allowExtraFields' => true, - 'extraFieldsMessage' => 'foo bar baz', - ]); + allowExtraFields: true, + extraFieldsMessage: 'foo bar baz', + ); $this->assertArrayHasKey('fields', $constraint->fields); $this->assertInstanceOf(Required::class, $constraint->fields['fields']); @@ -165,11 +155,11 @@ public function testEmptyFields() public function testEmptyFieldsInOptions() { - $constraint = new Collection([ - 'fields' => [], - 'allowExtraFields' => true, - 'extraFieldsMessage' => 'foo bar baz', - ]); + $constraint = new Collection( + fields: [], + allowExtraFields: true, + extraFieldsMessage: 'foo bar baz', + ); $this->assertSame([], $constraint->fields); $this->assertTrue($constraint->allowExtraFields); @@ -205,13 +195,13 @@ public function testEmptyConstraintListForField(?array $fieldConstraint) */ public function testEmptyConstraintListForFieldInOptions(?array $fieldConstraint) { - $constraint = new Collection([ - 'fields' => [ + $constraint = new Collection( + fields: [ 'foo' => $fieldConstraint, ], - 'allowExtraFields' => true, - 'extraFieldsMessage' => 'foo bar baz', - ]); + allowExtraFields: true, + extraFieldsMessage: 'foo bar baz', + ); $this->assertArrayHasKey('foo', $constraint->fields); $this->assertInstanceOf(Required::class, $constraint->fields['foo']); diff --git a/Tests/Constraints/CollectionValidatorTestCase.php b/Tests/Constraints/CollectionValidatorTestCase.php index 92260e966..8e03a9add 100644 --- a/Tests/Constraints/CollectionValidatorTestCase.php +++ b/Tests/Constraints/CollectionValidatorTestCase.php @@ -31,16 +31,16 @@ abstract protected function prepareTestData(array $contents); public function testNullIsValid() { - $this->validator->validate(null, new Collection(['fields' => [ - 'foo' => new Range(['min' => 4]), - ]])); + $this->validator->validate(null, new Collection(fields: [ + 'foo' => new Range(min: 4), + ])); $this->assertNoViolation(); } public function testFieldsAsDefaultOption() { - $constraint = new Range(['min' => 4]); + $constraint = new Range(min: 4); $data = $this->prepareTestData(['foo' => 'foobar']); @@ -56,14 +56,14 @@ public function testFieldsAsDefaultOption() public function testThrowsExceptionIfNotTraversable() { $this->expectException(UnexpectedValueException::class); - $this->validator->validate('foobar', new Collection(['fields' => [ - 'foo' => new Range(['min' => 4]), - ]])); + $this->validator->validate('foobar', new Collection(fields: [ + 'foo' => new Range(min: 4), + ])); } public function testWalkSingleConstraint() { - $constraint = new Range(['min' => 4]); + $constraint = new Range(min: 4); $array = [ 'foo' => 3, @@ -78,12 +78,12 @@ public function testWalkSingleConstraint() $data = $this->prepareTestData($array); - $this->validator->validate($data, new Collection([ - 'fields' => [ + $this->validator->validate($data, new Collection( + fields: [ 'foo' => $constraint, 'bar' => $constraint, ], - ])); + )); $this->assertNoViolation(); } @@ -91,7 +91,7 @@ public function testWalkSingleConstraint() public function testWalkMultipleConstraints() { $constraints = [ - new Range(['min' => 4]), + new Range(min: 4), new NotNull(), ]; @@ -108,19 +108,19 @@ public function testWalkMultipleConstraints() $data = $this->prepareTestData($array); - $this->validator->validate($data, new Collection([ - 'fields' => [ + $this->validator->validate($data, new Collection( + fields: [ 'foo' => $constraints, 'bar' => $constraints, ], - ])); + )); $this->assertNoViolation(); } public function testExtraFieldsDisallowed() { - $constraint = new Range(['min' => 4]); + $constraint = new Range(min: 4); $data = $this->prepareTestData([ 'foo' => 5, @@ -129,12 +129,12 @@ public function testExtraFieldsDisallowed() $this->expectValidateValueAt(0, '[foo]', $data['foo'], [$constraint]); - $this->validator->validate($data, new Collection([ - 'fields' => [ + $this->validator->validate($data, new Collection( + fields: [ 'foo' => $constraint, ], - 'extraFieldsMessage' => 'myMessage', - ])); + extraFieldsMessage: 'myMessage', + )); $this->buildViolation('myMessage') ->setParameter('{{ field }}', '"baz"') @@ -152,12 +152,12 @@ public function testExtraFieldsDisallowedWithOptionalValues() 'baz' => 6, ]); - $this->validator->validate($data, new Collection([ - 'fields' => [ + $this->validator->validate($data, new Collection( + fields: [ 'foo' => $constraint, ], - 'extraFieldsMessage' => 'myMessage', - ])); + extraFieldsMessage: 'myMessage', + )); $this->buildViolation('myMessage') ->setParameter('{{ field }}', '"baz"') @@ -174,15 +174,15 @@ public function testNullNotConsideredExtraField() 'foo' => null, ]); - $constraint = new Range(['min' => 4]); + $constraint = new Range(min: 4); $this->expectValidateValueAt(0, '[foo]', $data['foo'], [$constraint]); - $this->validator->validate($data, new Collection([ - 'fields' => [ + $this->validator->validate($data, new Collection( + fields: [ 'foo' => $constraint, ], - ])); + )); $this->assertNoViolation(); } @@ -194,16 +194,16 @@ public function testExtraFieldsAllowed() 'bar' => 6, ]); - $constraint = new Range(['min' => 4]); + $constraint = new Range(min: 4); $this->expectValidateValueAt(0, '[foo]', $data['foo'], [$constraint]); - $this->validator->validate($data, new Collection([ - 'fields' => [ + $this->validator->validate($data, new Collection( + fields: [ 'foo' => $constraint, ], - 'allowExtraFields' => true, - ])); + allowExtraFields: true, + )); $this->assertNoViolation(); } @@ -212,14 +212,14 @@ public function testMissingFieldsDisallowed() { $data = $this->prepareTestData([]); - $constraint = new Range(['min' => 4]); + $constraint = new Range(min: 4); - $this->validator->validate($data, new Collection([ - 'fields' => [ + $this->validator->validate($data, new Collection( + fields: [ 'foo' => $constraint, ], - 'missingFieldsMessage' => 'myMessage', - ])); + missingFieldsMessage: 'myMessage', + )); $this->buildViolation('myMessage') ->setParameter('{{ field }}', '"foo"') @@ -233,14 +233,14 @@ public function testMissingFieldsAllowed() { $data = $this->prepareTestData([]); - $constraint = new Range(['min' => 4]); + $constraint = new Range(min: 4); - $this->validator->validate($data, new Collection([ - 'fields' => [ + $this->validator->validate($data, new Collection( + fields: [ 'foo' => $constraint, ], - 'allowMissingFields' => true, - ])); + allowMissingFields: true, + )); $this->assertNoViolation(); } @@ -275,7 +275,7 @@ public function testOptionalFieldSingleConstraint() 'foo' => 5, ]; - $constraint = new Range(['min' => 4]); + $constraint = new Range(min: 4); $this->expectValidateValueAt(0, '[foo]', $array['foo'], [$constraint]); @@ -296,7 +296,7 @@ public function testOptionalFieldMultipleConstraints() $constraints = [ new NotNull(), - new Range(['min' => 4]), + new Range(min: 4), ]; $this->expectValidateValueAt(0, '[foo]', $array['foo'], $constraints); @@ -327,12 +327,12 @@ public function testRequiredFieldNotPresent() { $data = $this->prepareTestData([]); - $this->validator->validate($data, new Collection([ - 'fields' => [ + $this->validator->validate($data, new Collection( + fields: [ 'foo' => new Required(), ], - 'missingFieldsMessage' => 'myMessage', - ])); + missingFieldsMessage: 'myMessage', + )); $this->buildViolation('myMessage') ->setParameter('{{ field }}', '"foo"') @@ -348,7 +348,7 @@ public function testRequiredFieldSingleConstraint() 'foo' => 5, ]; - $constraint = new Range(['min' => 4]); + $constraint = new Range(min: 4); $this->expectValidateValueAt(0, '[foo]', $array['foo'], [$constraint]); @@ -369,7 +369,7 @@ public function testRequiredFieldMultipleConstraints() $constraints = [ new NotNull(), - new Range(['min' => 4]), + new Range(min: 4), ]; $this->expectValidateValueAt(0, '[foo]', $array['foo'], $constraints); @@ -389,15 +389,15 @@ public function testObjectShouldBeLeftUnchanged() 'foo' => 3, ]); - $constraint = new Range(['min' => 2]); + $constraint = new Range(min: 2); $this->expectValidateValueAt(0, '[foo]', $value['foo'], [$constraint]); - $this->validator->validate($value, new Collection([ - 'fields' => [ + $this->validator->validate($value, new Collection( + fields: [ 'foo' => $constraint, ], - ])); + )); $this->assertEquals([ 'foo' => 3, diff --git a/Tests/Constraints/CompareWithNullValueAtPropertyAtTestTrait.php b/Tests/Constraints/CompareWithNullValueAtPropertyAtTestTrait.php new file mode 100644 index 000000000..d99090295 --- /dev/null +++ b/Tests/Constraints/CompareWithNullValueAtPropertyAtTestTrait.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Tests\Constraints; + +use Symfony\Component\Validator\Tests\Constraints\Fixtures\TypedDummy; + +trait CompareWithNullValueAtPropertyAtTestTrait +{ + public function testCompareWithNullValueAtPropertyAt() + { + $constraint = $this->createConstraint(['propertyPath' => 'value']); + $constraint->message = 'Constraint Message'; + + $object = new ComparisonTest_Class(null); + $this->setObject($object); + + $this->validator->validate(5, $constraint); + + $this->assertNoViolation(); + } + + public function testCompareWithUninitializedPropertyAtPropertyPath() + { + $this->setObject(new TypedDummy()); + + $this->validator->validate(5, $this->createConstraint([ + 'message' => 'Constraint Message', + 'propertyPath' => 'value', + ])); + + $this->assertNoViolation(); + } +} diff --git a/Tests/Constraints/CompositeTest.php b/Tests/Constraints/CompositeTest.php index 127ad21dd..9329ef1a2 100644 --- a/Tests/Constraints/CompositeTest.php +++ b/Tests/Constraints/CompositeTest.php @@ -14,6 +14,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraints\Composite; +use Symfony\Component\Validator\Constraints\Length; use Symfony\Component\Validator\Constraints\NotBlank; use Symfony\Component\Validator\Constraints\NotNull; use Symfony\Component\Validator\Constraints\Valid; @@ -23,9 +24,14 @@ class ConcreteComposite extends Composite { public array|Constraint $constraints = []; - protected function getCompositeOption(): string + public function __construct(mixed $options = null, public array|Constraint $otherNested = []) { - return 'constraints'; + parent::__construct($options); + } + + protected function getCompositeOption(): array + { + return ['constraints', 'otherNested']; } public function getDefaultOption(): ?string @@ -44,11 +50,14 @@ public function testConstraintHasDefaultGroup() $constraint = new ConcreteComposite([ new NotNull(), new NotBlank(), + ], [ + new Length(exactly: 10), ]); $this->assertEquals(['Default'], $constraint->groups); $this->assertEquals(['Default'], $constraint->constraints[0]->groups); $this->assertEquals(['Default'], $constraint->constraints[1]->groups); + $this->assertEquals(['Default'], $constraint->otherNested[0]->groups); } public function testNestedCompositeConstraintHasDefaultGroup() @@ -66,13 +75,16 @@ public function testNestedCompositeConstraintHasDefaultGroup() public function testMergeNestedGroupsIfNoExplicitParentGroup() { $constraint = new ConcreteComposite([ - new NotNull(['groups' => 'Default']), - new NotBlank(['groups' => ['Default', 'Strict']]), + new NotNull(groups: ['Default']), + new NotBlank(groups: ['Default', 'Strict']), + ], [ + new Length(exactly: 10, groups: ['Default', 'Strict']), ]); $this->assertEquals(['Default', 'Strict'], $constraint->groups); $this->assertEquals(['Default'], $constraint->constraints[0]->groups); $this->assertEquals(['Default', 'Strict'], $constraint->constraints[1]->groups); + $this->assertEquals(['Default', 'Strict'], $constraint->otherNested[0]->groups); } public function testSetImplicitNestedGroupsIfExplicitParentGroup() @@ -82,20 +94,27 @@ public function testSetImplicitNestedGroupsIfExplicitParentGroup() new NotNull(), new NotBlank(), ], + 'otherNested' => [ + new Length(exactly: 10), + ], 'groups' => ['Default', 'Strict'], ]); $this->assertEquals(['Default', 'Strict'], $constraint->groups); $this->assertEquals(['Default', 'Strict'], $constraint->constraints[0]->groups); $this->assertEquals(['Default', 'Strict'], $constraint->constraints[1]->groups); + $this->assertEquals(['Default', 'Strict'], $constraint->otherNested[0]->groups); } public function testExplicitNestedGroupsMustBeSubsetOfExplicitParentGroups() { $constraint = new ConcreteComposite([ 'constraints' => [ - new NotNull(['groups' => 'Default']), - new NotBlank(['groups' => 'Strict']), + new NotNull(groups: ['Default']), + new NotBlank(groups: ['Strict']), + ], + 'otherNested' => [ + new Length(exactly: 10, groups: ['Strict']), ], 'groups' => ['Default', 'Strict'], ]); @@ -103,6 +122,7 @@ public function testExplicitNestedGroupsMustBeSubsetOfExplicitParentGroups() $this->assertEquals(['Default', 'Strict'], $constraint->groups); $this->assertEquals(['Default'], $constraint->constraints[0]->groups); $this->assertEquals(['Strict'], $constraint->constraints[1]->groups); + $this->assertEquals(['Strict'], $constraint->otherNested[0]->groups); } public function testFailIfExplicitNestedGroupsNotSubsetOfExplicitParentGroups() @@ -110,7 +130,21 @@ public function testFailIfExplicitNestedGroupsNotSubsetOfExplicitParentGroups() $this->expectException(ConstraintDefinitionException::class); new ConcreteComposite([ 'constraints' => [ - new NotNull(['groups' => ['Default', 'Foobar']]), + new NotNull(groups: ['Default', 'Foobar']), + ], + 'groups' => ['Default', 'Strict'], + ]); + } + + public function testFailIfExplicitNestedGroupsNotSubsetOfExplicitParentGroupsInOtherNested() + { + $this->expectException(ConstraintDefinitionException::class); + new ConcreteComposite([ + 'constraints' => [ + new NotNull(groups: ['Default']), + ], + 'otherNested' => [ + new NotNull(groups: ['Default', 'Foobar']), ], 'groups' => ['Default', 'Strict'], ]); @@ -119,8 +153,10 @@ public function testFailIfExplicitNestedGroupsNotSubsetOfExplicitParentGroups() public function testImplicitGroupNamesAreForwarded() { $constraint = new ConcreteComposite([ - new NotNull(['groups' => 'Default']), - new NotBlank(['groups' => 'Strict']), + new NotNull(groups: ['Default']), + new NotBlank(groups: ['Strict']), + ], [ + new Length(exactly: 10, groups: ['Default']), ]); $constraint->addImplicitGroupName('ImplicitGroup'); @@ -128,21 +164,33 @@ public function testImplicitGroupNamesAreForwarded() $this->assertEquals(['Default', 'Strict', 'ImplicitGroup'], $constraint->groups); $this->assertEquals(['Default', 'ImplicitGroup'], $constraint->constraints[0]->groups); $this->assertEquals(['Strict'], $constraint->constraints[1]->groups); + $this->assertEquals(['Default', 'ImplicitGroup'], $constraint->otherNested[0]->groups); } public function testSingleConstraintsAccepted() { $nestedConstraint = new NotNull(); - $constraint = new ConcreteComposite($nestedConstraint); + $otherNestedConstraint = new Length(exactly: 10); + $constraint = new ConcreteComposite($nestedConstraint, $otherNestedConstraint); $this->assertEquals([$nestedConstraint], $constraint->constraints); + $this->assertEquals([$otherNestedConstraint], $constraint->otherNested); } public function testFailIfNoConstraint() { $this->expectException(ConstraintDefinitionException::class); new ConcreteComposite([ - new NotNull(['groups' => 'Default']), + new NotNull(groups: ['Default']), + 'NotBlank', + ]); + } + + public function testFailIfNoConstraintInAnotherNested() + { + $this->expectException(ConstraintDefinitionException::class); + new ConcreteComposite([new NotNull()], [ + new NotNull(groups: ['Default']), 'NotBlank', ]); } @@ -151,7 +199,16 @@ public function testFailIfNoConstraintObject() { $this->expectException(ConstraintDefinitionException::class); new ConcreteComposite([ - new NotNull(['groups' => 'Default']), + new NotNull(groups: ['Default']), + new \ArrayObject(), + ]); + } + + public function testFailIfNoConstraintObjectInAnotherNested() + { + $this->expectException(ConstraintDefinitionException::class); + new ConcreteComposite([new NotNull()], [ + new NotNull(groups: ['Default']), new \ArrayObject(), ]); } @@ -163,4 +220,10 @@ public function testValidCantBeNested() new Valid(), ]); } + + public function testValidCantBeNestedInAnotherNested() + { + $this->expectException(ConstraintDefinitionException::class); + new ConcreteComposite([new NotNull()], [new Valid()]); + } } diff --git a/Tests/Constraints/CompoundTest.php b/Tests/Constraints/CompoundTest.php index d5cf9e9ce..9b515a48c 100644 --- a/Tests/Constraints/CompoundTest.php +++ b/Tests/Constraints/CompoundTest.php @@ -19,6 +19,9 @@ class CompoundTest extends TestCase { + /** + * @group legacy + */ public function testItCannotRedefineConstraintsOption() { $this->expectException(ConstraintDefinitionException::class); @@ -26,6 +29,24 @@ public function testItCannotRedefineConstraintsOption() new EmptyCompound(['constraints' => [new NotBlank()]]); } + public function testGroupsAndPayload() + { + $payload = new \stdClass(); + $compound = new EmptyCompound(groups: ['my-group', 'my-other-group'], payload: $payload); + + $this->assertSame(['my-group', 'my-other-group'], $compound->groups); + $this->assertSame($payload, $compound->payload); + } + + public function testGroupsAndPayloadInOptionsArray() + { + $payload = new \stdClass(); + $compound = new EmptyCompound(['groups' => ['my-group', 'my-other-group'], 'payload' => $payload]); + + $this->assertSame(['my-group', 'my-other-group'], $compound->groups); + $this->assertSame($payload, $compound->payload); + } + public function testCanDependOnNormalizedOptions() { $constraint = new ForwardingOptionCompound($min = 3); @@ -54,7 +75,7 @@ public function getDefaultOption(): ?string protected function getConstraints(array $options): array { return [ - new Length(['min' => $options['min'] ?? null]), + new Length(min: $options['min'] ?? null), ]; } } diff --git a/Tests/Constraints/CompoundValidatorTest.php b/Tests/Constraints/CompoundValidatorTest.php index 2f48657b2..9eb5c7add 100644 --- a/Tests/Constraints/CompoundValidatorTest.php +++ b/Tests/Constraints/CompoundValidatorTest.php @@ -11,11 +11,9 @@ namespace Symfony\Component\Validator\Tests\Constraints; -use Symfony\Component\Validator\Constraints\Compound; use Symfony\Component\Validator\Constraints\CompoundValidator; -use Symfony\Component\Validator\Constraints\Length; -use Symfony\Component\Validator\Constraints\NotBlank; use Symfony\Component\Validator\Test\ConstraintValidatorTestCase; +use Symfony\Component\Validator\Tests\Fixtures\DummyCompoundConstraint; class CompoundValidatorTest extends ConstraintValidatorTestCase { @@ -43,14 +41,3 @@ public function testValidateWithConstraints() $this->assertNoViolation(); } } - -class DummyCompoundConstraint extends Compound -{ - protected function getConstraints(array $options): array - { - return [ - new NotBlank(), - new Length(['max' => 3]), - ]; - } -} diff --git a/Tests/Constraints/CountValidatorTestCase.php b/Tests/Constraints/CountValidatorTestCase.php index 104c90773..da319cd8b 100644 --- a/Tests/Constraints/CountValidatorTestCase.php +++ b/Tests/Constraints/CountValidatorTestCase.php @@ -70,6 +70,8 @@ public static function getFiveOrMoreElements() } /** + * @group legacy + * * @dataProvider getThreeOrLessElements */ public function testValidValuesMax($value) @@ -92,6 +94,8 @@ public function testValidValuesMaxNamed($value) } /** + * @group legacy + * * @dataProvider getFiveOrMoreElements */ public function testValidValuesMin($value) @@ -114,6 +118,8 @@ public function testValidValuesMinNamed($value) } /** + * @group legacy + * * @dataProvider getFourElements */ public function testValidValuesExact($value) @@ -136,6 +142,8 @@ public function testValidValuesExactNamed($value) } /** + * @group legacy + * * @dataProvider getFiveOrMoreElements */ public function testTooManyValues($value) @@ -175,6 +183,8 @@ public function testTooManyValuesNamed($value) } /** + * @group legacy + * * @dataProvider getThreeOrLessElements */ public function testTooFewValues($value) @@ -214,6 +224,8 @@ public function testTooFewValuesNamed($value) } /** + * @group legacy + * * @dataProvider getFiveOrMoreElements */ public function testTooManyValuesExact($value) @@ -258,11 +270,11 @@ public function testTooManyValuesExactNamed($value) */ public function testTooFewValuesExact($value) { - $constraint = new Count([ - 'min' => 4, - 'max' => 4, - 'exactMessage' => 'myMessage', - ]); + $constraint = new Count( + min: 4, + max: 4, + exactMessage: 'myMessage', + ); $this->validator->validate($value, $constraint); @@ -283,9 +295,9 @@ public function testDefaultOption() $this->assertEquals(5, $constraint->max); } - public function testConstraintAnnotationDefaultOption() + public function testConstraintAttributeDefaultOption() { - $constraint = new Count(['value' => 5, 'exactMessage' => 'message']); + $constraint = new Count(exactly: 5, exactMessage: 'message'); $this->assertEquals(5, $constraint->min); $this->assertEquals(5, $constraint->max); @@ -296,15 +308,15 @@ public function testConstraintAnnotationDefaultOption() // is called with the right DivisibleBy constraint. public function testDivisibleBy() { - $constraint = new Count([ - 'divisibleBy' => 123, - 'divisibleByMessage' => 'foo {{ compared_value }}', - ]); - - $this->expectValidateValue(0, 3, [new DivisibleBy([ - 'value' => 123, - 'message' => 'foo {{ compared_value }}', - ])], $this->group); + $constraint = new Count( + divisibleBy: 123, + divisibleByMessage: 'foo {{ compared_value }}', + ); + + $this->expectValidateValue(0, 3, [new DivisibleBy( + value: 123, + message: 'foo {{ compared_value }}', + )], $this->group); $this->validator->validate(['foo', 'bar', 'ccc'], $constraint); diff --git a/Tests/Constraints/CountryValidatorTest.php b/Tests/Constraints/CountryValidatorTest.php index 4be378926..d6fa0c35e 100644 --- a/Tests/Constraints/CountryValidatorTest.php +++ b/Tests/Constraints/CountryValidatorTest.php @@ -84,9 +84,7 @@ public static function getValidCountries() */ public function testInvalidCountries($country) { - $constraint = new Country([ - 'message' => 'myMessage', - ]); + $constraint = new Country(message: 'myMessage'); $this->validator->validate($country, $constraint); @@ -109,9 +107,7 @@ public static function getInvalidCountries() */ public function testValidAlpha3Countries($country) { - $this->validator->validate($country, new Country([ - 'alpha3' => true, - ])); + $this->validator->validate($country, new Country(alpha3: true)); $this->assertNoViolation(); } @@ -130,10 +126,10 @@ public static function getValidAlpha3Countries() */ public function testInvalidAlpha3Countries($country) { - $constraint = new Country([ - 'alpha3' => true, - 'message' => 'myMessage', - ]); + $constraint = new Country( + alpha3: true, + message: 'myMessage', + ); $this->validator->validate($country, $constraint); diff --git a/Tests/Constraints/CssColorValidatorTest.php b/Tests/Constraints/CssColorValidatorTest.php index b458170d4..bc1087c47 100644 --- a/Tests/Constraints/CssColorValidatorTest.php +++ b/Tests/Constraints/CssColorValidatorTest.php @@ -15,6 +15,7 @@ use Symfony\Component\Validator\Constraints\CssColorValidator; use Symfony\Component\Validator\Exception\UnexpectedValueException; use Symfony\Component\Validator\Test\ConstraintValidatorTestCase; +use Symfony\Component\Validator\Tests\Constraints\Fixtures\StringableValue; final class CssColorValidatorTest extends ConstraintValidatorTestCase { @@ -80,6 +81,7 @@ public static function getValidAnyColor(): array ['rgba(255, 255, 255, 0.3)'], ['hsl(0, 0%, 20%)'], ['hsla(0, 0%, 20%, 0.4)'], + [new StringableValue('hsla(0, 0%, 20%, 0.4)')], ]; } @@ -336,7 +338,7 @@ public function testInvalidNamedColors($cssColor) public static function getInvalidNamedColors(): array { - return [['fabpot'], ['ngrekas'], ['symfony'], ['FABPOT'], ['NGREKAS'], ['SYMFONY']]; + return [['fabpot'], ['ngrekas'], ['symfony'], ['FABPOT'], ['NGREKAS'], ['SYMFONY'], [new StringableValue('SYMFONY')]]; } /** @@ -382,7 +384,12 @@ public function testInvalidRGBA($cssColor) public static function getInvalidRGBA(): array { - return [['rgba(999,999,999,999)'], ['rgba(-99,-99,-99,-99)'], ['rgba(a,b,c,d)'], ['rgba(99 99, 9 99, 99 9, . 9)']]; + return [ + ['rgba(999,999,999,999)'], + ['rgba(-99,-99,-99,-99)'], + ['rgba(a,b,c,d)'], + ['rgba(99 99, 9 99, 99 9, . 9)'], + ]; } /** @@ -428,7 +435,13 @@ public function testInvalidHSLA($cssColor) public static function getInvalidHSLA(): array { - return [['hsla(1000, 1000%, 20000%, 999)'], ['hsla(-100, -10%, -2%, 999)'], ['hsla(a, b, c, d)'], ['hsla(a, b%, c%, d)'], ['hsla( 9 99% , 99 9% , 9 %']]; + return [ + ['hsla(1000, 1000%, 20000%, 999)'], + ['hsla(-100, -10%, -2%, 999)'], + ['hsla(a, b, c, d)'], + ['hsla(a, b%, c%, d)'], + ['hsla( 9 99% , 99 9% , 9 %'], + ]; } /** diff --git a/Tests/Constraints/CurrencyValidatorTest.php b/Tests/Constraints/CurrencyValidatorTest.php index d63c5ab99..af4c20f8e 100644 --- a/Tests/Constraints/CurrencyValidatorTest.php +++ b/Tests/Constraints/CurrencyValidatorTest.php @@ -100,9 +100,7 @@ public static function getValidCurrencies() */ public function testInvalidCurrencies($currency) { - $constraint = new Currency([ - 'message' => 'myMessage', - ]); + $constraint = new Currency(message: 'myMessage'); $this->validator->validate($currency, $constraint); diff --git a/Tests/Constraints/DateTimeValidatorTest.php b/Tests/Constraints/DateTimeValidatorTest.php index 8da07c424..383f06215 100644 --- a/Tests/Constraints/DateTimeValidatorTest.php +++ b/Tests/Constraints/DateTimeValidatorTest.php @@ -53,6 +53,7 @@ public function testDateTimeWithDefaultFormat() $this->buildViolation('This value is not a valid datetime.') ->setParameter('{{ value }}', '"1995-03-24"') + ->setParameter('{{ format }}', '"Y-m-d H:i:s"') ->setCode(DateTime::INVALID_FORMAT_ERROR) ->assertRaised(); } @@ -62,9 +63,7 @@ public function testDateTimeWithDefaultFormat() */ public function testValidDateTimes($format, $dateTime) { - $constraint = new DateTime([ - 'format' => $format, - ]); + $constraint = new DateTime(format: $format); $this->validator->validate($dateTime, $constraint); @@ -87,15 +86,16 @@ public static function getValidDateTimes() */ public function testInvalidDateTimes($format, $dateTime, $code) { - $constraint = new DateTime([ - 'message' => 'myMessage', - 'format' => $format, - ]); + $constraint = new DateTime( + message: 'myMessage', + format: $format, + ); $this->validator->validate($dateTime, $constraint); $this->buildViolation('myMessage') ->setParameter('{{ value }}', '"'.$dateTime.'"') + ->setParameter('{{ format }}', '"'.$format.'"') ->setCode($code) ->assertRaised(); } @@ -124,15 +124,14 @@ public function testInvalidDateTimeNamed() $this->buildViolation('myMessage') ->setParameter('{{ value }}', '"2010-01-01 00:00:00"') + ->setParameter('{{ format }}', '"Y-m-d"') ->setCode(DateTime::INVALID_FORMAT_ERROR) ->assertRaised(); } public function testDateTimeWithTrailingData() { - $this->validator->validate('1995-05-10 00:00:00', new DateTime([ - 'format' => 'Y-m-d+', - ])); + $this->validator->validate('1995-05-10 00:00:00', new DateTime(format: 'Y-m-d+')); $this->assertNoViolation(); } } diff --git a/Tests/Constraints/DateValidatorTest.php b/Tests/Constraints/DateValidatorTest.php index 93dab41f2..65909ef83 100644 --- a/Tests/Constraints/DateValidatorTest.php +++ b/Tests/Constraints/DateValidatorTest.php @@ -58,7 +58,7 @@ public function testValidDates($date) */ public function testValidDatesWithNewLine(string $date) { - $this->validator->validate($date."\n", new Date(['message' => 'myMessage'])); + $this->validator->validate($date."\n", new Date(message: 'myMessage')); $this->buildViolation('myMessage') ->setParameter('{{ value }}', '"'.$date."\n\"") @@ -80,9 +80,7 @@ public static function getValidDates() */ public function testInvalidDates($date, $code) { - $constraint = new Date([ - 'message' => 'myMessage', - ]); + $constraint = new Date(message: 'myMessage'); $this->validator->validate($date, $constraint); diff --git a/Tests/Constraints/DisableAutoMappingTest.php b/Tests/Constraints/DisableAutoMappingTest.php index 709334e36..e7b6a8db7 100644 --- a/Tests/Constraints/DisableAutoMappingTest.php +++ b/Tests/Constraints/DisableAutoMappingTest.php @@ -23,6 +23,9 @@ */ class DisableAutoMappingTest extends TestCase { + /** + * @group legacy + */ public function testGroups() { $this->expectException(ConstraintDefinitionException::class); diff --git a/Tests/Constraints/DivisibleByValidatorTest.php b/Tests/Constraints/DivisibleByValidatorTest.php index 37ae2087e..be96ad2b4 100644 --- a/Tests/Constraints/DivisibleByValidatorTest.php +++ b/Tests/Constraints/DivisibleByValidatorTest.php @@ -21,6 +21,10 @@ */ class DivisibleByValidatorTest extends AbstractComparisonValidatorTestCase { + use InvalidComparisonToValueTestTrait; + use ThrowsOnInvalidStringDatesTestTrait; + use ValidComparisonToValueTrait; + protected function createValidator(): DivisibleByValidator { return new DivisibleByValidator(); @@ -28,7 +32,11 @@ protected function createValidator(): DivisibleByValidator protected static function createConstraint(?array $options = null): Constraint { - return new DivisibleBy($options); + if (null !== $options) { + return new DivisibleBy(...$options); + } + + return new DivisibleBy(); } protected function getErrorCode(): ?string @@ -102,9 +110,4 @@ public static function throwsOnNonNumericValuesProvider() [\ArrayIterator::class, new \ArrayIterator(), 12], ]; } - - public static function provideComparisonsToNullValueAtPropertyPath() - { - self::markTestSkipped('DivisibleByValidator rejects null values.'); - } } diff --git a/Tests/Constraints/EmailTest.php b/Tests/Constraints/EmailTest.php index 8489f9cfe..9436b4bd6 100644 --- a/Tests/Constraints/EmailTest.php +++ b/Tests/Constraints/EmailTest.php @@ -21,14 +21,14 @@ class EmailTest extends TestCase { public function testConstructorStrict() { - $subject = new Email(['mode' => Email::VALIDATION_MODE_STRICT]); + $subject = new Email(mode: Email::VALIDATION_MODE_STRICT); $this->assertEquals(Email::VALIDATION_MODE_STRICT, $subject->mode); } public function testConstructorHtml5AllowNoTld() { - $subject = new Email(['mode' => Email::VALIDATION_MODE_HTML5_ALLOW_NO_TLD]); + $subject = new Email(mode: Email::VALIDATION_MODE_HTML5_ALLOW_NO_TLD); $this->assertEquals(Email::VALIDATION_MODE_HTML5_ALLOW_NO_TLD, $subject->mode); } @@ -37,7 +37,7 @@ public function testUnknownModesTriggerException() { $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('The "mode" parameter value is not valid.'); - new Email(['mode' => 'Unknown Mode']); + new Email(mode: 'Unknown Mode'); } public function testUnknownModeArgumentsTriggerException() @@ -49,11 +49,14 @@ public function testUnknownModeArgumentsTriggerException() public function testNormalizerCanBeSet() { - $email = new Email(['normalizer' => 'trim']); + $email = new Email(normalizer: 'trim'); $this->assertEquals('trim', $email->normalizer); } + /** + * @group legacy + */ public function testInvalidNormalizerThrowsException() { $this->expectException(InvalidArgumentException::class); @@ -61,6 +64,9 @@ public function testInvalidNormalizerThrowsException() new Email(['normalizer' => 'Unknown Callable']); } + /** + * @group legacy + */ public function testInvalidNormalizerObjectThrowsException() { $this->expectException(InvalidArgumentException::class); diff --git a/Tests/Constraints/EmailValidatorTest.php b/Tests/Constraints/EmailValidatorTest.php index 35f938fb7..483b534e6 100644 --- a/Tests/Constraints/EmailValidatorTest.php +++ b/Tests/Constraints/EmailValidatorTest.php @@ -11,7 +11,6 @@ namespace Symfony\Component\Validator\Tests\Constraints; -use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Symfony\Component\Validator\Constraints\Email; use Symfony\Component\Validator\Constraints\EmailValidator; use Symfony\Component\Validator\Exception\UnexpectedValueException; @@ -22,8 +21,6 @@ */ class EmailValidatorTest extends ConstraintValidatorTestCase { - use ExpectDeprecationTrait; - protected function createValidator(): EmailValidator { return new EmailValidator(Email::VALIDATION_MODE_HTML5); @@ -95,36 +92,12 @@ public static function getValidEmails() ]; } - /** - * @group legacy - * - * @dataProvider getValidEmails - * @dataProvider getEmailsOnlyValidInLooseMode - */ - public function testValidInLooseModeEmails($email) - { - $this->validator->validate($email, new Email(['mode' => Email::VALIDATION_MODE_LOOSE])); - - $this->assertNoViolation(); - } - - public static function getEmailsOnlyValidInLooseMode() - { - return [ - ['example@example.co..uk'], - ['{}~!@!@£$%%^&*().!@£$%^&*()'], - ['example@example.co..uk'], - ['example@-example.com'], - [\sprintf('example@%s.com', str_repeat('a', 64))], - ]; - } - /** * @dataProvider getValidEmailsWithWhitespaces */ public function testValidNormalizedEmails($email) { - $this->validator->validate($email, new Email(['normalizer' => 'trim'])); + $this->validator->validate($email, new Email(normalizer: 'trim')); $this->assertNoViolation(); } @@ -137,35 +110,12 @@ public static function getValidEmailsWithWhitespaces() ]; } - /** - * @group legacy - * - * @dataProvider getValidEmailsWithWhitespaces - * @dataProvider getEmailsWithWhitespacesOnlyValidInLooseMode - */ - public function testValidNormalizedEmailsInLooseMode($email) - { - $this->validator->validate($email, new Email(['mode' => Email::VALIDATION_MODE_LOOSE, 'normalizer' => 'trim'])); - - $this->assertNoViolation(); - } - - public static function getEmailsWithWhitespacesOnlyValidInLooseMode() - { - return [ - ["\x09\x09example@example.co..uk\x09\x09"], - ["\x0A{}~!@!@£$%%^&*().!@£$%^&*()\x0A"], - ["\x0D\x0Dexample@example.co..uk\x0D\x0D"], - ["\x00example@-example.com"], - ]; - } - /** * @dataProvider getValidEmailsHtml5 */ public function testValidEmailsHtml5($email) { - $this->validator->validate($email, new Email(['mode' => Email::VALIDATION_MODE_HTML5])); + $this->validator->validate($email, new Email(mode: Email::VALIDATION_MODE_HTML5)); $this->assertNoViolation(); } @@ -185,9 +135,7 @@ public static function getValidEmailsHtml5() */ public function testInvalidEmails($email) { - $constraint = new Email([ - 'message' => 'myMessage', - ]); + $constraint = new Email(message: 'myMessage'); $this->validator->validate($email, $constraint); @@ -212,10 +160,10 @@ public static function getInvalidEmails() */ public function testInvalidHtml5Emails($email) { - $constraint = new Email([ - 'message' => 'myMessage', - 'mode' => Email::VALIDATION_MODE_HTML5, - ]); + $constraint = new Email( + message: 'myMessage', + mode: Email::VALIDATION_MODE_HTML5, + ); $this->validator->validate($email, $constraint); @@ -252,10 +200,10 @@ public static function getInvalidHtml5Emails() */ public function testInvalidAllowNoTldEmails($email) { - $constraint = new Email([ - 'message' => 'myMessage', - 'mode' => Email::VALIDATION_MODE_HTML5_ALLOW_NO_TLD, - ]); + $constraint = new Email( + message: 'myMessage', + mode: Email::VALIDATION_MODE_HTML5_ALLOW_NO_TLD, + ); $this->validator->validate($email, $constraint); @@ -278,7 +226,7 @@ public static function getInvalidAllowNoTldEmails() public function testModeStrict() { - $constraint = new Email(['mode' => Email::VALIDATION_MODE_STRICT]); + $constraint = new Email(mode: Email::VALIDATION_MODE_STRICT); $this->validator->validate('example@mywebsite.tld', $constraint); @@ -287,7 +235,7 @@ public function testModeStrict() public function testModeHtml5() { - $constraint = new Email(['mode' => Email::VALIDATION_MODE_HTML5]); + $constraint = new Email(mode: Email::VALIDATION_MODE_HTML5); $this->validator->validate('example@example..com', $constraint); @@ -299,34 +247,21 @@ public function testModeHtml5() public function testModeHtml5AllowNoTld() { - $constraint = new Email(['mode' => Email::VALIDATION_MODE_HTML5_ALLOW_NO_TLD]); + $constraint = new Email(mode: Email::VALIDATION_MODE_HTML5_ALLOW_NO_TLD); $this->validator->validate('example@example', $constraint); $this->assertNoViolation(); } - /** - * @group legacy - */ - public function testModeLoose() - { - $this->expectDeprecation('Since symfony/validator 6.2: The "loose" mode is deprecated. It will be removed in 7.0 and the default mode will be changed to "html5".'); - - $constraint = new Email(['mode' => Email::VALIDATION_MODE_LOOSE]); - - $this->validator->validate('example@example..com', $constraint); - - $this->assertNoViolation(); - } - public function testUnknownModesOnValidateTriggerException() { - $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('The "Symfony\Component\Validator\Constraints\Email::$mode" parameter value is not valid.'); $constraint = new Email(); $constraint->mode = 'Unknown Mode'; + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The "Symfony\Component\Validator\Constraints\Email::$mode" parameter value is not valid.'); + $this->validator->validate('example@example..com', $constraint); } @@ -335,10 +270,10 @@ public function testUnknownModesOnValidateTriggerException() */ public function testStrictWithInvalidEmails($email) { - $constraint = new Email([ - 'message' => 'myMessage', - 'mode' => Email::VALIDATION_MODE_STRICT, - ]); + $constraint = new Email( + message: 'myMessage', + mode: Email::VALIDATION_MODE_STRICT, + ); $this->validator->validate($email, $constraint); diff --git a/Tests/Constraints/EnableAutoMappingTest.php b/Tests/Constraints/EnableAutoMappingTest.php index 66ab42cdf..525a62ed5 100644 --- a/Tests/Constraints/EnableAutoMappingTest.php +++ b/Tests/Constraints/EnableAutoMappingTest.php @@ -23,6 +23,9 @@ */ class EnableAutoMappingTest extends TestCase { + /** + * @group legacy + */ public function testGroups() { $this->expectException(ConstraintDefinitionException::class); diff --git a/Tests/Constraints/EqualToValidatorTest.php b/Tests/Constraints/EqualToValidatorTest.php index 0a57fa8d3..c9a24ac4d 100644 --- a/Tests/Constraints/EqualToValidatorTest.php +++ b/Tests/Constraints/EqualToValidatorTest.php @@ -14,6 +14,7 @@ use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraints\EqualTo; use Symfony\Component\Validator\Constraints\EqualToValidator; +use Symfony\Component\Validator\Tests\Constraints\Fixtures\TypedDummy; use Symfony\Component\Validator\Tests\IcuCompatibilityTrait; /** @@ -22,6 +23,9 @@ class EqualToValidatorTest extends AbstractComparisonValidatorTestCase { use IcuCompatibilityTrait; + use InvalidComparisonToValueTestTrait; + use ThrowsOnInvalidStringDatesTestTrait; + use ValidComparisonToValueTrait; protected function createValidator(): EqualToValidator { @@ -30,7 +34,11 @@ protected function createValidator(): EqualToValidator protected static function createConstraint(?array $options = null): Constraint { - return new EqualTo($options); + if (null !== $options) { + return new EqualTo(...$options); + } + + return new EqualTo(); } protected function getErrorCode(): ?string @@ -71,10 +79,40 @@ public static function provideInvalidComparisons(): array ]; } - public static function provideComparisonsToNullValueAtPropertyPath(): array + public function testCompareWithNullValueAtPropertyAt() { - return [ - [5, '5', false], - ]; + $constraint = $this->createConstraint(['propertyPath' => 'value']); + $constraint->message = 'Constraint Message'; + + $object = new ComparisonTest_Class(null); + $this->setObject($object); + + $this->validator->validate(5, $constraint); + + $this->buildViolation('Constraint Message') + ->setParameter('{{ value }}', '5') + ->setParameter('{{ compared_value }}', 'null') + ->setParameter('{{ compared_value_type }}', 'null') + ->setParameter('{{ compared_value_path }}', 'value') + ->setCode($this->getErrorCode()) + ->assertRaised(); + } + + public function testCompareWithUninitializedPropertyAtPropertyPath() + { + $this->setObject(new TypedDummy()); + + $this->validator->validate(5, $this->createConstraint([ + 'message' => 'Constraint Message', + 'propertyPath' => 'value', + ])); + + $this->buildViolation('Constraint Message') + ->setParameter('{{ value }}', '5') + ->setParameter('{{ compared_value }}', 'null') + ->setParameter('{{ compared_value_type }}', 'null') + ->setParameter('{{ compared_value_path }}', 'value') + ->setCode($this->getErrorCode()) + ->assertRaised(); } } diff --git a/Tests/Constraints/ExpressionLanguageSyntaxTest.php b/Tests/Constraints/ExpressionLanguageSyntaxTest.php deleted file mode 100644 index bc9911d1b..000000000 --- a/Tests/Constraints/ExpressionLanguageSyntaxTest.php +++ /dev/null @@ -1,82 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Validator\Tests\Constraints; - -use PHPUnit\Framework\TestCase; -use Symfony\Component\Validator\Constraints\ExpressionLanguageSyntax; -use Symfony\Component\Validator\Constraints\ExpressionLanguageSyntaxValidator; -use Symfony\Component\Validator\Mapping\ClassMetadata; -use Symfony\Component\Validator\Mapping\Loader\AttributeLoader; - -/** - * @group legacy - */ -class ExpressionLanguageSyntaxTest extends TestCase -{ - public function testValidatedByStandardValidator() - { - $constraint = new ExpressionLanguageSyntax(); - - self::assertSame(ExpressionLanguageSyntaxValidator::class, $constraint->validatedBy()); - } - - /** - * @dataProvider provideServiceValidatedConstraints - */ - public function testValidatedByService(ExpressionLanguageSyntax $constraint) - { - self::assertSame('my_service', $constraint->validatedBy()); - } - - public static function provideServiceValidatedConstraints(): iterable - { - yield 'Doctrine style' => [new ExpressionLanguageSyntax(['service' => 'my_service'])]; - - yield 'named arguments' => [new ExpressionLanguageSyntax(service: 'my_service')]; - - $metadata = new ClassMetadata(ExpressionLanguageSyntaxDummy::class); - self::assertTrue((new AttributeLoader())->loadClassMetadata($metadata)); - - yield 'attribute' => [$metadata->properties['b']->constraints[0]]; - } - - public function testAttributes() - { - $metadata = new ClassMetadata(ExpressionLanguageSyntaxDummy::class); - self::assertTrue((new AttributeLoader())->loadClassMetadata($metadata)); - - [$aConstraint] = $metadata->properties['a']->getConstraints(); - self::assertNull($aConstraint->service); - self::assertNull($aConstraint->allowedVariables); - - [$bConstraint] = $metadata->properties['b']->getConstraints(); - self::assertSame('my_service', $bConstraint->service); - self::assertSame('myMessage', $bConstraint->message); - self::assertSame(['Default', 'ExpressionLanguageSyntaxDummy'], $bConstraint->groups); - - [$cConstraint] = $metadata->properties['c']->getConstraints(); - self::assertSame(['foo', 'bar'], $cConstraint->allowedVariables); - self::assertSame(['my_group'], $cConstraint->groups); - } -} - -class ExpressionLanguageSyntaxDummy -{ - #[ExpressionLanguageSyntax] - private $a; - - #[ExpressionLanguageSyntax(service: 'my_service', message: 'myMessage')] - private $b; - - #[ExpressionLanguageSyntax(allowedVariables: ['foo', 'bar'], groups: ['my_group'])] - private $c; -} diff --git a/Tests/Constraints/ExpressionLanguageSyntaxValidatorTest.php b/Tests/Constraints/ExpressionLanguageSyntaxValidatorTest.php deleted file mode 100644 index f44e606a4..000000000 --- a/Tests/Constraints/ExpressionLanguageSyntaxValidatorTest.php +++ /dev/null @@ -1,72 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Validator\Tests\Constraints; - -use Symfony\Component\ExpressionLanguage\ExpressionLanguage; -use Symfony\Component\Validator\Constraints\ExpressionLanguageSyntax; -use Symfony\Component\Validator\Constraints\ExpressionLanguageSyntaxValidator; -use Symfony\Component\Validator\ConstraintValidatorInterface; -use Symfony\Component\Validator\Test\ConstraintValidatorTestCase; - -/** - * @group legacy - */ -class ExpressionLanguageSyntaxValidatorTest extends ConstraintValidatorTestCase -{ - protected function createValidator(): ConstraintValidatorInterface - { - return new ExpressionLanguageSyntaxValidator(new ExpressionLanguage()); - } - - public function testExpressionValid() - { - $this->validator->validate('1 + 1', new ExpressionLanguageSyntax([ - 'message' => 'myMessage', - 'allowedVariables' => [], - ])); - - $this->assertNoViolation(); - } - - public function testExpressionWithoutNames() - { - $this->validator->validate('1 + 1', new ExpressionLanguageSyntax([ - 'message' => 'myMessage', - ])); - - $this->assertNoViolation(); - } - - public function testExpressionWithAllowedVariableName() - { - $this->validator->validate('a + 1', new ExpressionLanguageSyntax([ - 'message' => 'myMessage', - 'allowedVariables' => ['a'], - ])); - - $this->assertNoViolation(); - } - - public function testExpressionIsNotValid() - { - $this->validator->validate('a + 1', new ExpressionLanguageSyntax([ - 'message' => 'myMessage', - 'allowedVariables' => [], - ])); - - $this->buildViolation('myMessage') - ->setParameter('{{ syntax_error }}', '"Variable "a" is not valid around position 1 for expression `a + 1`."') - ->setInvalidValue('a + 1') - ->setCode(ExpressionLanguageSyntax::EXPRESSION_LANGUAGE_SYNTAX_ERROR) - ->assertRaised(); - } -} diff --git a/Tests/Constraints/ExpressionSyntaxTest.php b/Tests/Constraints/ExpressionSyntaxTest.php index 3f77cace2..8731a5d85 100644 --- a/Tests/Constraints/ExpressionSyntaxTest.php +++ b/Tests/Constraints/ExpressionSyntaxTest.php @@ -36,8 +36,6 @@ public function testValidatedByService(ExpressionSyntax $constraint) public static function provideServiceValidatedConstraints(): iterable { - yield 'Doctrine style' => [new ExpressionSyntax(['service' => 'my_service'])]; - yield 'named arguments' => [new ExpressionSyntax(service: 'my_service')]; $metadata = new ClassMetadata(ExpressionSyntaxDummy::class); @@ -46,6 +44,16 @@ public static function provideServiceValidatedConstraints(): iterable yield 'attribute' => [$metadata->properties['b']->constraints[0]]; } + /** + * @group legacy + */ + public function testValidatedByServiceDoctrineStyle() + { + $constraint = new ExpressionSyntax(['service' => 'my_service']); + + self::assertSame('my_service', $constraint->validatedBy()); + } + public function testAttributes() { $metadata = new ClassMetadata(ExpressionSyntaxDummy::class); diff --git a/Tests/Constraints/ExpressionSyntaxValidatorTest.php b/Tests/Constraints/ExpressionSyntaxValidatorTest.php index de316f47e..3ca4e655b 100644 --- a/Tests/Constraints/ExpressionSyntaxValidatorTest.php +++ b/Tests/Constraints/ExpressionSyntaxValidatorTest.php @@ -15,6 +15,7 @@ use Symfony\Component\Validator\Constraints\ExpressionSyntax; use Symfony\Component\Validator\Constraints\ExpressionSyntaxValidator; use Symfony\Component\Validator\Test\ConstraintValidatorTestCase; +use Symfony\Component\Validator\Tests\Constraints\Fixtures\StringableValue; class ExpressionSyntaxValidatorTest extends ConstraintValidatorTestCase { @@ -39,39 +40,61 @@ public function testEmptyStringIsValid() public function testExpressionValid() { - $this->validator->validate('1 + 1', new ExpressionSyntax([ - 'message' => 'myMessage', - 'allowedVariables' => [], - ])); + $this->validator->validate('1 + 1', new ExpressionSyntax( + message: 'myMessage', + allowedVariables: [], + )); + + $this->assertNoViolation(); + } + + public function testStringableExpressionValid() + { + $this->validator->validate(new StringableValue('1 + 1'), new ExpressionSyntax( + message: 'myMessage', + allowedVariables: [], + )); $this->assertNoViolation(); } public function testExpressionWithoutNames() { - $this->validator->validate('1 + 1', new ExpressionSyntax([ - 'message' => 'myMessage', - ])); + $this->validator->validate('1 + 1', new ExpressionSyntax(null, 'myMessage', null, [])); $this->assertNoViolation(); } public function testExpressionWithAllowedVariableName() { - $this->validator->validate('a + 1', new ExpressionSyntax([ - 'message' => 'myMessage', - 'allowedVariables' => ['a'], - ])); + $this->validator->validate('a + 1', new ExpressionSyntax( + message: 'myMessage', + allowedVariables: ['a'], + )); $this->assertNoViolation(); } public function testExpressionIsNotValid() { - $this->validator->validate('a + 1', new ExpressionSyntax([ - 'message' => 'myMessage', - 'allowedVariables' => [], - ])); + $this->validator->validate('a + 1', new ExpressionSyntax( + message: 'myMessage', + allowedVariables: [], + )); + + $this->buildViolation('myMessage') + ->setParameter('{{ syntax_error }}', '"Variable "a" is not valid around position 1 for expression `a + 1`."') + ->setInvalidValue('a + 1') + ->setCode(ExpressionSyntax::EXPRESSION_SYNTAX_ERROR) + ->assertRaised(); + } + + public function testStringableExpressionIsNotValid() + { + $this->validator->validate(new StringableValue('a + 1'), new ExpressionSyntax( + message: 'myMessage', + allowedVariables: [], + )); $this->buildViolation('myMessage') ->setParameter('{{ syntax_error }}', '"Variable "a" is not valid around position 1 for expression `a + 1`."') diff --git a/Tests/Constraints/ExpressionTest.php b/Tests/Constraints/ExpressionTest.php index 97fcab838..07abb071d 100644 --- a/Tests/Constraints/ExpressionTest.php +++ b/Tests/Constraints/ExpressionTest.php @@ -51,6 +51,9 @@ public function testMissingPattern() new Expression(null); } + /** + * @group legacy + */ public function testMissingPatternDoctrineStyle() { $this->expectException(MissingOptionsException::class); diff --git a/Tests/Constraints/ExpressionValidatorTest.php b/Tests/Constraints/ExpressionValidatorTest.php index c237c793f..21c9eb630 100644 --- a/Tests/Constraints/ExpressionValidatorTest.php +++ b/Tests/Constraints/ExpressionValidatorTest.php @@ -31,10 +31,10 @@ protected function createValidator(): ExpressionValidator public function testExpressionIsEvaluatedWithNullValue() { - $constraint = new Expression([ - 'expression' => 'false', - 'message' => 'myMessage', - ]); + $constraint = new Expression( + expression: 'false', + message: 'myMessage', + ); $this->validator->validate(null, $constraint); @@ -46,10 +46,10 @@ public function testExpressionIsEvaluatedWithNullValue() public function testExpressionIsEvaluatedWithEmptyStringValue() { - $constraint = new Expression([ - 'expression' => 'false', - 'message' => 'myMessage', - ]); + $constraint = new Expression( + expression: 'false', + message: 'myMessage', + ); $this->validator->validate('', $constraint); @@ -75,10 +75,10 @@ public function testSucceedingExpressionAtObjectLevel() public function testFailingExpressionAtObjectLevel() { - $constraint = new Expression([ - 'expression' => 'this.data == 1', - 'message' => 'myMessage', - ]); + $constraint = new Expression( + expression: 'this.data == 1', + message: 'myMessage', + ); $object = new Entity(); $object->data = '2'; @@ -109,10 +109,10 @@ public function testSucceedingExpressionAtObjectLevelWithToString() public function testFailingExpressionAtObjectLevelWithToString() { - $constraint = new Expression([ - 'expression' => 'this.data == 1', - 'message' => 'myMessage', - ]); + $constraint = new Expression( + expression: 'this.data == 1', + message: 'myMessage', + ); $object = new ToString(); $object->data = '2'; @@ -145,10 +145,10 @@ public function testSucceedingExpressionAtPropertyLevel() public function testFailingExpressionAtPropertyLevel() { - $constraint = new Expression([ - 'expression' => 'value == this.data', - 'message' => 'myMessage', - ]); + $constraint = new Expression( + expression: 'value == this.data', + message: 'myMessage', + ); $object = new Entity(); $object->data = '1'; @@ -187,10 +187,10 @@ public function testSucceedingExpressionAtNestedPropertyLevel() public function testFailingExpressionAtNestedPropertyLevel() { - $constraint = new Expression([ - 'expression' => 'value == this.data', - 'message' => 'myMessage', - ]); + $constraint = new Expression( + expression: 'value == this.data', + message: 'myMessage', + ); $object = new Entity(); $object->data = '1'; @@ -234,10 +234,10 @@ public function testSucceedingExpressionAtPropertyLevelWithoutRoot() */ public function testFailingExpressionAtPropertyLevelWithoutRoot() { - $constraint = new Expression([ - 'expression' => 'value == "1"', - 'message' => 'myMessage', - ]); + $constraint = new Expression( + expression: 'value == "1"', + message: 'myMessage', + ); $this->setRoot('2'); $this->setPropertyPath(''); @@ -254,9 +254,7 @@ public function testFailingExpressionAtPropertyLevelWithoutRoot() public function testExpressionLanguageUsage() { - $constraint = new Expression([ - 'expression' => 'false', - ]); + $constraint = new Expression(expression: 'false'); $expressionLanguage = $this->createMock(ExpressionLanguage::class); @@ -278,12 +276,12 @@ public function testExpressionLanguageUsage() public function testPassingCustomValues() { - $constraint = new Expression([ - 'expression' => 'value + custom == 2', - 'values' => [ + $constraint = new Expression( + expression: 'value + custom == 2', + values: [ 'custom' => 1, ], - ]); + ); $this->validator->validate(1, $constraint); @@ -292,13 +290,13 @@ public function testPassingCustomValues() public function testViolationOnPass() { - $constraint = new Expression([ - 'expression' => 'value + custom != 2', - 'values' => [ + $constraint = new Expression( + expression: 'value + custom != 2', + values: [ 'custom' => 1, ], - 'negate' => false, - ]); + negate: false, + ); $this->validator->validate(2, $constraint); @@ -311,10 +309,11 @@ public function testViolationOnPass() public function testIsValidExpression() { - $constraints = [new NotNull(), new Range(['min' => 2])]; + $constraints = [new NotNull(), new Range(min: 2)]; $constraint = new Expression( - ['expression' => 'is_valid(this.data, a)', 'values' => ['a' => $constraints]] + expression: 'is_valid(this.data, a)', + values: ['a' => $constraints], ); $object = new Entity(); @@ -331,10 +330,11 @@ public function testIsValidExpression() public function testIsValidExpressionInvalid() { - $constraints = [new Range(['min' => 2, 'max' => 5])]; + $constraints = [new Range(min: 2, max: 5)]; $constraint = new Expression( - ['expression' => 'is_valid(this.data, a)', 'values' => ['a' => $constraints]] + expression: 'is_valid(this.data, a)', + values: ['a' => $constraints], ); $object = new Entity(); diff --git a/Tests/Constraints/FileTest.php b/Tests/Constraints/FileTest.php index 5b70dc1d8..3e03f7881 100644 --- a/Tests/Constraints/FileTest.php +++ b/Tests/Constraints/FileTest.php @@ -14,6 +14,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\Validator\Constraints\File; use Symfony\Component\Validator\Exception\ConstraintDefinitionException; +use Symfony\Component\Validator\Exception\InvalidArgumentException; use Symfony\Component\Validator\Mapping\ClassMetadata; use Symfony\Component\Validator\Mapping\Loader\AttributeLoader; @@ -24,7 +25,7 @@ class FileTest extends TestCase */ public function testMaxSize($maxSize, $bytes, $binaryFormat) { - $file = new File(['maxSize' => $maxSize]); + $file = new File(maxSize: $maxSize); $this->assertSame($bytes, $file->maxSize); $this->assertSame($binaryFormat, $file->binaryFormat); @@ -33,7 +34,7 @@ public function testMaxSize($maxSize, $bytes, $binaryFormat) public function testMagicIsset() { - $file = new File(['maxSize' => 1]); + $file = new File(maxSize: 1); $this->assertTrue($file->__isset('maxSize')); $this->assertTrue($file->__isset('groups')); @@ -57,8 +58,10 @@ public function testMaxSizeCanBeSetAfterInitialization($maxSize, $bytes, $binary */ public function testInvalidValueForMaxSizeThrowsExceptionAfterInitialization($maxSize) { + $file = new File(maxSize: 1000); + $this->expectException(ConstraintDefinitionException::class); - $file = new File(['maxSize' => 1000]); + $file->maxSize = $maxSize; } @@ -67,7 +70,7 @@ public function testInvalidValueForMaxSizeThrowsExceptionAfterInitialization($ma */ public function testMaxSizeCannotBeSetToInvalidValueAfterInitialization($maxSize) { - $file = new File(['maxSize' => 1000]); + $file = new File(maxSize: 1000); try { $file->maxSize = $maxSize; @@ -77,13 +80,38 @@ public function testMaxSizeCannotBeSetToInvalidValueAfterInitialization($maxSize $this->assertSame(1000, $file->maxSize); } + public function testFilenameMaxLength() + { + $file = new File(filenameMaxLength: 30); + $this->assertSame(30, $file->filenameMaxLength); + } + + public function testDefaultFilenameCountUnitIsUsed() + { + $file = new File(); + self::assertSame(File::FILENAME_COUNT_BYTES, $file->filenameCountUnit); + } + + public function testFilenameCharsetDefaultsToNull() + { + $file = new File(); + self::assertNull($file->filenameCharset); + } + + public function testInvalidFilenameCountUnitThrowsException() + { + self::expectException(InvalidArgumentException::class); + self::expectExceptionMessage(\sprintf('The "filenameCountUnit" option must be one of the "%s::FILENAME_COUNT_*" constants ("%s" given).', File::class, 'nonExistentCountUnit')); + $file = new File(filenameCountUnit: 'nonExistentCountUnit'); + } + /** * @dataProvider provideInValidSizes */ public function testInvalidMaxSize($maxSize) { $this->expectException(ConstraintDefinitionException::class); - new File(['maxSize' => $maxSize]); + new File(maxSize: $maxSize); } public static function provideValidSizes() @@ -103,8 +131,8 @@ public static function provideValidSizes() ['1GI', 1073741824, true], ['2g', 2000000000, false], ['2G', 2000000000, false], - ['4g', 4 === \PHP_INT_SIZE ? 4000000000.0 : 4000000000, false], - ['4G', 4 === \PHP_INT_SIZE ? 4000000000.0 : 4000000000, false], + ['4g', 4 === \PHP_INT_SIZE ? '4000000000' : 4000000000, false], + ['4G', 4 === \PHP_INT_SIZE ? '4000000000' : 4000000000, false], ]; } @@ -123,7 +151,7 @@ public static function provideInvalidSizes() */ public function testBinaryFormat($maxSize, $guessedFormat, $binaryFormat) { - $file = new File(['maxSize' => $maxSize, 'binaryFormat' => $guessedFormat]); + $file = new File(maxSize: $maxSize, binaryFormat: $guessedFormat); $this->assertSame($binaryFormat, $file->binaryFormat); } @@ -160,6 +188,9 @@ public function testAttributes() self::assertSame(100000, $cConstraint->maxSize); self::assertSame(['my_group'], $cConstraint->groups); self::assertSame('some attached data', $cConstraint->payload); + self::assertSame(30, $cConstraint->filenameMaxLength); + self::assertSame('ISO-8859-15', $cConstraint->filenameCharset); + self::assertSame(File::FILENAME_COUNT_CODEPOINTS, $cConstraint->filenameCountUnit); } } @@ -171,6 +202,6 @@ class FileDummy #[File(maxSize: 100, notFoundMessage: 'myMessage')] private $b; - #[File(maxSize: '100K', groups: ['my_group'], payload: 'some attached data')] + #[File(maxSize: '100K', filenameMaxLength: 30, filenameCharset: 'ISO-8859-15', filenameCountUnit: File::FILENAME_COUNT_CODEPOINTS, groups: ['my_group'], payload: 'some attached data')] private $c; } diff --git a/Tests/Constraints/FileValidatorPathTest.php b/Tests/Constraints/FileValidatorPathTest.php index 9a8968801..37557aa1a 100644 --- a/Tests/Constraints/FileValidatorPathTest.php +++ b/Tests/Constraints/FileValidatorPathTest.php @@ -22,9 +22,9 @@ protected function getFile($filename) public function testFileNotFound() { - $constraint = new File([ - 'notFoundMessage' => 'myMessage', - ]); + $constraint = new File( + notFoundMessage: 'myMessage', + ); $this->validator->validate('foobar', $constraint); diff --git a/Tests/Constraints/FileValidatorTestCase.php b/Tests/Constraints/FileValidatorTestCase.php index 960a8f3b6..b1ebf530e 100644 --- a/Tests/Constraints/FileValidatorTestCase.php +++ b/Tests/Constraints/FileValidatorTestCase.php @@ -168,10 +168,10 @@ public function testMaxSizeExceeded($bytesWritten, $limit, $sizeAsString, $limit fwrite($this->file, '0'); fclose($this->file); - $constraint = new File([ - 'maxSize' => $limit, - 'maxSizeMessage' => 'myMessage', - ]); + $constraint = new File( + maxSize: $limit, + maxSizeMessage: 'myMessage', + ); $this->validator->validate($this->getFile($this->path), $constraint); @@ -220,10 +220,10 @@ public function testMaxSizeNotExceeded($bytesWritten, $limit) fwrite($this->file, '0'); fclose($this->file); - $constraint = new File([ - 'maxSize' => $limit, - 'maxSizeMessage' => 'myMessage', - ]); + $constraint = new File( + maxSize: $limit, + maxSizeMessage: 'myMessage', + ); $this->validator->validate($this->getFile($this->path), $constraint); @@ -233,9 +233,7 @@ public function testMaxSizeNotExceeded($bytesWritten, $limit) public function testInvalidMaxSize() { $this->expectException(ConstraintDefinitionException::class); - new File([ - 'maxSize' => '1abc', - ]); + new File(maxSize: '1abc'); } public static function provideBinaryFormatTests() @@ -269,11 +267,11 @@ public function testBinaryFormat($bytesWritten, $limit, $binaryFormat, $sizeAsSt fwrite($this->file, '0'); fclose($this->file); - $constraint = new File([ - 'maxSize' => $limit, - 'binaryFormat' => $binaryFormat, - 'maxSizeMessage' => 'myMessage', - ]); + $constraint = new File( + maxSize: $limit, + binaryFormat: $binaryFormat, + maxSizeMessage: 'myMessage', + ); $this->validator->validate($this->getFile($this->path), $constraint); @@ -322,9 +320,7 @@ public function testValidMimeType() ->method('getMimeType') ->willReturn('image/jpg'); - $constraint = new File([ - 'mimeTypes' => ['image/png', 'image/jpg'], - ]); + $constraint = new File(mimeTypes: ['image/png', 'image/jpg']); $this->validator->validate($file, $constraint); @@ -346,19 +342,14 @@ public function testValidWildcardMimeType() ->method('getMimeType') ->willReturn('image/jpg'); - $constraint = new File([ - 'mimeTypes' => ['image/*'], - ]); + $constraint = new File(mimeTypes: ['image/*']); $this->validator->validate($file, $constraint); $this->assertNoViolation(); } - /** - * @dataProvider provideMimeTypeConstraints - */ - public function testInvalidMimeType(File $constraint) + public function testInvalidMimeType() { $file = $this ->getMockBuilder(\Symfony\Component\HttpFoundation\File\File::class) @@ -373,7 +364,7 @@ public function testInvalidMimeType(File $constraint) ->method('getMimeType') ->willReturn('application/pdf'); - $this->validator->validate($file, $constraint); + $this->validator->validate($file, new File(mimeTypes: ['image/png', 'image/jpg'], mimeTypesMessage: 'myMessage')); $this->buildViolation('myMessage') ->setParameter('{{ type }}', '"application/pdf"') @@ -384,15 +375,36 @@ public function testInvalidMimeType(File $constraint) ->assertRaised(); } - public static function provideMimeTypeConstraints(): iterable + /** + * @group legacy + */ + public function testInvalidMimeTypeDoctrineStyle() { - yield 'Doctrine style' => [new File([ + $file = $this + ->getMockBuilder(\Symfony\Component\HttpFoundation\File\File::class) + ->setConstructorArgs([__DIR__.'/Fixtures/foo']) + ->getMock(); + $file + ->expects($this->once()) + ->method('getPathname') + ->willReturn($this->path); + $file + ->expects($this->once()) + ->method('getMimeType') + ->willReturn('application/pdf'); + + $this->validator->validate($file, new File([ 'mimeTypes' => ['image/png', 'image/jpg'], 'mimeTypesMessage' => 'myMessage', - ])]; - yield 'named arguments' => [ - new File(mimeTypes: ['image/png', 'image/jpg'], mimeTypesMessage: 'myMessage'), - ]; + ])); + + $this->buildViolation('myMessage') + ->setParameter('{{ type }}', '"application/pdf"') + ->setParameter('{{ types }}', '"image/png", "image/jpg"') + ->setParameter('{{ file }}', '"'.$this->path.'"') + ->setParameter('{{ name }}', '"'.basename($this->path).'"') + ->setCode(File::INVALID_MIME_TYPE_ERROR) + ->assertRaised(); } public function testInvalidWildcardMimeType() @@ -410,10 +422,10 @@ public function testInvalidWildcardMimeType() ->method('getMimeType') ->willReturn('application/pdf'); - $constraint = new File([ - 'mimeTypes' => ['image/*', 'image/jpg'], - 'mimeTypesMessage' => 'myMessage', - ]); + $constraint = new File( + mimeTypes: ['image/*', 'image/jpg'], + mimeTypesMessage: 'myMessage', + ); $this->validator->validate($file, $constraint); @@ -426,14 +438,11 @@ public function testInvalidWildcardMimeType() ->assertRaised(); } - /** - * @dataProvider provideDisallowEmptyConstraints - */ - public function testDisallowEmpty(File $constraint) + public function testDisallowEmpty() { ftruncate($this->file, 0); - $this->validator->validate($this->getFile($this->path), $constraint); + $this->validator->validate($this->getFile($this->path), new File(disallowEmptyMessage: 'myMessage')); $this->buildViolation('myMessage') ->setParameter('{{ file }}', '"'.$this->path.'"') @@ -442,14 +451,22 @@ public function testDisallowEmpty(File $constraint) ->assertRaised(); } - public static function provideDisallowEmptyConstraints(): iterable + /** + * @group legacy + */ + public function testDisallowEmptyDoctrineStyle() { - yield 'Doctrine style' => [new File([ + ftruncate($this->file, 0); + + $this->validator->validate($this->getFile($this->path), new File([ 'disallowEmptyMessage' => 'myMessage', - ])]; - yield 'named arguments' => [ - new File(disallowEmptyMessage: 'myMessage'), - ]; + ])); + + $this->buildViolation('myMessage') + ->setParameter('{{ file }}', '"'.$this->path.'"') + ->setParameter('{{ name }}', '"'.basename($this->path).'"') + ->setCode(File::EMPTY_ERROR) + ->assertRaised(); } /** @@ -459,7 +476,7 @@ public function testUploadedFileError($error, $message, array $params = [], $max { $file = new UploadedFile(tempnam(sys_get_temp_dir(), 'file-validator-test-'), 'originalName', 'mime', $error); - $constraint = new File([ + $constraint = new File(...[ $message => 'myMessage', 'maxSize' => $maxSize, ]); @@ -519,10 +536,11 @@ public static function uploadedFileErrorProvider() public function testNegativeMaxSize() { + $file = new File(); + $this->expectException(ConstraintDefinitionException::class); $this->expectExceptionMessage('"-1" is not a valid maximum size.'); - $file = new File(); $file->maxSize = -1; } @@ -657,11 +675,11 @@ public function testUploadedFileExtensions() /** * @dataProvider provideFilenameMaxLengthIsTooLong */ - public function testFilenameMaxLengthIsTooLong(File $constraintFile, string $messageViolation) + public function testFilenameMaxLengthIsTooLong(File $constraintFile, string $filename, string $messageViolation) { file_put_contents($this->path, '1'); - $file = new UploadedFile($this->path, 'myFileWithATooLongOriginalFileName', null, null, true); + $file = new UploadedFile($this->path, $filename, null, null, true); $this->validator->validate($file, $constraintFile); $this->buildViolation($messageViolation) @@ -675,26 +693,83 @@ public function testFilenameMaxLengthIsTooLong(File $constraintFile, string $mes public static function provideFilenameMaxLengthIsTooLong(): \Generator { - yield 'Simple case with only the parameter "filenameMaxLength" ' => [ + yield 'Codepoints and UTF-8 : default' => [ new File(filenameMaxLength: 30), + 'myFileWithATooLongOriginalFileName', 'The filename is too long. It should have {{ filename_max_length }} character or less.|The filename is too long. It should have {{ filename_max_length }} characters or less.', ]; - yield 'Case with the parameter "filenameMaxLength" and a custom error message' => [ - new File(filenameMaxLength: 20, filenameTooLongMessage: 'Your filename is too long. Please use at maximum {{ filename_max_length }} characters'), - 'Your filename is too long. Please use at maximum {{ filename_max_length }} characters', + yield 'Codepoints and UTF-8: custom error message' => [ + new File(filenameMaxLength: 20, filenameTooLongMessage: 'myMessage'), + 'myFileWithATooLongOriginalFileName', + 'myMessage', + ]; + + yield 'Graphemes' => [ + new File(filenameMaxLength: 1, filenameCountUnit: File::FILENAME_COUNT_GRAPHEMES, filenameTooLongMessage: 'myMessage'), + "A\u{0300}A\u{0300}", + 'myMessage', + ]; + + yield 'Bytes' => [ + new File(filenameMaxLength: 5, filenameCountUnit: File::FILENAME_COUNT_BYTES, filenameTooLongMessage: 'myMessage'), + "A\u{0300}A\u{0300}", + 'myMessage', ]; } - public function testFilenameMaxLength() + /** + * @dataProvider provideFilenameCountUnit + */ + public function testValidCountUnitFilenameMaxLength(int $maxLength, string $countUnit) { file_put_contents($this->path, '1'); - $file = new UploadedFile($this->path, 'tinyOriginalFileName', null, null, true); - $this->validator->validate($file, new File(filenameMaxLength: 20)); + $file = new UploadedFile($this->path, "A\u{0300}", null, null, true); + $this->validator->validate($file, new File(filenameMaxLength: $maxLength, filenameCountUnit: $countUnit)); $this->assertNoViolation(); } + /** + * @dataProvider provideFilenameCharset + */ + public function testFilenameCharset(string $filename, string $charset, bool $isValid) + { + file_put_contents($this->path, '1'); + + $file = new UploadedFile($this->path, $filename, null, null, true); + $this->validator->validate($file, new File(filenameCharset: $charset, filenameCharsetMessage: 'myMessage')); + + if ($isValid) { + $this->assertNoViolation(); + } else { + $this->buildViolation('myMessage') + ->setParameter('{{ name }}', '"'.$filename.'"') + ->setParameter('{{ charset }}', $charset) + ->setCode(File::FILENAME_INVALID_CHARACTERS) + ->assertRaised(); + } + } + + public static function provideFilenameCountUnit(): array + { + return [ + 'graphemes' => [1, File::FILENAME_COUNT_GRAPHEMES], + 'codepoints' => [2, File::FILENAME_COUNT_CODEPOINTS], + 'bytes' => [3, File::FILENAME_COUNT_BYTES], + ]; + } + + public static function provideFilenameCharset(): array + { + return [ + ['é', 'utf8', true], + ["\xE9", 'CP1252', true], + ["\xE9", 'XXX', false], + ["\xE9", 'utf8', false], + ]; + } + abstract protected function getFile($filename); } diff --git a/Tests/Constraints/Fixtures/StringableValue.php b/Tests/Constraints/Fixtures/StringableValue.php new file mode 100644 index 000000000..0cd2f4ddb --- /dev/null +++ b/Tests/Constraints/Fixtures/StringableValue.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Tests\Constraints\Fixtures; + +class StringableValue implements \Stringable +{ + public function __construct(private string $value) + { + } + + public function __toString(): string + { + return $this->value; + } +} diff --git a/Tests/Constraints/Fixtures/WhenTestWithAttributes.php b/Tests/Constraints/Fixtures/WhenTestWithAttributes.php index a683eb3c6..31258dc0d 100644 --- a/Tests/Constraints/Fixtures/WhenTestWithAttributes.php +++ b/Tests/Constraints/Fixtures/WhenTestWithAttributes.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Validator\Tests\Constraints\Fixtures; use Symfony\Component\Validator\Constraints\Callback; +use Symfony\Component\Validator\Constraints\Length; use Symfony\Component\Validator\Constraints\NotBlank; use Symfony\Component\Validator\Constraints\NotNull; use Symfony\Component\Validator\Constraints\When; @@ -36,6 +37,9 @@ class WhenTestWithAttributes #[When(expression: 'true', constraints: new NotNull(), groups: ['foo'])] private $qux; + #[When(expression: 'true', constraints: new NotNull(), otherwise: new Length(exactly: 10), groups: ['foo'])] + private $quux; + #[When(expression: 'true', constraints: [ new NotNull(), new NotBlank(), diff --git a/Tests/Constraints/Fixtures/WhenTestWithClosure.php b/Tests/Constraints/Fixtures/WhenTestWithClosure.php new file mode 100644 index 000000000..de0f07daa --- /dev/null +++ b/Tests/Constraints/Fixtures/WhenTestWithClosure.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Tests\Constraints\Fixtures; + +use Symfony\Component\Validator\Constraints\Callback; +use Symfony\Component\Validator\Constraints\NotBlank; +use Symfony\Component\Validator\Constraints\NotNull; +use Symfony\Component\Validator\Constraints\When; + +#[When(expression: static function () { + return true; + }, constraints: new Callback('isValid') +)] +class WhenTestWithClosure +{ + #[When(expression: static function () { + return true; + }, constraints: [ + new NotNull(), + new NotBlank(), + ])] + private $foo; +} diff --git a/Tests/Constraints/Fixtures/test_landscape.svg b/Tests/Constraints/Fixtures/test_landscape.svg new file mode 100644 index 000000000..e1212da08 --- /dev/null +++ b/Tests/Constraints/Fixtures/test_landscape.svg @@ -0,0 +1,2 @@ + + diff --git a/Tests/Constraints/Fixtures/test_landscape_height.svg b/Tests/Constraints/Fixtures/test_landscape_height.svg new file mode 100644 index 000000000..7a54631f1 --- /dev/null +++ b/Tests/Constraints/Fixtures/test_landscape_height.svg @@ -0,0 +1,2 @@ + + diff --git a/Tests/Constraints/Fixtures/test_landscape_width.svg b/Tests/Constraints/Fixtures/test_landscape_width.svg new file mode 100644 index 000000000..a64c0b1e0 --- /dev/null +++ b/Tests/Constraints/Fixtures/test_landscape_width.svg @@ -0,0 +1,2 @@ + + diff --git a/Tests/Constraints/Fixtures/test_landscape_width_height.svg b/Tests/Constraints/Fixtures/test_landscape_width_height.svg new file mode 100644 index 000000000..ec7b52445 --- /dev/null +++ b/Tests/Constraints/Fixtures/test_landscape_width_height.svg @@ -0,0 +1,2 @@ + + diff --git a/Tests/Constraints/Fixtures/test_no_size.svg b/Tests/Constraints/Fixtures/test_no_size.svg new file mode 100644 index 000000000..e0af766e8 --- /dev/null +++ b/Tests/Constraints/Fixtures/test_no_size.svg @@ -0,0 +1,2 @@ + + diff --git a/Tests/Constraints/Fixtures/test_portrait.svg b/Tests/Constraints/Fixtures/test_portrait.svg new file mode 100644 index 000000000..d17c991be --- /dev/null +++ b/Tests/Constraints/Fixtures/test_portrait.svg @@ -0,0 +1,2 @@ + + diff --git a/Tests/Constraints/Fixtures/test_square.svg b/Tests/Constraints/Fixtures/test_square.svg new file mode 100644 index 000000000..ffac7f14a --- /dev/null +++ b/Tests/Constraints/Fixtures/test_square.svg @@ -0,0 +1,2 @@ + + diff --git a/Tests/Constraints/GreaterThanOrEqualValidatorTest.php b/Tests/Constraints/GreaterThanOrEqualValidatorTest.php index aeaf46f56..ae9f2034c 100644 --- a/Tests/Constraints/GreaterThanOrEqualValidatorTest.php +++ b/Tests/Constraints/GreaterThanOrEqualValidatorTest.php @@ -21,7 +21,11 @@ */ class GreaterThanOrEqualValidatorTest extends AbstractComparisonValidatorTestCase { + use CompareWithNullValueAtPropertyAtTestTrait; use IcuCompatibilityTrait; + use InvalidComparisonToValueTestTrait; + use ThrowsOnInvalidStringDatesTestTrait; + use ValidComparisonToValueTrait; protected function createValidator(): GreaterThanOrEqualValidator { @@ -30,7 +34,11 @@ protected function createValidator(): GreaterThanOrEqualValidator protected static function createConstraint(?array $options = null): Constraint { - return new GreaterThanOrEqual($options); + if (null !== $options) { + return new GreaterThanOrEqual(...$options); + } + + return new GreaterThanOrEqual(); } protected function getErrorCode(): ?string @@ -73,11 +81,4 @@ public static function provideInvalidComparisons(): array ['b', '"b"', 'c', '"c"', 'string'], ]; } - - public static function provideComparisonsToNullValueAtPropertyPath(): array - { - return [ - [5, '5', true], - ]; - } } diff --git a/Tests/Constraints/GreaterThanOrEqualValidatorWithPositiveOrZeroConstraintTest.php b/Tests/Constraints/GreaterThanOrEqualValidatorWithPositiveOrZeroConstraintTest.php index 92b21bd5e..47f190851 100644 --- a/Tests/Constraints/GreaterThanOrEqualValidatorWithPositiveOrZeroConstraintTest.php +++ b/Tests/Constraints/GreaterThanOrEqualValidatorWithPositiveOrZeroConstraintTest.php @@ -12,15 +12,20 @@ namespace Symfony\Component\Validator\Tests\Constraints; use Symfony\Component\Validator\Constraint; -use Symfony\Component\Validator\Constraints\AbstractComparison; +use Symfony\Component\Validator\Constraints\GreaterThanOrEqualValidator; use Symfony\Component\Validator\Constraints\PositiveOrZero; use Symfony\Component\Validator\Exception\ConstraintDefinitionException; /** * @author Jan Schädlich */ -class GreaterThanOrEqualValidatorWithPositiveOrZeroConstraintTest extends GreaterThanOrEqualValidatorTest +class GreaterThanOrEqualValidatorWithPositiveOrZeroConstraintTest extends AbstractComparisonValidatorTestCase { + protected function createValidator(): GreaterThanOrEqualValidator + { + return new GreaterThanOrEqualValidator(); + } + protected static function createConstraint(?array $options = null): Constraint { return new PositiveOrZero($options); @@ -39,6 +44,14 @@ public static function provideValidComparisons(): array ]; } + public static function provideValidComparisonsToPropertyPath(): array + { + return [ + [5], + [6], + ]; + } + public static function provideInvalidComparisons(): array { return [ @@ -48,6 +61,9 @@ public static function provideInvalidComparisons(): array ]; } + /** + * @group legacy + */ public function testThrowsConstraintExceptionIfPropertyPath() { $this->expectException(ConstraintDefinitionException::class); @@ -56,6 +72,9 @@ public function testThrowsConstraintExceptionIfPropertyPath() return new PositiveOrZero(['propertyPath' => 'field']); } + /** + * @group legacy + */ public function testThrowsConstraintExceptionIfValue() { $this->expectException(ConstraintDefinitionException::class); @@ -69,15 +88,11 @@ public function testThrowsConstraintExceptionIfValue() */ public function testThrowsConstraintExceptionIfNoValueOrPropertyPath($options) { - $this->expectException(ConstraintDefinitionException::class); - $this->expectExceptionMessage('requires either the "value" or "propertyPath" option to be set.'); $this->markTestSkipped('Value option always set for PositiveOrZero constraint'); } public function testThrowsConstraintExceptionIfBothValueAndPropertyPath() { - $this->expectException(ConstraintDefinitionException::class); - $this->expectExceptionMessage('requires only one of the "value" or "propertyPath" options to be set, not both.'); $this->markTestSkipped('Value option is set for PositiveOrZero constraint automatically'); } @@ -86,11 +101,6 @@ public function testInvalidValuePath() $this->markTestSkipped('PropertyPath option is not used in PositiveOrZero constraint'); } - public static function provideAllValidComparisons(): array - { - self::markTestSkipped('The "value" option cannot be used in the PositiveOrZero constraint'); - } - /** * @dataProvider provideValidComparisonsToPropertyPath */ @@ -104,31 +114,8 @@ public function testNoViolationOnNullObjectWithPropertyPath() $this->markTestSkipped('PropertyPath option is not used in PositiveOrZero constraint'); } - /** - * @dataProvider throwsOnInvalidStringDatesProvider - */ - public function testThrowsOnInvalidStringDates(AbstractComparison $constraint, $expectedMessage, $value) - { - $this->markTestSkipped('The compared value cannot be an invalid string date because it is hardcoded to 0.'); - } - public function testInvalidComparisonToPropertyPathAddsPathAsParameter() { $this->markTestSkipped('PropertyPath option is not used in PositiveOrZero constraint'); } - - public static function throwsOnInvalidStringDatesProvider(): array - { - self::markTestSkipped('The "value" option cannot be used in the PositiveOrZero constraint'); - } - - public static function provideAllInvalidComparisons(): array - { - self::markTestSkipped('The "value" option cannot be used in the Negative constraint'); - } - - public static function provideComparisonsToNullValueAtPropertyPath(): array - { - self::markTestSkipped('PropertyPath option is not used in PositiveOrZero constraint'); - } } diff --git a/Tests/Constraints/GreaterThanValidatorTest.php b/Tests/Constraints/GreaterThanValidatorTest.php index 709e01c26..0e74da15f 100644 --- a/Tests/Constraints/GreaterThanValidatorTest.php +++ b/Tests/Constraints/GreaterThanValidatorTest.php @@ -21,7 +21,11 @@ */ class GreaterThanValidatorTest extends AbstractComparisonValidatorTestCase { + use CompareWithNullValueAtPropertyAtTestTrait; use IcuCompatibilityTrait; + use InvalidComparisonToValueTestTrait; + use ThrowsOnInvalidStringDatesTestTrait; + use ValidComparisonToValueTrait; protected function createValidator(): GreaterThanValidator { @@ -30,7 +34,11 @@ protected function createValidator(): GreaterThanValidator protected static function createConstraint(?array $options = null): Constraint { - return new GreaterThan($options); + if (null !== $options) { + return new GreaterThan(...$options); + } + + return new GreaterThan(); } protected function getErrorCode(): ?string @@ -75,11 +83,4 @@ public static function provideInvalidComparisons(): array ['22', '"22"', '22', '"22"', 'string'], ]; } - - public static function provideComparisonsToNullValueAtPropertyPath(): array - { - return [ - [5, '5', true], - ]; - } } diff --git a/Tests/Constraints/GreaterThanValidatorWithPositiveConstraintTest.php b/Tests/Constraints/GreaterThanValidatorWithPositiveConstraintTest.php index bd826e33f..6b58bff85 100644 --- a/Tests/Constraints/GreaterThanValidatorWithPositiveConstraintTest.php +++ b/Tests/Constraints/GreaterThanValidatorWithPositiveConstraintTest.php @@ -12,15 +12,20 @@ namespace Symfony\Component\Validator\Tests\Constraints; use Symfony\Component\Validator\Constraint; -use Symfony\Component\Validator\Constraints\AbstractComparison; +use Symfony\Component\Validator\Constraints\GreaterThanValidator; use Symfony\Component\Validator\Constraints\Positive; use Symfony\Component\Validator\Exception\ConstraintDefinitionException; /** * @author Jan Schädlich */ -class GreaterThanValidatorWithPositiveConstraintTest extends GreaterThanValidatorTest +class GreaterThanValidatorWithPositiveConstraintTest extends AbstractComparisonValidatorTestCase { + protected function createValidator(): GreaterThanValidator + { + return new GreaterThanValidator(); + } + protected static function createConstraint(?array $options = null): Constraint { return new Positive($options); @@ -36,6 +41,13 @@ public static function provideValidComparisons(): array ]; } + public static function provideValidComparisonsToPropertyPath(): array + { + return [ + [6], + ]; + } + public static function provideInvalidComparisons(): array { return [ @@ -46,6 +58,9 @@ public static function provideInvalidComparisons(): array ]; } + /** + * @group legacy + */ public function testThrowsConstraintExceptionIfPropertyPath() { $this->expectException(ConstraintDefinitionException::class); @@ -54,6 +69,9 @@ public function testThrowsConstraintExceptionIfPropertyPath() return new Positive(['propertyPath' => 'field']); } + /** + * @group legacy + */ public function testThrowsConstraintExceptionIfValue() { $this->expectException(ConstraintDefinitionException::class); @@ -67,15 +85,11 @@ public function testThrowsConstraintExceptionIfValue() */ public function testThrowsConstraintExceptionIfNoValueOrPropertyPath($options) { - $this->expectException(ConstraintDefinitionException::class); - $this->expectExceptionMessage('requires either the "value" or "propertyPath" option to be set.'); $this->markTestSkipped('Value option always set for Positive constraint.'); } public function testThrowsConstraintExceptionIfBothValueAndPropertyPath() { - $this->expectException(ConstraintDefinitionException::class); - $this->expectExceptionMessage('requires only one of the "value" or "propertyPath" options to be set, not both.'); $this->markTestSkipped('Value option is set for Positive constraint automatically'); } @@ -89,11 +103,6 @@ public function testInvalidValuePath() $this->markTestSkipped('PropertyPath option is not used in Positive constraint'); } - public static function provideAllValidComparisons(): array - { - self::markTestSkipped('The "value" option cannot be used in the Positive constraint'); - } - /** * @dataProvider provideValidComparisonsToPropertyPath */ @@ -102,31 +111,8 @@ public function testValidComparisonToPropertyPath($comparedValue) $this->markTestSkipped('PropertyPath option is not used in Positive constraint'); } - /** - * @dataProvider throwsOnInvalidStringDatesProvider - */ - public function testThrowsOnInvalidStringDates(AbstractComparison $constraint, $expectedMessage, $value) - { - $this->markTestSkipped('The compared value cannot be an invalid string date because it is hardcoded to 0.'); - } - public function testInvalidComparisonToPropertyPathAddsPathAsParameter() { $this->markTestSkipped('PropertyPath option is not used in Positive constraint'); } - - public static function throwsOnInvalidStringDatesProvider(): array - { - self::markTestSkipped('The "value" option cannot be used in the Positive constraint'); - } - - public static function provideAllInvalidComparisons(): array - { - self::markTestSkipped('The "value" option cannot be used in the Positive constraint'); - } - - public static function provideComparisonsToNullValueAtPropertyPath(): array - { - self::markTestSkipped('PropertyPath option is not used in PositiveOrZero constraint'); - } } diff --git a/Tests/Constraints/HostnameValidatorTest.php b/Tests/Constraints/HostnameValidatorTest.php index f0b03e019..2471fe0b5 100644 --- a/Tests/Constraints/HostnameValidatorTest.php +++ b/Tests/Constraints/HostnameValidatorTest.php @@ -57,7 +57,7 @@ public function testValidTldDomainsPassValidationIfTldRequired($domain) */ public function testValidTldDomainsPassValidationIfTldNotRequired($domain) { - $this->validator->validate($domain, new Hostname(['requireTld' => false])); + $this->validator->validate($domain, new Hostname(requireTld: false)); $this->assertNoViolation(); } @@ -81,9 +81,7 @@ public static function getValidMultilevelDomains() */ public function testInvalidDomainsRaiseViolationIfTldRequired($domain) { - $this->validator->validate($domain, new Hostname([ - 'message' => 'myMessage', - ])); + $this->validator->validate($domain, new Hostname(message: 'myMessage')); $this->buildViolation('myMessage') ->setParameter('{{ value }}', '"'.$domain.'"') @@ -96,10 +94,10 @@ public function testInvalidDomainsRaiseViolationIfTldRequired($domain) */ public function testInvalidDomainsRaiseViolationIfTldNotRequired($domain) { - $this->validator->validate($domain, new Hostname([ - 'message' => 'myMessage', - 'requireTld' => false, - ])); + $this->validator->validate($domain, new Hostname( + message: 'myMessage', + requireTld: false, + )); $this->buildViolation('myMessage') ->setParameter('{{ value }}', '"'.$domain.'"') @@ -123,7 +121,7 @@ public static function getInvalidDomains() */ public function testReservedDomainsPassValidationIfTldNotRequired($domain) { - $this->validator->validate($domain, new Hostname(['requireTld' => false])); + $this->validator->validate($domain, new Hostname(requireTld: false)); $this->assertNoViolation(); } @@ -133,10 +131,10 @@ public function testReservedDomainsPassValidationIfTldNotRequired($domain) */ public function testReservedDomainsRaiseViolationIfTldRequired($domain) { - $this->validator->validate($domain, new Hostname([ - 'message' => 'myMessage', - 'requireTld' => true, - ])); + $this->validator->validate($domain, new Hostname( + message: 'myMessage', + requireTld: true, + )); $this->buildViolation('myMessage') ->setParameter('{{ value }}', '"'.$domain.'"') @@ -176,7 +174,7 @@ public function testReservedDomainsRaiseViolationIfTldRequiredNamed() */ public function testTopLevelDomainsPassValidationIfTldNotRequired($domain) { - $this->validator->validate($domain, new Hostname(['requireTld' => false])); + $this->validator->validate($domain, new Hostname(requireTld: false)); $this->assertNoViolation(); } @@ -186,10 +184,10 @@ public function testTopLevelDomainsPassValidationIfTldNotRequired($domain) */ public function testTopLevelDomainsRaiseViolationIfTldRequired($domain) { - $this->validator->validate($domain, new Hostname([ - 'message' => 'myMessage', - 'requireTld' => true, - ])); + $this->validator->validate($domain, new Hostname( + message: 'myMessage', + requireTld: true, + )); $this->buildViolation('myMessage') ->setParameter('{{ value }}', '"'.$domain.'"') diff --git a/Tests/Constraints/IbanValidatorTest.php b/Tests/Constraints/IbanValidatorTest.php index a9f59b09c..184924d5e 100644 --- a/Tests/Constraints/IbanValidatorTest.php +++ b/Tests/Constraints/IbanValidatorTest.php @@ -66,6 +66,7 @@ public static function getValidIbans() return [ ['CH9300762011623852957'], // Switzerland without spaces ['CH93 0076 2011 6238 5295 7'], // Switzerland with multiple spaces + ['ch93 0076 2011 6238 5295 7'], // Switzerland lower case // Country list // http://www.rbs.co.uk/corporate/international/g0/guide-to-international-business/regulatory-information/iban/iban-example.ashx @@ -90,6 +91,8 @@ public static function getValidIbans() ['FO97 5432 0388 8999 44'], // Faroe Islands ['FI21 1234 5600 0007 85'], // Finland ['FR14 2004 1010 0505 0001 3M02 606'], // France + ["FR14\xc2\xa02004\xc2\xa01010\xc2\xa00505\xc2\xa00001\xc2\xa03M02\xc2\xa0606"], // France with non-breaking spaces + ["FR14\xe2\x80\xaf2004\xe2\x80\xaf1010\xe2\x80\xaf0505\xe2\x80\xaf0001\xe2\x80\xaf3M02\xe2\x80\xaf606"], // France with narrow non-breaking spaces ['GE29 NB00 0000 0101 9049 17'], // Georgia ['DE89 3704 0044 0532 0130 00'], // Germany ['GI75 NWBK 0000 0000 7099 453'], // Gibraltar @@ -485,9 +488,7 @@ public static function getIbansWithInvalidCountryCode() private function assertViolationRaised($iban, $code) { - $constraint = new Iban([ - 'message' => 'myMessage', - ]); + $constraint = new Iban(message: 'myMessage'); $this->validator->validate($iban, $constraint); diff --git a/Tests/Constraints/IdenticalToValidatorTest.php b/Tests/Constraints/IdenticalToValidatorTest.php index 2855e83f0..97164b74c 100644 --- a/Tests/Constraints/IdenticalToValidatorTest.php +++ b/Tests/Constraints/IdenticalToValidatorTest.php @@ -14,6 +14,7 @@ use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraints\IdenticalTo; use Symfony\Component\Validator\Constraints\IdenticalToValidator; +use Symfony\Component\Validator\Tests\Constraints\Fixtures\TypedDummy; use Symfony\Component\Validator\Tests\IcuCompatibilityTrait; /** @@ -22,6 +23,9 @@ class IdenticalToValidatorTest extends AbstractComparisonValidatorTestCase { use IcuCompatibilityTrait; + use InvalidComparisonToValueTestTrait; + use ThrowsOnInvalidStringDatesTestTrait; + use ValidComparisonToValueTrait; protected function createValidator(): IdenticalToValidator { @@ -30,7 +34,11 @@ protected function createValidator(): IdenticalToValidator protected static function createConstraint(?array $options = null): Constraint { - return new IdenticalTo($options); + if (null !== $options) { + return new IdenticalTo(...$options); + } + + return new IdenticalTo(); } protected function getErrorCode(): ?string @@ -90,10 +98,40 @@ public static function provideInvalidComparisons(): array ]; } - public static function provideComparisonsToNullValueAtPropertyPath(): array + public function testCompareWithNullValueAtPropertyAt() { - return [ - [5, '5', false], - ]; + $constraint = $this->createConstraint(['propertyPath' => 'value']); + $constraint->message = 'Constraint Message'; + + $object = new ComparisonTest_Class(null); + $this->setObject($object); + + $this->validator->validate(5, $constraint); + + $this->buildViolation('Constraint Message') + ->setParameter('{{ value }}', '5') + ->setParameter('{{ compared_value }}', 'null') + ->setParameter('{{ compared_value_type }}', 'null') + ->setParameter('{{ compared_value_path }}', 'value') + ->setCode($this->getErrorCode()) + ->assertRaised(); + } + + public function testCompareWithUninitializedPropertyAtPropertyPath() + { + $this->setObject(new TypedDummy()); + + $this->validator->validate(5, $this->createConstraint([ + 'message' => 'Constraint Message', + 'propertyPath' => 'value', + ])); + + $this->buildViolation('Constraint Message') + ->setParameter('{{ value }}', '5') + ->setParameter('{{ compared_value }}', 'null') + ->setParameter('{{ compared_value_type }}', 'null') + ->setParameter('{{ compared_value_path }}', 'value') + ->setCode($this->getErrorCode()) + ->assertRaised(); } } diff --git a/Tests/Constraints/ImageValidatorTest.php b/Tests/Constraints/ImageValidatorTest.php index e1c048af9..7a7aa9197 100644 --- a/Tests/Constraints/ImageValidatorTest.php +++ b/Tests/Constraints/ImageValidatorTest.php @@ -15,7 +15,6 @@ use Symfony\Component\Mime\MimeTypes; use Symfony\Component\Validator\Constraints\Image; use Symfony\Component\Validator\Constraints\ImageValidator; -use Symfony\Component\Validator\Exception\ConstraintDefinitionException; use Symfony\Component\Validator\Test\ConstraintValidatorTestCase; /** @@ -75,12 +74,10 @@ public function testValidImage() /** * Checks that the logic from FileValidator still works. - * - * @dataProvider provideConstraintsWithNotFoundMessage */ - public function testFileNotFound(Image $constraint) + public function testFileNotFound() { - $this->validator->validate('foobar', $constraint); + $this->validator->validate('foobar', new Image(notFoundMessage: 'myMessage')); $this->buildViolation('myMessage') ->setParameter('{{ file }}', '"foobar"') @@ -88,36 +85,40 @@ public function testFileNotFound(Image $constraint) ->assertRaised(); } - public static function provideConstraintsWithNotFoundMessage(): iterable + /** + * Checks that the logic from FileValidator still works. + * + * @group legacy + */ + public function testFileNotFoundDoctrineStyle() { - yield 'Doctrine style' => [new Image([ + $this->validator->validate('foobar', new Image([ 'notFoundMessage' => 'myMessage', - ])]; - yield 'Named arguments' => [ - new Image(notFoundMessage: 'myMessage'), - ]; + ])); + + $this->buildViolation('myMessage') + ->setParameter('{{ file }}', '"foobar"') + ->setCode(Image::NOT_FOUND_ERROR) + ->assertRaised(); } public function testValidSize() { - $constraint = new Image([ - 'minWidth' => 1, - 'maxWidth' => 2, - 'minHeight' => 1, - 'maxHeight' => 2, - ]); + $constraint = new Image( + minWidth: 1, + maxWidth: 2, + minHeight: 1, + maxHeight: 2, + ); $this->validator->validate($this->image, $constraint); $this->assertNoViolation(); } - /** - * @dataProvider provideMinWidthConstraints - */ - public function testWidthTooSmall(Image $constraint) + public function testWidthTooSmall() { - $this->validator->validate($this->image, $constraint); + $this->validator->validate($this->image, new Image(minWidth: 3, minWidthMessage: 'myMessage')); $this->buildViolation('myMessage') ->setParameter('{{ width }}', '2') @@ -126,23 +127,26 @@ public function testWidthTooSmall(Image $constraint) ->assertRaised(); } - public static function provideMinWidthConstraints(): iterable + /** + * @group legacy + */ + public function testWidthTooSmallDoctrineStyle() { - yield 'Doctrine style' => [new Image([ + $this->validator->validate($this->image, new Image([ 'minWidth' => 3, 'minWidthMessage' => 'myMessage', - ])]; - yield 'Named arguments' => [ - new Image(minWidth: 3, minWidthMessage: 'myMessage'), - ]; + ])); + + $this->buildViolation('myMessage') + ->setParameter('{{ width }}', '2') + ->setParameter('{{ min_width }}', '3') + ->setCode(Image::TOO_NARROW_ERROR) + ->assertRaised(); } - /** - * @dataProvider provideMaxWidthConstraints - */ - public function testWidthTooBig(Image $constraint) + public function testWidthTooBig() { - $this->validator->validate($this->image, $constraint); + $this->validator->validate($this->image, new Image(maxWidth: 1, maxWidthMessage: 'myMessage')); $this->buildViolation('myMessage') ->setParameter('{{ width }}', '2') @@ -151,23 +155,26 @@ public function testWidthTooBig(Image $constraint) ->assertRaised(); } - public static function provideMaxWidthConstraints(): iterable + /** + * @group legacy + */ + public function testWidthTooBigDoctrineStyle() { - yield 'Doctrine style' => [new Image([ + $this->validator->validate($this->image, new Image([ 'maxWidth' => 1, 'maxWidthMessage' => 'myMessage', - ])]; - yield 'Named arguments' => [ - new Image(maxWidth: 1, maxWidthMessage: 'myMessage'), - ]; + ])); + + $this->buildViolation('myMessage') + ->setParameter('{{ width }}', '2') + ->setParameter('{{ max_width }}', '1') + ->setCode(Image::TOO_WIDE_ERROR) + ->assertRaised(); } - /** - * @dataProvider provideMinHeightConstraints - */ - public function testHeightTooSmall(Image $constraint) + public function testHeightTooSmall() { - $this->validator->validate($this->image, $constraint); + $this->validator->validate($this->image, new Image(minHeight: 3, minHeightMessage: 'myMessage')); $this->buildViolation('myMessage') ->setParameter('{{ height }}', '2') @@ -176,23 +183,26 @@ public function testHeightTooSmall(Image $constraint) ->assertRaised(); } - public static function provideMinHeightConstraints(): iterable + /** + * @group legacy + */ + public function testHeightTooSmallDoctrineStyle() { - yield 'Doctrine style' => [new Image([ + $this->validator->validate($this->image, new Image([ 'minHeight' => 3, 'minHeightMessage' => 'myMessage', - ])]; - yield 'Named arguments' => [ - new Image(minHeight: 3, minHeightMessage: 'myMessage'), - ]; + ])); + + $this->buildViolation('myMessage') + ->setParameter('{{ height }}', '2') + ->setParameter('{{ min_height }}', '3') + ->setCode(Image::TOO_LOW_ERROR) + ->assertRaised(); } - /** - * @dataProvider provideMaxHeightConstraints - */ - public function testHeightTooBig(Image $constraint) + public function testHeightTooBig() { - $this->validator->validate($this->image, $constraint); + $this->validator->validate($this->image, new Image(maxHeight: 1, maxHeightMessage: 'myMessage')); $this->buildViolation('myMessage') ->setParameter('{{ height }}', '2') @@ -201,23 +211,26 @@ public function testHeightTooBig(Image $constraint) ->assertRaised(); } - public static function provideMaxHeightConstraints(): iterable + /** + * @group legacy + */ + public function testHeightTooBigDoctrineStyle() { - yield 'Doctrine style' => [new Image([ + $this->validator->validate($this->image, new Image([ 'maxHeight' => 1, 'maxHeightMessage' => 'myMessage', - ])]; - yield 'Named arguments' => [ - new Image(maxHeight: 1, maxHeightMessage: 'myMessage'), - ]; + ])); + + $this->buildViolation('myMessage') + ->setParameter('{{ height }}', '2') + ->setParameter('{{ max_height }}', '1') + ->setCode(Image::TOO_HIGH_ERROR) + ->assertRaised(); } - /** - * @dataProvider provideMinPixelsConstraints - */ - public function testPixelsTooFew(Image $constraint) + public function testPixelsTooFew() { - $this->validator->validate($this->image, $constraint); + $this->validator->validate($this->image, new Image(minPixels: 5, minPixelsMessage: 'myMessage')); $this->buildViolation('myMessage') ->setParameter('{{ pixels }}', '4') @@ -228,23 +241,28 @@ public function testPixelsTooFew(Image $constraint) ->assertRaised(); } - public static function provideMinPixelsConstraints(): iterable + /** + * @group legacy + */ + public function testPixelsTooFewDoctrineStyle() { - yield 'Doctrine style' => [new Image([ + $this->validator->validate($this->image, new Image([ 'minPixels' => 5, 'minPixelsMessage' => 'myMessage', - ])]; - yield 'Named arguments' => [ - new Image(minPixels: 5, minPixelsMessage: 'myMessage'), - ]; + ])); + + $this->buildViolation('myMessage') + ->setParameter('{{ pixels }}', '4') + ->setParameter('{{ min_pixels }}', '5') + ->setParameter('{{ height }}', '2') + ->setParameter('{{ width }}', '2') + ->setCode(Image::TOO_FEW_PIXEL_ERROR) + ->assertRaised(); } - /** - * @dataProvider provideMaxPixelsConstraints - */ - public function testPixelsTooMany(Image $constraint) + public function testPixelsTooMany() { - $this->validator->validate($this->image, $constraint); + $this->validator->validate($this->image, new Image(maxPixels: 3, maxPixelsMessage: 'myMessage')); $this->buildViolation('myMessage') ->setParameter('{{ pixels }}', '4') @@ -255,83 +273,45 @@ public function testPixelsTooMany(Image $constraint) ->assertRaised(); } - public static function provideMaxPixelsConstraints(): iterable + /** + * @group legacy + */ + public function testPixelsTooManyDoctrineStyle() { - yield 'Doctrine style' => [new Image([ + $this->validator->validate($this->image, new Image([ 'maxPixels' => 3, 'maxPixelsMessage' => 'myMessage', - ])]; - yield 'Named arguments' => [ - new Image(maxPixels: 3, maxPixelsMessage: 'myMessage'), - ]; - } - - public function testInvalidMinWidth() - { - $this->expectException(ConstraintDefinitionException::class); - $constraint = new Image([ - 'minWidth' => '1abc', - ]); - - $this->validator->validate($this->image, $constraint); - } - - public function testInvalidMaxWidth() - { - $this->expectException(ConstraintDefinitionException::class); - $constraint = new Image([ - 'maxWidth' => '1abc', - ]); - - $this->validator->validate($this->image, $constraint); - } - - public function testInvalidMinHeight() - { - $this->expectException(ConstraintDefinitionException::class); - $constraint = new Image([ - 'minHeight' => '1abc', - ]); + ])); - $this->validator->validate($this->image, $constraint); - } - - public function testInvalidMaxHeight() - { - $this->expectException(ConstraintDefinitionException::class); - $constraint = new Image([ - 'maxHeight' => '1abc', - ]); - - $this->validator->validate($this->image, $constraint); - } - - public function testInvalidMinPixels() - { - $this->expectException(ConstraintDefinitionException::class); - $constraint = new Image([ - 'minPixels' => '1abc', - ]); - - $this->validator->validate($this->image, $constraint); + $this->buildViolation('myMessage') + ->setParameter('{{ pixels }}', '4') + ->setParameter('{{ max_pixels }}', '3') + ->setParameter('{{ height }}', '2') + ->setParameter('{{ width }}', '2') + ->setCode(Image::TOO_MANY_PIXEL_ERROR) + ->assertRaised(); } - public function testInvalidMaxPixels() + public function testRatioTooSmall() { - $this->expectException(ConstraintDefinitionException::class); - $constraint = new Image([ - 'maxPixels' => '1abc', - ]); + $this->validator->validate($this->image, new Image(minRatio: 2, minRatioMessage: 'myMessage')); - $this->validator->validate($this->image, $constraint); + $this->buildViolation('myMessage') + ->setParameter('{{ ratio }}', 1) + ->setParameter('{{ min_ratio }}', 2) + ->setCode(Image::RATIO_TOO_SMALL_ERROR) + ->assertRaised(); } /** - * @dataProvider provideMinRatioConstraints + * @group legacy */ - public function testRatioTooSmall(Image $constraint) + public function testRatioTooSmallDoctrineStyle() { - $this->validator->validate($this->image, $constraint); + $this->validator->validate($this->image, new Image([ + 'minRatio' => 2, + 'minRatioMessage' => 'myMessage', + ])); $this->buildViolation('myMessage') ->setParameter('{{ ratio }}', 1) @@ -340,23 +320,9 @@ public function testRatioTooSmall(Image $constraint) ->assertRaised(); } - public static function provideMinRatioConstraints(): iterable - { - yield 'Doctrine style' => [new Image([ - 'minRatio' => 2, - 'minRatioMessage' => 'myMessage', - ])]; - yield 'Named arguments' => [ - new Image(minRatio: 2, minRatioMessage: 'myMessage'), - ]; - } - - /** - * @dataProvider provideMaxRatioConstraints - */ - public function testRatioTooBig(Image $constraint) + public function testRatioTooBig() { - $this->validator->validate($this->image, $constraint); + $this->validator->validate($this->image, new Image(maxRatio: 0.5, maxRatioMessage: 'myMessage')); $this->buildViolation('myMessage') ->setParameter('{{ ratio }}', 1) @@ -365,22 +331,26 @@ public function testRatioTooBig(Image $constraint) ->assertRaised(); } - public static function provideMaxRatioConstraints(): iterable + /** + * @group legacy + */ + public function testRatioTooBigDoctrineStyle() { - yield 'Doctrine style' => [new Image([ + $this->validator->validate($this->image, new Image([ 'maxRatio' => 0.5, 'maxRatioMessage' => 'myMessage', - ])]; - yield 'Named arguments' => [ - new Image(maxRatio: 0.5, maxRatioMessage: 'myMessage'), - ]; + ])); + + $this->buildViolation('myMessage') + ->setParameter('{{ ratio }}', 1) + ->setParameter('{{ max_ratio }}', 0.5) + ->setCode(Image::RATIO_TOO_BIG_ERROR) + ->assertRaised(); } public function testMaxRatioUsesTwoDecimalsOnly() { - $constraint = new Image([ - 'maxRatio' => 1.33, - ]); + $constraint = new Image(maxRatio: 1.33); $this->validator->validate($this->image4By3, $constraint); @@ -389,9 +359,7 @@ public function testMaxRatioUsesTwoDecimalsOnly() public function testMinRatioUsesInputMoreDecimals() { - $constraint = new Image([ - 'minRatio' => 4 / 3, - ]); + $constraint = new Image(minRatio: 4 / 3); $this->validator->validate($this->image4By3, $constraint); @@ -400,41 +368,33 @@ public function testMinRatioUsesInputMoreDecimals() public function testMaxRatioUsesInputMoreDecimals() { - $constraint = new Image([ - 'maxRatio' => 16 / 9, - ]); + $constraint = new Image(maxRatio: 16 / 9); $this->validator->validate($this->image16By9, $constraint); $this->assertNoViolation(); } - public function testInvalidMinRatio() + public function testSquareNotAllowed() { - $this->expectException(ConstraintDefinitionException::class); - $constraint = new Image([ - 'minRatio' => '1abc', - ]); + $this->validator->validate($this->image, new Image(allowSquare: false, allowSquareMessage: 'myMessage')); - $this->validator->validate($this->image, $constraint); - } - - public function testInvalidMaxRatio() - { - $this->expectException(ConstraintDefinitionException::class); - $constraint = new Image([ - 'maxRatio' => '1abc', - ]); - - $this->validator->validate($this->image, $constraint); + $this->buildViolation('myMessage') + ->setParameter('{{ width }}', 2) + ->setParameter('{{ height }}', 2) + ->setCode(Image::SQUARE_NOT_ALLOWED_ERROR) + ->assertRaised(); } /** - * @dataProvider provideAllowSquareConstraints + * @group legacy */ - public function testSquareNotAllowed(Image $constraint) + public function testSquareNotAllowedDoctrineStyle() { - $this->validator->validate($this->image, $constraint); + $this->validator->validate($this->image, new Image([ + 'allowSquare' => false, + 'allowSquareMessage' => 'myMessage', + ])); $this->buildViolation('myMessage') ->setParameter('{{ width }}', 2) @@ -443,23 +403,26 @@ public function testSquareNotAllowed(Image $constraint) ->assertRaised(); } - public static function provideAllowSquareConstraints(): iterable + public function testLandscapeNotAllowed() { - yield 'Doctrine style' => [new Image([ - 'allowSquare' => false, - 'allowSquareMessage' => 'myMessage', - ])]; - yield 'Named arguments' => [ - new Image(allowSquare: false, allowSquareMessage: 'myMessage'), - ]; + $this->validator->validate($this->imageLandscape, new Image(allowLandscape: false, allowLandscapeMessage: 'myMessage')); + + $this->buildViolation('myMessage') + ->setParameter('{{ width }}', 2) + ->setParameter('{{ height }}', 1) + ->setCode(Image::LANDSCAPE_NOT_ALLOWED_ERROR) + ->assertRaised(); } /** - * @dataProvider provideAllowLandscapeConstraints + * @group legacy */ - public function testLandscapeNotAllowed(Image $constraint) + public function testLandscapeNotAllowedDoctrineStyle() { - $this->validator->validate($this->imageLandscape, $constraint); + $this->validator->validate($this->imageLandscape, new Image([ + 'allowLandscape' => false, + 'allowLandscapeMessage' => 'myMessage', + ])); $this->buildViolation('myMessage') ->setParameter('{{ width }}', 2) @@ -468,23 +431,26 @@ public function testLandscapeNotAllowed(Image $constraint) ->assertRaised(); } - public static function provideAllowLandscapeConstraints(): iterable + public function testPortraitNotAllowed() { - yield 'Doctrine style' => [new Image([ - 'allowLandscape' => false, - 'allowLandscapeMessage' => 'myMessage', - ])]; - yield 'Named arguments' => [ - new Image(allowLandscape: false, allowLandscapeMessage: 'myMessage'), - ]; + $this->validator->validate($this->imagePortrait, new Image(allowPortrait: false, allowPortraitMessage: 'myMessage')); + + $this->buildViolation('myMessage') + ->setParameter('{{ width }}', 1) + ->setParameter('{{ height }}', 2) + ->setCode(Image::PORTRAIT_NOT_ALLOWED_ERROR) + ->assertRaised(); } /** - * @dataProvider provideAllowPortraitConstraints + * @group legacy */ - public function testPortraitNotAllowed(Image $constraint) + public function testPortraitNotAllowedDoctrineStyle() { - $this->validator->validate($this->imagePortrait, $constraint); + $this->validator->validate($this->imagePortrait, new Image([ + 'allowPortrait' => false, + 'allowPortraitMessage' => 'myMessage', + ])); $this->buildViolation('myMessage') ->setParameter('{{ width }}', 1) @@ -493,26 +459,39 @@ public function testPortraitNotAllowed(Image $constraint) ->assertRaised(); } - public static function provideAllowPortraitConstraints(): iterable + public function testCorrupted() { - yield 'Doctrine style' => [new Image([ - 'allowPortrait' => false, - 'allowPortraitMessage' => 'myMessage', - ])]; - yield 'Named arguments' => [ - new Image(allowPortrait: false, allowPortraitMessage: 'myMessage'), - ]; + if (!\function_exists('imagecreatefromstring')) { + $this->markTestSkipped('This test require GD extension'); + } + + $constraint = new Image(detectCorrupted: true, corruptedMessage: 'myMessage'); + + $this->validator->validate($this->image, $constraint); + + $this->assertNoViolation(); + + $this->validator->validate($this->imageCorrupted, $constraint); + + $this->buildViolation('myMessage') + ->setCode(Image::CORRUPTED_IMAGE_ERROR) + ->assertRaised(); } /** - * @dataProvider provideDetectCorruptedConstraints + * @group legacy */ - public function testCorrupted(Image $constraint) + public function testCorruptedDoctrineStyle() { if (!\function_exists('imagecreatefromstring')) { $this->markTestSkipped('This test require GD extension'); } + $constraint = new Image([ + 'detectCorrupted' => true, + 'corruptedMessage' => 'myMessage', + ]); + $this->validator->validate($this->image, $constraint); $this->assertNoViolation(); @@ -539,23 +518,33 @@ public function testInvalidMimeType() ->assertRaised(); } - public static function provideDetectCorruptedConstraints(): iterable + public function testInvalidMimeTypeWithNarrowedSet() { - yield 'Doctrine style' => [new Image([ - 'detectCorrupted' => true, - 'corruptedMessage' => 'myMessage', - ])]; - yield 'Named arguments' => [ - new Image(detectCorrupted: true, corruptedMessage: 'myMessage'), - ]; + $this->validator->validate($this->image, new Image(mimeTypes: [ + 'image/jpeg', + 'image/png', + ])); + + $this->buildViolation('The mime type of the file is invalid ({{ type }}). Allowed mime types are {{ types }}.') + ->setParameter('{{ file }}', \sprintf('"%s"', $this->image)) + ->setParameter('{{ type }}', '"image/gif"') + ->setParameter('{{ types }}', '"image/jpeg", "image/png"') + ->setParameter('{{ name }}', '"test.gif"') + ->setCode(Image::INVALID_MIME_TYPE_ERROR) + ->assertRaised(); } /** - * @dataProvider provideInvalidMimeTypeWithNarrowedSet + * @group legacy */ - public function testInvalidMimeTypeWithNarrowedSet(Image $constraint) + public function testInvalidMimeTypeWithNarrowedSetDoctrineStyle() { - $this->validator->validate($this->image, $constraint); + $this->validator->validate($this->image, new Image([ + 'mimeTypes' => [ + 'image/jpeg', + 'image/png', + ], + ])); $this->buildViolation('The mime type of the file is invalid ({{ type }}). Allowed mime types are {{ types }}.') ->setParameter('{{ file }}', \sprintf('"%s"', $this->image)) @@ -566,19 +555,139 @@ public function testInvalidMimeTypeWithNarrowedSet(Image $constraint) ->assertRaised(); } - public static function provideInvalidMimeTypeWithNarrowedSet() + /** @dataProvider provideSvgWithViolation */ + public function testSvgWithViolation(string $image, Image $constraint, string $violation, array $parameters = []) { - yield 'Doctrine style' => [new Image([ - 'mimeTypes' => [ - 'image/jpeg', - 'image/png', + $this->validator->validate($image, $constraint); + + $this->buildViolation('myMessage') + ->setCode($violation) + ->setParameters($parameters) + ->assertRaised(); + } + + public static function provideSvgWithViolation(): iterable + { + yield 'No size svg' => [ + __DIR__.'/Fixtures/test_no_size.svg', + new Image(allowLandscape: false, sizeNotDetectedMessage: 'myMessage'), + Image::SIZE_NOT_DETECTED_ERROR, + ]; + + yield 'Landscape SVG not allowed' => [ + __DIR__.'/Fixtures/test_landscape.svg', + new Image(allowLandscape: false, allowLandscapeMessage: 'myMessage'), + Image::LANDSCAPE_NOT_ALLOWED_ERROR, + [ + '{{ width }}' => 500, + '{{ height }}' => 200, ], - ])]; - yield 'Named arguments' => [ - new Image(mimeTypes: [ - 'image/jpeg', - 'image/png', - ]), + ]; + + yield 'Portrait SVG not allowed' => [ + __DIR__.'/Fixtures/test_portrait.svg', + new Image(allowPortrait: false, allowPortraitMessage: 'myMessage'), + Image::PORTRAIT_NOT_ALLOWED_ERROR, + [ + '{{ width }}' => 200, + '{{ height }}' => 500, + ], + ]; + + yield 'Square SVG not allowed' => [ + __DIR__.'/Fixtures/test_square.svg', + new Image(allowSquare: false, allowSquareMessage: 'myMessage'), + Image::SQUARE_NOT_ALLOWED_ERROR, + [ + '{{ width }}' => 500, + '{{ height }}' => 500, + ], + ]; + + yield 'Landscape with width attribute SVG allowed' => [ + __DIR__.'/Fixtures/test_landscape_width.svg', + new Image(allowLandscape: false, allowLandscapeMessage: 'myMessage'), + Image::LANDSCAPE_NOT_ALLOWED_ERROR, + [ + '{{ width }}' => 600, + '{{ height }}' => 200, + ], + ]; + + yield 'Landscape with height attribute SVG not allowed' => [ + __DIR__.'/Fixtures/test_landscape_height.svg', + new Image(allowLandscape: false, allowLandscapeMessage: 'myMessage'), + Image::LANDSCAPE_NOT_ALLOWED_ERROR, + [ + '{{ width }}' => 500, + '{{ height }}' => 300, + ], + ]; + + yield 'Landscape with width and height attribute SVG not allowed' => [ + __DIR__.'/Fixtures/test_landscape_width_height.svg', + new Image(allowLandscape: false, allowLandscapeMessage: 'myMessage'), + Image::LANDSCAPE_NOT_ALLOWED_ERROR, + [ + '{{ width }}' => 600, + '{{ height }}' => 300, + ], + ]; + + yield 'SVG Min ratio 2' => [ + __DIR__.'/Fixtures/test_square.svg', + new Image(minRatio: 2, minRatioMessage: 'myMessage'), + Image::RATIO_TOO_SMALL_ERROR, + [ + '{{ ratio }}' => '1', + '{{ min_ratio }}' => '2', + ], + ]; + + yield 'SVG Min ratio 0.5' => [ + __DIR__.'/Fixtures/test_square.svg', + new Image(maxRatio: 0.5, maxRatioMessage: 'myMessage'), + Image::RATIO_TOO_BIG_ERROR, + [ + '{{ ratio }}' => '1', + '{{ max_ratio }}' => '0.5', + ], + ]; + } + + /** @dataProvider provideSvgWithoutViolation */ + public function testSvgWithoutViolation(string $image, Image $constraint) + { + $this->validator->validate($image, $constraint); + + $this->assertNoViolation(); + } + + public static function provideSvgWithoutViolation(): iterable + { + yield 'Landscape SVG allowed' => [ + __DIR__.'/Fixtures/test_landscape.svg', + new Image(allowLandscape: true, allowLandscapeMessage: 'myMessage'), + ]; + + yield 'Portrait SVG allowed' => [ + __DIR__.'/Fixtures/test_portrait.svg', + new Image(allowPortrait: true, allowPortraitMessage: 'myMessage'), + ]; + + yield 'Square SVG allowed' => [ + __DIR__.'/Fixtures/test_square.svg', + new Image(allowSquare: true, allowSquareMessage: 'myMessage'), + ]; + + yield 'SVG Min ratio 1' => [ + __DIR__.'/Fixtures/test_square.svg', + new Image(minRatio: 1, minRatioMessage: 'myMessage'), + ]; + + yield 'SVG Max ratio 1' => [ + __DIR__.'/Fixtures/test_square.svg', + new Image(maxRatio: 1, maxRatioMessage: 'myMessage'), ]; } diff --git a/Tests/Constraints/InvalidComparisonToValueTestTrait.php b/Tests/Constraints/InvalidComparisonToValueTestTrait.php new file mode 100644 index 000000000..5b261d504 --- /dev/null +++ b/Tests/Constraints/InvalidComparisonToValueTestTrait.php @@ -0,0 +1,77 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Tests\Constraints; + +use Symfony\Component\Intl\Util\IntlTestHelper; + +trait InvalidComparisonToValueTestTrait +{ + /** + * @dataProvider provideAllInvalidComparisons + */ + public function testInvalidComparisonToValue($dirtyValue, $dirtyValueAsString, $comparedValue, $comparedValueString, $comparedValueType) + { + // Conversion of dates to string differs between ICU versions + // Make sure we have the correct version loaded + if ($dirtyValue instanceof \DateTimeInterface) { + IntlTestHelper::requireIntl($this, '57.1'); + } + + $constraint = $this->createConstraint(['value' => $comparedValue]); + $constraint->message = 'Constraint Message'; + + $this->validator->validate($dirtyValue, $constraint); + + $this->buildViolation('Constraint Message') + ->setParameter('{{ value }}', $dirtyValueAsString) + ->setParameter('{{ compared_value }}', $comparedValueString) + ->setParameter('{{ compared_value_type }}', $comparedValueType) + ->setCode($this->getErrorCode()) + ->assertRaised(); + } + + public function testInvalidComparisonToPropertyPathAddsPathAsParameter() + { + [$dirtyValue, $dirtyValueAsString, $comparedValue, $comparedValueString, $comparedValueType] = current($this->provideAllInvalidComparisons()); + + $constraint = $this->createConstraint(['propertyPath' => 'value']); + $constraint->message = 'Constraint Message'; + + $object = new ComparisonTest_Class($comparedValue); + + $this->setObject($object); + + $this->validator->validate($dirtyValue, $constraint); + + $this->buildViolation('Constraint Message') + ->setParameter('{{ value }}', $dirtyValueAsString) + ->setParameter('{{ compared_value }}', $comparedValueString) + ->setParameter('{{ compared_value_path }}', 'value') + ->setParameter('{{ compared_value_type }}', $comparedValueType) + ->setCode($this->getErrorCode()) + ->assertRaised(); + } + + public static function provideAllInvalidComparisons(): array + { + // The provider runs before setUp(), so we need to manually fix + // the default timezone + $timezone = date_default_timezone_get(); + date_default_timezone_set('UTC'); + + $comparisons = self::addPhp5Dot5Comparisons(static::provideInvalidComparisons()); + + date_default_timezone_set($timezone); + + return $comparisons; + } +} diff --git a/Tests/Constraints/IpTest.php b/Tests/Constraints/IpTest.php index 7f391153f..2d740ae88 100644 --- a/Tests/Constraints/IpTest.php +++ b/Tests/Constraints/IpTest.php @@ -24,11 +24,14 @@ class IpTest extends TestCase { public function testNormalizerCanBeSet() { - $ip = new Ip(['normalizer' => 'trim']); + $ip = new Ip(normalizer: 'trim'); $this->assertEquals('trim', $ip->normalizer); } + /** + * @group legacy + */ public function testInvalidNormalizerThrowsException() { $this->expectException(InvalidArgumentException::class); @@ -36,6 +39,9 @@ public function testInvalidNormalizerThrowsException() new Ip(['normalizer' => 'Unknown Callable']); } + /** + * @group legacy + */ public function testInvalidNormalizerObjectThrowsException() { $this->expectException(InvalidArgumentException::class); diff --git a/Tests/Constraints/IpValidatorTest.php b/Tests/Constraints/IpValidatorTest.php index 82068184d..e37d61bb6 100644 --- a/Tests/Constraints/IpValidatorTest.php +++ b/Tests/Constraints/IpValidatorTest.php @@ -47,9 +47,7 @@ public function testExpectsStringCompatibleType() public function testInvalidValidatorVersion() { $this->expectException(ConstraintDefinitionException::class); - new Ip([ - 'version' => 666, - ]); + new Ip(version: 666); } /** @@ -57,9 +55,7 @@ public function testInvalidValidatorVersion() */ public function testValidIpsV4($ip) { - $this->validator->validate($ip, new Ip([ - 'version' => Ip::V4, - ])); + $this->validator->validate($ip, new Ip(version: Ip::V4)); $this->assertNoViolation(); } @@ -83,10 +79,10 @@ public static function getValidIpsV4() */ public function testValidIpsV4WithWhitespaces($ip) { - $this->validator->validate($ip, new Ip([ - 'version' => Ip::V4, - 'normalizer' => 'trim', - ])); + $this->validator->validate($ip, new Ip( + version: Ip::V4, + normalizer: 'trim', + )); $this->assertNoViolation(); } @@ -118,9 +114,7 @@ public static function getValidIpsV4WithWhitespaces() */ public function testValidIpsV6($ip) { - $this->validator->validate($ip, new Ip([ - 'version' => Ip::V6, - ])); + $this->validator->validate($ip, new Ip(version: Ip::V6)); $this->assertNoViolation(); } @@ -155,9 +149,7 @@ public static function getValidIpsV6() */ public function testValidIpsAll($ip) { - $this->validator->validate($ip, new Ip([ - 'version' => Ip::ALL, - ])); + $this->validator->validate($ip, new Ip(version: Ip::ALL)); $this->assertNoViolation(); } @@ -172,10 +164,10 @@ public static function getValidIpsAll() */ public function testInvalidIpsV4($ip) { - $constraint = new Ip([ - 'version' => Ip::V4, - 'message' => 'myMessage', - ]); + $constraint = new Ip( + version: Ip::V4, + message: 'myMessage', + ); $this->validator->validate($ip, $constraint); @@ -185,6 +177,33 @@ public function testInvalidIpsV4($ip) ->assertRaised(); } + /** + * @dataProvider getValidPublicIpsV4 + */ + public function testInvalidNoPublicIpsV4($ip) + { + $constraint = new Ip( + version: Ip::V4_NO_PUBLIC, + message: 'myMessage', + ); + + $this->validator->validate($ip, $constraint); + + $this->buildViolation('myMessage') + ->setParameter('{{ value }}', '"'.$ip.'"') + ->setCode(Ip::INVALID_IP_ERROR) + ->assertRaised(); + } + + public static function getValidPublicIpsV4() + { + return [ + ['8.0.0.0'], + ['90.0.0.0'], + ['110.0.0.110'], + ]; + } + public static function getInvalidIpsV4() { return [ @@ -201,14 +220,24 @@ public static function getInvalidIpsV4() } /** - * @dataProvider getInvalidPrivateIpsV4 + * @dataProvider getValidPrivateIpsV4 + */ + public function testValidPrivateIpsV4($ip) + { + $this->validator->validate($ip, new Ip(version: Ip::V4_ONLY_PRIVATE)); + + $this->assertNoViolation(); + } + + /** + * @dataProvider getValidPrivateIpsV4 */ public function testInvalidPrivateIpsV4($ip) { - $constraint = new Ip([ - 'version' => Ip::V4_NO_PRIV, - 'message' => 'myMessage', - ]); + $constraint = new Ip( + version: Ip::V4_NO_PRIVATE, + message: 'myMessage', + ); $this->validator->validate($ip, $constraint); @@ -218,7 +247,25 @@ public function testInvalidPrivateIpsV4($ip) ->assertRaised(); } - public static function getInvalidPrivateIpsV4() + /** + * @dataProvider getInvalidPrivateIpsV4 + */ + public function testInvalidOnlyPrivateIpsV4($ip) + { + $constraint = new Ip( + version: Ip::V4_ONLY_PRIVATE, + message: 'myMessage', + ); + + $this->validator->validate($ip, $constraint); + + $this->buildViolation('myMessage') + ->setParameter('{{ value }}', '"'.$ip.'"') + ->setCode(Ip::INVALID_IP_ERROR) + ->assertRaised(); + } + + public static function getValidPrivateIpsV4() { return [ ['10.0.0.0'], @@ -227,15 +274,30 @@ public static function getInvalidPrivateIpsV4() ]; } + public static function getInvalidPrivateIpsV4() + { + return array_merge(self::getValidPublicIpsV4(), self::getValidReservedIpsV4()); + } + /** - * @dataProvider getInvalidReservedIpsV4 + * @dataProvider getValidReservedIpsV4 + */ + public function testValidReservedIpsV4($ip) + { + $this->validator->validate($ip, new Ip(version: Ip::V4_ONLY_RESERVED)); + + $this->assertNoViolation(); + } + + /** + * @dataProvider getValidReservedIpsV4 */ public function testInvalidReservedIpsV4($ip) { - $constraint = new Ip([ - 'version' => Ip::V4_NO_RES, - 'message' => 'myMessage', - ]); + $constraint = new Ip( + version: Ip::V4_NO_RESERVED, + message: 'myMessage', + ); $this->validator->validate($ip, $constraint); @@ -245,7 +307,25 @@ public function testInvalidReservedIpsV4($ip) ->assertRaised(); } - public static function getInvalidReservedIpsV4() + /** + * @dataProvider getInvalidReservedIpsV4 + */ + public function testInvalidOnlyReservedIpsV4($ip) + { + $constraint = new Ip( + version: Ip::V4_ONLY_RESERVED, + message: 'myMessage', + ); + + $this->validator->validate($ip, $constraint); + + $this->buildViolation('myMessage') + ->setParameter('{{ value }}', '"'.$ip.'"') + ->setCode(Ip::INVALID_IP_ERROR) + ->assertRaised(); + } + + public static function getValidReservedIpsV4() { return [ ['0.0.0.0'], @@ -254,15 +334,20 @@ public static function getInvalidReservedIpsV4() ]; } + public static function getInvalidReservedIpsV4() + { + return array_merge(self::getValidPublicIpsV4(), self::getValidPrivateIpsV4()); + } + /** * @dataProvider getInvalidPublicIpsV4 */ public function testInvalidPublicIpsV4($ip) { - $constraint = new Ip([ - 'version' => Ip::V4_ONLY_PUBLIC, - 'message' => 'myMessage', - ]); + $constraint = new Ip( + version: Ip::V4_ONLY_PUBLIC, + message: 'myMessage', + ); $this->validator->validate($ip, $constraint); @@ -274,7 +359,7 @@ public function testInvalidPublicIpsV4($ip) public static function getInvalidPublicIpsV4() { - return array_merge(self::getInvalidPrivateIpsV4(), self::getInvalidReservedIpsV4()); + return array_merge(self::getValidPrivateIpsV4(), self::getValidReservedIpsV4()); } /** @@ -282,10 +367,10 @@ public static function getInvalidPublicIpsV4() */ public function testInvalidIpsV6($ip) { - $constraint = new Ip([ - 'version' => Ip::V6, - 'message' => 'myMessage', - ]); + $constraint = new Ip( + version: Ip::V6, + message: 'myMessage', + ); $this->validator->validate($ip, $constraint); @@ -319,10 +404,10 @@ public static function getInvalidIpsV6() */ public function testInvalidPrivateIpsV6($ip) { - $constraint = new Ip([ - 'version' => Ip::V6_NO_PRIV, - 'message' => 'myMessage', - ]); + $constraint = new Ip( + version: Ip::V6_NO_PRIVATE, + message: 'myMessage', + ); $this->validator->validate($ip, $constraint); @@ -346,10 +431,10 @@ public static function getInvalidPrivateIpsV6() */ public function testInvalidReservedIpsV6($ip) { - $constraint = new Ip([ - 'version' => Ip::V6_NO_RES, - 'message' => 'myMessage', - ]); + $constraint = new Ip( + version: Ip::V6_NO_RESERVED, + message: 'myMessage', + ); $this->validator->validate($ip, $constraint); @@ -372,10 +457,10 @@ public static function getInvalidReservedIpsV6() */ public function testInvalidPublicIpsV6($ip) { - $constraint = new Ip([ - 'version' => Ip::V6_ONLY_PUBLIC, - 'message' => 'myMessage', - ]); + $constraint = new Ip( + version: Ip::V6_ONLY_PUBLIC, + message: 'myMessage', + ); $this->validator->validate($ip, $constraint); @@ -395,10 +480,10 @@ public static function getInvalidPublicIpsV6() */ public function testInvalidIpsAll($ip) { - $constraint = new Ip([ - 'version' => Ip::ALL, - 'message' => 'myMessage', - ]); + $constraint = new Ip( + version: Ip::ALL, + message: 'myMessage', + ); $this->validator->validate($ip, $constraint); @@ -418,10 +503,10 @@ public static function getInvalidIpsAll() */ public function testInvalidPrivateIpsAll($ip) { - $constraint = new Ip([ - 'version' => Ip::ALL_NO_PRIV, - 'message' => 'myMessage', - ]); + $constraint = new Ip( + version: Ip::ALL_NO_PRIVATE, + message: 'myMessage', + ); $this->validator->validate($ip, $constraint); @@ -433,7 +518,7 @@ public function testInvalidPrivateIpsAll($ip) public static function getInvalidPrivateIpsAll() { - return array_merge(self::getInvalidPrivateIpsV4(), self::getInvalidPrivateIpsV6()); + return array_merge(self::getValidPrivateIpsV4(), self::getInvalidPrivateIpsV6()); } /** @@ -441,10 +526,10 @@ public static function getInvalidPrivateIpsAll() */ public function testInvalidReservedIpsAll($ip) { - $constraint = new Ip([ - 'version' => Ip::ALL_NO_RES, - 'message' => 'myMessage', - ]); + $constraint = new Ip( + version: Ip::ALL_NO_RESERVED, + message: 'myMessage', + ); $this->validator->validate($ip, $constraint); @@ -456,7 +541,7 @@ public function testInvalidReservedIpsAll($ip) public static function getInvalidReservedIpsAll() { - return array_merge(self::getInvalidReservedIpsV4(), self::getInvalidReservedIpsV6()); + return array_merge(self::getValidReservedIpsV4(), self::getInvalidReservedIpsV6()); } /** @@ -464,10 +549,10 @@ public static function getInvalidReservedIpsAll() */ public function testInvalidPublicIpsAll($ip) { - $constraint = new Ip([ - 'version' => Ip::ALL_ONLY_PUBLIC, - 'message' => 'myMessage', - ]); + $constraint = new Ip( + version: Ip::ALL_ONLY_PUBLIC, + message: 'myMessage', + ); $this->validator->validate($ip, $constraint); diff --git a/Tests/Constraints/IsFalseValidatorTest.php b/Tests/Constraints/IsFalseValidatorTest.php index e59764764..c6e2ccef6 100644 --- a/Tests/Constraints/IsFalseValidatorTest.php +++ b/Tests/Constraints/IsFalseValidatorTest.php @@ -36,12 +36,9 @@ public function testFalseIsValid() $this->assertNoViolation(); } - /** - * @dataProvider provideInvalidConstraints - */ - public function testTrueIsInvalid(IsFalse $constraint) + public function testTrueIsInvalid() { - $this->validator->validate(true, $constraint); + $this->validator->validate(true, new IsFalse(message: 'myMessage')); $this->buildViolation('myMessage') ->setParameter('{{ value }}', 'true') @@ -49,11 +46,20 @@ public function testTrueIsInvalid(IsFalse $constraint) ->assertRaised(); } - public static function provideInvalidConstraints(): iterable + /** + * @group legacy + */ + public function testTrueIsInvalidDoctrineStyle() { - yield 'Doctrine style' => [new IsFalse([ + $constraint = new IsFalse([ 'message' => 'myMessage', - ])]; - yield 'named parameters' => [new IsFalse(message: 'myMessage')]; + ]); + + $this->validator->validate(true, $constraint); + + $this->buildViolation('myMessage') + ->setParameter('{{ value }}', 'true') + ->setCode(IsFalse::NOT_FALSE_ERROR) + ->assertRaised(); } } diff --git a/Tests/Constraints/IsNullValidatorTest.php b/Tests/Constraints/IsNullValidatorTest.php index f0ff58f34..ed6beffc4 100644 --- a/Tests/Constraints/IsNullValidatorTest.php +++ b/Tests/Constraints/IsNullValidatorTest.php @@ -34,9 +34,7 @@ public function testNullIsValid() */ public function testInvalidValues($value, $valueAsString) { - $constraint = new IsNull([ - 'message' => 'myMessage', - ]); + $constraint = new IsNull(message: 'myMessage'); $this->validator->validate($value, $constraint); diff --git a/Tests/Constraints/IsTrueValidatorTest.php b/Tests/Constraints/IsTrueValidatorTest.php index 1dc47f4b0..4a9eb7702 100644 --- a/Tests/Constraints/IsTrueValidatorTest.php +++ b/Tests/Constraints/IsTrueValidatorTest.php @@ -36,12 +36,9 @@ public function testTrueIsValid() $this->assertNoViolation(); } - /** - * @dataProvider provideInvalidConstraints - */ - public function testFalseIsInvalid(IsTrue $constraint) + public function testFalseIsInvalid() { - $this->validator->validate(false, $constraint); + $this->validator->validate(false, new IsTrue(message: 'myMessage')); $this->buildViolation('myMessage') ->setParameter('{{ value }}', 'false') @@ -49,11 +46,18 @@ public function testFalseIsInvalid(IsTrue $constraint) ->assertRaised(); } - public static function provideInvalidConstraints(): iterable + /** + * @group legacy + */ + public function testFalseIsInvalidDoctrineStyle() { - yield 'Doctrine style' => [new IsTrue([ + $this->validator->validate(false, new IsTrue([ 'message' => 'myMessage', - ])]; - yield 'named parameters' => [new IsTrue(message: 'myMessage')]; + ])); + + $this->buildViolation('myMessage') + ->setParameter('{{ value }}', 'false') + ->setCode(IsTrue::NOT_TRUE_ERROR) + ->assertRaised(); } } diff --git a/Tests/Constraints/IsbnValidatorTest.php b/Tests/Constraints/IsbnValidatorTest.php index 3b257026f..3ae3864d5 100644 --- a/Tests/Constraints/IsbnValidatorTest.php +++ b/Tests/Constraints/IsbnValidatorTest.php @@ -150,9 +150,7 @@ public function testExpectsStringCompatibleType() */ public function testValidIsbn10($isbn) { - $constraint = new Isbn([ - 'type' => 'isbn10', - ]); + $constraint = new Isbn(type: 'isbn10'); $this->validator->validate($isbn, $constraint); @@ -160,6 +158,8 @@ public function testValidIsbn10($isbn) } /** + * @group legacy + * * @dataProvider getInvalidIsbn10 */ public function testInvalidIsbn10($isbn, $code) @@ -195,7 +195,7 @@ public function testInvalidIsbn10Named() */ public function testValidIsbn13($isbn) { - $constraint = new Isbn(['type' => 'isbn13']); + $constraint = new Isbn(type: 'isbn13'); $this->validator->validate($isbn, $constraint); @@ -203,6 +203,8 @@ public function testValidIsbn13($isbn) } /** + * @group legacy + * * @dataProvider getInvalidIsbn13 */ public function testInvalidIsbn13($isbn, $code) @@ -220,16 +222,21 @@ public function testInvalidIsbn13($isbn, $code) ->assertRaised(); } - public function testInvalidIsbn13Named() + /** + * @dataProvider getInvalidIsbn13 + */ + public function testInvalidIsbn13Named($isbn, $code) { - $this->validator->validate( - '2723442284', - new Isbn(type: Isbn::ISBN_13, isbn13Message: 'myMessage') + $constraint = new Isbn( + type: Isbn::ISBN_13, + isbn13Message: 'myMessage', ); + $this->validator->validate($isbn, $constraint); + $this->buildViolation('myMessage') - ->setParameter('{{ value }}', '"2723442284"') - ->setCode(Isbn::TOO_SHORT_ERROR) + ->setParameter('{{ value }}', '"'.$isbn.'"') + ->setCode($code) ->assertRaised(); } @@ -250,9 +257,7 @@ public function testValidIsbnAny($isbn) */ public function testInvalidIsbnAnyIsbn10($isbn, $code) { - $constraint = new Isbn([ - 'bothIsbnMessage' => 'myMessage', - ]); + $constraint = new Isbn(bothIsbnMessage: 'myMessage'); $this->validator->validate($isbn, $constraint); @@ -272,9 +277,7 @@ public function testInvalidIsbnAnyIsbn10($isbn, $code) */ public function testInvalidIsbnAnyIsbn13($isbn, $code) { - $constraint = new Isbn([ - 'bothIsbnMessage' => 'myMessage', - ]); + $constraint = new Isbn(bothIsbnMessage: 'myMessage'); $this->validator->validate($isbn, $constraint); diff --git a/Tests/Constraints/IsinValidatorTest.php b/Tests/Constraints/IsinValidatorTest.php index dca4a423f..b1ac3be20 100644 --- a/Tests/Constraints/IsinValidatorTest.php +++ b/Tests/Constraints/IsinValidatorTest.php @@ -130,9 +130,7 @@ public static function getIsinWithValidFormatButIncorrectChecksum() private function assertViolationRaised($isin, $code) { - $constraint = new Isin([ - 'message' => 'myMessage', - ]); + $constraint = new Isin(message: 'myMessage'); $this->validator->validate($isin, $constraint); diff --git a/Tests/Constraints/IssnValidatorTest.php b/Tests/Constraints/IssnValidatorTest.php index 9eece3eb9..6351ab620 100644 --- a/Tests/Constraints/IssnValidatorTest.php +++ b/Tests/Constraints/IssnValidatorTest.php @@ -119,10 +119,10 @@ public function testExpectsStringCompatibleType() */ public function testCaseSensitiveIssns($issn) { - $constraint = new Issn([ - 'caseSensitive' => true, - 'message' => 'myMessage', - ]); + $constraint = new Issn( + caseSensitive: true, + message: 'myMessage', + ); $this->validator->validate($issn, $constraint); @@ -137,10 +137,10 @@ public function testCaseSensitiveIssns($issn) */ public function testRequireHyphenIssns($issn) { - $constraint = new Issn([ - 'requireHyphen' => true, - 'message' => 'myMessage', - ]); + $constraint = new Issn( + requireHyphen: true, + message: 'myMessage', + ); $this->validator->validate($issn, $constraint); @@ -167,9 +167,7 @@ public function testValidIssn($issn) */ public function testInvalidIssn($issn, $code) { - $constraint = new Issn([ - 'message' => 'myMessage', - ]); + $constraint = new Issn(message: 'myMessage'); $this->validator->validate($issn, $constraint); diff --git a/Tests/Constraints/JsonValidatorTest.php b/Tests/Constraints/JsonValidatorTest.php index 92d8a20a7..123cb95fe 100644 --- a/Tests/Constraints/JsonValidatorTest.php +++ b/Tests/Constraints/JsonValidatorTest.php @@ -37,9 +37,7 @@ public function testJsonIsValid($value) */ public function testInvalidValues($value) { - $constraint = new Json([ - 'message' => 'myMessageTest', - ]); + $constraint = new Json(message: 'myMessageTest'); $this->validator->validate($value, $constraint); diff --git a/Tests/Constraints/LanguageValidatorTest.php b/Tests/Constraints/LanguageValidatorTest.php index 575243080..9522fba75 100644 --- a/Tests/Constraints/LanguageValidatorTest.php +++ b/Tests/Constraints/LanguageValidatorTest.php @@ -83,9 +83,7 @@ public static function getValidLanguages() */ public function testInvalidLanguages($language) { - $constraint = new Language([ - 'message' => 'myMessage', - ]); + $constraint = new Language(message: 'myMessage'); $this->validator->validate($language, $constraint); @@ -108,9 +106,7 @@ public static function getInvalidLanguages() */ public function testValidAlpha3Languages($language) { - $this->validator->validate($language, new Language([ - 'alpha3' => true, - ])); + $this->validator->validate($language, new Language(alpha3: true)); $this->assertNoViolation(); } @@ -129,10 +125,10 @@ public static function getValidAlpha3Languages() */ public function testInvalidAlpha3Languages($language) { - $constraint = new Language([ - 'alpha3' => true, - 'message' => 'myMessage', - ]); + $constraint = new Language( + alpha3: true, + message: 'myMessage', + ); $this->validator->validate($language, $constraint); @@ -172,9 +168,7 @@ public function testValidateUsingCountrySpecificLocale() \Locale::setDefault('fr_FR'); $existingLanguage = 'en'; - $this->validator->validate($existingLanguage, new Language([ - 'message' => 'aMessage', - ])); + $this->validator->validate($existingLanguage, new Language(message: 'aMessage')); $this->assertNoViolation(); } diff --git a/Tests/Constraints/LengthTest.php b/Tests/Constraints/LengthTest.php index 6fc75d883..6e292cb35 100644 --- a/Tests/Constraints/LengthTest.php +++ b/Tests/Constraints/LengthTest.php @@ -24,11 +24,18 @@ class LengthTest extends TestCase { public function testNormalizerCanBeSet() { - $length = new Length(['min' => 0, 'max' => 10, 'normalizer' => 'trim']); + $length = new Length( + min: 0, + max: 10, + normalizer: 'trim', + ); $this->assertEquals('trim', $length->normalizer); } + /** + * @group legacy + */ public function testInvalidNormalizerThrowsException() { $this->expectException(InvalidArgumentException::class); @@ -36,6 +43,9 @@ public function testInvalidNormalizerThrowsException() new Length(['min' => 0, 'max' => 10, 'normalizer' => 'Unknown Callable']); } + /** + * @group legacy + */ public function testInvalidNormalizerObjectThrowsException() { $this->expectException(InvalidArgumentException::class); @@ -45,13 +55,13 @@ public function testInvalidNormalizerObjectThrowsException() public function testDefaultCountUnitIsUsed() { - $length = new Length(['min' => 0, 'max' => 10]); + $length = new Length(min: 0, max: 10); $this->assertSame(Length::COUNT_CODEPOINTS, $length->countUnit); } public function testNonDefaultCountUnitCanBeSet() { - $length = new Length(['min' => 0, 'max' => 10, 'countUnit' => Length::COUNT_GRAPHEMES]); + $length = new Length(min: 0, max: 10, countUnit: Length::COUNT_GRAPHEMES); $this->assertSame(Length::COUNT_GRAPHEMES, $length->countUnit); } @@ -59,7 +69,7 @@ public function testInvalidCountUnitThrowsException() { $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage(\sprintf('The "countUnit" option must be one of the "%s"::COUNT_* constants ("%s" given).', Length::class, 'nonExistentCountUnit')); - new Length(['min' => 0, 'max' => 10, 'countUnit' => 'nonExistentCountUnit']); + new Length(min: 0, max: 10, countUnit: 'nonExistentCountUnit'); } public function testConstraintDefaultOption() @@ -70,9 +80,9 @@ public function testConstraintDefaultOption() self::assertEquals(5, $constraint->max); } - public function testConstraintAnnotationDefaultOption() + public function testConstraintAttributeDefaultOption() { - $constraint = new Length(['value' => 5, 'exactMessage' => 'message']); + $constraint = new Length(exactly: 5, exactMessage: 'message'); self::assertEquals(5, $constraint->min); self::assertEquals(5, $constraint->max); diff --git a/Tests/Constraints/LengthValidatorTest.php b/Tests/Constraints/LengthValidatorTest.php index 000b9d900..10f61f50b 100644 --- a/Tests/Constraints/LengthValidatorTest.php +++ b/Tests/Constraints/LengthValidatorTest.php @@ -25,17 +25,17 @@ protected function createValidator(): LengthValidator public function testNullIsValid() { - $this->validator->validate(null, new Length(['value' => 6])); + $this->validator->validate(null, new Length(exactly: 6)); $this->assertNoViolation(); } public function testEmptyStringIsInvalid() { - $this->validator->validate('', new Length([ - 'value' => $limit = 6, - 'exactMessage' => 'myMessage', - ])); + $this->validator->validate('', new Length( + exactly: $limit = 6, + exactMessage: 'myMessage', + )); $this->buildViolation('myMessage') ->setParameter('{{ value }}', '""') @@ -50,7 +50,7 @@ public function testEmptyStringIsInvalid() public function testExpectsStringCompatibleType() { $this->expectException(UnexpectedValueException::class); - $this->validator->validate(new \stdClass(), new Length(['value' => 5])); + $this->validator->validate(new \stdClass(), new Length(exactly: 5)); } public static function getThreeOrLessCharacters() @@ -116,9 +116,9 @@ public static function getThreeCharactersWithWhitespaces() /** * @dataProvider getFiveOrMoreCharacters */ - public function testValidValuesMin($value) + public function testValidValuesMin(int|string $value) { - $constraint = new Length(['min' => 5]); + $constraint = new Length(min: 5); $this->validator->validate($value, $constraint); $this->assertNoViolation(); @@ -127,9 +127,9 @@ public function testValidValuesMin($value) /** * @dataProvider getThreeOrLessCharacters */ - public function testValidValuesMax($value) + public function testValidValuesMax(int|string $value) { - $constraint = new Length(['max' => 3]); + $constraint = new Length(max: 3); $this->validator->validate($value, $constraint); $this->assertNoViolation(); @@ -138,7 +138,7 @@ public function testValidValuesMax($value) /** * @dataProvider getFourCharacters */ - public function testValidValuesExact($value) + public function testValidValuesExact(int|string $value) { $constraint = new Length(4); $this->validator->validate($value, $constraint); @@ -151,7 +151,7 @@ public function testValidValuesExact($value) */ public function testValidNormalizedValues($value) { - $constraint = new Length(['min' => 3, 'max' => 3, 'normalizer' => 'trim']); + $constraint = new Length(min: 3, max: 3, normalizer: 'trim'); $this->validator->validate($value, $constraint); $this->assertNoViolation(); @@ -184,12 +184,12 @@ public function testValidBytesValues() /** * @dataProvider getThreeOrLessCharacters */ - public function testInvalidValuesMin($value, $valueLength) + public function testInvalidValuesMin(int|string $value, int $valueLength) { - $constraint = new Length([ - 'min' => 4, - 'minMessage' => 'myMessage', - ]); + $constraint = new Length( + min: 4, + minMessage: 'myMessage', + ); $this->validator->validate($value, $constraint); @@ -206,7 +206,7 @@ public function testInvalidValuesMin($value, $valueLength) /** * @dataProvider getThreeOrLessCharacters */ - public function testInvalidValuesMinNamed($value, $valueLength) + public function testInvalidValuesMinNamed(int|string $value, int $valueLength) { $constraint = new Length(min: 4, minMessage: 'myMessage'); @@ -225,12 +225,12 @@ public function testInvalidValuesMinNamed($value, $valueLength) /** * @dataProvider getFiveOrMoreCharacters */ - public function testInvalidValuesMax($value, $valueLength) + public function testInvalidValuesMax(int|string $value, int $valueLength) { - $constraint = new Length([ - 'max' => 4, - 'maxMessage' => 'myMessage', - ]); + $constraint = new Length( + max: 4, + maxMessage: 'myMessage', + ); $this->validator->validate($value, $constraint); @@ -247,7 +247,7 @@ public function testInvalidValuesMax($value, $valueLength) /** * @dataProvider getFiveOrMoreCharacters */ - public function testInvalidValuesMaxNamed($value, $valueLength) + public function testInvalidValuesMaxNamed(int|string $value, int $valueLength) { $constraint = new Length(max: 4, maxMessage: 'myMessage'); @@ -266,13 +266,13 @@ public function testInvalidValuesMaxNamed($value, $valueLength) /** * @dataProvider getThreeOrLessCharacters */ - public function testInvalidValuesExactLessThanFour($value, $valueLength) + public function testInvalidValuesExactLessThanFour(int|string $value, int $valueLength) { - $constraint = new Length([ - 'min' => 4, - 'max' => 4, - 'exactMessage' => 'myMessage', - ]); + $constraint = new Length( + min: 4, + max: 4, + exactMessage: 'myMessage', + ); $this->validator->validate($value, $constraint); @@ -289,7 +289,7 @@ public function testInvalidValuesExactLessThanFour($value, $valueLength) /** * @dataProvider getThreeOrLessCharacters */ - public function testInvalidValuesExactLessThanFourNamed($value, $valueLength) + public function testInvalidValuesExactLessThanFourNamed(int|string $value, int $valueLength) { $constraint = new Length(exactly: 4, exactMessage: 'myMessage'); @@ -308,13 +308,13 @@ public function testInvalidValuesExactLessThanFourNamed($value, $valueLength) /** * @dataProvider getFiveOrMoreCharacters */ - public function testInvalidValuesExactMoreThanFour($value, $valueLength) + public function testInvalidValuesExactMoreThanFour(int|string $value, int $valueLength) { - $constraint = new Length([ - 'min' => 4, - 'max' => 4, - 'exactMessage' => 'myMessage', - ]); + $constraint = new Length( + min: 4, + max: 4, + exactMessage: 'myMessage', + ); $this->validator->validate($value, $constraint); @@ -333,12 +333,12 @@ public function testInvalidValuesExactMoreThanFour($value, $valueLength) */ public function testOneCharset($value, $charset, $isValid) { - $constraint = new Length([ - 'min' => 1, - 'max' => 1, - 'charset' => $charset, - 'charsetMessage' => 'myMessage', - ]); + $constraint = new Length( + min: 1, + max: 1, + charset: $charset, + charsetMessage: 'myMessage', + ); $this->validator->validate($value, $constraint); diff --git a/Tests/Constraints/LessThanOrEqualValidatorTest.php b/Tests/Constraints/LessThanOrEqualValidatorTest.php index ba81bbd55..9a84043ca 100644 --- a/Tests/Constraints/LessThanOrEqualValidatorTest.php +++ b/Tests/Constraints/LessThanOrEqualValidatorTest.php @@ -21,7 +21,11 @@ */ class LessThanOrEqualValidatorTest extends AbstractComparisonValidatorTestCase { + use CompareWithNullValueAtPropertyAtTestTrait; use IcuCompatibilityTrait; + use InvalidComparisonToValueTestTrait; + use ThrowsOnInvalidStringDatesTestTrait; + use ValidComparisonToValueTrait; protected function createValidator(): LessThanOrEqualValidator { @@ -30,7 +34,11 @@ protected function createValidator(): LessThanOrEqualValidator protected static function createConstraint(?array $options = null): Constraint { - return new LessThanOrEqual($options); + if (null !== $options) { + return new LessThanOrEqual(...$options); + } + + return new LessThanOrEqual(); } protected function getErrorCode(): ?string @@ -76,11 +84,4 @@ public static function provideInvalidComparisons(): array ['c', '"c"', 'b', '"b"', 'string'], ]; } - - public static function provideComparisonsToNullValueAtPropertyPath(): array - { - return [ - [5, '5', true], - ]; - } } diff --git a/Tests/Constraints/LessThanOrEqualValidatorWithNegativeOrZeroConstraintTest.php b/Tests/Constraints/LessThanOrEqualValidatorWithNegativeOrZeroConstraintTest.php index 2ec049f4f..685bb58a6 100644 --- a/Tests/Constraints/LessThanOrEqualValidatorWithNegativeOrZeroConstraintTest.php +++ b/Tests/Constraints/LessThanOrEqualValidatorWithNegativeOrZeroConstraintTest.php @@ -12,14 +12,20 @@ namespace Symfony\Component\Validator\Tests\Constraints; use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\Constraints\LessThanOrEqualValidator; use Symfony\Component\Validator\Constraints\NegativeOrZero; use Symfony\Component\Validator\Exception\ConstraintDefinitionException; /** * @author Jan Schädlich */ -class LessThanOrEqualValidatorWithNegativeOrZeroConstraintTest extends LessThanOrEqualValidatorTest +class LessThanOrEqualValidatorWithNegativeOrZeroConstraintTest extends AbstractComparisonValidatorTestCase { + protected function createValidator(): LessThanOrEqualValidator + { + return new LessThanOrEqualValidator(); + } + protected static function createConstraint(?array $options = null): Constraint { return new NegativeOrZero($options); @@ -36,6 +42,14 @@ public static function provideValidComparisons(): array ]; } + public static function provideValidComparisonsToPropertyPath(): array + { + return [ + [4], + [5], + ]; + } + public static function provideInvalidComparisons(): array { return [ @@ -45,6 +59,9 @@ public static function provideInvalidComparisons(): array ]; } + /** + * @group legacy + */ public function testThrowsConstraintExceptionIfPropertyPath() { $this->expectException(ConstraintDefinitionException::class); @@ -53,6 +70,9 @@ public function testThrowsConstraintExceptionIfPropertyPath() return new NegativeOrZero(['propertyPath' => 'field']); } + /** + * @group legacy + */ public function testThrowsConstraintExceptionIfValue() { $this->expectException(ConstraintDefinitionException::class); @@ -66,15 +86,11 @@ public function testThrowsConstraintExceptionIfValue() */ public function testThrowsConstraintExceptionIfNoValueOrPropertyPath($options) { - $this->expectException(ConstraintDefinitionException::class); - $this->expectExceptionMessage('requires either the "value" or "propertyPath" option to be set.'); $this->markTestSkipped('Value option always set for NegativeOrZero constraint'); } public function testThrowsConstraintExceptionIfBothValueAndPropertyPath() { - $this->expectException(ConstraintDefinitionException::class); - $this->expectExceptionMessage('requires only one of the "value" or "propertyPath" options to be set, not both.'); $this->markTestSkipped('Value option is set for NegativeOrZero constraint automatically'); } @@ -88,11 +104,6 @@ public function testInvalidValuePath() $this->markTestSkipped('PropertyPath option is not used in NegativeOrZero constraint'); } - public static function provideAllValidComparisons(): array - { - self::markTestSkipped('The "value" option cannot be used in the NegativeOrZero constraint'); - } - /** * @dataProvider provideValidComparisonsToPropertyPath */ @@ -105,30 +116,4 @@ public function testInvalidComparisonToPropertyPathAddsPathAsParameter() { $this->markTestSkipped('PropertyPath option is not used in NegativeOrZero constraint'); } - - /** - * @dataProvider provideComparisonsToNullValueAtPropertyPath - */ - public function testCompareWithNullValueAtPropertyAt($dirtyValue, $dirtyValueAsString, $isValid) - { - $this->markTestSkipped('PropertyPath option is not used in NegativeOrZero constraint'); - } - - /** - * @dataProvider provideComparisonsToNullValueAtPropertyPath - */ - public function testCompareWithUninitializedPropertyAtPropertyPath($dirtyValue, $dirtyValueAsString, $isValid) - { - $this->markTestSkipped('PropertyPath option is not used in NegativeOrZero constraint'); - } - - public static function throwsOnInvalidStringDatesProvider(): array - { - self::markTestSkipped('The "value" option cannot be used in the NegativeOrZero constraint'); - } - - public static function provideAllInvalidComparisons(): array - { - self::markTestSkipped('The "value" option cannot be used in the NegativeOrZero constraint'); - } } diff --git a/Tests/Constraints/LessThanValidatorTest.php b/Tests/Constraints/LessThanValidatorTest.php index 0c4afb7b5..da7f929cd 100644 --- a/Tests/Constraints/LessThanValidatorTest.php +++ b/Tests/Constraints/LessThanValidatorTest.php @@ -21,7 +21,11 @@ */ class LessThanValidatorTest extends AbstractComparisonValidatorTestCase { + use CompareWithNullValueAtPropertyAtTestTrait; use IcuCompatibilityTrait; + use InvalidComparisonToValueTestTrait; + use ThrowsOnInvalidStringDatesTestTrait; + use ValidComparisonToValueTrait; protected function createValidator(): LessThanValidator { @@ -30,7 +34,11 @@ protected function createValidator(): LessThanValidator protected static function createConstraint(?array $options = null): Constraint { - return new LessThan($options); + if (null !== $options) { + return new LessThan(...$options); + } + + return new LessThan(); } protected function getErrorCode(): ?string @@ -74,11 +82,4 @@ public static function provideInvalidComparisons(): array ['333', '"333"', '22', '"22"', 'string'], ]; } - - public static function provideComparisonsToNullValueAtPropertyPath(): array - { - return [ - [5, '5', true], - ]; - } } diff --git a/Tests/Constraints/LessThanValidatorWithNegativeConstraintTest.php b/Tests/Constraints/LessThanValidatorWithNegativeConstraintTest.php index 982eccd30..5174a951d 100644 --- a/Tests/Constraints/LessThanValidatorWithNegativeConstraintTest.php +++ b/Tests/Constraints/LessThanValidatorWithNegativeConstraintTest.php @@ -12,14 +12,20 @@ namespace Symfony\Component\Validator\Tests\Constraints; use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\Constraints\LessThanValidator; use Symfony\Component\Validator\Constraints\Negative; use Symfony\Component\Validator\Exception\ConstraintDefinitionException; /** * @author Jan Schädlich */ -class LessThanValidatorWithNegativeConstraintTest extends LessThanValidatorTest +class LessThanValidatorWithNegativeConstraintTest extends AbstractComparisonValidatorTestCase { + protected function createValidator(): LessThanValidator + { + return new LessThanValidator(); + } + protected static function createConstraint(?array $options = null): Constraint { return new Negative($options); @@ -35,6 +41,13 @@ public static function provideValidComparisons(): array ]; } + public static function provideValidComparisonsToPropertyPath(): array + { + return [ + [4], + ]; + } + public static function provideInvalidComparisons(): array { return [ @@ -45,6 +58,9 @@ public static function provideInvalidComparisons(): array ]; } + /** + * @group legacy + */ public function testThrowsConstraintExceptionIfPropertyPath() { $this->expectException(ConstraintDefinitionException::class); @@ -53,6 +69,9 @@ public function testThrowsConstraintExceptionIfPropertyPath() return new Negative(['propertyPath' => 'field']); } + /** + * @group legacy + */ public function testThrowsConstraintExceptionIfValue() { $this->expectException(ConstraintDefinitionException::class); @@ -66,15 +85,11 @@ public function testThrowsConstraintExceptionIfValue() */ public function testThrowsConstraintExceptionIfNoValueOrPropertyPath($options) { - $this->expectException(ConstraintDefinitionException::class); - $this->expectExceptionMessage('requires either the "value" or "propertyPath" option to be set.'); $this->markTestSkipped('Value option always set for Negative constraint'); } public function testThrowsConstraintExceptionIfBothValueAndPropertyPath() { - $this->expectException(ConstraintDefinitionException::class); - $this->expectExceptionMessage('requires only one of the "value" or "propertyPath" options to be set, not both.'); $this->markTestSkipped('Value option is set for Negative constraint automatically'); } @@ -88,11 +103,6 @@ public function testInvalidValuePath() $this->markTestSkipped('PropertyPath option is not used in Negative constraint'); } - public static function provideAllValidComparisons(): array - { - self::markTestSkipped('The "value" option cannot be used in the Negative constraint'); - } - /** * @dataProvider provideValidComparisonsToPropertyPath */ @@ -101,34 +111,8 @@ public function testValidComparisonToPropertyPath($comparedValue) $this->markTestSkipped('PropertyPath option is not used in Negative constraint'); } - public static function throwsOnInvalidStringDatesProvider(): array - { - self::markTestSkipped('The "value" option cannot be used in the Negative constraint'); - } - - /** - * @dataProvider provideComparisonsToNullValueAtPropertyPath - */ - public function testCompareWithNullValueAtPropertyAt($dirtyValue, $dirtyValueAsString, $isValid) - { - $this->markTestSkipped('PropertyPath option is not used in Negative constraint'); - } - - /** - * @dataProvider provideComparisonsToNullValueAtPropertyPath - */ - public function testCompareWithUninitializedPropertyAtPropertyPath($dirtyValue, $dirtyValueAsString, $isValid) - { - $this->markTestSkipped('PropertyPath option is not used in Negative constraint'); - } - public function testInvalidComparisonToPropertyPathAddsPathAsParameter() { $this->markTestSkipped('PropertyPath option is not used in Negative constraint'); } - - public static function provideAllInvalidComparisons(): array - { - self::markTestSkipped('The "value" option cannot be used in the Negative constraint'); - } } diff --git a/Tests/Constraints/LocaleValidatorTest.php b/Tests/Constraints/LocaleValidatorTest.php index ac166b33d..3b3819512 100644 --- a/Tests/Constraints/LocaleValidatorTest.php +++ b/Tests/Constraints/LocaleValidatorTest.php @@ -71,9 +71,7 @@ public static function getValidLocales() */ public function testInvalidLocales($locale) { - $constraint = new Locale([ - 'message' => 'myMessage', - ]); + $constraint = new Locale(message: 'myMessage'); $this->validator->validate($locale, $constraint); @@ -93,9 +91,7 @@ public static function getInvalidLocales() public function testTooLongLocale() { - $constraint = new Locale([ - 'message' => 'myMessage', - ]); + $constraint = new Locale(message: 'myMessage'); $locale = str_repeat('a', (\defined('INTL_MAX_LOCALE_LEN') ? \INTL_MAX_LOCALE_LEN : 85) + 1); $this->validator->validate($locale, $constraint); @@ -111,9 +107,7 @@ public function testTooLongLocale() */ public function testValidLocalesWithCanonicalization(string $locale) { - $constraint = new Locale([ - 'message' => 'myMessage', - ]); + $constraint = new Locale(message: 'myMessage'); $this->validator->validate($locale, $constraint); @@ -125,10 +119,10 @@ public function testValidLocalesWithCanonicalization(string $locale) */ public function testValidLocalesWithoutCanonicalization(string $locale) { - $constraint = new Locale([ - 'message' => 'myMessage', - 'canonicalize' => false, - ]); + $constraint = new Locale( + message: 'myMessage', + canonicalize: false, + ); $this->validator->validate($locale, $constraint); @@ -140,10 +134,10 @@ public function testValidLocalesWithoutCanonicalization(string $locale) */ public function testInvalidLocalesWithoutCanonicalization(string $locale) { - $constraint = new Locale([ - 'message' => 'myMessage', - 'canonicalize' => false, - ]); + $constraint = new Locale( + message: 'myMessage', + canonicalize: false, + ); $this->validator->validate($locale, $constraint); @@ -166,7 +160,7 @@ public function testInvalidLocaleWithoutCanonicalizationNamed() ->assertRaised(); } - public static function getUncanonicalizedLocales(): iterable + public static function getUncanonicalizedLocales(): array { return [ ['en-US'], diff --git a/Tests/Constraints/LuhnValidatorTest.php b/Tests/Constraints/LuhnValidatorTest.php index b0571ebd0..9eb33bde6 100644 --- a/Tests/Constraints/LuhnValidatorTest.php +++ b/Tests/Constraints/LuhnValidatorTest.php @@ -76,9 +76,7 @@ public static function getValidNumbers() */ public function testInvalidNumbers($number, $code) { - $constraint = new Luhn([ - 'message' => 'myMessage', - ]); + $constraint = new Luhn(message: 'myMessage'); $this->validator->validate($number, $constraint); diff --git a/Tests/Constraints/MacAddressTest.php b/Tests/Constraints/MacAddressTest.php new file mode 100644 index 000000000..94cbfedd4 --- /dev/null +++ b/Tests/Constraints/MacAddressTest.php @@ -0,0 +1,63 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Tests\Constraints; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Validator\Constraints\MacAddress; +use Symfony\Component\Validator\Mapping\ClassMetadata; +use Symfony\Component\Validator\Mapping\Loader\AttributeLoader; + +/** + * @author Ninos Ego + */ +class MacAddressTest extends TestCase +{ + public function testNormalizerCanBeSet() + { + $mac = new MacAddress(normalizer: 'trim'); + + $this->assertEquals(trim(...), $mac->normalizer); + } + + public function testAttributes() + { + $metadata = new ClassMetadata(MacAddressDummy::class); + $loader = new AttributeLoader(); + self::assertTrue($loader->loadClassMetadata($metadata)); + + [$aConstraint] = $metadata->properties['a']->getConstraints(); + self::assertSame('myMessage', $aConstraint->message); + self::assertEquals(trim(...), $aConstraint->normalizer); + self::assertSame(MacAddress::ALL, $aConstraint->type); + self::assertSame(['Default', 'MacAddressDummy'], $aConstraint->groups); + + [$bConstraint] = $metadata->properties['b']->getConstraints(); + self::assertSame(MacAddress::LOCAL_UNICAST, $bConstraint->type); + self::assertSame(['Default', 'MacAddressDummy'], $bConstraint->groups); + + [$cConstraint] = $metadata->properties['c']->getConstraints(); + self::assertSame(['my_group'], $cConstraint->groups); + self::assertSame('some attached data', $cConstraint->payload); + } +} + +class MacAddressDummy +{ + #[MacAddress(message: 'myMessage', normalizer: 'trim')] + private $a; + + #[MacAddress(type: MacAddress::LOCAL_UNICAST)] + private $b; + + #[MacAddress(groups: ['my_group'], payload: 'some attached data')] + private $c; +} diff --git a/Tests/Constraints/MacAddressValidatorTest.php b/Tests/Constraints/MacAddressValidatorTest.php new file mode 100644 index 000000000..5abb7487b --- /dev/null +++ b/Tests/Constraints/MacAddressValidatorTest.php @@ -0,0 +1,565 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Tests\Constraints; + +use Symfony\Component\Validator\Constraints\MacAddress; +use Symfony\Component\Validator\Constraints\MacAddressValidator; +use Symfony\Component\Validator\Exception\ConstraintDefinitionException; +use Symfony\Component\Validator\Exception\UnexpectedValueException; +use Symfony\Component\Validator\Test\ConstraintValidatorTestCase; + +/** + * @author Ninos Ego + */ +class MacAddressValidatorTest extends ConstraintValidatorTestCase +{ + protected function createValidator(): MacAddressValidator + { + return new MacAddressValidator(); + } + + public function testNullIsValid() + { + $this->validator->validate(null, new MacAddress()); + + $this->assertNoViolation(); + } + + public function testEmptyStringIsValid() + { + $this->validator->validate('', new MacAddress()); + + $this->assertNoViolation(); + } + + public function testExpectsStringCompatibleType() + { + $this->expectException(UnexpectedValueException::class); + $this->validator->validate(new \stdClass(), new MacAddress()); + } + + public function testInvalidValidatorType() + { + $this->expectException(ConstraintDefinitionException::class); + new MacAddress(type: 666); + } + + /** + * @dataProvider getValidMacs + */ + public function testValidMac($mac) + { + $this->validator->validate($mac, new MacAddress()); + + $this->assertNoViolation(); + } + + /** + * @dataProvider getNotValidMacs + */ + public function testNotValidMac($mac) + { + $this->validator->validate($mac, new MacAddress()); + + $this->buildViolation('This value is not a valid MAC address.') + ->setParameter('{{ value }}', '"'.$mac.'"') + ->setCode(MacAddress::INVALID_MAC_ERROR) + ->assertRaised(); + } + + public static function getValidMacs(): array + { + return [ + ['00:00:00:00:00:00'], + ['00-00-00-00-00-00'], + ['ff:ff:ff:ff:ff:ff'], + ['ff-ff-ff-ff-ff-ff'], + ['FF:FF:FF:FF:FF:FF'], + ['FF-FF-FF-FF-FF-FF'], + ['FFFF.FFFF.FFFF'], + ]; + } + + public static function getNotValidMacs(): array + { + return [ + ['00:00:00:00:00'], + ['00:00:00:00:00:0G'], + ['GG:GG:GG:GG:GG:GG'], + ['GG-GG-GG-GG-GG-GG'], + ['GGGG.GGGG.GGGG'], + ]; + } + + public static function getValidLocalUnicastMacs(): array + { + return [ + ['02:00:00:00:00:00'], + ['16-00-00-00-00-00'], + ['2a-00-00-00-00-00'], + ['3e-00-00-00-00-00'], + ['3E00.0000.0000'], + ]; + } + + public static function getValidLocalMulticastMacs(): array + { + return [ + ['03:00:00:00:00:00'], + ['17-00-00-00-00-00'], + ['2b-00-00-00-00-00'], + ['3f-00-00-00-00-00'], + ['3F00.0000.0000'], + ]; + } + + public static function getValidUniversalUnicastMacs(): array + { + return [ + ['00:00:00:00:00:00'], + ['14-00-00-00-00-00'], + ['28-00-00-00-00-00'], + ['3c-00-00-00-00-00'], + ['3C00.0000.0000'], + ]; + } + + public static function getValidUniversalMulticastMacs(): array + { + return [ + ['01:00:00:00:00:00'], + ['15-00-00-00-00-00'], + ['29-00-00-00-00-00'], + ['3d-00-00-00-00-00'], + ['3D00.0000.0000'], + ]; + } + + public static function getValidBroadcastMacs(): array + { + return [ + ['ff:ff:ff:ff:ff:ff'], + ['FF-ff-FF-ff-FF-ff'], + ['fFff.ffff.fffF'], + ]; + } + + /** + * @dataProvider getValidLocalUnicastMacs + * @dataProvider getValidLocalMulticastMacs + * @dataProvider getValidUniversalUnicastMacs + * @dataProvider getValidUniversalMulticastMacs + */ + public function testValidAllNoBroadcastMacs($mac) + { + $this->validator->validate($mac, new MacAddress(type: MacAddress::ALL_NO_BROADCAST)); + + $this->assertNoViolation(); + } + + /** + * @dataProvider getValidBroadcastMacs + */ + public function testInvalidAllNoBroadcastMacs($mac) + { + $constraint = new MacAddress('myMessage', type: MacAddress::ALL_NO_BROADCAST); + + $this->validator->validate($mac, $constraint); + + $this->buildViolation('myMessage') + ->setParameter('{{ value }}', '"'.$mac.'"') + ->setCode(MacAddress::INVALID_MAC_ERROR) + ->assertRaised(); + } + + /** + * @dataProvider getValidLocalUnicastMacs + * @dataProvider getValidLocalMulticastMacs + * @dataProvider getValidBroadcastMacs + */ + public function testValidLocalMacs($mac) + { + $this->validator->validate($mac, new MacAddress(type: MacAddress::LOCAL_ALL)); + + $this->assertNoViolation(); + } + + /** + * @dataProvider getValidUniversalUnicastMacs + * @dataProvider getValidUniversalMulticastMacs + */ + public function testInvalidLocalMacs($mac) + { + $constraint = new MacAddress('myMessage', type: MacAddress::LOCAL_ALL); + + $this->validator->validate($mac, $constraint); + + $this->buildViolation('myMessage') + ->setParameter('{{ value }}', '"'.$mac.'"') + ->setCode(MacAddress::INVALID_MAC_ERROR) + ->assertRaised(); + } + + /** + * @dataProvider getValidLocalUnicastMacs + * @dataProvider getValidLocalMulticastMacs + */ + public function testValidLocalNoBroadcastMacs($mac) + { + $this->validator->validate($mac, new MacAddress(type: MacAddress::LOCAL_NO_BROADCAST)); + + $this->assertNoViolation(); + } + + /** + * @dataProvider getValidUniversalUnicastMacs + * @dataProvider getValidUniversalMulticastMacs + * @dataProvider getValidBroadcastMacs + */ + public function testInvalidLocalNoBroadcastMacs($mac) + { + $constraint = new MacAddress('myMessage', type: MacAddress::LOCAL_NO_BROADCAST); + + $this->validator->validate($mac, $constraint); + + $this->buildViolation('myMessage') + ->setParameter('{{ value }}', '"'.$mac.'"') + ->setCode(MacAddress::INVALID_MAC_ERROR) + ->assertRaised(); + } + + /** + * @dataProvider getValidLocalUnicastMacs + */ + public function testValidLocalUnicastMacs($mac) + { + $this->validator->validate($mac, new MacAddress(type: MacAddress::LOCAL_UNICAST)); + + $this->assertNoViolation(); + } + + /** + * @dataProvider getValidLocalMulticastMacs + * @dataProvider getValidUniversalUnicastMacs + * @dataProvider getValidUniversalMulticastMacs + */ + public function testInvalidLocalUnicastMacs($mac) + { + $constraint = new MacAddress('myMessage', type: MacAddress::LOCAL_UNICAST); + + $this->validator->validate($mac, $constraint); + + $this->buildViolation('myMessage') + ->setParameter('{{ value }}', '"'.$mac.'"') + ->setCode(MacAddress::INVALID_MAC_ERROR) + ->assertRaised(); + } + + /** + * @dataProvider getValidLocalMulticastMacs + * @dataProvider getValidBroadcastMacs + */ + public function testValidLocalMulticastMacs($mac) + { + $this->validator->validate($mac, new MacAddress(type: MacAddress::LOCAL_MULTICAST)); + + $this->assertNoViolation(); + } + + /** + * @dataProvider getValidLocalUnicastMacs + * @dataProvider getValidUniversalUnicastMacs + * @dataProvider getValidUniversalMulticastMacs + */ + public function testInvalidLocalMulticastMacs($mac) + { + $constraint = new MacAddress('myMessage', type: MacAddress::LOCAL_MULTICAST); + + $this->validator->validate($mac, $constraint); + + $this->buildViolation('myMessage') + ->setParameter('{{ value }}', '"'.$mac.'"') + ->setCode(MacAddress::INVALID_MAC_ERROR) + ->assertRaised(); + } + + /** + * @dataProvider getValidLocalMulticastMacs + */ + public function testValidLocalMulticastNoBroadcastMacs($mac) + { + $this->validator->validate($mac, new MacAddress(type: MacAddress::LOCAL_MULTICAST_NO_BROADCAST)); + + $this->assertNoViolation(); + } + + /** + * @dataProvider getValidLocalUnicastMacs + * @dataProvider getValidUniversalUnicastMacs + * @dataProvider getValidUniversalMulticastMacs + * @dataProvider getValidBroadcastMacs + */ + public function testInvalidLocalMulticastNoBroadcastMacs($mac) + { + $constraint = new MacAddress('myMessage', type: MacAddress::LOCAL_MULTICAST_NO_BROADCAST); + + $this->validator->validate($mac, $constraint); + + $this->buildViolation('myMessage') + ->setParameter('{{ value }}', '"'.$mac.'"') + ->setCode(MacAddress::INVALID_MAC_ERROR) + ->assertRaised(); + } + + /** + * @dataProvider getValidUniversalUnicastMacs + * @dataProvider getValidUniversalMulticastMacs + */ + public function testValidUniversalMacs($mac) + { + $this->validator->validate($mac, new MacAddress(type: MacAddress::UNIVERSAL_ALL)); + + $this->assertNoViolation(); + } + + /** + * @dataProvider getValidLocalUnicastMacs + * @dataProvider getValidLocalMulticastMacs + */ + public function testInvalidUniversalMacs($mac) + { + $constraint = new MacAddress('myMessage', type: MacAddress::UNIVERSAL_ALL); + + $this->validator->validate($mac, $constraint); + + $this->buildViolation('myMessage') + ->setParameter('{{ value }}', '"'.$mac.'"') + ->setCode(MacAddress::INVALID_MAC_ERROR) + ->assertRaised(); + } + + /** + * @dataProvider getValidUniversalUnicastMacs + */ + public function testValidUniversalUnicastMacs($mac) + { + $this->validator->validate($mac, new MacAddress(type: MacAddress::UNIVERSAL_UNICAST)); + + $this->assertNoViolation(); + } + + /** + * @dataProvider getValidLocalUnicastMacs + * @dataProvider getValidLocalMulticastMacs + * @dataProvider getValidUniversalMulticastMacs + */ + public function testInvalidUniversalUnicastMacs($mac) + { + $constraint = new MacAddress('myMessage', type: MacAddress::UNIVERSAL_UNICAST); + + $this->validator->validate($mac, $constraint); + + $this->buildViolation('myMessage') + ->setParameter('{{ value }}', '"'.$mac.'"') + ->setCode(MacAddress::INVALID_MAC_ERROR) + ->assertRaised(); + } + + /** + * @dataProvider getValidUniversalMulticastMacs + */ + public function testValidUniversalMulticastMacs($mac) + { + $this->validator->validate($mac, new MacAddress(type: MacAddress::UNIVERSAL_MULTICAST)); + + $this->assertNoViolation(); + } + + /** + * @dataProvider getValidLocalUnicastMacs + * @dataProvider getValidLocalMulticastMacs + * @dataProvider getValidUniversalUnicastMacs + */ + public function testInvalidUniversalMulticastMacs($mac) + { + $constraint = new MacAddress('myMessage', type: MacAddress::UNIVERSAL_MULTICAST); + + $this->validator->validate($mac, $constraint); + + $this->buildViolation('myMessage') + ->setParameter('{{ value }}', '"'.$mac.'"') + ->setCode(MacAddress::INVALID_MAC_ERROR) + ->assertRaised(); + } + + /** + * @dataProvider getValidLocalUnicastMacs + * @dataProvider getValidUniversalUnicastMacs + */ + public function testUnicastMacs($mac) + { + $this->validator->validate($mac, new MacAddress(type: MacAddress::UNICAST_ALL)); + + $this->assertNoViolation(); + } + + /** + * @dataProvider getValidLocalMulticastMacs + * @dataProvider getValidUniversalMulticastMacs + */ + public function testInvalidUnicastMacs($mac) + { + $constraint = new MacAddress('myMessage', type: MacAddress::UNICAST_ALL); + + $this->validator->validate($mac, $constraint); + + $this->buildViolation('myMessage') + ->setParameter('{{ value }}', '"'.$mac.'"') + ->setCode(MacAddress::INVALID_MAC_ERROR) + ->assertRaised(); + } + + /** + * @dataProvider getValidLocalMulticastMacs + * @dataProvider getValidUniversalMulticastMacs + * @dataProvider getValidBroadcastMacs + */ + public function testMulticastMacs($mac) + { + $this->validator->validate($mac, new MacAddress(type: MacAddress::MULTICAST_ALL)); + + $this->assertNoViolation(); + } + + /** + * @dataProvider getValidLocalUnicastMacs + * @dataProvider getValidUniversalUnicastMacs + */ + public function testInvalidMulticastMacs($mac) + { + $constraint = new MacAddress('myMessage', type: MacAddress::MULTICAST_ALL); + + $this->validator->validate($mac, $constraint); + + $this->buildViolation('myMessage') + ->setParameter('{{ value }}', '"'.$mac.'"') + ->setCode(MacAddress::INVALID_MAC_ERROR) + ->assertRaised(); + } + + /** + * @dataProvider getValidLocalMulticastMacs + * @dataProvider getValidUniversalMulticastMacs + */ + public function testMulticastNoBroadcastMacs($mac) + { + $this->validator->validate($mac, new MacAddress(type: MacAddress::MULTICAST_NO_BROADCAST)); + + $this->assertNoViolation(); + } + + /** + * @dataProvider getValidLocalUnicastMacs + * @dataProvider getValidUniversalUnicastMacs + * @dataProvider getValidBroadcastMacs + */ + public function testInvalidMulticastNoBroadcastMacs($mac) + { + $constraint = new MacAddress('myMessage', type: MacAddress::MULTICAST_NO_BROADCAST); + + $this->validator->validate($mac, $constraint); + + $this->buildViolation('myMessage') + ->setParameter('{{ value }}', '"'.$mac.'"') + ->setCode(MacAddress::INVALID_MAC_ERROR) + ->assertRaised(); + } + + /** + * @dataProvider getValidBroadcastMacs + */ + public function testBroadcastMacs($mac) + { + $this->validator->validate($mac, new MacAddress(type: MacAddress::BROADCAST)); + + $this->assertNoViolation(); + } + + /** + * @dataProvider getValidLocalUnicastMacs + * @dataProvider getValidLocalMulticastMacs + * @dataProvider getValidUniversalUnicastMacs + * @dataProvider getValidUniversalMulticastMacs + */ + public function testInvalidBroadcastMacs($mac) + { + $constraint = new MacAddress('myMessage', type: MacAddress::BROADCAST); + + $this->validator->validate($mac, $constraint); + + $this->buildViolation('myMessage') + ->setParameter('{{ value }}', '"'.$mac.'"') + ->setCode(MacAddress::INVALID_MAC_ERROR) + ->assertRaised(); + } + + /** + * @dataProvider getValidMacsWithWhitespaces + */ + public function testValidMacsWithWhitespaces($mac) + { + $this->validator->validate($mac, new MacAddress(normalizer: 'trim')); + + $this->assertNoViolation(); + } + + public static function getValidMacsWithWhitespaces(): array + { + return [ + ["\x2000:00:00:00:00:00"], + ["\x09\x0900-00-00-00-00-00"], + ["ff:ff:ff:ff:ff:ff\x0A"], + ["ff-ff-ff-ff-ff-ff\x0D\x0D"], + ["\x00FF:FF:FF:FF:FF:FF\x00"], + ["\x0B\x0BFF-FF-FF-FF-FF-FF\x0B\x0B"], + ]; + } + + /** + * @dataProvider getInvalidMacs + */ + public function testInvalidMacs($mac) + { + $constraint = new MacAddress('myMessage'); + + $this->validator->validate($mac, $constraint); + + $this->buildViolation('myMessage') + ->setParameter('{{ value }}', '"'.$mac.'"') + ->setCode(MacAddress::INVALID_MAC_ERROR) + ->assertRaised(); + } + + public static function getInvalidMacs(): array + { + return [ + ['0'], + ['00:00'], + ['00:00:00'], + ['00:00:00:00'], + ['00:00:00:00:00'], + ['00:00:00:00:00:000'], + ['-00:00:00:00:00:00'], + ['foobar'], + ]; + } +} diff --git a/Tests/Constraints/NoSuspiciousCharactersValidatorTest.php b/Tests/Constraints/NoSuspiciousCharactersValidatorTest.php index d15e41660..c38a431f5 100644 --- a/Tests/Constraints/NoSuspiciousCharactersValidatorTest.php +++ b/Tests/Constraints/NoSuspiciousCharactersValidatorTest.php @@ -32,7 +32,7 @@ protected function createValidator(): NoSuspiciousCharactersValidator */ public function testNonSuspiciousStrings(string $string, array $options = []) { - $this->validator->validate($string, new NoSuspiciousCharacters($options)); + $this->validator->validate($string, new NoSuspiciousCharacters(...$options)); $this->assertNoViolation(); } @@ -58,7 +58,7 @@ public static function provideNonSuspiciousStrings(): iterable */ public function testSuspiciousStrings(string $string, array $options, array $errors) { - $this->validator->validate($string, new NoSuspiciousCharacters($options)); + $this->validator->validate($string, new NoSuspiciousCharacters(...$options)); $violations = null; diff --git a/Tests/Constraints/NotBlankTest.php b/Tests/Constraints/NotBlankTest.php index 77435a37a..d04a65f1c 100644 --- a/Tests/Constraints/NotBlankTest.php +++ b/Tests/Constraints/NotBlankTest.php @@ -24,7 +24,7 @@ class NotBlankTest extends TestCase { public function testNormalizerCanBeSet() { - $notBlank = new NotBlank(['normalizer' => 'trim']); + $notBlank = new NotBlank(normalizer: 'trim'); $this->assertEquals('trim', $notBlank->normalizer); } @@ -45,6 +45,9 @@ public function testAttributes() self::assertSame('myMessage', $bConstraint->message); } + /** + * @group legacy + */ public function testInvalidNormalizerThrowsException() { $this->expectException(InvalidArgumentException::class); @@ -52,6 +55,9 @@ public function testInvalidNormalizerThrowsException() new NotBlank(['normalizer' => 'Unknown Callable']); } + /** + * @group legacy + */ public function testInvalidNormalizerObjectThrowsException() { $this->expectException(InvalidArgumentException::class); diff --git a/Tests/Constraints/NotBlankValidatorTest.php b/Tests/Constraints/NotBlankValidatorTest.php index 8d1ba3d0f..42d5f3a60 100644 --- a/Tests/Constraints/NotBlankValidatorTest.php +++ b/Tests/Constraints/NotBlankValidatorTest.php @@ -45,9 +45,7 @@ public static function getValidValues() public function testNullIsInvalid() { - $constraint = new NotBlank([ - 'message' => 'myMessage', - ]); + $constraint = new NotBlank(message: 'myMessage'); $this->validator->validate(null, $constraint); @@ -59,9 +57,7 @@ public function testNullIsInvalid() public function testBlankIsInvalid() { - $constraint = new NotBlank([ - 'message' => 'myMessage', - ]); + $constraint = new NotBlank(message: 'myMessage'); $this->validator->validate('', $constraint); @@ -73,9 +69,7 @@ public function testBlankIsInvalid() public function testFalseIsInvalid() { - $constraint = new NotBlank([ - 'message' => 'myMessage', - ]); + $constraint = new NotBlank(message: 'myMessage'); $this->validator->validate(false, $constraint); @@ -87,9 +81,7 @@ public function testFalseIsInvalid() public function testEmptyArrayIsInvalid() { - $constraint = new NotBlank([ - 'message' => 'myMessage', - ]); + $constraint = new NotBlank(message: 'myMessage'); $this->validator->validate([], $constraint); @@ -101,10 +93,10 @@ public function testEmptyArrayIsInvalid() public function testAllowNullTrue() { - $constraint = new NotBlank([ - 'message' => 'myMessage', - 'allowNull' => true, - ]); + $constraint = new NotBlank( + message: 'myMessage', + allowNull: true, + ); $this->validator->validate(null, $constraint); $this->assertNoViolation(); @@ -112,10 +104,10 @@ public function testAllowNullTrue() public function testAllowNullFalse() { - $constraint = new NotBlank([ - 'message' => 'myMessage', - 'allowNull' => false, - ]); + $constraint = new NotBlank( + message: 'myMessage', + allowNull: false, + ); $this->validator->validate(null, $constraint); @@ -130,10 +122,10 @@ public function testAllowNullFalse() */ public function testNormalizedStringIsInvalid($value) { - $constraint = new NotBlank([ - 'message' => 'myMessage', - 'normalizer' => 'trim', - ]); + $constraint = new NotBlank( + message: 'myMessage', + normalizer: 'trim', + ); $this->validator->validate($value, $constraint); diff --git a/Tests/Constraints/NotCompromisedPasswordValidatorTest.php b/Tests/Constraints/NotCompromisedPasswordValidatorTest.php index 253529444..11c325d53 100644 --- a/Tests/Constraints/NotCompromisedPasswordValidatorTest.php +++ b/Tests/Constraints/NotCompromisedPasswordValidatorTest.php @@ -87,7 +87,7 @@ public function testInvalidPassword() public function testThresholdReached() { - $constraint = new NotCompromisedPassword(['threshold' => 3]); + $constraint = new NotCompromisedPassword(threshold: 3); $this->validator->validate(self::PASSWORD_LEAKED, $constraint); $this->buildViolation($constraint->message) @@ -95,20 +95,21 @@ public function testThresholdReached() ->assertRaised(); } - /** - * @dataProvider provideConstraintsWithThreshold - */ - public function testThresholdNotReached(NotCompromisedPassword $constraint) + public function testThresholdNotReached() { - $this->validator->validate(self::PASSWORD_LEAKED, $constraint); + $this->validator->validate(self::PASSWORD_LEAKED, new NotCompromisedPassword(threshold: 10)); $this->assertNoViolation(); } - public static function provideConstraintsWithThreshold(): iterable + /** + * @group legacy + */ + public function testThresholdNotReachedDoctrineStyle() { - yield 'Doctrine style' => [new NotCompromisedPassword(['threshold' => 10])]; - yield 'named arguments' => [new NotCompromisedPassword(threshold: 10)]; + $this->validator->validate(self::PASSWORD_LEAKED, new NotCompromisedPassword(['threshold' => 10])); + + $this->assertNoViolation(); } public function testValidPassword() @@ -208,19 +209,21 @@ public function testApiError() $this->validator->validate(self::PASSWORD_TRIGGERING_AN_ERROR, new NotCompromisedPassword()); } - /** - * @dataProvider provideErrorSkippingConstraints - */ - public function testApiErrorSkipped(NotCompromisedPassword $constraint) + public function testApiErrorSkipped() { - $this->validator->validate(self::PASSWORD_TRIGGERING_AN_ERROR, $constraint); - $this->assertTrue(true); // No exception have been thrown + $this->expectNotToPerformAssertions(); + + $this->validator->validate(self::PASSWORD_TRIGGERING_AN_ERROR, new NotCompromisedPassword(skipOnError: true)); } - public static function provideErrorSkippingConstraints(): iterable + /** + * @group legacy + */ + public function testApiErrorSkippedDoctrineStyle() { - yield 'Doctrine style' => [new NotCompromisedPassword(['skipOnError' => true])]; - yield 'named arguments' => [new NotCompromisedPassword(skipOnError: true)]; + $this->expectNotToPerformAssertions(); + + $this->validator->validate(self::PASSWORD_TRIGGERING_AN_ERROR, new NotCompromisedPassword(['skipOnError' => true])); } private function createHttpClientStub(?string $returnValue = null): HttpClientInterface diff --git a/Tests/Constraints/NotEqualToValidatorTest.php b/Tests/Constraints/NotEqualToValidatorTest.php index fc5275e3c..2f6948db9 100644 --- a/Tests/Constraints/NotEqualToValidatorTest.php +++ b/Tests/Constraints/NotEqualToValidatorTest.php @@ -21,7 +21,11 @@ */ class NotEqualToValidatorTest extends AbstractComparisonValidatorTestCase { + use CompareWithNullValueAtPropertyAtTestTrait; use IcuCompatibilityTrait; + use InvalidComparisonToValueTestTrait; + use ThrowsOnInvalidStringDatesTestTrait; + use ValidComparisonToValueTrait; protected function createValidator(): NotEqualToValidator { @@ -30,7 +34,11 @@ protected function createValidator(): NotEqualToValidator protected static function createConstraint(?array $options = null): Constraint { - return new NotEqualTo($options); + if (null !== $options) { + return new NotEqualTo(...$options); + } + + return new NotEqualTo(); } protected function getErrorCode(): ?string @@ -70,11 +78,4 @@ public static function provideInvalidComparisons(): array [new ComparisonTest_Class(5), '5', new ComparisonTest_Class(5), '5', __NAMESPACE__.'\ComparisonTest_Class'], ]; } - - public static function provideComparisonsToNullValueAtPropertyPath(): array - { - return [ - [5, '5', true], - ]; - } } diff --git a/Tests/Constraints/NotIdenticalToValidatorTest.php b/Tests/Constraints/NotIdenticalToValidatorTest.php index 0376403e2..9831d26cb 100644 --- a/Tests/Constraints/NotIdenticalToValidatorTest.php +++ b/Tests/Constraints/NotIdenticalToValidatorTest.php @@ -21,7 +21,11 @@ */ class NotIdenticalToValidatorTest extends AbstractComparisonValidatorTestCase { + use CompareWithNullValueAtPropertyAtTestTrait; use IcuCompatibilityTrait; + use InvalidComparisonToValueTestTrait; + use ThrowsOnInvalidStringDatesTestTrait; + use ValidComparisonToValueTrait; protected function createValidator(): NotIdenticalToValidator { @@ -30,7 +34,11 @@ protected function createValidator(): NotIdenticalToValidator protected static function createConstraint(?array $options = null): Constraint { - return new NotIdenticalTo($options); + if (null !== $options) { + return new NotIdenticalTo(...$options); + } + + return new NotIdenticalTo(); } protected function getErrorCode(): ?string @@ -87,11 +95,4 @@ public static function provideInvalidComparisons(): array [$object, '2', $object, '2', __NAMESPACE__.'\ComparisonTest_Class'], ]; } - - public static function provideComparisonsToNullValueAtPropertyPath(): array - { - return [ - [5, '5', true], - ]; - } } diff --git a/Tests/Constraints/NotNullValidatorTest.php b/Tests/Constraints/NotNullValidatorTest.php index 82156e326..fec2ec12a 100644 --- a/Tests/Constraints/NotNullValidatorTest.php +++ b/Tests/Constraints/NotNullValidatorTest.php @@ -42,12 +42,9 @@ public static function getValidValues() ]; } - /** - * @dataProvider provideInvalidConstraints - */ - public function testNullIsInvalid(NotNull $constraint) + public function testNullIsInvalid() { - $this->validator->validate(null, $constraint); + $this->validator->validate(null, new NotNull(message: 'myMessage')); $this->buildViolation('myMessage') ->setParameter('{{ value }}', 'null') @@ -55,11 +52,18 @@ public function testNullIsInvalid(NotNull $constraint) ->assertRaised(); } - public static function provideInvalidConstraints(): iterable + /** + * @group legacy + */ + public function testNullIsInvalidDoctrineStyle() { - yield 'Doctrine style' => [new NotNull([ + $this->validator->validate(null, new NotNull([ 'message' => 'myMessage', - ])]; - yield 'named parameters' => [new NotNull(message: 'myMessage')]; + ])); + + $this->buildViolation('myMessage') + ->setParameter('{{ value }}', 'null') + ->setCode(NotNull::IS_NULL_ERROR) + ->assertRaised(); } } diff --git a/Tests/Constraints/PasswordStrengthValidatorTest.php b/Tests/Constraints/PasswordStrengthValidatorTest.php index 21dabcad7..fb063f4a7 100644 --- a/Tests/Constraints/PasswordStrengthValidatorTest.php +++ b/Tests/Constraints/PasswordStrengthValidatorTest.php @@ -14,6 +14,7 @@ use Symfony\Component\Validator\Constraints\PasswordStrength; use Symfony\Component\Validator\Constraints\PasswordStrengthValidator; use Symfony\Component\Validator\Test\ConstraintValidatorTestCase; +use Symfony\Component\Validator\Tests\Constraints\Fixtures\StringableValue; class PasswordStrengthValidatorTest extends ConstraintValidatorTestCase { @@ -25,7 +26,7 @@ protected function createValidator(): PasswordStrengthValidator /** * @dataProvider getValidValues */ - public function testValidValues(string $value, int $expectedStrength) + public function testValidValues(string|\Stringable $value, int $expectedStrength) { $this->validator->validate($value, new PasswordStrength(minScore: $expectedStrength)); @@ -39,6 +40,7 @@ public function testValidValues(string $value, int $expectedStrength) $this->buildViolation('The password strength is too low. Please use a stronger password.') ->setCode(PasswordStrength::PASSWORD_STRENGTH_ERROR) + ->setParameter('{{ strength }}', $expectedStrength) ->assertRaised(); } @@ -48,18 +50,21 @@ public static function getValidValues(): iterable yield ['Reasonable-pwd', PasswordStrength::STRENGTH_MEDIUM]; yield ['This 1s a very g00d Pa55word! ;-)', PasswordStrength::STRENGTH_VERY_STRONG]; yield ['pudding-smack-👌🏼-fox-😎', PasswordStrength::STRENGTH_VERY_STRONG]; + yield [new StringableValue('How-is-this'), PasswordStrength::STRENGTH_WEAK]; } /** * @dataProvider provideInvalidConstraints */ - public function testThePasswordIsWeak(PasswordStrength $constraint, string $password, string $expectedMessage, string $expectedCode, array $parameters = []) + public function testThePasswordIsWeak(PasswordStrength $constraint, string $password, string $expectedMessage, string $expectedCode, string $strength) { $this->validator->validate($password, $constraint); $this->buildViolation($expectedMessage) ->setCode($expectedCode) - ->setParameters($parameters) + ->setParameters([ + '{{ strength }}' => $strength, + ]) ->assertRaised(); } @@ -70,18 +75,37 @@ public static function provideInvalidConstraints(): iterable 'password', 'The password strength is too low. Please use a stronger password.', PasswordStrength::PASSWORD_STRENGTH_ERROR, + '0', ]; yield [ new PasswordStrength(minScore: PasswordStrength::STRENGTH_VERY_STRONG), 'Good password?', 'The password strength is too low. Please use a stronger password.', PasswordStrength::PASSWORD_STRENGTH_ERROR, + '1', ]; yield [ new PasswordStrength(message: 'This password should be strong.'), 'password', 'This password should be strong.', PasswordStrength::PASSWORD_STRENGTH_ERROR, + '0', ]; } + + /** + * @dataProvider getPasswordValues + */ + public function testStrengthEstimator(string $password, int $expectedStrength) + { + self::assertSame($expectedStrength, PasswordStrengthValidator::estimateStrength((string) $password)); + } + + public static function getPasswordValues(): iterable + { + yield ['How-is-this', PasswordStrength::STRENGTH_WEAK]; + yield ['Reasonable-pwd', PasswordStrength::STRENGTH_MEDIUM]; + yield ['This 1s a very g00d Pa55word! ;-)', PasswordStrength::STRENGTH_VERY_STRONG]; + yield ['pudding-smack-👌🏼-fox-😎', PasswordStrength::STRENGTH_VERY_STRONG]; + } } diff --git a/Tests/Constraints/PasswordStrengthValidatorWithClosureTest.php b/Tests/Constraints/PasswordStrengthValidatorWithClosureTest.php new file mode 100644 index 000000000..3d24e8c5b --- /dev/null +++ b/Tests/Constraints/PasswordStrengthValidatorWithClosureTest.php @@ -0,0 +1,104 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Tests\Constraints; + +use Symfony\Component\Validator\Constraints\PasswordStrength; +use Symfony\Component\Validator\Constraints\PasswordStrengthValidator; +use Symfony\Component\Validator\Test\ConstraintValidatorTestCase; +use Symfony\Component\Validator\Tests\Constraints\Fixtures\StringableValue; + +class PasswordStrengthValidatorWithClosureTest extends ConstraintValidatorTestCase +{ + protected function createValidator(): PasswordStrengthValidator + { + return new PasswordStrengthValidator(static function (string $value) { + $length = \strlen($value); + + return match (true) { + $length < 6 => PasswordStrength::STRENGTH_VERY_WEAK, + $length < 10 => PasswordStrength::STRENGTH_WEAK, + $length < 15 => PasswordStrength::STRENGTH_MEDIUM, + $length < 20 => PasswordStrength::STRENGTH_STRONG, + default => PasswordStrength::STRENGTH_VERY_STRONG, + }; + }); + } + + /** + * @dataProvider getValidValues + */ + public function testValidValues(string|\Stringable $value, int $expectedStrength) + { + $this->validator->validate($value, new PasswordStrength(minScore: $expectedStrength)); + + $this->assertNoViolation(); + + if (PasswordStrength::STRENGTH_VERY_STRONG === $expectedStrength) { + return; + } + + $this->validator->validate($value, new PasswordStrength(minScore: $expectedStrength + 1)); + + $this->buildViolation('The password strength is too low. Please use a stronger password.') + ->setCode(PasswordStrength::PASSWORD_STRENGTH_ERROR) + ->setParameter('{{ strength }}', $expectedStrength) + ->assertRaised(); + } + + public static function getValidValues(): iterable + { + yield ['az34tyu', PasswordStrength::STRENGTH_WEAK]; + yield ['A med1um one', PasswordStrength::STRENGTH_MEDIUM]; + yield ['a str0ng3r one doh', PasswordStrength::STRENGTH_STRONG]; + yield [new StringableValue('HeloW0rld'), PasswordStrength::STRENGTH_WEAK]; + } + + /** + * @dataProvider provideInvalidConstraints + */ + public function testThePasswordIsWeak(PasswordStrength $constraint, string $password, string $expectedMessage, string $expectedCode, string $strength) + { + $this->validator->validate($password, $constraint); + + $this->buildViolation($expectedMessage) + ->setCode($expectedCode) + ->setParameters([ + '{{ strength }}' => $strength, + ]) + ->assertRaised(); + } + + public static function provideInvalidConstraints(): iterable + { + yield [ + new PasswordStrength(), + 'password', + 'The password strength is too low. Please use a stronger password.', + PasswordStrength::PASSWORD_STRENGTH_ERROR, + (string) PasswordStrength::STRENGTH_WEAK, + ]; + yield [ + new PasswordStrength(minScore: PasswordStrength::STRENGTH_VERY_STRONG), + 'Good password?', + 'The password strength is too low. Please use a stronger password.', + PasswordStrength::PASSWORD_STRENGTH_ERROR, + (string) PasswordStrength::STRENGTH_MEDIUM, + ]; + yield [ + new PasswordStrength(message: 'This password should be strong.'), + 'password', + 'This password should be strong.', + PasswordStrength::PASSWORD_STRENGTH_ERROR, + (string) PasswordStrength::STRENGTH_WEAK, + ]; + } +} diff --git a/Tests/Constraints/RangeTest.php b/Tests/Constraints/RangeTest.php index a306b104a..01481e8bc 100644 --- a/Tests/Constraints/RangeTest.php +++ b/Tests/Constraints/RangeTest.php @@ -18,6 +18,9 @@ class RangeTest extends TestCase { + /** + * @group legacy + */ public function testThrowsConstraintExceptionIfBothMinLimitAndPropertyPath() { $this->expectException(ConstraintDefinitionException::class); @@ -35,6 +38,9 @@ public function testThrowsConstraintExceptionIfBothMinLimitAndPropertyPathNamed( new Range(min: 'min', minPropertyPath: 'minPropertyPath'); } + /** + * @group legacy + */ public function testThrowsConstraintExceptionIfBothMaxLimitAndPropertyPath() { $this->expectException(ConstraintDefinitionException::class); @@ -56,7 +62,7 @@ public function testThrowsConstraintExceptionIfNoLimitNorPropertyPath() { $this->expectException(MissingOptionsException::class); $this->expectExceptionMessage('Either option "min", "minPropertyPath", "max" or "maxPropertyPath" must be given'); - new Range([]); + new Range(); } public function testThrowsNoDefaultOptionConfiguredException() @@ -86,6 +92,9 @@ public function testThrowsConstraintDefinitionExceptionIfBothMinAndMaxAndMaxMess new Range(min: 'min', max: 'max', maxMessage: 'maxMessage'); } + /** + * @group legacy + */ public function testThrowsConstraintDefinitionExceptionIfBothMinAndMaxAndMinMessageAndMaxMessageOptions() { $this->expectException(ConstraintDefinitionException::class); @@ -98,6 +107,9 @@ public function testThrowsConstraintDefinitionExceptionIfBothMinAndMaxAndMinMess ]); } + /** + * @group legacy + */ public function testThrowsConstraintDefinitionExceptionIfBothMinAndMaxAndMinMessageOptions() { $this->expectException(ConstraintDefinitionException::class); @@ -109,6 +121,9 @@ public function testThrowsConstraintDefinitionExceptionIfBothMinAndMaxAndMinMess ]); } + /** + * @group legacy + */ public function testThrowsConstraintDefinitionExceptionIfBothMinAndMaxAndMaxMessageOptions() { $this->expectException(ConstraintDefinitionException::class); diff --git a/Tests/Constraints/RangeValidatorTest.php b/Tests/Constraints/RangeValidatorTest.php index e0fff6f85..423c8d460 100644 --- a/Tests/Constraints/RangeValidatorTest.php +++ b/Tests/Constraints/RangeValidatorTest.php @@ -30,7 +30,7 @@ protected function createValidator(): RangeValidator public function testNullIsValid() { - $this->validator->validate(null, new Range(['min' => 10, 'max' => 20])); + $this->validator->validate(null, new Range(min: 10, max: 20)); $this->assertNoViolation(); } @@ -70,6 +70,8 @@ public static function getMoreThanTwenty(): array } /** + * @group legacy + * * @dataProvider getTenToTwenty */ public function testValidValuesMin($value) @@ -92,6 +94,8 @@ public function testValidValuesMinNamed($value) } /** + * @group legacy + * * @dataProvider getTenToTwenty */ public function testValidValuesMax($value) @@ -114,6 +118,8 @@ public function testValidValuesMaxNamed($value) } /** + * @group legacy + * * @dataProvider getTenToTwenty */ public function testValidValuesMinMax($value) @@ -136,6 +142,8 @@ public function testValidValuesMinMaxNamed($value) } /** + * @group legacy + * * @dataProvider getLessThanTen */ public function testInvalidValuesMin($value, $formattedValue) @@ -171,6 +179,8 @@ public function testInvalidValuesMinNamed($value, $formattedValue) } /** + * @group legacy + * * @dataProvider getMoreThanTwenty */ public function testInvalidValuesMax($value, $formattedValue) @@ -206,6 +216,8 @@ public function testInvalidValuesMaxNamed($value, $formattedValue) } /** + * @group legacy + * * @dataProvider getMoreThanTwenty */ public function testInvalidValuesCombinedMax($value, $formattedValue) @@ -244,6 +256,8 @@ public function testInvalidValuesCombinedMaxNamed($value, $formattedValue) } /** + * @group legacy + * * @dataProvider getLessThanTen */ public function testInvalidValuesCombinedMin($value, $formattedValue) @@ -345,7 +359,7 @@ public static function getLaterThanTwentiethMarch2014(): array */ public function testValidDatesMin($value) { - $constraint = new Range(['min' => 'March 10, 2014']); + $constraint = new Range(min: 'March 10, 2014'); $this->validator->validate($value, $constraint); $this->assertNoViolation(); @@ -356,7 +370,7 @@ public function testValidDatesMin($value) */ public function testValidDatesMax($value) { - $constraint = new Range(['max' => 'March 20, 2014']); + $constraint = new Range(max: 'March 20, 2014'); $this->validator->validate($value, $constraint); $this->assertNoViolation(); @@ -367,7 +381,7 @@ public function testValidDatesMax($value) */ public function testValidDatesMinMax($value) { - $constraint = new Range(['min' => 'March 10, 2014', 'max' => 'March 20, 2014']); + $constraint = new Range(min: 'March 10, 2014', max: 'March 20, 2014'); $this->validator->validate($value, $constraint); $this->assertNoViolation(); @@ -382,10 +396,10 @@ public function testInvalidDatesMin(\DateTimeInterface $value, string $dateTimeA // Make sure we have the correct version loaded IntlTestHelper::requireIntl($this, '57.1'); - $constraint = new Range([ - 'min' => 'March 10, 2014', - 'minMessage' => 'myMessage', - ]); + $constraint = new Range( + min: 'March 10, 2014', + minMessage: 'myMessage', + ); $this->validator->validate($value, $constraint); @@ -405,10 +419,10 @@ public function testInvalidDatesMax(\DateTimeInterface $value, string $dateTimeA // Make sure we have the correct version loaded IntlTestHelper::requireIntl($this, '57.1'); - $constraint = new Range([ - 'max' => 'March 20, 2014', - 'maxMessage' => 'myMessage', - ]); + $constraint = new Range( + max: 'March 20, 2014', + maxMessage: 'myMessage', + ); $this->validator->validate($value, $constraint); @@ -428,11 +442,11 @@ public function testInvalidDatesCombinedMax(\DateTimeInterface $value, string $d // Make sure we have the correct version loaded IntlTestHelper::requireIntl($this, '57.1'); - $constraint = new Range([ - 'min' => 'March 10, 2014', - 'max' => 'March 20, 2014', - 'notInRangeMessage' => 'myNotInRangeMessage', - ]); + $constraint = new Range( + min: 'March 10, 2014', + max: 'March 20, 2014', + notInRangeMessage: 'myNotInRangeMessage', + ); $this->validator->validate($value, $constraint); @@ -453,11 +467,11 @@ public function testInvalidDatesCombinedMin($value, $dateTimeAsString) // Make sure we have the correct version loaded IntlTestHelper::requireIntl($this, '57.1'); - $constraint = new Range([ - 'min' => 'March 10, 2014', - 'max' => 'March 20, 2014', - 'notInRangeMessage' => 'myNotInRangeMessage', - ]); + $constraint = new Range( + min: 'March 10, 2014', + max: 'March 20, 2014', + notInRangeMessage: 'myNotInRangeMessage', + ); $this->validator->validate($value, $constraint); @@ -482,10 +496,10 @@ public function getInvalidValues(): array public function testNonNumeric() { - $constraint = new Range([ - 'min' => 10, - 'max' => 20, - ]); + $constraint = new Range( + min: 10, + max: 20, + ); $this->validator->validate('abcd', $constraint); @@ -497,9 +511,9 @@ public function testNonNumeric() public function testNonNumericWithParsableDatetimeMinAndMaxNull() { - $constraint = new Range([ - 'min' => 'March 10, 2014', - ]); + $constraint = new Range( + min: 'March 10, 2014', + ); $this->validator->validate('abcd', $constraint); @@ -511,9 +525,9 @@ public function testNonNumericWithParsableDatetimeMinAndMaxNull() public function testNonNumericWithParsableDatetimeMaxAndMinNull() { - $constraint = new Range([ - 'max' => 'March 20, 2014', - ]); + $constraint = new Range( + max: 'March 20, 2014', + ); $this->validator->validate('abcd', $constraint); @@ -525,10 +539,10 @@ public function testNonNumericWithParsableDatetimeMaxAndMinNull() public function testNonNumericWithParsableDatetimeMinAndMax() { - $constraint = new Range([ - 'min' => 'March 10, 2014', - 'max' => 'March 20, 2014', - ]); + $constraint = new Range( + min: 'March 10, 2014', + max: 'March 20, 2014', + ); $this->validator->validate('abcd', $constraint); @@ -540,10 +554,10 @@ public function testNonNumericWithParsableDatetimeMinAndMax() public function testNonNumericWithNonParsableDatetimeMin() { - $constraint = new Range([ - 'min' => 'March 40, 2014', - 'max' => 'March 20, 2014', - ]); + $constraint = new Range( + min: 'March 40, 2014', + max: 'March 20, 2014', + ); $this->validator->validate('abcd', $constraint); @@ -555,10 +569,10 @@ public function testNonNumericWithNonParsableDatetimeMin() public function testNonNumericWithNonParsableDatetimeMax() { - $constraint = new Range([ - 'min' => 'March 10, 2014', - 'max' => 'March 50, 2014', - ]); + $constraint = new Range( + min: 'March 10, 2014', + max: 'March 50, 2014', + ); $this->validator->validate('abcd', $constraint); @@ -570,10 +584,10 @@ public function testNonNumericWithNonParsableDatetimeMax() public function testNonNumericWithNonParsableDatetimeMinAndMax() { - $constraint = new Range([ - 'min' => 'March 40, 2014', - 'max' => 'March 50, 2014', - ]); + $constraint = new Range( + min: 'March 40, 2014', + max: 'March 50, 2014', + ); $this->validator->validate('abcd', $constraint); @@ -591,10 +605,10 @@ public function testThrowsOnInvalidStringDates($expectedMessage, $value, $min, $ $this->expectException(ConstraintDefinitionException::class); $this->expectExceptionMessage($expectedMessage); - $this->validator->validate($value, new Range([ - 'min' => $min, - 'max' => $max, - ])); + $this->validator->validate($value, new Range( + min: $min, + max: $max, + )); } public static function throwsOnInvalidStringDatesProvider(): array @@ -612,15 +626,17 @@ public function testNoViolationOnNullObjectWithPropertyPaths() { $this->setObject(null); - $this->validator->validate(1, new Range([ - 'minPropertyPath' => 'minPropertyPath', - 'maxPropertyPath' => 'maxPropertyPath', - ])); + $this->validator->validate(1, new Range( + minPropertyPath: 'minPropertyPath', + maxPropertyPath: 'maxPropertyPath', + )); $this->assertNoViolation(); } /** + * @group legacy + * * @dataProvider getTenToTwenty */ public function testValidValuesMinPropertyPath($value) @@ -653,9 +669,9 @@ public function testValidValuesMaxPropertyPath($value) { $this->setObject(new Limit(20)); - $this->validator->validate($value, new Range([ - 'maxPropertyPath' => 'value', - ])); + $this->validator->validate($value, new Range( + maxPropertyPath: 'value', + )); $this->assertNoViolation(); } @@ -679,10 +695,10 @@ public function testValidValuesMinMaxPropertyPath($value) { $this->setObject(new MinMax(10, 20)); - $this->validator->validate($value, new Range([ - 'minPropertyPath' => 'min', - 'maxPropertyPath' => 'max', - ])); + $this->validator->validate($value, new Range( + minPropertyPath: 'min', + maxPropertyPath: 'max', + )); $this->assertNoViolation(); } @@ -694,10 +710,10 @@ public function testInvalidValuesMinPropertyPath($value, $formattedValue) { $this->setObject(new Limit(10)); - $constraint = new Range([ - 'minPropertyPath' => 'value', - 'minMessage' => 'myMessage', - ]); + $constraint = new Range( + minPropertyPath: 'value', + minMessage: 'myMessage', + ); $this->validator->validate($value, $constraint); @@ -716,10 +732,10 @@ public function testInvalidValuesMaxPropertyPath($value, $formattedValue) { $this->setObject(new Limit(20)); - $constraint = new Range([ - 'maxPropertyPath' => 'value', - 'maxMessage' => 'myMessage', - ]); + $constraint = new Range( + maxPropertyPath: 'value', + maxMessage: 'myMessage', + ); $this->validator->validate($value, $constraint); @@ -732,6 +748,8 @@ public function testInvalidValuesMaxPropertyPath($value, $formattedValue) } /** + * @group legacy + * * @dataProvider getMoreThanTwenty */ public function testInvalidValuesCombinedMaxPropertyPath($value, $formattedValue) @@ -782,6 +800,8 @@ public function testInvalidValuesCombinedMaxPropertyPathNamed($value, $formatted } /** + * @group legacy + * * @dataProvider getLessThanTen */ public function testInvalidValuesCombinedMinPropertyPath($value, $formattedValue) @@ -838,11 +858,11 @@ public function testViolationOnNullObjectWithDefinedMin($value, $formattedValue) { $this->setObject(null); - $this->validator->validate($value, new Range([ - 'min' => 10, - 'maxPropertyPath' => 'max', - 'minMessage' => 'myMessage', - ])); + $this->validator->validate($value, new Range( + min: 10, + maxPropertyPath: 'max', + minMessage: 'myMessage', + )); $this->buildViolation('myMessage') ->setParameter('{{ value }}', $formattedValue) @@ -859,11 +879,11 @@ public function testViolationOnNullObjectWithDefinedMax($value, $formattedValue) { $this->setObject(null); - $this->validator->validate($value, new Range([ - 'minPropertyPath' => 'min', - 'max' => 20, - 'maxMessage' => 'myMessage', - ])); + $this->validator->validate($value, new Range( + minPropertyPath: 'min', + max: 20, + maxMessage: 'myMessage', + )); $this->buildViolation('myMessage') ->setParameter('{{ value }}', $formattedValue) @@ -880,7 +900,7 @@ public function testValidDatesMinPropertyPath($value) { $this->setObject(new Limit('March 10, 2014')); - $this->validator->validate($value, new Range(['minPropertyPath' => 'value'])); + $this->validator->validate($value, new Range(minPropertyPath: 'value')); $this->assertNoViolation(); } @@ -892,7 +912,7 @@ public function testValidDatesMaxPropertyPath($value) { $this->setObject(new Limit('March 20, 2014')); - $constraint = new Range(['maxPropertyPath' => 'value']); + $constraint = new Range(maxPropertyPath: 'value'); $this->validator->validate($value, $constraint); $this->assertNoViolation(); @@ -905,7 +925,7 @@ public function testValidDatesMinMaxPropertyPath($value) { $this->setObject(new MinMax('March 10, 2014', 'March 20, 2014')); - $constraint = new Range(['minPropertyPath' => 'min', 'maxPropertyPath' => 'max']); + $constraint = new Range(minPropertyPath: 'min', maxPropertyPath: 'max'); $this->validator->validate($value, $constraint); $this->assertNoViolation(); @@ -922,10 +942,10 @@ public function testInvalidDatesMinPropertyPath($value, $dateTimeAsString) $this->setObject(new Limit('March 10, 2014')); - $constraint = new Range([ - 'minPropertyPath' => 'value', - 'minMessage' => 'myMessage', - ]); + $constraint = new Range( + minPropertyPath: 'value', + minMessage: 'myMessage', + ); $this->validator->validate($value, $constraint); @@ -948,10 +968,10 @@ public function testInvalidDatesMaxPropertyPath($value, $dateTimeAsString) $this->setObject(new Limit('March 20, 2014')); - $constraint = new Range([ - 'maxPropertyPath' => 'value', - 'maxMessage' => 'myMessage', - ]); + $constraint = new Range( + maxPropertyPath: 'value', + maxMessage: 'myMessage', + ); $this->validator->validate($value, $constraint); @@ -974,11 +994,11 @@ public function testInvalidDatesCombinedMaxPropertyPath($value, $dateTimeAsStrin $this->setObject(new MinMax('March 10, 2014', 'March 20, 2014')); - $constraint = new Range([ - 'minPropertyPath' => 'min', - 'maxPropertyPath' => 'max', - 'notInRangeMessage' => 'myNotInRangeMessage', - ]); + $constraint = new Range( + minPropertyPath: 'min', + maxPropertyPath: 'max', + notInRangeMessage: 'myNotInRangeMessage', + ); $this->validator->validate($value, $constraint); @@ -1003,11 +1023,11 @@ public function testInvalidDatesCombinedMinPropertyPath($value, $dateTimeAsStrin $this->setObject(new MinMax('March 10, 2014', 'March 20, 2014')); - $constraint = new Range([ - 'minPropertyPath' => 'min', - 'maxPropertyPath' => 'max', - 'notInRangeMessage' => 'myNotInRangeMessage', - ]); + $constraint = new Range( + minPropertyPath: 'min', + maxPropertyPath: 'max', + notInRangeMessage: 'myNotInRangeMessage', + ); $this->validator->validate($value, $constraint); @@ -1027,7 +1047,7 @@ public function testMinPropertyPathReferencingUninitializedProperty() $object->max = 5; $this->setObject($object); - $this->validator->validate(5, new Range(['minPropertyPath' => 'min', 'maxPropertyPath' => 'max'])); + $this->validator->validate(5, new Range(minPropertyPath: 'min', maxPropertyPath: 'max')); $this->assertNoViolation(); } @@ -1038,14 +1058,14 @@ public function testMaxPropertyPathReferencingUninitializedProperty() $object->min = 5; $this->setObject($object); - $this->validator->validate(5, new Range(['minPropertyPath' => 'min', 'maxPropertyPath' => 'max'])); + $this->validator->validate(5, new Range(minPropertyPath: 'min', maxPropertyPath: 'max')); $this->assertNoViolation(); } public static function provideMessageIfMinAndMaxSet(): array { - $notInRangeMessage = (new Range(['min' => '']))->notInRangeMessage; + $notInRangeMessage = (new Range(min: ''))->notInRangeMessage; return [ [ @@ -1068,7 +1088,7 @@ public static function provideMessageIfMinAndMaxSet(): array */ public function testMessageIfMinAndMaxSet(array $constraintExtraOptions, int $value, string $expectedMessage, string $expectedCode) { - $constraint = new Range(array_merge(['min' => 1, 'max' => 10], $constraintExtraOptions)); + $constraint = new Range(...array_merge(['min' => 1, 'max' => 10], $constraintExtraOptions)); $this->validator->validate($value, $constraint); $this diff --git a/Tests/Constraints/RegexTest.php b/Tests/Constraints/RegexTest.php index 4df86e434..853e0d785 100644 --- a/Tests/Constraints/RegexTest.php +++ b/Tests/Constraints/RegexTest.php @@ -70,10 +70,10 @@ public static function provideHtmlPatterns() */ public function testGetHtmlPattern($pattern, $htmlPattern, $match = true) { - $constraint = new Regex([ - 'pattern' => $pattern, - 'match' => $match, - ]); + $constraint = new Regex( + pattern: $pattern, + match: $match, + ); $this->assertSame($pattern, $constraint->pattern); $this->assertSame($htmlPattern, $constraint->getHtmlPattern()); @@ -81,10 +81,10 @@ public function testGetHtmlPattern($pattern, $htmlPattern, $match = true) public function testGetCustomHtmlPattern() { - $constraint = new Regex([ - 'pattern' => '((?![0-9]$|[a-z]+).)*', - 'htmlPattern' => 'foobar', - ]); + $constraint = new Regex( + pattern: '((?![0-9]$|[a-z]+).)*', + htmlPattern: 'foobar', + ); $this->assertSame('((?![0-9]$|[a-z]+).)*', $constraint->pattern); $this->assertSame('foobar', $constraint->getHtmlPattern()); @@ -92,11 +92,14 @@ public function testGetCustomHtmlPattern() public function testNormalizerCanBeSet() { - $regex = new Regex(['pattern' => '/^[0-9]+$/', 'normalizer' => 'trim']); + $regex = new Regex(pattern: '/^[0-9]+$/', normalizer: 'trim'); $this->assertEquals('trim', $regex->normalizer); } + /** + * @group legacy + */ public function testInvalidNormalizerThrowsException() { $this->expectException(InvalidArgumentException::class); @@ -104,6 +107,9 @@ public function testInvalidNormalizerThrowsException() new Regex(['pattern' => '/^[0-9]+$/', 'normalizer' => 'Unknown Callable']); } + /** + * @group legacy + */ public function testInvalidNormalizerObjectThrowsException() { $this->expectException(InvalidArgumentException::class); @@ -142,6 +148,9 @@ public function testMissingPattern() new Regex(null); } + /** + * @group legacy + */ public function testMissingPatternDoctrineStyle() { $this->expectException(MissingOptionsException::class); diff --git a/Tests/Constraints/RegexValidatorTest.php b/Tests/Constraints/RegexValidatorTest.php index 82739f0e3..bafc752c3 100644 --- a/Tests/Constraints/RegexValidatorTest.php +++ b/Tests/Constraints/RegexValidatorTest.php @@ -25,14 +25,14 @@ protected function createValidator(): RegexValidator public function testNullIsValid() { - $this->validator->validate(null, new Regex(['pattern' => '/^[0-9]+$/'])); + $this->validator->validate(null, new Regex(pattern: '/^[0-9]+$/')); $this->assertNoViolation(); } public function testEmptyStringIsValid() { - $this->validator->validate('', new Regex(['pattern' => '/^[0-9]+$/'])); + $this->validator->validate('', new Regex(pattern: '/^[0-9]+$/')); $this->assertNoViolation(); } @@ -40,7 +40,7 @@ public function testEmptyStringIsValid() public function testExpectsStringCompatibleType() { $this->expectException(UnexpectedValueException::class); - $this->validator->validate(new \stdClass(), new Regex(['pattern' => '/^[0-9]+$/'])); + $this->validator->validate(new \stdClass(), new Regex(pattern: '/^[0-9]+$/')); } /** @@ -48,13 +48,15 @@ public function testExpectsStringCompatibleType() */ public function testValidValues($value) { - $constraint = new Regex(['pattern' => '/^[0-9]+$/']); + $constraint = new Regex(pattern: '/^[0-9]+$/'); $this->validator->validate($value, $constraint); $this->assertNoViolation(); } /** + * @group legacy + * * @dataProvider getValidValuesWithWhitespaces */ public function testValidValuesWithWhitespaces($value) @@ -105,6 +107,8 @@ public static function getValidValuesWithWhitespaces() } /** + * @group legacy + * * @dataProvider getInvalidValues */ public function testInvalidValues($value) diff --git a/Tests/Constraints/SequentiallyValidatorTest.php b/Tests/Constraints/SequentiallyValidatorTest.php index 657ff2637..4c8a48e10 100644 --- a/Tests/Constraints/SequentiallyValidatorTest.php +++ b/Tests/Constraints/SequentiallyValidatorTest.php @@ -33,7 +33,7 @@ public function testWalkThroughConstraints() { $constraints = [ new Type('number'), - new Range(['min' => 4]), + new Range(min: 4), ]; $value = 6; @@ -50,7 +50,7 @@ public function testStopsAtFirstConstraintWithViolations() { $constraints = [ new Type('string'), - new Regex(['pattern' => '[a-z]']), + new Regex(pattern: '[a-z]'), new NotEqualTo('Foo'), ]; @@ -68,20 +68,20 @@ public function testNestedConstraintsAreNotExecutedWhenGroupDoesNotMatch() { $validator = Validation::createValidator(); - $violations = $validator->validate(50, new Sequentially([ - 'constraints' => [ - new GreaterThan([ - 'groups' => 'senior', - 'value' => 55, - ]), - new Range([ - 'groups' => 'adult', - 'min' => 18, - 'max' => 55, - ]), + $violations = $validator->validate(50, new Sequentially( + constraints: [ + new GreaterThan( + groups: ['senior'], + value: 55, + ), + new Range( + groups: ['adult'], + min: 18, + max: 55, + ), ], - 'groups' => ['adult', 'senior'], - ]), 'adult'); + groups: ['adult', 'senior'], + ), 'adult'); $this->assertCount(0, $violations); } diff --git a/Tests/Constraints/ThrowsOnInvalidStringDatesTestTrait.php b/Tests/Constraints/ThrowsOnInvalidStringDatesTestTrait.php new file mode 100644 index 000000000..ac40492fd --- /dev/null +++ b/Tests/Constraints/ThrowsOnInvalidStringDatesTestTrait.php @@ -0,0 +1,41 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Tests\Constraints; + +use Symfony\Component\Validator\Constraints\AbstractComparison; +use Symfony\Component\Validator\Exception\ConstraintDefinitionException; + +trait ThrowsOnInvalidStringDatesTestTrait +{ + /** + * @dataProvider throwsOnInvalidStringDatesProvider + */ + public function testThrowsOnInvalidStringDates(AbstractComparison $constraint, $expectedMessage, $value) + { + $this->expectException(ConstraintDefinitionException::class); + $this->expectExceptionMessage($expectedMessage); + + $this->validator->validate($value, $constraint); + } + + public static function throwsOnInvalidStringDatesProvider(): array + { + $constraint = static::createConstraint([ + 'value' => 'foo', + ]); + + return [ + [$constraint, \sprintf('The compared value "foo" could not be converted to a "DateTimeImmutable" instance in the "%s" constraint.', $constraint::class), new \DateTimeImmutable()], + [$constraint, \sprintf('The compared value "foo" could not be converted to a "DateTime" instance in the "%s" constraint.', $constraint::class), new \DateTime()], + ]; + } +} diff --git a/Tests/Constraints/TimeValidatorTest.php b/Tests/Constraints/TimeValidatorTest.php index 5d9027a17..7c1a9feb9 100644 --- a/Tests/Constraints/TimeValidatorTest.php +++ b/Tests/Constraints/TimeValidatorTest.php @@ -87,9 +87,7 @@ public static function getValidTimes() */ public function testValidTimesWithoutSeconds(string $time) { - $this->validator->validate($time, new Time([ - 'withSeconds' => false, - ])); + $this->validator->validate($time, new Time(withSeconds: false)); $this->assertNoViolation(); } @@ -143,9 +141,7 @@ public static function getInvalidTimesWithoutSeconds() */ public function testInvalidTimes($time, $code) { - $constraint = new Time([ - 'message' => 'myMessage', - ]); + $constraint = new Time(message: 'myMessage'); $this->validator->validate($time, $constraint); diff --git a/Tests/Constraints/TimezoneTest.php b/Tests/Constraints/TimezoneTest.php index 42a38a711..41fed2386 100644 --- a/Tests/Constraints/TimezoneTest.php +++ b/Tests/Constraints/TimezoneTest.php @@ -25,12 +25,12 @@ class TimezoneTest extends TestCase public function testValidTimezoneConstraints() { new Timezone(); - new Timezone(['zone' => \DateTimeZone::ALL]); + new Timezone(zone: \DateTimeZone::ALL); new Timezone(\DateTimeZone::ALL_WITH_BC); - new Timezone([ - 'zone' => \DateTimeZone::PER_COUNTRY, - 'countryCode' => 'AR', - ]); + new Timezone( + zone: \DateTimeZone::PER_COUNTRY, + countryCode: 'AR', + ); $this->addToAssertionCount(1); } @@ -38,16 +38,16 @@ public function testValidTimezoneConstraints() public function testExceptionForGroupedTimezonesByCountryWithWrongZone() { $this->expectException(ConstraintDefinitionException::class); - new Timezone([ - 'zone' => \DateTimeZone::ALL, - 'countryCode' => 'AR', - ]); + new Timezone( + zone: \DateTimeZone::ALL, + countryCode: 'AR', + ); } public function testExceptionForGroupedTimezonesByCountryWithoutZone() { $this->expectException(ConstraintDefinitionException::class); - new Timezone(['countryCode' => 'AR']); + new Timezone(countryCode: 'AR'); } /** @@ -56,7 +56,7 @@ public function testExceptionForGroupedTimezonesByCountryWithoutZone() public function testExceptionForInvalidGroupedTimezones(int $zone) { $this->expectException(ConstraintDefinitionException::class); - new Timezone(['zone' => $zone]); + new Timezone(zone: $zone); } public static function provideInvalidZones(): iterable diff --git a/Tests/Constraints/TimezoneValidatorTest.php b/Tests/Constraints/TimezoneValidatorTest.php index c4127c920..7ed4d79ed 100644 --- a/Tests/Constraints/TimezoneValidatorTest.php +++ b/Tests/Constraints/TimezoneValidatorTest.php @@ -92,9 +92,7 @@ public static function getValidTimezones(): iterable */ public function testValidGroupedTimezones(string $timezone, int $zone) { - $constraint = new Timezone([ - 'zone' => $zone, - ]); + $constraint = new Timezone(zone: $zone); $this->validator->validate($timezone, $constraint); @@ -125,9 +123,7 @@ public static function getValidGroupedTimezones(): iterable */ public function testInvalidTimezoneWithoutZone(string $timezone) { - $constraint = new Timezone([ - 'message' => 'myMessage', - ]); + $constraint = new Timezone(message: 'myMessage'); $this->validator->validate($timezone, $constraint); @@ -150,10 +146,10 @@ public static function getInvalidTimezones(): iterable */ public function testInvalidGroupedTimezones(string $timezone, int $zone) { - $constraint = new Timezone([ - 'zone' => $zone, - 'message' => 'myMessage', - ]); + $constraint = new Timezone( + zone: $zone, + message: 'myMessage', + ); $this->validator->validate($timezone, $constraint); @@ -193,10 +189,10 @@ public function testInvalidGroupedTimezoneNamed() */ public function testValidGroupedTimezonesByCountry(string $timezone, string $country) { - $constraint = new Timezone([ - 'zone' => \DateTimeZone::PER_COUNTRY, - 'countryCode' => $country, - ]); + $constraint = new Timezone( + zone: \DateTimeZone::PER_COUNTRY, + countryCode: $country, + ); $this->validator->validate($timezone, $constraint); @@ -230,11 +226,11 @@ public static function getValidGroupedTimezonesByCountry(): iterable */ public function testInvalidGroupedTimezonesByCountry(string $timezone, string $countryCode) { - $constraint = new Timezone([ - 'message' => 'myMessage', - 'zone' => \DateTimeZone::PER_COUNTRY, - 'countryCode' => $countryCode, - ]); + $constraint = new Timezone( + message: 'myMessage', + zone: \DateTimeZone::PER_COUNTRY, + countryCode: $countryCode, + ); $this->validator->validate($timezone, $constraint); @@ -255,11 +251,11 @@ public static function getInvalidGroupedTimezonesByCountry(): iterable public function testGroupedTimezonesWithInvalidCountry() { - $constraint = new Timezone([ - 'message' => 'myMessage', - 'zone' => \DateTimeZone::PER_COUNTRY, - 'countryCode' => 'foobar', - ]); + $constraint = new Timezone( + message: 'myMessage', + zone: \DateTimeZone::PER_COUNTRY, + countryCode: 'foobar', + ); $this->validator->validate('Europe/Amsterdam', $constraint); @@ -291,9 +287,7 @@ public function testDeprecatedTimezonesAreValidWithBC(string $timezone) */ public function testDeprecatedTimezonesAreInvalidWithoutBC(string $timezone) { - $constraint = new Timezone([ - 'message' => 'myMessage', - ]); + $constraint = new Timezone(message: 'myMessage'); $this->validator->validate($timezone, $constraint); @@ -337,10 +331,10 @@ public function testIntlCompatibility() $this->markTestSkipped('"Europe/Saratov" is expired until 2017, current version is '.$tzDbVersion); } - $constraint = new Timezone([ - 'message' => 'myMessage', - 'intlCompatible' => true, - ]); + $constraint = new Timezone( + message: 'myMessage', + intlCompatible: true, + ); $this->validator->validate('Europe/Saratov', $constraint); diff --git a/Tests/Constraints/TypeValidatorTest.php b/Tests/Constraints/TypeValidatorTest.php index 8b4fe2531..8e9e1aa3b 100644 --- a/Tests/Constraints/TypeValidatorTest.php +++ b/Tests/Constraints/TypeValidatorTest.php @@ -26,7 +26,7 @@ protected function createValidator(): TypeValidator public function testNullIsValid() { - $constraint = new Type(['type' => 'integer']); + $constraint = new Type(type: 'integer'); $this->validator->validate(null, $constraint); @@ -35,7 +35,7 @@ public function testNullIsValid() public function testEmptyIsValidIfString() { - $constraint = new Type(['type' => 'string']); + $constraint = new Type(type: 'string'); $this->validator->validate('', $constraint); @@ -44,10 +44,10 @@ public function testEmptyIsValidIfString() public function testEmptyIsInvalidIfNoString() { - $constraint = new Type([ - 'type' => 'integer', - 'message' => 'myMessage', - ]); + $constraint = new Type( + type: 'integer', + message: 'myMessage', + ); $this->validator->validate('', $constraint); @@ -63,7 +63,7 @@ public function testEmptyIsInvalidIfNoString() */ public function testValidValues($value, $type) { - $constraint = new Type(['type' => $type]); + $constraint = new Type(type: $type); $this->validator->validate($value, $constraint); @@ -97,6 +97,10 @@ public static function getValidValues() [1.5, 'finite-number'], ['12345', 'string'], [[], 'array'], + [[], 'list'], + [[1, 2, 3], 'list'], + [['abc' => 1], 'associative_array'], + [[1 => 1], 'associative_array'], [$object, 'object'], [$object, 'stdClass'], [$file, 'resource'], @@ -119,10 +123,10 @@ public static function getValidValues() */ public function testInvalidValues($value, $type, $valueAsString) { - $constraint = new Type([ - 'type' => $type, - 'message' => 'myMessage', - ]); + $constraint = new Type( + type: $type, + message: 'myMessage', + ); $this->validator->validate($value, $constraint); @@ -166,6 +170,12 @@ public static function getInvalidValues() [$file, 'float', 'resource'], [$file, 'string', 'resource'], [$file, 'object', 'resource'], + [[1 => 1], 'list', 'array'], + [['abc' => 1], 'list', 'array'], + ['abcd1', 'list', '"abcd1"'], + [[], 'associative_array', 'array'], + [[1, 2, 3], 'associative_array', 'array'], + ['abcd1', 'associative_array', '"abcd1"'], ['12a34', 'digit', '"12a34"'], ['1a#23', 'alnum', '"1a#23"'], ['abcd1', 'alpha', '"abcd1"'], @@ -185,7 +195,7 @@ public static function getInvalidValues() */ public function testValidValuesMultipleTypes($value, array $types) { - $constraint = new Type(['type' => $types]); + $constraint = new Type(type: $types); $this->validator->validate($value, $constraint); @@ -200,12 +210,9 @@ public static function getValidValuesMultipleTypes() ]; } - /** - * @dataProvider provideConstraintsWithMultipleTypes - */ - public function testInvalidValuesMultipleTypes(Type $constraint) + public function testInvalidValuesMultipleTypes() { - $this->validator->validate('12345', $constraint); + $this->validator->validate('12345', new Type(type: ['boolean', 'array'], message: 'myMessage')); $this->buildViolation('myMessage') ->setParameter('{{ value }}', '"12345"') @@ -214,13 +221,21 @@ public function testInvalidValuesMultipleTypes(Type $constraint) ->assertRaised(); } - public static function provideConstraintsWithMultipleTypes() + /** + * @group legacy + */ + public function testInvalidValuesMultipleTypesDoctrineStyle() { - yield 'Doctrine style' => [new Type([ + $this->validator->validate('12345', new Type([ 'type' => ['boolean', 'array'], 'message' => 'myMessage', - ])]; - yield 'named arguments' => [new Type(type: ['boolean', 'array'], message: 'myMessage')]; + ])); + + $this->buildViolation('myMessage') + ->setParameter('{{ value }}', '"12345"') + ->setParameter('{{ type }}', implode('|', ['boolean', 'array'])) + ->setCode(Type::INVALID_TYPE_ERROR) + ->assertRaised(); } protected static function createFile() diff --git a/Tests/Constraints/UlidTest.php b/Tests/Constraints/UlidTest.php index 14046e37a..86cc32266 100644 --- a/Tests/Constraints/UlidTest.php +++ b/Tests/Constraints/UlidTest.php @@ -13,6 +13,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\Validator\Constraints\Ulid; +use Symfony\Component\Validator\Exception\ConstraintDefinitionException; use Symfony\Component\Validator\Mapping\ClassMetadata; use Symfony\Component\Validator\Mapping\Loader\AttributeLoader; @@ -27,11 +28,20 @@ public function testAttributes() [$bConstraint] = $metadata->properties['b']->getConstraints(); self::assertSame('myMessage', $bConstraint->message); self::assertSame(['Default', 'UlidDummy'], $bConstraint->groups); + self::assertSame(Ulid::FORMAT_BASE_58, $bConstraint->format); [$cConstraint] = $metadata->properties['c']->getConstraints(); self::assertSame(['my_group'], $cConstraint->groups); self::assertSame('some attached data', $cConstraint->payload); } + + public function testUnexpectedValidationFormat() + { + $this->expectException(ConstraintDefinitionException::class); + $this->expectExceptionMessage('The "invalid" validation format is not supported.'); + + new Ulid(format: 'invalid'); + } } class UlidDummy @@ -39,7 +49,7 @@ class UlidDummy #[Ulid] private $a; - #[Ulid(message: 'myMessage')] + #[Ulid(message: 'myMessage', format: Ulid::FORMAT_BASE_58)] private $b; #[Ulid(groups: ['my_group'], payload: 'some attached data')] diff --git a/Tests/Constraints/UlidValidatorTest.php b/Tests/Constraints/UlidValidatorTest.php index abe5490d1..172ace189 100644 --- a/Tests/Constraints/UlidValidatorTest.php +++ b/Tests/Constraints/UlidValidatorTest.php @@ -53,24 +53,39 @@ public function testValidUlid() $this->assertNoViolation(); } + public function testValidUlidAsBase58() + { + $this->validator->validate('1CCD2w4mK2m455S2BAXFht', new Ulid(format: Ulid::FORMAT_BASE_58)); + + $this->assertNoViolation(); + } + + public function testValidUlidAsRfc4122() + { + $this->validator->validate('01912bf3-feff-fa6c-00f2-90d2f2e00564', new Ulid(format: Ulid::FORMAT_RFC_4122)); + + $this->assertNoViolation(); + } + /** * @dataProvider getInvalidUlids */ public function testInvalidUlid(string $ulid, string $code) { - $constraint = new Ulid([ - 'message' => 'testMessage', - ]); + $constraint = new Ulid(message: 'testMessage'); $this->validator->validate($ulid, $constraint); $this->buildViolation('testMessage') - ->setParameter('{{ value }}', '"'.$ulid.'"') + ->setParameters([ + '{{ value }}' => '"'.$ulid.'"', + '{{ format }}' => Ulid::FORMAT_BASE_32, + ]) ->setCode($code) ->assertRaised(); } - public static function getInvalidUlids() + public static function getInvalidUlids(): array { return [ ['01ARZ3NDEKTSV4RRFFQ69G5FA', Ulid::TOO_SHORT_ERROR], @@ -81,6 +96,64 @@ public static function getInvalidUlids() ]; } + /** + * @dataProvider getInvalidBase58Ulids + */ + public function testInvalidBase58Ulid(string $ulid, string $code) + { + $constraint = new Ulid(message: 'testMessage', format: Ulid::FORMAT_BASE_58); + + $this->validator->validate($ulid, $constraint); + + $this->buildViolation('testMessage') + ->setParameters([ + '{{ value }}' => '"'.$ulid.'"', + '{{ format }}' => Ulid::FORMAT_BASE_58, + ]) + ->setCode($code) + ->assertRaised(); + } + + public static function getInvalidBase58Ulids(): array + { + return [ + ['1CCD2w4mK2m455S2BAXFh', Ulid::TOO_SHORT_ERROR], + ['1CCD2w4mK2m455S2BAXFhttt', Ulid::TOO_LONG_ERROR], + ['1CCD2w4mK2m455S2BAXFhO', Ulid::INVALID_CHARACTERS_ERROR], + ['not-even-ulid-like', Ulid::TOO_SHORT_ERROR], + ]; + } + + /** + * @dataProvider getInvalidRfc4122Ulids + */ + public function testInvalidInvalid4122Ulid(string $ulid, string $code) + { + $constraint = new Ulid(message: 'testMessage', format: Ulid::FORMAT_RFC_4122); + + $this->validator->validate($ulid, $constraint); + + $this->buildViolation('testMessage') + ->setParameters([ + '{{ value }}' => '"'.$ulid.'"', + '{{ format }}' => Ulid::FORMAT_RFC_4122, + ]) + ->setCode($code) + ->assertRaised(); + } + + public static function getInvalidRfc4122Ulids(): array + { + return [ + ['01912bf3-f5b7-e55d', Ulid::TOO_SHORT_ERROR], + ['01912bf3-f5b7-e55d-d21f-5ef032cd8e29999999', Ulid::TOO_LONG_ERROR], + ['01912bf3-f5b7-e55d-d21f-5ef032cd8eZZ', Ulid::INVALID_CHARACTERS_ERROR], + ['not-even-ulid-like', Ulid::TOO_SHORT_ERROR], + ['01912bf30feff0fa6c000f2090d2f2e00564', Ulid::INVALID_FORMAT_ERROR], + ['019-2bf3-feff-fa6c-00f2-90d2f2e00564', Ulid::INVALID_FORMAT_ERROR], + ]; + } + public function testInvalidUlidNamed() { $constraint = new Ulid(message: 'testMessage'); @@ -88,7 +161,10 @@ public function testInvalidUlidNamed() $this->validator->validate('01ARZ3NDEKTSV4RRFFQ69G5FA', $constraint); $this->buildViolation('testMessage') - ->setParameter('{{ value }}', '"01ARZ3NDEKTSV4RRFFQ69G5FA"') + ->setParameters([ + '{{ value }}' => '"01ARZ3NDEKTSV4RRFFQ69G5FA"', + '{{ format }}' => Ulid::FORMAT_BASE_32, + ]) ->setCode(Ulid::TOO_SHORT_ERROR) ->assertRaised(); } diff --git a/Tests/Constraints/UniqueTest.php b/Tests/Constraints/UniqueTest.php index 7d882a9c3..9fe2599fd 100644 --- a/Tests/Constraints/UniqueTest.php +++ b/Tests/Constraints/UniqueTest.php @@ -37,6 +37,9 @@ public function testAttributes() self::assertSame('intval', $dConstraint->normalizer); } + /** + * @group legacy + */ public function testInvalidNormalizerThrowsException() { $this->expectException(InvalidArgumentException::class); @@ -44,6 +47,9 @@ public function testInvalidNormalizerThrowsException() new Unique(['normalizer' => 'Unknown Callable']); } + /** + * @group legacy + */ public function testInvalidNormalizerObjectThrowsException() { $this->expectException(InvalidArgumentException::class); diff --git a/Tests/Constraints/UniqueValidatorTest.php b/Tests/Constraints/UniqueValidatorTest.php index 8fe05ad92..12efb7698 100644 --- a/Tests/Constraints/UniqueValidatorTest.php +++ b/Tests/Constraints/UniqueValidatorTest.php @@ -63,9 +63,7 @@ public static function getValidValues() */ public function testInvalidValues($value, $expectedMessageParam) { - $constraint = new Unique([ - 'message' => 'myMessage', - ]); + $constraint = new Unique(message: 'myMessage'); $this->validator->validate($value, $constraint); $this->buildViolation('myMessage') @@ -118,9 +116,7 @@ public function testExpectsUniqueObjects($callback) $value = [$object1, $object2, $object3]; - $this->validator->validate($value, new Unique([ - 'normalizer' => $callback, - ])); + $this->validator->validate($value, new Unique(normalizer: $callback)); $this->assertNoViolation(); } @@ -144,10 +140,10 @@ public function testExpectsNonUniqueObjects($callback) $value = [$object1, $object2, $object3]; - $this->validator->validate($value, new Unique([ - 'message' => 'myMessage', - 'normalizer' => $callback, - ])); + $this->validator->validate($value, new Unique( + message: 'myMessage', + normalizer: $callback, + )); $this->buildViolation('myMessage') ->setParameter('{{ value }}', 'array') @@ -168,10 +164,10 @@ public static function getCallback(): array public function testExpectsInvalidNonStrictComparison() { - $this->validator->validate([1, '1', 1.0, '1.0'], new Unique([ - 'message' => 'myMessage', - 'normalizer' => 'intval', - ])); + $this->validator->validate([1, '1', 1.0, '1.0'], new Unique( + message: 'myMessage', + normalizer: 'intval', + )); $this->buildViolation('myMessage') ->setParameter('{{ value }}', '1') @@ -183,9 +179,7 @@ public function testExpectsValidNonStrictComparison() { $callback = static fn ($item) => (int) $item; - $this->validator->validate([1, '2', 3, '4.0'], new Unique([ - 'normalizer' => $callback, - ])); + $this->validator->validate([1, '2', 3, '4.0'], new Unique(normalizer: $callback)); $this->assertNoViolation(); } @@ -194,10 +188,10 @@ public function testExpectsInvalidCaseInsensitiveComparison() { $callback = static fn ($item) => mb_strtolower($item); - $this->validator->validate(['Hello', 'hello', 'HELLO', 'hellO'], new Unique([ - 'message' => 'myMessage', - 'normalizer' => $callback, - ])); + $this->validator->validate(['Hello', 'hello', 'HELLO', 'hellO'], new Unique( + message: 'myMessage', + normalizer: $callback, + )); $this->buildViolation('myMessage') ->setParameter('{{ value }}', '"hello"') @@ -209,9 +203,7 @@ public function testExpectsValidCaseInsensitiveComparison() { $callback = static fn ($item) => mb_strtolower($item); - $this->validator->validate(['Hello', 'World'], new Unique([ - 'normalizer' => $callback, - ])); + $this->validator->validate(['Hello', 'World'], new Unique(normalizer: $callback)); $this->assertNoViolation(); } @@ -248,9 +240,10 @@ public static function getInvalidFieldNames(): array */ public function testInvalidCollectionValues(array $value, array $fields, string $expectedMessageParam) { - $this->validator->validate($value, new Unique([ - 'message' => 'myMessage', - ], fields: $fields)); + $this->validator->validate($value, new Unique( + message: 'myMessage', + fields: $fields, + )); $this->buildViolation('myMessage') ->setParameter('{{ value }}', $expectedMessageParam) @@ -310,6 +303,161 @@ public function testArrayOfObjectsUnique() $this->assertNoViolation(); } + public function testErrorPath() + { + $array = [ + new DummyClassOne(), + new DummyClassOne(), + new DummyClassOne(), + ]; + + $array[0]->code = 'a1'; + $array[1]->code = 'a2'; + $array[2]->code = 'a1'; + + $this->validator->validate( + $array, + new Unique( + normalizer: [self::class, 'normalizeDummyClassOne'], + fields: 'code', + errorPath: 'code', + ) + ); + + $this->buildViolation('This collection should contain only unique elements.') + ->setParameter('{{ value }}', 'array') + ->setCode(Unique::IS_NOT_UNIQUE) + ->atPath('property.path[2].code') + ->assertRaised(); + } + + public function testErrorPathWithIteratorAggregate() + { + $array = new \ArrayObject([ + new DummyClassOne(), + new DummyClassOne(), + new DummyClassOne(), + ]); + + $array[0]->code = 'a1'; + $array[1]->code = 'a2'; + $array[2]->code = 'a1'; + + $this->validator->validate( + $array, + new Unique( + normalizer: [self::class, 'normalizeDummyClassOne'], + fields: 'code', + errorPath: 'code', + ) + ); + + $this->buildViolation('This collection should contain only unique elements.') + ->setParameter('{{ value }}', 'array') + ->setCode(Unique::IS_NOT_UNIQUE) + ->atPath('property.path[2].code') + ->assertRaised(); + } + + public function testErrorPathWithNonList() + { + $array = [ + 'a' => new DummyClassOne(), + 'b' => new DummyClassOne(), + 'c' => new DummyClassOne(), + ]; + + $array['a']->code = 'a1'; + $array['b']->code = 'a2'; + $array['c']->code = 'a1'; + + $this->validator->validate( + $array, + new Unique( + normalizer: [self::class, 'normalizeDummyClassOne'], + fields: 'code', + errorPath: 'code', + ) + ); + + $this->buildViolation('This collection should contain only unique elements.') + ->setParameter('{{ value }}', 'array') + ->setCode(Unique::IS_NOT_UNIQUE) + ->atPath('property.path[c].code') + ->assertRaised(); + } + + public function testWithoutStopOnFirstError() + { + $this->validator->validate( + ['a1', 'a2', 'a1', 'a1', 'a2'], + new Unique(stopOnFirstError: false), + ); + + $this + ->buildViolation('This collection should contain only unique elements.') + ->setParameter('{{ value }}', '"a1"') + ->setCode(Unique::IS_NOT_UNIQUE) + ->atPath('property.path[2]') + + ->buildNextViolation('This collection should contain only unique elements.') + ->setParameter('{{ value }}', '"a1"') + ->setCode(Unique::IS_NOT_UNIQUE) + ->atPath('property.path[3]') + + ->buildNextViolation('This collection should contain only unique elements.') + ->setParameter('{{ value }}', '"a2"') + ->setCode(Unique::IS_NOT_UNIQUE) + ->atPath('property.path[4]') + + ->assertRaised(); + } + + public function testWithoutStopOnFirstErrorWithErrorPath() + { + $array = [ + new DummyClassOne(), + new DummyClassOne(), + new DummyClassOne(), + new DummyClassOne(), + new DummyClassOne(), + ]; + + $array[0]->code = 'a1'; + $array[1]->code = 'a2'; + $array[2]->code = 'a1'; + $array[3]->code = 'a1'; + $array[4]->code = 'a2'; + + $this->validator->validate( + $array, + new Unique( + normalizer: [self::class, 'normalizeDummyClassOne'], + fields: 'code', + errorPath: 'code', + stopOnFirstError: false, + ) + ); + + $this + ->buildViolation('This collection should contain only unique elements.') + ->setParameter('{{ value }}', 'array') + ->setCode(Unique::IS_NOT_UNIQUE) + ->atPath('property.path[2].code') + + ->buildNextViolation('This collection should contain only unique elements.') + ->setParameter('{{ value }}', 'array') + ->setCode(Unique::IS_NOT_UNIQUE) + ->atPath('property.path[3].code') + + ->buildNextViolation('This collection should contain only unique elements.') + ->setParameter('{{ value }}', 'array') + ->setCode(Unique::IS_NOT_UNIQUE) + ->atPath('property.path[4].code') + + ->assertRaised(); + } + public static function normalizeDummyClassOne(DummyClassOne $obj): array { return [ diff --git a/Tests/Constraints/UrlTest.php b/Tests/Constraints/UrlTest.php index a7b053904..cbc9bc18c 100644 --- a/Tests/Constraints/UrlTest.php +++ b/Tests/Constraints/UrlTest.php @@ -24,23 +24,29 @@ class UrlTest extends TestCase { public function testNormalizerCanBeSet() { - $url = new Url(['normalizer' => 'trim']); + $url = new Url(normalizer: 'trim', requireTld: true); $this->assertEquals('trim', $url->normalizer); } + /** + * @group legacy + */ public function testInvalidNormalizerThrowsException() { $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('The "normalizer" option must be a valid callable ("string" given).'); - new Url(['normalizer' => 'Unknown Callable']); + new Url(['normalizer' => 'Unknown Callable', 'requireTld' => true]); } + /** + * @group legacy + */ public function testInvalidNormalizerObjectThrowsException() { $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('The "normalizer" option must be a valid callable ("stdClass" given).'); - new Url(['normalizer' => new \stdClass()]); + new Url(['normalizer' => new \stdClass(), 'requireTld' => true]); } public function testAttributes() @@ -52,28 +58,50 @@ public function testAttributes() self::assertSame(['http', 'https'], $aConstraint->protocols); self::assertFalse($aConstraint->relativeProtocol); self::assertNull($aConstraint->normalizer); + self::assertFalse($aConstraint->requireTld); [$bConstraint] = $metadata->properties['b']->getConstraints(); self::assertSame(['ftp', 'gopher'], $bConstraint->protocols); self::assertSame('trim', $bConstraint->normalizer); self::assertSame('myMessage', $bConstraint->message); self::assertSame(['Default', 'UrlDummy'], $bConstraint->groups); + self::assertFalse($bConstraint->requireTld); [$cConstraint] = $metadata->properties['c']->getConstraints(); self::assertTrue($cConstraint->relativeProtocol); self::assertSame(['my_group'], $cConstraint->groups); self::assertSame('some attached data', $cConstraint->payload); + self::assertFalse($cConstraint->requireTld); + + [$dConstraint] = $metadata->properties['d']->getConstraints(); + self::assertSame(['http', 'https'], $dConstraint->protocols); + self::assertFalse($dConstraint->relativeProtocol); + self::assertNull($dConstraint->normalizer); + self::assertTrue($dConstraint->requireTld); + } + + /** + * @group legacy + */ + public function testRequireTldDefaultsToFalse() + { + $constraint = new Url(); + + $this->assertFalse($constraint->requireTld); } } class UrlDummy { - #[Url] + #[Url(requireTld: false)] private $a; - #[Url(message: 'myMessage', protocols: ['ftp', 'gopher'], normalizer: 'trim')] + #[Url(message: 'myMessage', protocols: ['ftp', 'gopher'], normalizer: 'trim', requireTld: false)] private $b; - #[Url(relativeProtocol: true, groups: ['my_group'], payload: 'some attached data')] + #[Url(relativeProtocol: true, groups: ['my_group'], payload: 'some attached data', requireTld: false)] private $c; + + #[Url(requireTld: true)] + private $d; } diff --git a/Tests/Constraints/UrlValidatorTest.php b/Tests/Constraints/UrlValidatorTest.php index 6fe8c67c7..535714d1e 100644 --- a/Tests/Constraints/UrlValidatorTest.php +++ b/Tests/Constraints/UrlValidatorTest.php @@ -25,21 +25,21 @@ protected function createValidator(): UrlValidator public function testNullIsValid() { - $this->validator->validate(null, new Url()); + $this->validator->validate(null, new Url(requireTld: true)); $this->assertNoViolation(); } public function testEmptyStringIsValid() { - $this->validator->validate('', new Url()); + $this->validator->validate('', new Url(requireTld: true)); $this->assertNoViolation(); } public function testEmptyStringFromObjectIsValid() { - $this->validator->validate(new EmailProvider(), new Url()); + $this->validator->validate(new EmailProvider(), new Url(requireTld: true)); $this->assertNoViolation(); } @@ -47,7 +47,7 @@ public function testEmptyStringFromObjectIsValid() public function testExpectsStringCompatibleType() { $this->expectException(UnexpectedValueException::class); - $this->validator->validate(new \stdClass(), new Url()); + $this->validator->validate(new \stdClass(), new Url(requireTld: true)); } /** @@ -55,7 +55,7 @@ public function testExpectsStringCompatibleType() */ public function testValidUrls($url) { - $this->validator->validate($url, new Url()); + $this->validator->validate($url, new Url(requireTld: false)); $this->assertNoViolation(); } @@ -65,7 +65,7 @@ public function testValidUrls($url) */ public function testValidUrlsWithNewLine($url) { - $this->validator->validate($url."\n", new Url()); + $this->validator->validate($url."\n", new Url(requireTld: false)); $this->buildViolation('This value is not a valid URL.') ->setParameter('{{ value }}', '"'.$url."\n".'"') @@ -78,7 +78,10 @@ public function testValidUrlsWithNewLine($url) */ public function testValidUrlsWithWhitespaces($url) { - $this->validator->validate($url, new Url(['normalizer' => 'trim'])); + $this->validator->validate($url, new Url( + normalizer: 'trim', + requireTld: true, + )); $this->assertNoViolation(); } @@ -89,9 +92,10 @@ public function testValidUrlsWithWhitespaces($url) */ public function testValidRelativeUrl($url) { - $constraint = new Url([ - 'relativeProtocol' => true, - ]); + $constraint = new Url( + relativeProtocol: true, + requireTld: false, + ); $this->validator->validate($url, $constraint); @@ -104,9 +108,7 @@ public function testValidRelativeUrl($url) */ public function testValidRelativeUrlWithNewLine(string $url) { - $constraint = new Url([ - 'relativeProtocol' => true, - ]); + $constraint = new Url(relativeProtocol: true, requireTld: false); $this->validator->validate($url."\n", $constraint); @@ -231,9 +233,10 @@ public static function getValidUrlsWithWhitespaces() */ public function testInvalidUrls($url) { - $constraint = new Url([ - 'message' => 'myMessage', - ]); + $constraint = new Url( + message: 'myMessage', + requireTld: false, + ); $this->validator->validate($url, $constraint); @@ -249,10 +252,11 @@ public function testInvalidUrls($url) */ public function testInvalidRelativeUrl($url) { - $constraint = new Url([ - 'message' => 'myMessage', - 'relativeProtocol' => true, - ]); + $constraint = new Url( + message: 'myMessage', + relativeProtocol: true, + requireTld: false, + ); $this->validator->validate($url, $constraint); @@ -329,11 +333,12 @@ public static function getInvalidUrls() /** * @dataProvider getValidCustomUrls */ - public function testCustomProtocolIsValid($url) + public function testCustomProtocolIsValid($url, $requireTld) { - $constraint = new Url([ - 'protocols' => ['ftp', 'file', 'git'], - ]); + $constraint = new Url( + protocols: ['ftp', 'file', 'git'], + requireTld: $requireTld, + ); $this->validator->validate($url, $constraint); @@ -343,11 +348,48 @@ public function testCustomProtocolIsValid($url) public static function getValidCustomUrls() { return [ - ['ftp://example.com'], - ['file://127.0.0.1'], - ['git://[::1]/'], + ['ftp://example.com', true], + ['file://127.0.0.1', false], + ['git://[::1]/', false], ]; } + + /** + * @dataProvider getUrlsForRequiredTld + */ + public function testRequiredTld(string $url, bool $requireTld, bool $isValid) + { + $constraint = new Url(requireTld: $requireTld); + + $this->validator->validate($url, $constraint); + + if ($isValid) { + $this->assertNoViolation(); + } else { + $this->buildViolation($constraint->tldMessage) + ->setParameter('{{ value }}', '"'.$url.'"') + ->setCode(Url::MISSING_TLD_ERROR) + ->assertRaised(); + } + } + + public static function getUrlsForRequiredTld(): iterable + { + yield ['https://aaa/', true, false]; + yield ['https://aaa/', false, true]; + yield ['https://localhost/', true, false]; + yield ['https://localhost/', false, true]; + yield ['http://127.0.0.1/', false, true]; + yield ['http://127.0.0.1/', true, false]; + yield ['http://user.pass@local/', false, true]; + yield ['http://user.pass@local/', true, false]; + yield ['https://example.com/', true, true]; + yield ['https://example.com/', false, true]; + yield ['http://foo/bar.png', false, true]; + yield ['http://foo/bar.png', true, false]; + yield ['https://example.com.org/', true, true]; + yield ['https://example.com.org/', false, true]; + } } class EmailProvider diff --git a/Tests/Constraints/UuidTest.php b/Tests/Constraints/UuidTest.php index 3da8b8133..22901a9db 100644 --- a/Tests/Constraints/UuidTest.php +++ b/Tests/Constraints/UuidTest.php @@ -24,11 +24,14 @@ class UuidTest extends TestCase { public function testNormalizerCanBeSet() { - $uuid = new Uuid(['normalizer' => 'trim']); + $uuid = new Uuid(normalizer: 'trim'); $this->assertEquals('trim', $uuid->normalizer); } + /** + * @group legacy + */ public function testInvalidNormalizerThrowsException() { $this->expectException(InvalidArgumentException::class); @@ -36,6 +39,9 @@ public function testInvalidNormalizerThrowsException() new Uuid(['normalizer' => 'Unknown Callable']); } + /** + * @group legacy + */ public function testInvalidNormalizerObjectThrowsException() { $this->expectException(InvalidArgumentException::class); diff --git a/Tests/Constraints/UuidValidatorTest.php b/Tests/Constraints/UuidValidatorTest.php index da78e4cc7..84edc6612 100644 --- a/Tests/Constraints/UuidValidatorTest.php +++ b/Tests/Constraints/UuidValidatorTest.php @@ -44,9 +44,10 @@ public function testEmptyStringIsValid() public function testExpectsUuidConstraintCompatibleType() { - $this->expectException(UnexpectedTypeException::class); $constraint = $this->createStub(Constraint::class); + $this->expectException(UnexpectedTypeException::class); + $this->validator->validate('216fff40-98d9-11e3-a5e2-0800200c9a66', $constraint); } @@ -92,7 +93,7 @@ public static function getValidStrictUuids() */ public function testValidStrictUuidsWithWhitespaces($uuid, $versions = null) { - $constraint = new Uuid(['normalizer' => 'trim']); + $constraint = new Uuid(normalizer: 'trim'); if (null !== $versions) { $constraint->versions = $versions; @@ -130,9 +131,7 @@ public function testValidStrictUuidWithWhitespacesNamed() */ public function testInvalidStrictUuids($uuid, $code, $versions = null) { - $constraint = new Uuid([ - 'message' => 'testMessage', - ]); + $constraint = new Uuid(message: 'testMessage'); if (null !== $versions) { $constraint->versions = $versions; @@ -194,9 +193,7 @@ public static function getInvalidStrictUuids() */ public function testValidNonStrictUuids($uuid) { - $constraint = new Uuid([ - 'strict' => false, - ]); + $constraint = new Uuid(strict: false); $this->validator->validate($uuid, $constraint); @@ -225,10 +222,10 @@ public static function getValidNonStrictUuids() */ public function testInvalidNonStrictUuids($uuid, $code) { - $constraint = new Uuid([ - 'strict' => false, - 'message' => 'myMessage', - ]); + $constraint = new Uuid( + strict: false, + message: 'myMessage', + ); $this->validator->validate($uuid, $constraint); @@ -269,9 +266,7 @@ public function testInvalidNonStrictUuidNamed() */ public function testTimeBasedUuid(string $uid, bool $expectedTimeBased) { - $constraint = new Uuid([ - 'versions' => Uuid::TIME_BASED_VERSIONS, - ]); + $constraint = new Uuid(versions: Uuid::TIME_BASED_VERSIONS); $this->validator->validate($uid, $constraint); diff --git a/Tests/Constraints/ValidComparisonToValueTrait.php b/Tests/Constraints/ValidComparisonToValueTrait.php new file mode 100644 index 000000000..c4c70d8ce --- /dev/null +++ b/Tests/Constraints/ValidComparisonToValueTrait.php @@ -0,0 +1,41 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Tests\Constraints; + +trait ValidComparisonToValueTrait +{ + /** + * @dataProvider provideAllValidComparisons + */ + public function testValidComparisonToValue($dirtyValue, $comparisonValue) + { + $constraint = $this->createConstraint(['value' => $comparisonValue]); + + $this->validator->validate($dirtyValue, $constraint); + + $this->assertNoViolation(); + } + + public static function provideAllValidComparisons(): array + { + // The provider runs before setUp(), so we need to manually fix + // the default timezone + $timezone = date_default_timezone_get(); + date_default_timezone_set('UTC'); + + $comparisons = self::addPhp5Dot5Comparisons(static::provideValidComparisons()); + + date_default_timezone_set($timezone); + + return $comparisons; + } +} diff --git a/Tests/Constraints/ValidTest.php b/Tests/Constraints/ValidTest.php index c56cdedd5..a862171f1 100644 --- a/Tests/Constraints/ValidTest.php +++ b/Tests/Constraints/ValidTest.php @@ -23,7 +23,7 @@ class ValidTest extends TestCase { public function testGroupsCanBeSet() { - $constraint = new Valid(['groups' => 'foo']); + $constraint = new Valid(groups: ['foo']); $this->assertSame(['foo'], $constraint->groups); } diff --git a/Tests/Constraints/WeekTest.php b/Tests/Constraints/WeekTest.php new file mode 100644 index 000000000..0fc9aac62 --- /dev/null +++ b/Tests/Constraints/WeekTest.php @@ -0,0 +1,101 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Tests\Constraints; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Validator\Constraints\Week; +use Symfony\Component\Validator\Exception\ConstraintDefinitionException; +use Symfony\Component\Validator\Mapping\ClassMetadata; +use Symfony\Component\Validator\Mapping\Loader\AttributeLoader; + +class WeekTest extends TestCase +{ + public function testWithoutArgument() + { + $week = new Week(); + + $this->assertNull($week->min); + $this->assertNull($week->max); + } + + public function testConstructor() + { + $week = new Week(min: '2010-W01', max: '2010-W02'); + + $this->assertSame('2010-W01', $week->min); + $this->assertSame('2010-W02', $week->max); + } + + public function testMinYearIsAfterMaxYear() + { + $this->expectException(ConstraintDefinitionException::class); + $this->expectExceptionMessage('The "Symfony\Component\Validator\Constraints\Week" constraint requires the min week to be less than or equal to the max week.'); + + new Week(min: '2011-W01', max: '2010-W02'); + } + + public function testMinWeekIsAfterMaxWeek() + { + $this->expectException(ConstraintDefinitionException::class); + $this->expectExceptionMessage('The "Symfony\Component\Validator\Constraints\Week" constraint requires the min week to be less than or equal to the max week.'); + + new Week(min: '2010-W02', max: '2010-W01'); + } + + public function testMinAndMaxWeeksAreTheSame() + { + $week = new Week(min: '2010-W01', max: '2010-W01'); + + $this->assertSame('2010-W01', $week->min); + $this->assertSame('2010-W01', $week->max); + } + + public function testMinIsBadlyFormatted() + { + $this->expectException(ConstraintDefinitionException::class); + $this->expectExceptionMessage('The "Symfony\Component\Validator\Constraints\Week" constraint requires the min week to be in the ISO 8601 format if set.'); + + new Week(min: '2010-01'); + } + + public function testMaxIsBadlyFormatted() + { + $this->expectException(ConstraintDefinitionException::class); + $this->expectExceptionMessage('The "Symfony\Component\Validator\Constraints\Week" constraint requires the max week to be in the ISO 8601 format if set.'); + + new Week(max: '2010-01'); + } + + public function testAttributes() + { + $metadata = new ClassMetadata(WeekDummy::class); + $loader = new AttributeLoader(); + $this->assertTrue($loader->loadClassMetadata($metadata)); + + [$aConstraint] = $metadata->properties['a']->getConstraints(); + $this->assertNull($aConstraint->min); + $this->assertNull($aConstraint->max); + + [$bConstraint] = $metadata->properties['b']->getConstraints(); + $this->assertSame('2010-W01', $bConstraint->min); + $this->assertSame('2010-W02', $bConstraint->max); + } +} + +class WeekDummy +{ + #[Week] + private string $a; + + #[Week(min: '2010-W01', max: '2010-W02')] + private string $b; +} diff --git a/Tests/Constraints/WeekValidatorTest.php b/Tests/Constraints/WeekValidatorTest.php new file mode 100644 index 000000000..0a5f0936c --- /dev/null +++ b/Tests/Constraints/WeekValidatorTest.php @@ -0,0 +1,142 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Tests\Constraints; + +use Symfony\Component\Validator\Constraints\Week; +use Symfony\Component\Validator\Constraints\WeekValidator; +use Symfony\Component\Validator\Exception\UnexpectedValueException; +use Symfony\Component\Validator\Test\ConstraintValidatorTestCase; +use Symfony\Component\Validator\Tests\Constraints\Fixtures\StringableValue; + +class WeekValidatorTest extends ConstraintValidatorTestCase +{ + protected function createValidator(): WeekValidator + { + return new WeekValidator(); + } + + /** + * @dataProvider provideWeekNumber + */ + public function testWeekIsValidWeekNumber(string|\Stringable $value, bool $expectedViolation) + { + $constraint = new Week(); + $this->validator->validate($value, $constraint); + + if ($expectedViolation) { + $this->buildViolation('This value is not a valid week.') + ->setCode(Week::INVALID_WEEK_NUMBER_ERROR) + ->setParameter('{{ value }}', $value) + ->assertRaised(); + + return; + } + + $this->assertNoViolation(); + } + + public static function provideWeekNumber() + { + yield ['2015-W53', false]; // 2015 has 53 weeks + yield ['2020-W53', false]; // 2020 also has 53 weeks + yield ['2024-W53', true]; // 2024 has 52 weeks + yield [new StringableValue('2024-W53'), true]; + } + + public function testBounds() + { + $constraint = new Week(min: '2015-W10', max: '2016-W25'); + + $this->validator->validate('2015-W10', $constraint); + $this->assertNoViolation(); + + $this->validator->validate('2016-W25', $constraint); + $this->assertNoViolation(); + } + + public function testTooLow() + { + $constraint = new Week(min: '2015-W10'); + + $this->validator->validate('2015-W08', $constraint); + $this->buildViolation('This value should not be before week "{{ min }}".') + ->setInvalidValue('2015-W08') + ->setParameter('{{ min }}', '2015-W10') + ->setCode(Week::TOO_LOW_ERROR) + ->assertRaised(); + } + + public function testTooHigh() + { + $constraint = new Week(max: '2016-W25'); + + $this->validator->validate('2016-W30', $constraint); + $this->buildViolation('This value should not be after week "{{ max }}".') + ->setInvalidValue('2016-W30') + ->setParameter('{{ max }}', '2016-W25') + ->setCode(Week::TOO_HIGH_ERROR) + ->assertRaised(); + } + + public function testWithNewLine() + { + $this->validator->validate("2015-W10\n", new Week()); + + $this->buildViolation('This value does not represent a valid week in the ISO 8601 format.') + ->setCode(Week::INVALID_FORMAT_ERROR) + ->assertRaised(); + } + + /** + * @dataProvider provideInvalidValues + */ + public function testInvalidValues(string $value) + { + $this->validator->validate($value, new Week()); + + $this->buildViolation('This value does not represent a valid week in the ISO 8601 format.') + ->setCode(Week::INVALID_FORMAT_ERROR) + ->assertRaised(); + } + + /** + * @dataProvider provideInvalidTypes + */ + public function testNonStringValues(mixed $value) + { + $this->expectException(UnexpectedValueException::class); + $this->expectExceptionMessageMatches('/Expected argument of type "string", ".*" given/'); + + $this->validator->validate($value, new Week()); + } + + public static function provideInvalidValues() + { + yield ['1970-01']; + yield ['1970-W00']; + yield ['1970-W54']; + yield ['1970-W100']; + yield ['1970-W01-01']; + yield ['-W01']; + yield ['24-W01']; + } + + public static function provideInvalidTypes() + { + yield [true]; + yield [false]; + yield [1]; + yield [1.1]; + yield [[]]; + yield [new \stdClass()]; + } +} diff --git a/Tests/Constraints/WhenTest.php b/Tests/Constraints/WhenTest.php index 72b08d6bd..6f82c6429 100644 --- a/Tests/Constraints/WhenTest.php +++ b/Tests/Constraints/WhenTest.php @@ -11,24 +11,24 @@ namespace Symfony\Component\Validator\Tests\Constraints; -use Doctrine\Common\Annotations\AnnotationReader; use PHPUnit\Framework\TestCase; -use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Symfony\Component\Validator\Constraints\Callback; +use Symfony\Component\Validator\Constraints\Length; use Symfony\Component\Validator\Constraints\NotBlank; use Symfony\Component\Validator\Constraints\NotNull; use Symfony\Component\Validator\Constraints\When; use Symfony\Component\Validator\Exception\ConstraintDefinitionException; use Symfony\Component\Validator\Exception\MissingOptionsException; use Symfony\Component\Validator\Mapping\ClassMetadata; -use Symfony\Component\Validator\Mapping\Loader\AnnotationLoader; use Symfony\Component\Validator\Mapping\Loader\AttributeLoader; use Symfony\Component\Validator\Tests\Constraints\Fixtures\WhenTestWithAttributes; +use Symfony\Component\Validator\Tests\Constraints\Fixtures\WhenTestWithClosure; final class WhenTest extends TestCase { - use ExpectDeprecationTrait; - + /** + * @group legacy + */ public function testMissingOptionsExceptionIsThrown() { $this->expectException(MissingOptionsException::class); @@ -49,24 +49,13 @@ public function testNonConstraintsAreRejected() { $this->expectException(ConstraintDefinitionException::class); $this->expectExceptionMessage('The value "foo" is not an instance of Constraint in constraint "Symfony\Component\Validator\Constraints\When"'); - new When('true', [ - 'foo', - ]); + new When('true', ['foo']); } - /** - * @group legacy - */ - public function testAnnotations() + public function testAttributes() { - $loader = new AnnotationLoader(new AnnotationReader()); - $metadata = new ClassMetadata(WhenTestWithAnnotations::class); - - $this->expectDeprecation('Since symfony/validator 6.4: Class "Symfony\Component\Validator\Tests\Constraints\WhenTestWithAnnotations" uses Doctrine Annotations to configure validation constraints, which is deprecated. Use PHP attributes instead.'); - $this->expectDeprecation('Since symfony/validator 6.4: Property "Symfony\Component\Validator\Tests\Constraints\WhenTestWithAnnotations::$foo" uses Doctrine Annotations to configure validation constraints, which is deprecated. Use PHP attributes instead.'); - $this->expectDeprecation('Since symfony/validator 6.4: Property "Symfony\Component\Validator\Tests\Constraints\WhenTestWithAnnotations::$bar" uses Doctrine Annotations to configure validation constraints, which is deprecated. Use PHP attributes instead.'); - $this->expectDeprecation('Since symfony/validator 6.4: Property "Symfony\Component\Validator\Tests\Constraints\WhenTestWithAnnotations::$qux" uses Doctrine Annotations to configure validation constraints, which is deprecated. Use PHP attributes instead.'); - $this->expectDeprecation('Since symfony/validator 6.4: Method "Symfony\Component\Validator\Tests\Constraints\WhenTestWithAnnotations::getBaz()" uses Doctrine Annotations to configure validation constraints, which is deprecated. Use PHP attributes instead.'); + $loader = new AttributeLoader(); + $metadata = new ClassMetadata(WhenTestWithAttributes::class); self::assertTrue($loader->loadClassMetadata($metadata)); @@ -75,49 +64,41 @@ public function testAnnotations() self::assertInstanceOf(When::class, $classConstraint); self::assertSame('true', $classConstraint->expression); self::assertEquals([ - new Callback([ - 'callback' => 'callback', - 'groups' => ['Default', 'WhenTestWithAnnotations'], - ]), + new Callback( + callback: 'callback', + groups: ['Default', 'WhenTestWithAttributes'], + ), ], $classConstraint->constraints); + self::assertSame([], $classConstraint->otherwise); [$fooConstraint] = $metadata->properties['foo']->getConstraints(); self::assertInstanceOf(When::class, $fooConstraint); self::assertSame('true', $fooConstraint->expression); self::assertEquals([ - new NotNull([ - 'groups' => ['Default', 'WhenTestWithAnnotations'], - ]), - new NotBlank([ - 'groups' => ['Default', 'WhenTestWithAnnotations'], - ]), + new NotNull(groups: ['Default', 'WhenTestWithAttributes']), + new NotBlank(groups: ['Default', 'WhenTestWithAttributes']), ], $fooConstraint->constraints); - self::assertSame(['Default', 'WhenTestWithAnnotations'], $fooConstraint->groups); + self::assertSame([], $fooConstraint->otherwise); + self::assertSame(['Default', 'WhenTestWithAttributes'], $fooConstraint->groups); [$barConstraint] = $metadata->properties['bar']->getConstraints(); self::assertInstanceOf(When::class, $barConstraint); self::assertSame('false', $barConstraint->expression); self::assertEquals([ - new NotNull([ - 'groups' => ['foo'], - ]), - new NotBlank([ - 'groups' => ['foo'], - ]), + new NotNull(groups: ['foo']), + new NotBlank(groups: ['foo']), ], $barConstraint->constraints); + self::assertSame([], $barConstraint->otherwise); self::assertSame(['foo'], $barConstraint->groups); [$quxConstraint] = $metadata->properties['qux']->getConstraints(); self::assertInstanceOf(When::class, $quxConstraint); self::assertSame('true', $quxConstraint->expression); - self::assertEquals([ - new NotNull([ - 'groups' => ['foo'], - ]), - ], $quxConstraint->constraints); + self::assertEquals([new NotNull(groups: ['foo'])], $quxConstraint->constraints); + self::assertSame([], $quxConstraint->otherwise); self::assertSame(['foo'], $quxConstraint->groups); [$bazConstraint] = $metadata->getters['baz']->getConstraints(); @@ -125,118 +106,52 @@ public function testAnnotations() self::assertInstanceOf(When::class, $bazConstraint); self::assertSame('true', $bazConstraint->expression); self::assertEquals([ - new NotNull([ - 'groups' => ['Default', 'WhenTestWithAnnotations'], - ]), - new NotBlank([ - 'groups' => ['Default', 'WhenTestWithAnnotations'], - ]), + new NotNull(groups: ['Default', 'WhenTestWithAttributes']), + new NotBlank(groups: ['Default', 'WhenTestWithAttributes']), ], $bazConstraint->constraints); - self::assertSame(['Default', 'WhenTestWithAnnotations'], $bazConstraint->groups); + self::assertSame([], $bazConstraint->otherwise); + self::assertSame(['Default', 'WhenTestWithAttributes'], $bazConstraint->groups); + + [$quuxConstraint] = $metadata->properties['quux']->getConstraints(); + + self::assertInstanceOf(When::class, $quuxConstraint); + self::assertSame('true', $quuxConstraint->expression); + self::assertEquals([new NotNull(groups: ['foo'])], $quuxConstraint->constraints); + self::assertEquals([new Length(exactly: 10, groups: ['foo'])], $quuxConstraint->otherwise); + self::assertSame(['foo'], $quuxConstraint->groups); } - public function testAttributes() + /** + * @requires PHP 8.5 + */ + public function testAttributesWithClosure() { $loader = new AttributeLoader(); - $metadata = new ClassMetadata(WhenTestWithAttributes::class); + $metadata = new ClassMetadata(WhenTestWithClosure::class); self::assertTrue($loader->loadClassMetadata($metadata)); [$classConstraint] = $metadata->getConstraints(); self::assertInstanceOf(When::class, $classConstraint); - self::assertSame('true', $classConstraint->expression); + self::assertInstanceOf(\Closure::class, $classConstraint->expression); self::assertEquals([ - new Callback([ - 'callback' => 'callback', - 'groups' => ['Default', 'WhenTestWithAttributes'], - ]), + new Callback( + callback: 'isValid', + groups: ['Default', 'WhenTestWithClosure'], + ), ], $classConstraint->constraints); + self::assertSame([], $classConstraint->otherwise); [$fooConstraint] = $metadata->properties['foo']->getConstraints(); self::assertInstanceOf(When::class, $fooConstraint); - self::assertSame('true', $fooConstraint->expression); + self::assertInstanceOf(\Closure::class, $fooConstraint->expression); self::assertEquals([ - new NotNull([ - 'groups' => ['Default', 'WhenTestWithAttributes'], - ]), - new NotBlank([ - 'groups' => ['Default', 'WhenTestWithAttributes'], - ]), + new NotNull(groups: ['Default', 'WhenTestWithClosure']), + new NotBlank(groups: ['Default', 'WhenTestWithClosure']), ], $fooConstraint->constraints); - self::assertSame(['Default', 'WhenTestWithAttributes'], $fooConstraint->groups); - - [$barConstraint] = $metadata->properties['bar']->getConstraints(); - - self::assertInstanceOf(When::class, $barConstraint); - self::assertSame('false', $barConstraint->expression); - self::assertEquals([ - new NotNull([ - 'groups' => ['foo'], - ]), - new NotBlank([ - 'groups' => ['foo'], - ]), - ], $barConstraint->constraints); - self::assertSame(['foo'], $barConstraint->groups); - - [$quxConstraint] = $metadata->properties['qux']->getConstraints(); - - self::assertInstanceOf(When::class, $quxConstraint); - self::assertSame('true', $quxConstraint->expression); - self::assertEquals([ - new NotNull([ - 'groups' => ['foo'], - ]), - ], $quxConstraint->constraints); - self::assertSame(['foo'], $quxConstraint->groups); - - [$bazConstraint] = $metadata->getters['baz']->getConstraints(); - - self::assertInstanceOf(When::class, $bazConstraint); - self::assertSame('true', $bazConstraint->expression); - self::assertEquals([ - new NotNull([ - 'groups' => ['Default', 'WhenTestWithAttributes'], - ]), - new NotBlank([ - 'groups' => ['Default', 'WhenTestWithAttributes'], - ]), - ], $bazConstraint->constraints); - self::assertSame(['Default', 'WhenTestWithAttributes'], $bazConstraint->groups); - } -} - -/** - * @When(expression="true", constraints={@Callback("callback")}) - */ -class WhenTestWithAnnotations -{ - /** - * @When(expression="true", constraints={@NotNull, @NotBlank}) - */ - private $foo; - - /** - * @When(expression="false", constraints={@NotNull, @NotBlank}, groups={"foo"}) - */ - private $bar; - - /** - * @When(expression="true", constraints=@NotNull, groups={"foo"}) - */ - private $qux; - - /** - * @When(expression="true", constraints={@NotNull, @NotBlank}) - */ - public function getBaz() - { - return null; - } - - public function callback() - { + self::assertSame([], $fooConstraint->otherwise); + self::assertSame(['Default', 'WhenTestWithClosure'], $fooConstraint->groups); } } diff --git a/Tests/Constraints/WhenValidatorTest.php b/Tests/Constraints/WhenValidatorTest.php index cba81b25a..35d8b8ce9 100644 --- a/Tests/Constraints/WhenValidatorTest.php +++ b/Tests/Constraints/WhenValidatorTest.php @@ -13,6 +13,7 @@ use Symfony\Component\Validator\Constraints\Blank; use Symfony\Component\Validator\Constraints\Callback; +use Symfony\Component\Validator\Constraints\Length; use Symfony\Component\Validator\Constraints\NegativeOrZero; use Symfony\Component\Validator\Constraints\NotBlank; use Symfony\Component\Validator\Constraints\NotNull; @@ -33,10 +34,38 @@ public function testConstraintsAreExecuted() $this->expectValidateValue(0, 'Foo', $constraints); - $this->validator->validate('Foo', new When([ - 'expression' => 'true', - 'constraints' => $constraints, - ])); + $this->validator->validate('Foo', new When( + expression: 'true', + constraints: $constraints, + )); + } + + public function testConstraintsAreExecutedWhenClosureIsTrue() + { + $constraints = [ + new NotNull(), + new NotBlank(), + ]; + + $this->expectValidateValue(0, 'Foo', $constraints); + + $this->validator->validate('Foo', new When( + expression: static fn () => true, + constraints: $constraints, + )); + } + + public function testClosureTakesSubject() + { + $subject = new \stdClass(); + $this->setObject($subject); + + $this->validator->validate($subject, new When( + expression: static function ($closureSubject) use ($subject) { + self::assertSame($subject, $closureSubject); + }, + constraints: new NotNull(), + )); } public function testConstraintIsExecuted() @@ -44,10 +73,38 @@ public function testConstraintIsExecuted() $constraint = new NotNull(); $this->expectValidateValue(0, 'Foo', [$constraint]); - $this->validator->validate('Foo', new When([ - 'expression' => 'true', - 'constraints' => $constraint, - ])); + $this->validator->validate('Foo', new When( + expression: 'true', + constraints: $constraint, + )); + } + + public function testOtherwiseIsExecutedWhenFalse() + { + $constraint = new NotNull(); + $otherwise = new Length(exactly: 10); + + $this->expectValidateValue(0, 'Foo', [$otherwise]); + + $this->validator->validate('Foo', new When( + expression: 'false', + constraints: $constraint, + otherwise: $otherwise, + )); + } + + public function testOtherwiseIsExecutedWhenClosureReturnsFalse() + { + $constraint = new NotNull(); + $otherwise = new Length(exactly: 10); + + $this->expectValidateValue(0, 'Foo', [$otherwise]); + + $this->validator->validate('Foo', new When( + expression: static fn () => false, + constraints: $constraint, + otherwise: $otherwise, + )); } public function testConstraintsAreExecutedWithNull() @@ -58,10 +115,10 @@ public function testConstraintsAreExecutedWithNull() $this->expectValidateValue(0, null, $constraints); - $this->validator->validate(null, new When([ - 'expression' => 'true', - 'constraints' => $constraints, - ])); + $this->validator->validate(null, new When( + expression: 'true', + constraints: $constraints, + )); } public function testConstraintsAreExecutedWithObject() @@ -79,10 +136,35 @@ public function testConstraintsAreExecutedWithObject() $this->expectValidateValue(0, $number->value, $constraints); - $this->validator->validate($number->value, new When([ - 'expression' => 'this.type === "positive"', - 'constraints' => $constraints, - ])); + $this->validator->validate($number->value, new When( + expression: 'this.type === "positive"', + constraints: $constraints, + )); + } + + public function testConstraintsAreExecutedWithNestedObject() + { + $parent = new \stdClass(); + $parent->child = new \stdClass(); + $parent->ok = true; + + $number = new \stdClass(); + $number->value = 1; + + $this->setObject($parent); + $this->setPropertyPath('child.value'); + $this->setRoot($parent); + + $constraints = [ + new PositiveOrZero(), + ]; + + $this->expectValidateValue(0, $number->value, $constraints); + + $this->validator->validate($number->value, new When( + expression: 'context.getRoot().ok === true', + constraints: $constraints, + )); } public function testConstraintsAreExecutedWithValue() @@ -93,10 +175,10 @@ public function testConstraintsAreExecutedWithValue() $this->expectValidateValue(0, 'foo', $constraints); - $this->validator->validate('foo', new When([ - 'expression' => 'value === "foo"', - 'constraints' => $constraints, - ])); + $this->validator->validate('foo', new When( + expression: 'value === "foo"', + constraints: $constraints, + )); } public function testConstraintsAreExecutedWithExpressionValues() @@ -107,14 +189,14 @@ public function testConstraintsAreExecutedWithExpressionValues() $this->expectValidateValue(0, 'foo', $constraints); - $this->validator->validate('foo', new When([ - 'expression' => 'activated && value === compared_value', - 'constraints' => $constraints, - 'values' => [ + $this->validator->validate('foo', new When( + expression: 'activated && value === compared_value', + constraints: $constraints, + values: [ 'activated' => true, 'compared_value' => 'foo', ], - ])); + )); } public function testConstraintsNotExecuted() @@ -126,10 +208,25 @@ public function testConstraintsNotExecuted() $this->expectNoValidate(); - $this->validator->validate('', new When([ - 'expression' => 'false', - 'constraints' => $constraints, - ])); + $this->validator->validate('', new When( + expression: 'false', + constraints: $constraints, + )); + + $this->assertNoViolation(); + } + + public function testOtherwiseIsExecutedWhenTrue() + { + $constraints = [new NotNull()]; + + $this->expectValidateValue(0, '', $constraints); + + $this->validator->validate('', new When( + expression: 'true', + constraints: $constraints, + otherwise: new Length(exactly: 10), + )); $this->assertNoViolation(); } @@ -149,10 +246,10 @@ public function testConstraintsNotExecutedWithObject() $this->expectNoValidate(); - $this->validator->validate($number->value, new When([ - 'expression' => 'this.type !== "positive"', - 'constraints' => $constraints, - ])); + $this->validator->validate($number->value, new When( + expression: 'this.type !== "positive"', + constraints: $constraints, + )); $this->assertNoViolation(); } @@ -165,10 +262,10 @@ public function testConstraintsNotExecutedWithValue() $this->expectNoValidate(); - $this->validator->validate('foo', new When([ - 'expression' => 'value === null', - 'constraints' => $constraints, - ])); + $this->validator->validate('foo', new When( + expression: 'value === null', + constraints: $constraints, + )); $this->assertNoViolation(); } @@ -181,14 +278,14 @@ public function testConstraintsNotExecutedWithExpressionValues() $this->expectNoValidate(); - $this->validator->validate('foo', new When([ - 'expression' => 'activated && value === compared_value', - 'constraints' => $constraints, - 'values' => [ + $this->validator->validate('foo', new When( + expression: 'activated && value === compared_value', + constraints: $constraints, + values: [ 'activated' => true, 'compared_value' => 'bar', ], - ])); + )); $this->assertNoViolation(); } @@ -196,9 +293,7 @@ public function testConstraintsNotExecutedWithExpressionValues() public function testConstraintViolations() { $constraints = [ - new Blank([ - 'message' => 'my_message', - ]), + new Blank(message: 'my_message'), ]; $this->expectFailingValueValidation( 0, diff --git a/Tests/Constraints/WordCountTest.php b/Tests/Constraints/WordCountTest.php new file mode 100644 index 000000000..50ed8081d --- /dev/null +++ b/Tests/Constraints/WordCountTest.php @@ -0,0 +1,141 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Tests\Constraints; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Validator\Constraints\WordCount; +use Symfony\Component\Validator\Exception\ConstraintDefinitionException; +use Symfony\Component\Validator\Exception\MissingOptionsException; +use Symfony\Component\Validator\Mapping\ClassMetadata; +use Symfony\Component\Validator\Mapping\Loader\AttributeLoader; + +/** + * @requires extension intl + */ +class WordCountTest extends TestCase +{ + public function testLocaleIsSet() + { + $wordCount = new WordCount(min: 1, locale: 'en'); + + $this->assertSame('en', $wordCount->locale); + } + + public function testOnlyMinIsSet() + { + $wordCount = new WordCount(1); + + $this->assertSame(1, $wordCount->min); + $this->assertNull($wordCount->max); + $this->assertNull($wordCount->locale); + } + + public function testOnlyMaxIsSet() + { + $wordCount = new WordCount(max: 1); + + $this->assertNull($wordCount->min); + $this->assertSame(1, $wordCount->max); + $this->assertNull($wordCount->locale); + } + + public function testMinIsNegative() + { + $this->expectException(ConstraintDefinitionException::class); + $this->expectExceptionMessage('The "Symfony\Component\Validator\Constraints\WordCount" constraint requires the min word count to be a positive integer if set.'); + + new WordCount(-1); + } + + public function testMinIsZero() + { + $this->expectException(ConstraintDefinitionException::class); + $this->expectExceptionMessage('The "Symfony\Component\Validator\Constraints\WordCount" constraint requires the min word count to be a positive integer if set.'); + + new WordCount(0); + } + + public function testMaxIsNegative() + { + $this->expectException(ConstraintDefinitionException::class); + $this->expectExceptionMessage('The "Symfony\Component\Validator\Constraints\WordCount" constraint requires the max word count to be a positive integer if set.'); + + new WordCount(max: -1); + } + + public function testMaxIsZero() + { + $this->expectException(ConstraintDefinitionException::class); + $this->expectExceptionMessage('The "Symfony\Component\Validator\Constraints\WordCount" constraint requires the max word count to be a positive integer if set.'); + + new WordCount(max: 0); + } + + public function testNothingIsSet() + { + $this->expectException(MissingOptionsException::class); + $this->expectExceptionMessage('Either option "min" or "max" must be given for constraint "Symfony\Component\Validator\Constraints\WordCount".'); + + new WordCount(); + } + + public function testMaxIsLessThanMin() + { + $this->expectException(ConstraintDefinitionException::class); + $this->expectExceptionMessage('The "Symfony\Component\Validator\Constraints\WordCount" constraint requires the min word count to be less than or equal to the max word count.'); + + new WordCount(2, 1); + } + + public function testMinAndMaxAreEquals() + { + $wordCount = new WordCount(1, 1); + + $this->assertSame(1, $wordCount->min); + $this->assertSame(1, $wordCount->max); + $this->assertNull($wordCount->locale); + } + + public function testAttributes() + { + $metadata = new ClassMetadata(WordCountDummy::class); + $loader = new AttributeLoader(); + $this->assertTrue($loader->loadClassMetadata($metadata)); + + [$aConstraint] = $metadata->properties['a']->getConstraints(); + $this->assertSame(1, $aConstraint->min); + $this->assertNull($aConstraint->max); + $this->assertNull($aConstraint->locale); + + [$bConstraint] = $metadata->properties['b']->getConstraints(); + $this->assertSame(2, $bConstraint->min); + $this->assertSame(5, $bConstraint->max); + $this->assertNull($bConstraint->locale); + + [$cConstraint] = $metadata->properties['c']->getConstraints(); + $this->assertSame(3, $cConstraint->min); + $this->assertNull($cConstraint->max); + $this->assertSame('en', $cConstraint->locale); + } +} + +class WordCountDummy +{ + #[WordCount(min: 1)] + private string $a; + + #[WordCount(min: 2, max: 5)] + private string $b; + + #[WordCount(min: 3, locale: 'en')] + private string $c; +} diff --git a/Tests/Constraints/WordCountValidatorTest.php b/Tests/Constraints/WordCountValidatorTest.php new file mode 100644 index 000000000..ce1256f92 --- /dev/null +++ b/Tests/Constraints/WordCountValidatorTest.php @@ -0,0 +1,98 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Tests\Constraints; + +use Symfony\Component\Validator\Constraints\WordCount; +use Symfony\Component\Validator\Constraints\WordCountValidator; +use Symfony\Component\Validator\Exception\UnexpectedValueException; +use Symfony\Component\Validator\Test\ConstraintValidatorTestCase; +use Symfony\Component\Validator\Tests\Constraints\Fixtures\StringableValue; + +/** + * @requires extension intl + */ +class WordCountValidatorTest extends ConstraintValidatorTestCase +{ + protected function createValidator(): WordCountValidator + { + return new WordCountValidator(); + } + + /** + * @dataProvider provideValidValues + */ + public function testValidWordCount(string|\Stringable|null $value, int $expectedWordCount) + { + $this->validator->validate($value, new WordCount(min: $expectedWordCount, max: $expectedWordCount)); + + $this->assertNoViolation(); + } + + public function testTooShort() + { + $constraint = new WordCount(min: 4, minMessage: 'myMessage'); + $this->validator->validate('my ascii string', $constraint); + + $this->buildViolation('myMessage') + ->setParameter('{{ count }}', 3) + ->setParameter('{{ min }}', 4) + ->setPlural(4) + ->setInvalidValue('my ascii string') + ->assertRaised(); + } + + public function testTooLong() + { + $constraint = new WordCount(max: 3, maxMessage: 'myMessage'); + $this->validator->validate('my beautiful ascii string', $constraint); + + $this->buildViolation('myMessage') + ->setParameter('{{ count }}', 4) + ->setParameter('{{ max }}', 3) + ->setPlural(3) + ->setInvalidValue('my beautiful ascii string') + ->assertRaised(); + } + + /** + * @dataProvider provideInvalidTypes + */ + public function testNonStringValues(mixed $value) + { + $this->expectException(UnexpectedValueException::class); + $this->expectExceptionMessageMatches('/Expected argument of type "string", ".*" given/'); + + $this->validator->validate($value, new WordCount(min: 1)); + } + + public static function provideValidValues() + { + yield ['my ascii string', 3]; + yield [" with a\nnewline", 3]; + yield ['皆さん、こんにちは。', 4]; + yield ['你好,世界!这是一个测试。', 9]; + yield [new StringableValue('my ûtf 8'), 3]; + yield [null, 1]; // null should always pass and eventually be handled by NotNullValidator + yield ['', 1]; // empty string should always pass and eventually be handled by NotBlankValidator + yield ['My String 0', 3]; + } + + public static function provideInvalidTypes() + { + yield [true]; + yield [false]; + yield [1]; + yield [1.1]; + yield [[]]; + yield [new \stdClass()]; + } +} diff --git a/Tests/Constraints/YamlTest.php b/Tests/Constraints/YamlTest.php new file mode 100644 index 000000000..c9529fc12 --- /dev/null +++ b/Tests/Constraints/YamlTest.php @@ -0,0 +1,57 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Tests\Constraints; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Validator\Constraints\Yaml; +use Symfony\Component\Validator\Mapping\ClassMetadata; +use Symfony\Component\Validator\Mapping\Loader\AttributeLoader; +use Symfony\Component\Yaml\Yaml as YamlParser; + +/** + * @author Kev + */ +class YamlTest extends TestCase +{ + public function testAttributes() + { + $metadata = new ClassMetadata(YamlDummy::class); + $loader = new AttributeLoader(); + self::assertTrue($loader->loadClassMetadata($metadata)); + + [$bConstraint] = $metadata->properties['b']->getConstraints(); + self::assertSame('myMessage', $bConstraint->message); + self::assertSame(['Default', 'YamlDummy'], $bConstraint->groups); + + [$cConstraint] = $metadata->properties['c']->getConstraints(); + self::assertSame(['my_group'], $cConstraint->groups); + self::assertSame('some attached data', $cConstraint->payload); + + [$cConstraint] = $metadata->properties['d']->getConstraints(); + self::assertSame(YamlParser::PARSE_CONSTANT | YamlParser::PARSE_CUSTOM_TAGS, $cConstraint->flags); + } +} + +class YamlDummy +{ + #[Yaml] + private $a; + + #[Yaml(message: 'myMessage')] + private $b; + + #[Yaml(groups: ['my_group'], payload: 'some attached data')] + private $c; + + #[Yaml(flags: YamlParser::PARSE_CONSTANT | YamlParser::PARSE_CUSTOM_TAGS)] + private $d; +} diff --git a/Tests/Constraints/YamlValidatorTest.php b/Tests/Constraints/YamlValidatorTest.php new file mode 100644 index 000000000..5a90ccf03 --- /dev/null +++ b/Tests/Constraints/YamlValidatorTest.php @@ -0,0 +1,148 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Tests\Constraints; + +use Symfony\Component\Validator\Constraints\Yaml; +use Symfony\Component\Validator\Constraints\YamlValidator; +use Symfony\Component\Validator\Test\ConstraintValidatorTestCase; +use Symfony\Component\Yaml\Yaml as YamlParser; + +/** + * @author Kev + */ +class YamlValidatorTest extends ConstraintValidatorTestCase +{ + protected function createValidator(): YamlValidator + { + return new YamlValidator(); + } + + /** + * @dataProvider getValidValues + */ + public function testYamlIsValid($value) + { + $this->validator->validate($value, new Yaml()); + + $this->assertNoViolation(); + } + + public function testYamlWithFlags() + { + $this->validator->validate('date: 2023-01-01', new Yaml(flags: YamlParser::PARSE_DATETIME)); + $this->assertNoViolation(); + } + + /** + * @dataProvider getInvalidValues + */ + public function testInvalidValues($value, $message, $line) + { + $constraint = new Yaml( + message: 'myMessageTest', + ); + + $this->validator->validate($value, $constraint); + + $this->buildViolation('myMessageTest') + ->setParameter('{{ error }}', $message) + ->setParameter('{{ line }}', $line) + ->setCode(Yaml::INVALID_YAML_ERROR) + ->assertRaised(); + } + + public function testInvalidFlags() + { + $value = 'tags: [!tagged app.myclass]'; + $this->validator->validate($value, new Yaml()); + $this->buildViolation('This value is not valid YAML.') + ->setParameter('{{ error }}', 'Tags support is not enabled. Enable the "Yaml::PARSE_CUSTOM_TAGS" flag to use "!tagged" at line 1 (near "tags: [!tagged app.myclass]").') + ->setParameter('{{ line }}', 1) + ->setCode(Yaml::INVALID_YAML_ERROR) + ->assertRaised(); + } + + /** + * @dataProvider getDeprecationOnLinesData + */ + public function testDeprecationTriggersParseException(int $yamlLine, string $yamlValue) + { + $lines = explode("\n", $yamlValue); + $errorLine = end($lines); + $expectedError = 'This is a simulated deprecation at line '.$yamlLine.' (near "'.$errorLine.'")'; + + $constraint = new Yaml( + message: 'myMessageTest', + flags: YamlParser::PARSE_OBJECT, + ); + $this->validator->validate($yamlValue, $constraint); + $this->buildViolation('myMessageTest') + ->setParameter('{{ error }}', $expectedError) + ->setParameter('{{ line }}', $yamlLine) + ->setCode(Yaml::INVALID_YAML_ERROR) + ->assertRaised(); + } + + public static function getValidValues() + { + return [ + ['planet_diameters: {earth: 12742, mars: 6779, saturn: 116460, mercury: 4879}'], + ["key:\n value"], + [null], + [''], + ['"null"'], + ['null'], + ['"string"'], + ['1'], + ['true'], + [1], + ]; + } + + public static function getInvalidValues(): array + { + return [ + ['{:INVALID]', 'Malformed unquoted YAML string at line 1 (near "{:INVALID]").', 1], + ["key:\nvalue", 'Unable to parse at line 2 (near "value").', 2], + ]; + } + + /** + * @return array + */ + public static function getDeprecationOnLinesData(): array + { + $serialized = serialize(new DeprecatedObjectFixture()); + + return [ + 'deprecation at line 1' => [1, "object: !php/object '".$serialized."'"], + 'deprecation at line 2' => [2, "valid: yaml\nobject: !php/object '".$serialized."'"], + 'deprecation at line 5' => [5, "line1: value\nline2: value\nline3: value\nline4: value\nobject: !php/object '".$serialized."'"], + ]; + } +} + +/** + * Fixture class for triggering deprecation during unserialize. + */ +class DeprecatedObjectFixture +{ + public function __serialize(): array + { + return []; + } + + public function __unserialize(array $data): void + { + @trigger_error('This is a simulated deprecation', \E_USER_DEPRECATED); + } +} diff --git a/Tests/ContainerConstraintValidatorFactoryTest.php b/Tests/ContainerConstraintValidatorFactoryTest.php index 63b7f6f96..980a70e40 100644 --- a/Tests/ContainerConstraintValidatorFactoryTest.php +++ b/Tests/ContainerConstraintValidatorFactoryTest.php @@ -49,7 +49,6 @@ public function testGetInstanceReturnsService() public function testGetInstanceInvalidValidatorClass() { - $this->expectException(ValidatorException::class); $constraint = $this->createMock(Constraint::class); $constraint ->expects($this->once()) @@ -57,6 +56,9 @@ public function testGetInstanceInvalidValidatorClass() ->willReturn('Fully\\Qualified\\ConstraintValidator\\Class\\Name'); $factory = new ContainerConstraintValidatorFactory(new Container()); + + $this->expectException(ValidatorException::class); + $factory->getInstance($constraint); } } diff --git a/Tests/DependencyInjection/AddConstraintValidatorsPassTest.php b/Tests/DependencyInjection/AddConstraintValidatorsPassTest.php index 052c88f85..1b85c7e2a 100644 --- a/Tests/DependencyInjection/AddConstraintValidatorsPassTest.php +++ b/Tests/DependencyInjection/AddConstraintValidatorsPassTest.php @@ -47,8 +47,6 @@ public function testThatConstraintValidatorServicesAreProcessed() public function testAbstractConstraintValidator() { - $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('The service "my_abstract_constraint_validator" tagged "validator.constraint_validator" must not be abstract.'); $container = new ContainerBuilder(); $container->register('validator.validator_factory') ->addArgument([]); @@ -58,6 +56,10 @@ public function testAbstractConstraintValidator() ->addTag('validator.constraint_validator'); $addConstraintValidatorsPass = new AddConstraintValidatorsPass(); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The service "my_abstract_constraint_validator" tagged "validator.constraint_validator" must not be abstract.'); + $addConstraintValidatorsPass->process($container); } diff --git a/Tests/Fixtures/Annotation/Entity.php b/Tests/Fixtures/Annotation/Entity.php deleted file mode 100644 index bbfa452b6..000000000 --- a/Tests/Fixtures/Annotation/Entity.php +++ /dev/null @@ -1,160 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Validator\Tests\Fixtures\Annotation; - -use Symfony\Component\Validator\Constraints as Assert; -use Symfony\Component\Validator\Context\ExecutionContextInterface; -use Symfony\Component\Validator\Tests\Fixtures\EntityInterfaceB; - -/** - * @Symfony\Component\Validator\Tests\Fixtures\ConstraintA - * @Assert\GroupSequence({"Foo", "Entity"}) - * @Assert\Callback({"Symfony\Component\Validator\Tests\Fixtures\CallbackClass", "callback"}) - * @Assert\Sequentially({ - * @Assert\Expression("this.getFirstName() != null") - * }) - */ -class Entity extends EntityParent implements EntityInterfaceB -{ - /** - * @Assert\NotNull - * @Assert\Range(min=3) - * @Assert\All({@Assert\NotNull, @Assert\Range(min=3)}), - * @Assert\All(constraints={@Assert\NotNull, @Assert\Range(min=3)}) - * @Assert\Collection(fields={ - * "foo" = {@Assert\NotNull, @Assert\Range(min=3)}, - * "bar" = @Assert\Range(min=5), - * "baz" = @Assert\Required({@Assert\Email()}), - * "qux" = @Assert\Optional({@Assert\NotBlank()}) - * }, allowExtraFields=true) - * @Assert\Choice(choices={"A", "B"}, message="Must be one of %choices%") - * @Assert\AtLeastOneOf({@Assert\NotNull, @Assert\Range(min=3)}, message="foo", includeInternalMessages=false) - * @Assert\Sequentially({@Assert\NotBlank, @Assert\Range(min=5)}) - */ - public $firstName; - /** - * @Assert\Valid - */ - public $childA; - /** - * @Assert\Valid - */ - public $childB; - protected $lastName; - public $reference; - public $reference2; - private $internal; - public $data = 'Overridden data'; - public $initialized = false; - /** - * @Assert\Type("integer") - */ - protected ?int $other; - - public function __construct($internal = null) - { - $this->internal = $internal; - } - - public function getFirstName() - { - return $this->firstName; - } - - public function getInternal() - { - return $this->internal.' from getter'; - } - - public function setLastName($lastName) - { - $this->lastName = $lastName; - } - - /** - * @Assert\NotNull - */ - public function getLastName() - { - return $this->lastName; - } - - public function getValid() - { - } - - /** - * @Assert\IsTrue - */ - public function isValid() - { - return 'valid'; - } - - /** - * @Assert\IsTrue - */ - public function hasPermissions() - { - return 'permissions'; - } - - public function getData() - { - return 'Overridden data'; - } - - /** - * @Assert\Callback(payload="foo") - */ - public function validateMe(ExecutionContextInterface $context) - { - } - - /** - * @Assert\Callback - */ - public static function validateMeStatic($object, ExecutionContextInterface $context) - { - } - - public function getChildA(): mixed - { - return $this->childA; - } - - /** - * @param mixed $childA - */ - public function setChildA($childA) - { - $this->childA = $childA; - } - - public function getChildB(): mixed - { - return $this->childB; - } - - /** - * @param mixed $childB - */ - public function setChildB($childB) - { - $this->childB = $childB; - } - - public function getReference() - { - return $this->reference; - } -} diff --git a/Tests/Fixtures/Annotation/EntityParent.php b/Tests/Fixtures/Annotation/EntityParent.php deleted file mode 100644 index d938043ca..000000000 --- a/Tests/Fixtures/Annotation/EntityParent.php +++ /dev/null @@ -1,38 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Validator\Tests\Fixtures\Annotation; - -use Symfony\Component\Validator\Constraints\NotNull; -use Symfony\Component\Validator\Tests\Fixtures\EntityInterfaceA; - -class EntityParent implements EntityInterfaceA -{ - protected $firstName; - private $internal; - private $data = 'Data'; - private $child; - - /** - * @NotNull - */ - protected ?int $other; - - public function getData() - { - return 'Data'; - } - - public function getChild() - { - return $this->child; - } -} diff --git a/Tests/Fixtures/Annotation/GroupSequenceProviderEntity.php b/Tests/Fixtures/Annotation/GroupSequenceProviderEntity.php deleted file mode 100644 index 447cb6f35..000000000 --- a/Tests/Fixtures/Annotation/GroupSequenceProviderEntity.php +++ /dev/null @@ -1,37 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Validator\Tests\Fixtures\Annotation; - -use Symfony\Component\Validator\Constraints as Assert; -use Symfony\Component\Validator\Constraints\GroupSequence; -use Symfony\Component\Validator\GroupSequenceProviderInterface; - -/** - * @Assert\GroupSequenceProvider - */ -class GroupSequenceProviderEntity implements GroupSequenceProviderInterface -{ - public $firstName; - public $lastName; - - protected $sequence = []; - - public function __construct($sequence) - { - $this->sequence = $sequence; - } - - public function getGroupSequence(): array|GroupSequence - { - return $this->sequence; - } -} diff --git a/Tests/Fixtures/Attribute/Entity.php b/Tests/Fixtures/Attribute/Entity.php deleted file mode 100644 index aa954144c..000000000 --- a/Tests/Fixtures/Attribute/Entity.php +++ /dev/null @@ -1,150 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Validator\Tests\Fixtures\Attribute; - -use Symfony\Component\Validator\Constraints as Assert; -use Symfony\Component\Validator\Context\ExecutionContextInterface; -use Symfony\Component\Validator\Tests\Fixtures\CallbackClass; -use Symfony\Component\Validator\Tests\Fixtures\ConstraintA; -use Symfony\Component\Validator\Tests\Fixtures\EntityInterfaceB; - -#[ - ConstraintA, - Assert\GroupSequence(['Foo', 'Entity']), - Assert\Callback([CallbackClass::class, 'callback']), -] -/** - * @Assert\Sequentially({ - * @Assert\Expression("this.getFirstName() != null") - * }) - */ -class Entity extends EntityParent implements EntityInterfaceB -{ - /** - * @Assert\All({@Assert\NotNull, @Assert\Range(min=3)}), - * @Assert\All(constraints={@Assert\NotNull, @Assert\Range(min=3)}) - * @Assert\Collection(fields={ - * "foo" = {@Assert\NotNull, @Assert\Range(min=3)}, - * "bar" = @Assert\Range(min=5), - * "baz" = @Assert\Required({@Assert\Email()}), - * "qux" = @Assert\Optional({@Assert\NotBlank()}) - * }, allowExtraFields=true) - * @Assert\Choice(choices={"A", "B"}, message="Must be one of %choices%") - * @Assert\AtLeastOneOf({@Assert\NotNull, @Assert\Range(min=3)}, message="foo", includeInternalMessages=false) - * @Assert\Sequentially({@Assert\NotBlank, @Assert\Range(min=5)}) - */ - #[ - Assert\NotNull, - Assert\Range(min: 3), - ] - public string $firstName; - #[Assert\Valid] - public $childA; - #[Assert\Valid] - public $childB; - protected $lastName; - public $reference; - public $reference2; - private $internal; - public $data = 'Overridden data'; - public $initialized = false; - #[Assert\Type('integer')] - protected ?int $other; - - public function __construct($internal = null) - { - $this->internal = $internal; - } - - public function getFirstName() - { - return $this->firstName; - } - - public function getInternal() - { - return $this->internal.' from getter'; - } - - public function setLastName($lastName) - { - $this->lastName = $lastName; - } - - #[Assert\NotNull] - public function getLastName() - { - return $this->lastName; - } - - public function getValid() - { - } - - #[Assert\IsTrue] - public function isValid() - { - return 'valid'; - } - - #[Assert\IsTrue] - public function hasPermissions() - { - return 'permissions'; - } - - public function getData() - { - return 'Overridden data'; - } - - #[Assert\Callback(payload: 'foo')] - public function validateMe(ExecutionContextInterface $context) - { - } - - #[Assert\Callback] - public static function validateMeStatic($object, ExecutionContextInterface $context) - { - } - - public function getChildA(): mixed - { - return $this->childA; - } - - /** - * @param mixed $childA - */ - public function setChildA($childA) - { - $this->childA = $childA; - } - - public function getChildB(): mixed - { - return $this->childB; - } - - /** - * @param mixed $childB - */ - public function setChildB($childB) - { - $this->childB = $childB; - } - - public function getReference() - { - return $this->reference; - } -} diff --git a/Tests/Fixtures/Attribute/EntityParent.php b/Tests/Fixtures/Attribute/EntityParent.php deleted file mode 100644 index ca72c0fa9..000000000 --- a/Tests/Fixtures/Attribute/EntityParent.php +++ /dev/null @@ -1,36 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Validator\Tests\Fixtures\Attribute; - -use Symfony\Component\Validator\Constraints\NotNull; -use Symfony\Component\Validator\Tests\Fixtures\EntityInterfaceA; - -class EntityParent implements EntityInterfaceA -{ - protected string $firstName; - private $internal; - private $data = 'Data'; - private $child; - - #[NotNull] - protected ?int $other; - - public function getData() - { - return 'Data'; - } - - public function getChild() - { - return $this->child; - } -} diff --git a/Tests/Fixtures/Attribute/GroupSequenceProviderEntity.php b/Tests/Fixtures/Attribute/GroupSequenceProviderEntity.php deleted file mode 100644 index db16eef46..000000000 --- a/Tests/Fixtures/Attribute/GroupSequenceProviderEntity.php +++ /dev/null @@ -1,35 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Validator\Tests\Fixtures\Attribute; - -use Symfony\Component\Validator\Constraints as Assert; -use Symfony\Component\Validator\Constraints\GroupSequence; -use Symfony\Component\Validator\GroupSequenceProviderInterface; - -#[Assert\GroupSequenceProvider] -class GroupSequenceProviderEntity implements GroupSequenceProviderInterface -{ - public $firstName; - public $lastName; - - protected $sequence = []; - - public function __construct($sequence) - { - $this->sequence = $sequence; - } - - public function getGroupSequence(): array|GroupSequence - { - return $this->sequence; - } -} diff --git a/Tests/Fixtures/ConstraintA.php b/Tests/Fixtures/ConstraintA.php index b5cf5ac76..51e8ae6a7 100644 --- a/Tests/Fixtures/ConstraintA.php +++ b/Tests/Fixtures/ConstraintA.php @@ -13,7 +13,6 @@ use Symfony\Component\Validator\Constraint; -/** @Annotation */ #[\Attribute] class ConstraintA extends Constraint { diff --git a/Tests/Fixtures/ConstraintB.php b/Tests/Fixtures/ConstraintB.php index 53d43d01f..c70329b8e 100644 --- a/Tests/Fixtures/ConstraintB.php +++ b/Tests/Fixtures/ConstraintB.php @@ -13,7 +13,6 @@ use Symfony\Component\Validator\Constraint; -/** @Annotation */ class ConstraintB extends Constraint { public function getTargets(): string|array diff --git a/Tests/Fixtures/ConstraintC.php b/Tests/Fixtures/ConstraintC.php index e87ed28f4..8143420ac 100644 --- a/Tests/Fixtures/ConstraintC.php +++ b/Tests/Fixtures/ConstraintC.php @@ -13,7 +13,6 @@ use Symfony\Component\Validator\Constraint; -/** @Annotation */ class ConstraintC extends Constraint { public $option1; diff --git a/Tests/Fixtures/ConstraintWithValue.php b/Tests/Fixtures/ConstraintWithValue.php index c1f672793..ef64a655f 100644 --- a/Tests/Fixtures/ConstraintWithValue.php +++ b/Tests/Fixtures/ConstraintWithValue.php @@ -13,7 +13,6 @@ use Symfony\Component\Validator\Constraint; -/** @Annotation */ class ConstraintWithValue extends Constraint { public $property; diff --git a/Tests/Fixtures/ConstraintWithValueAsDefault.php b/Tests/Fixtures/ConstraintWithValueAsDefault.php index 1cddd4a55..8a4944c46 100644 --- a/Tests/Fixtures/ConstraintWithValueAsDefault.php +++ b/Tests/Fixtures/ConstraintWithValueAsDefault.php @@ -13,7 +13,6 @@ use Symfony\Component\Validator\Constraint; -/** @Annotation */ class ConstraintWithValueAsDefault extends Constraint { public $property; diff --git a/Tests/Fixtures/DummyCompoundConstraint.php b/Tests/Fixtures/DummyCompoundConstraint.php new file mode 100644 index 000000000..e4f7bafaa --- /dev/null +++ b/Tests/Fixtures/DummyCompoundConstraint.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Tests\Fixtures; + +use Symfony\Component\Validator\Constraints\Compound; +use Symfony\Component\Validator\Constraints\Length; +use Symfony\Component\Validator\Constraints\NotBlank; +use Symfony\Component\Validator\Constraints\Regex; + +class DummyCompoundConstraint extends Compound +{ + protected function getConstraints(array $options): array + { + return [ + new NotBlank(), + new Length(max: 3), + new Regex('/[a-z]+/'), + new Regex('/[0-9]+/'), + ]; + } +} diff --git a/Tests/Fixtures/DummyCompoundConstraintWithGroups.php b/Tests/Fixtures/DummyCompoundConstraintWithGroups.php new file mode 100644 index 000000000..f106fb577 --- /dev/null +++ b/Tests/Fixtures/DummyCompoundConstraintWithGroups.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Tests\Fixtures; + +use Symfony\Component\Validator\Constraints\Compound; +use Symfony\Component\Validator\Constraints\Length; +use Symfony\Component\Validator\Constraints\NotBlank; + +class DummyCompoundConstraintWithGroups extends Compound +{ + protected function getConstraints(array $options): array + { + return [ + new NotBlank(groups: ['not_blank']), + new Length(['max' => 3], groups: ['max_length']), + ]; + } +} diff --git a/Tests/Fixtures/DummyEntityConstraintWithoutNamedArguments.php b/Tests/Fixtures/DummyEntityConstraintWithoutNamedArguments.php new file mode 100644 index 000000000..880f73cae --- /dev/null +++ b/Tests/Fixtures/DummyEntityConstraintWithoutNamedArguments.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Tests\Fixtures; + +class DummyEntityConstraintWithoutNamedArguments +{ +} diff --git a/Tests/Fixtures/EntityStaticCar.php b/Tests/Fixtures/EntityStaticCar.php index af90ddc74..3afaaf843 100644 --- a/Tests/Fixtures/EntityStaticCar.php +++ b/Tests/Fixtures/EntityStaticCar.php @@ -18,6 +18,6 @@ class EntityStaticCar extends EntityStaticVehicle { public static function loadValidatorMetadata(ClassMetadata $metadata) { - $metadata->addPropertyConstraint('wheels', new Length(['max' => 99])); + $metadata->addPropertyConstraint('wheels', new Length(max: 99)); } } diff --git a/Tests/Fixtures/EntityStaticCarTurbo.php b/Tests/Fixtures/EntityStaticCarTurbo.php index d559074db..cb0efe281 100644 --- a/Tests/Fixtures/EntityStaticCarTurbo.php +++ b/Tests/Fixtures/EntityStaticCarTurbo.php @@ -18,6 +18,6 @@ class EntityStaticCarTurbo extends EntityStaticCar { public static function loadValidatorMetadata(ClassMetadata $metadata) { - $metadata->addPropertyConstraint('wheels', new Length(['max' => 99])); + $metadata->addPropertyConstraint('wheels', new Length(max: 99)); } } diff --git a/Tests/Fixtures/EntityStaticVehicle.php b/Tests/Fixtures/EntityStaticVehicle.php index 1190318fa..429bffdeb 100644 --- a/Tests/Fixtures/EntityStaticVehicle.php +++ b/Tests/Fixtures/EntityStaticVehicle.php @@ -20,6 +20,6 @@ class EntityStaticVehicle public static function loadValidatorMetadata(ClassMetadata $metadata) { - $metadata->addPropertyConstraint('wheels', new Length(['max' => 99])); + $metadata->addPropertyConstraint('wheels', new Length(max: 99)); } } diff --git a/Tests/Fixtures/FakeMetadataFactory.php b/Tests/Fixtures/FakeMetadataFactory.php index 6e673ee9f..f905b66fd 100644 --- a/Tests/Fixtures/FakeMetadataFactory.php +++ b/Tests/Fixtures/FakeMetadataFactory.php @@ -21,10 +21,10 @@ class FakeMetadataFactory implements MetadataFactoryInterface public function getMetadataFor($class): MetadataInterface { - $hash = null; + $objectId = null; if (\is_object($class)) { - $hash = spl_object_hash($class); + $objectId = spl_object_id($class); $class = $class::class; } @@ -33,8 +33,8 @@ public function getMetadataFor($class): MetadataInterface } if (!isset($this->metadatas[$class])) { - if (isset($this->metadatas[$hash])) { - return $this->metadatas[$hash]; + if (isset($this->metadatas[$objectId])) { + return $this->metadatas[$objectId]; } throw new NoSuchMetadataException(sprintf('No metadata for "%s"', $class)); @@ -45,10 +45,10 @@ public function getMetadataFor($class): MetadataInterface public function hasMetadataFor($class): bool { - $hash = null; + $objectId = null; if (\is_object($class)) { - $hash = spl_object_hash($class); + $objectId = spl_object_id($class); $class = $class::class; } @@ -56,7 +56,7 @@ public function hasMetadataFor($class): bool return false; } - return isset($this->metadatas[$class]) || isset($this->metadatas[$hash]); + return isset($this->metadatas[$class]) || isset($this->metadatas[$objectId]); } public function addMetadata($metadata) @@ -66,7 +66,7 @@ public function addMetadata($metadata) public function addMetadataForValue($value, MetadataInterface $metadata) { - $key = \is_object($value) ? spl_object_hash($value) : $value; + $key = \is_object($value) ? spl_object_id($value) : $value; $this->metadatas[$key] = $metadata; } } diff --git a/Tests/Fixtures/NestedAttribute/Entity.php b/Tests/Fixtures/NestedAttribute/Entity.php index 2384e8734..3558d5624 100644 --- a/Tests/Fixtures/NestedAttribute/Entity.php +++ b/Tests/Fixtures/NestedAttribute/Entity.php @@ -22,7 +22,7 @@ Assert\GroupSequence(['Foo', 'Entity']), Assert\Callback([CallbackClass::class, 'callback']), Assert\Sequentially([ - new Assert\Expression('this.getFirstName() != null') + new Assert\Expression('this.getFirstName() != null'), ]) ] class Entity extends EntityParent implements EntityInterfaceB diff --git a/Tests/Mapping/ClassMetadataTest.php b/Tests/Mapping/ClassMetadataTest.php index a94295728..edfacae16 100644 --- a/Tests/Mapping/ClassMetadataTest.php +++ b/Tests/Mapping/ClassMetadataTest.php @@ -281,17 +281,21 @@ public function testGroupSequencesFailIfContainingDefault() public function testGroupSequenceFailsIfGroupSequenceProviderIsSet() { - $this->expectException(GroupDefinitionException::class); $metadata = new ClassMetadata(self::PROVIDERCLASS); $metadata->setGroupSequenceProvider(true); + + $this->expectException(GroupDefinitionException::class); + $metadata->setGroupSequence(['GroupSequenceProviderEntity', 'Foo']); } public function testGroupSequenceProviderFailsIfGroupSequenceIsSet() { - $this->expectException(GroupDefinitionException::class); $metadata = new ClassMetadata(self::PROVIDERCLASS); $metadata->setGroupSequence(['GroupSequenceProviderEntity', 'Foo']); + + $this->expectException(GroupDefinitionException::class); + $metadata->setGroupSequenceProvider(true); } diff --git a/Tests/Mapping/Factory/BlackHoleMetadataFactoryTest.php b/Tests/Mapping/Factory/BlackHoleMetadataFactoryTest.php index 549bc518b..1fff11301 100644 --- a/Tests/Mapping/Factory/BlackHoleMetadataFactoryTest.php +++ b/Tests/Mapping/Factory/BlackHoleMetadataFactoryTest.php @@ -20,14 +20,11 @@ class BlackHoleMetadataFactoryTest extends TestCase public function testGetMetadataForThrowsALogicException() { $this->expectException(LogicException::class); - $metadataFactory = new BlackHoleMetadataFactory(); - $metadataFactory->getMetadataFor('foo'); + (new BlackHoleMetadataFactory())->getMetadataFor('foo'); } public function testHasMetadataForReturnsFalse() { - $metadataFactory = new BlackHoleMetadataFactory(); - - $this->assertFalse($metadataFactory->hasMetadataFor('foo')); + $this->assertFalse((new BlackHoleMetadataFactory())->hasMetadataFor('foo')); } } diff --git a/Tests/Mapping/Factory/LazyLoadingMetadataFactoryTest.php b/Tests/Mapping/Factory/LazyLoadingMetadataFactoryTest.php index 3d10506ae..68f279ecf 100644 --- a/Tests/Mapping/Factory/LazyLoadingMetadataFactoryTest.php +++ b/Tests/Mapping/Factory/LazyLoadingMetadataFactoryTest.php @@ -38,8 +38,8 @@ public function testLoadClassMetadataWithInterface() $metadata = $factory->getMetadataFor(self::PARENT_CLASS); $constraints = [ - new ConstraintA(['groups' => ['Default', 'EntityParent']]), - new ConstraintA(['groups' => ['Default', 'EntityInterfaceA', 'EntityParent']]), + new ConstraintA(groups: ['Default', 'EntityParent']), + new ConstraintA(groups: ['Default', 'EntityInterfaceA', 'EntityParent']), ]; $this->assertEquals($constraints, $metadata->getConstraints()); @@ -51,31 +51,31 @@ public function testMergeParentConstraints() $metadata = $factory->getMetadataFor(self::CLASS_NAME); $constraints = [ - new ConstraintA(['groups' => [ + new ConstraintA(groups: [ 'Default', 'Entity', - ]]), - new ConstraintA(['groups' => [ + ]), + new ConstraintA(groups: [ 'Default', 'EntityParent', 'Entity', - ]]), - new ConstraintA(['groups' => [ + ]), + new ConstraintA(groups: [ 'Default', 'EntityInterfaceA', 'EntityParent', 'Entity', - ]]), - new ConstraintA(['groups' => [ + ]), + new ConstraintA(groups: [ 'Default', 'EntityInterfaceB', 'Entity', - ]]), - new ConstraintA(['groups' => [ + ]), + new ConstraintA(groups: [ 'Default', 'EntityParentInterface', 'Entity', - ]]), + ]), ]; $this->assertEquals($constraints, $metadata->getConstraints()); @@ -87,8 +87,8 @@ public function testCachedMetadata() $factory = new LazyLoadingMetadataFactory(new TestLoader(), $cache); $expectedConstraints = [ - new ConstraintA(['groups' => ['Default', 'EntityParent']]), - new ConstraintA(['groups' => ['Default', 'EntityInterfaceA', 'EntityParent']]), + new ConstraintA(groups: ['Default', 'EntityParent']), + new ConstraintA(groups: ['Default', 'EntityInterfaceA', 'EntityParent']), ]; $metadata = $factory->getMetadataFor(self::PARENT_CLASS); @@ -109,14 +109,17 @@ public function testCachedMetadata() public function testNonClassNameStringValues() { - $this->expectException(NoSuchMetadataException::class); $testedValue = 'error@example.com'; $loader = $this->createMock(LoaderInterface::class); $cache = $this->createMock(CacheItemPoolInterface::class); - $factory = new LazyLoadingMetadataFactory($loader, $cache); $cache ->expects($this->never()) ->method('getItem'); + + $factory = new LazyLoadingMetadataFactory($loader, $cache); + + $this->expectException(NoSuchMetadataException::class); + $factory->getMetadataFor($testedValue); } diff --git a/Tests/Mapping/Loader/AnnotationLoaderWithHybridAnnotationsTest.php b/Tests/Mapping/Loader/AnnotationLoaderWithHybridAnnotationsTest.php deleted file mode 100644 index 453e7b6df..000000000 --- a/Tests/Mapping/Loader/AnnotationLoaderWithHybridAnnotationsTest.php +++ /dev/null @@ -1,85 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Validator\Tests\Mapping\Loader; - -use Doctrine\Common\Annotations\AnnotationReader; -use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; -use Symfony\Component\Validator\Constraints\NotBlank; -use Symfony\Component\Validator\Mapping\ClassMetadata; -use Symfony\Component\Validator\Mapping\Loader\AnnotationLoader; - -/** - * @group legacy - */ -class AnnotationLoaderWithHybridAnnotationsTest extends AttributeLoaderTest -{ - use ExpectDeprecationTrait; - - public function testLoadClassMetadataReturnsTrueIfSuccessful() - { - $this->expectDeprecation('Since symfony/validator 6.4: Class "Symfony\Component\Validator\Tests\Fixtures\Attribute\Entity" uses Doctrine Annotations to configure validation constraints, which is deprecated. Use PHP attributes instead.'); - $this->expectDeprecation('Since symfony/validator 6.4: Property "Symfony\Component\Validator\Tests\Fixtures\Attribute\Entity::$firstName" uses Doctrine Annotations to configure validation constraints, which is deprecated. Use PHP attributes instead.'); - - parent::testLoadClassMetadataReturnsTrueIfSuccessful(); - } - - public function testLoadClassMetadata() - { - $this->expectDeprecation('Since symfony/validator 6.4: Class "Symfony\Component\Validator\Tests\Fixtures\Attribute\Entity" uses Doctrine Annotations to configure validation constraints, which is deprecated. Use PHP attributes instead.'); - $this->expectDeprecation('Since symfony/validator 6.4: Property "Symfony\Component\Validator\Tests\Fixtures\Attribute\Entity::$firstName" uses Doctrine Annotations to configure validation constraints, which is deprecated. Use PHP attributes instead.'); - - parent::testLoadClassMetadata(); - } - - public function testLoadClassMetadataAndMerge() - { - $this->expectDeprecation('Since symfony/validator 6.4: Class "Symfony\Component\Validator\Tests\Fixtures\Attribute\Entity" uses Doctrine Annotations to configure validation constraints, which is deprecated. Use PHP attributes instead.'); - $this->expectDeprecation('Since symfony/validator 6.4: Property "Symfony\Component\Validator\Tests\Fixtures\Attribute\Entity::$firstName" uses Doctrine Annotations to configure validation constraints, which is deprecated. Use PHP attributes instead.'); - - parent::testLoadClassMetadataAndMerge(); - } - - public function testLoadClassMetadataWithOtherAnnotations() - { - $loader = $this->createAnnotationLoader(); - $metadata = new ClassMetadata(EntityWithOtherAnnotations::class); - - $this->assertTrue($loader->loadClassMetadata($metadata)); - } - - protected function createAnnotationLoader(): AnnotationLoader - { - return new AnnotationLoader(new AnnotationReader()); - } - - protected function getFixtureNamespace(): string - { - return 'Symfony\Component\Validator\Tests\Fixtures\Attribute'; - } -} - -/** - * @Annotation - * @Target({"PROPERTY"}) - */ -class SomeAnnotation -{ -} - -class EntityWithOtherAnnotations -{ - /** - * @SomeAnnotation - */ - #[NotBlank] - public ?string $name = null; -} diff --git a/Tests/Mapping/Loader/AnnotationLoaderWithLegacyAnnotationsTest.php b/Tests/Mapping/Loader/AnnotationLoaderWithLegacyAnnotationsTest.php deleted file mode 100644 index 86d6d8b21..000000000 --- a/Tests/Mapping/Loader/AnnotationLoaderWithLegacyAnnotationsTest.php +++ /dev/null @@ -1,94 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Validator\Tests\Mapping\Loader; - -use Doctrine\Common\Annotations\AnnotationReader; -use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; -use Symfony\Component\Validator\Mapping\Loader\AnnotationLoader; - -/** - * @group legacy - */ -class AnnotationLoaderWithLegacyAnnotationsTest extends AttributeLoaderTest -{ - use ExpectDeprecationTrait; - - public function testLoadClassMetadataReturnsTrueIfSuccessful() - { - $this->expectDeprecation('Since symfony/validator 6.4: Class "Symfony\Component\Validator\Tests\Fixtures\Annotation\Entity" uses Doctrine Annotations to configure validation constraints, which is deprecated. Use PHP attributes instead.'); - $this->expectDeprecation('Since symfony/validator 6.4: Property "Symfony\Component\Validator\Tests\Fixtures\Annotation\Entity::$firstName" uses Doctrine Annotations to configure validation constraints, which is deprecated. Use PHP attributes instead.'); - $this->expectDeprecation('Since symfony/validator 6.4: Property "Symfony\Component\Validator\Tests\Fixtures\Annotation\Entity::$childA" uses Doctrine Annotations to configure validation constraints, which is deprecated. Use PHP attributes instead.'); - $this->expectDeprecation('Since symfony/validator 6.4: Property "Symfony\Component\Validator\Tests\Fixtures\Annotation\Entity::$childB" uses Doctrine Annotations to configure validation constraints, which is deprecated. Use PHP attributes instead.'); - $this->expectDeprecation('Since symfony/validator 6.4: Method "Symfony\Component\Validator\Tests\Fixtures\Annotation\Entity::getLastName()" uses Doctrine Annotations to configure validation constraints, which is deprecated. Use PHP attributes instead.'); - $this->expectDeprecation('Since symfony/validator 6.4: Method "Symfony\Component\Validator\Tests\Fixtures\Annotation\Entity::isValid()" uses Doctrine Annotations to configure validation constraints, which is deprecated. Use PHP attributes instead.'); - $this->expectDeprecation('Since symfony/validator 6.4: Method "Symfony\Component\Validator\Tests\Fixtures\Annotation\Entity::hasPermissions()" uses Doctrine Annotations to configure validation constraints, which is deprecated. Use PHP attributes instead.'); - $this->expectDeprecation('Since symfony/validator 6.4: Method "Symfony\Component\Validator\Tests\Fixtures\Annotation\Entity::validateMe()" uses Doctrine Annotations to configure validation constraints, which is deprecated. Use PHP attributes instead.'); - $this->expectDeprecation('Since symfony/validator 6.4: Method "Symfony\Component\Validator\Tests\Fixtures\Annotation\Entity::validateMeStatic()" uses Doctrine Annotations to configure validation constraints, which is deprecated. Use PHP attributes instead.'); - - parent::testLoadClassMetadataReturnsTrueIfSuccessful(); - } - - public function testLoadClassMetadata() - { - $this->expectDeprecation('Since symfony/validator 6.4: Class "Symfony\Component\Validator\Tests\Fixtures\Annotation\Entity" uses Doctrine Annotations to configure validation constraints, which is deprecated. Use PHP attributes instead.'); - $this->expectDeprecation('Since symfony/validator 6.4: Property "Symfony\Component\Validator\Tests\Fixtures\Annotation\Entity::$firstName" uses Doctrine Annotations to configure validation constraints, which is deprecated. Use PHP attributes instead.'); - $this->expectDeprecation('Since symfony/validator 6.4: Property "Symfony\Component\Validator\Tests\Fixtures\Annotation\Entity::$childA" uses Doctrine Annotations to configure validation constraints, which is deprecated. Use PHP attributes instead.'); - $this->expectDeprecation('Since symfony/validator 6.4: Property "Symfony\Component\Validator\Tests\Fixtures\Annotation\Entity::$childB" uses Doctrine Annotations to configure validation constraints, which is deprecated. Use PHP attributes instead.'); - $this->expectDeprecation('Since symfony/validator 6.4: Method "Symfony\Component\Validator\Tests\Fixtures\Annotation\Entity::getLastName()" uses Doctrine Annotations to configure validation constraints, which is deprecated. Use PHP attributes instead.'); - $this->expectDeprecation('Since symfony/validator 6.4: Method "Symfony\Component\Validator\Tests\Fixtures\Annotation\Entity::isValid()" uses Doctrine Annotations to configure validation constraints, which is deprecated. Use PHP attributes instead.'); - $this->expectDeprecation('Since symfony/validator 6.4: Method "Symfony\Component\Validator\Tests\Fixtures\Annotation\Entity::hasPermissions()" uses Doctrine Annotations to configure validation constraints, which is deprecated. Use PHP attributes instead.'); - $this->expectDeprecation('Since symfony/validator 6.4: Method "Symfony\Component\Validator\Tests\Fixtures\Annotation\Entity::validateMe()" uses Doctrine Annotations to configure validation constraints, which is deprecated. Use PHP attributes instead.'); - $this->expectDeprecation('Since symfony/validator 6.4: Method "Symfony\Component\Validator\Tests\Fixtures\Annotation\Entity::validateMeStatic()" uses Doctrine Annotations to configure validation constraints, which is deprecated. Use PHP attributes instead.'); - - parent::testLoadClassMetadata(); - } - - public function testLoadParentClassMetadata() - { - $this->expectDeprecation('Since symfony/validator 6.4: Property "Symfony\Component\Validator\Tests\Fixtures\Annotation\EntityParent::$other" uses Doctrine Annotations to configure validation constraints, which is deprecated. Use PHP attributes instead.'); - - parent::testLoadParentClassMetadata(); - } - - public function testLoadClassMetadataAndMerge() - { - $this->expectDeprecation('Since symfony/validator 6.4: Property "Symfony\Component\Validator\Tests\Fixtures\Annotation\EntityParent::$other" uses Doctrine Annotations to configure validation constraints, which is deprecated. Use PHP attributes instead.'); - $this->expectDeprecation('Since symfony/validator 6.4: Class "Symfony\Component\Validator\Tests\Fixtures\Annotation\Entity" uses Doctrine Annotations to configure validation constraints, which is deprecated. Use PHP attributes instead.'); - $this->expectDeprecation('Since symfony/validator 6.4: Property "Symfony\Component\Validator\Tests\Fixtures\Annotation\Entity::$firstName" uses Doctrine Annotations to configure validation constraints, which is deprecated. Use PHP attributes instead.'); - $this->expectDeprecation('Since symfony/validator 6.4: Property "Symfony\Component\Validator\Tests\Fixtures\Annotation\Entity::$childA" uses Doctrine Annotations to configure validation constraints, which is deprecated. Use PHP attributes instead.'); - $this->expectDeprecation('Since symfony/validator 6.4: Property "Symfony\Component\Validator\Tests\Fixtures\Annotation\Entity::$childB" uses Doctrine Annotations to configure validation constraints, which is deprecated. Use PHP attributes instead.'); - $this->expectDeprecation('Since symfony/validator 6.4: Method "Symfony\Component\Validator\Tests\Fixtures\Annotation\Entity::getLastName()" uses Doctrine Annotations to configure validation constraints, which is deprecated. Use PHP attributes instead.'); - $this->expectDeprecation('Since symfony/validator 6.4: Method "Symfony\Component\Validator\Tests\Fixtures\Annotation\Entity::isValid()" uses Doctrine Annotations to configure validation constraints, which is deprecated. Use PHP attributes instead.'); - $this->expectDeprecation('Since symfony/validator 6.4: Method "Symfony\Component\Validator\Tests\Fixtures\Annotation\Entity::hasPermissions()" uses Doctrine Annotations to configure validation constraints, which is deprecated. Use PHP attributes instead.'); - $this->expectDeprecation('Since symfony/validator 6.4: Method "Symfony\Component\Validator\Tests\Fixtures\Annotation\Entity::validateMe()" uses Doctrine Annotations to configure validation constraints, which is deprecated. Use PHP attributes instead.'); - $this->expectDeprecation('Since symfony/validator 6.4: Method "Symfony\Component\Validator\Tests\Fixtures\Annotation\Entity::validateMeStatic()" uses Doctrine Annotations to configure validation constraints, which is deprecated. Use PHP attributes instead.'); - - parent::testLoadClassMetadataAndMerge(); - } - - public function testLoadGroupSequenceProviderAnnotation() - { - $this->expectDeprecation('Since symfony/validator 6.4: Class "Symfony\Component\Validator\Tests\Fixtures\Annotation\GroupSequenceProviderEntity" uses Doctrine Annotations to configure validation constraints, which is deprecated. Use PHP attributes instead.'); - - parent::testLoadGroupSequenceProviderAnnotation(); - } - - protected function createAnnotationLoader(): AnnotationLoader - { - return new AnnotationLoader(new AnnotationReader()); - } - - protected function getFixtureNamespace(): string - { - return 'Symfony\Component\Validator\Tests\Fixtures\Annotation'; - } -} diff --git a/Tests/Mapping/Loader/AttributeLoaderTest.php b/Tests/Mapping/Loader/AttributeLoaderTest.php index c8975dbd2..9285117f9 100644 --- a/Tests/Mapping/Loader/AttributeLoaderTest.php +++ b/Tests/Mapping/Loader/AttributeLoaderTest.php @@ -29,7 +29,6 @@ use Symfony\Component\Validator\Constraints\Type; use Symfony\Component\Validator\Constraints\Valid; use Symfony\Component\Validator\Mapping\ClassMetadata; -use Symfony\Component\Validator\Mapping\Loader\AnnotationLoader; use Symfony\Component\Validator\Mapping\Loader\AttributeLoader; use Symfony\Component\Validator\Tests\Fixtures\ConstraintA; @@ -37,7 +36,7 @@ class AttributeLoaderTest extends TestCase { public function testLoadClassMetadataReturnsTrueIfSuccessful() { - $loader = $this->createAnnotationLoader(); + $loader = $this->createAttributeLoader(); $metadata = new ClassMetadata($this->getFixtureNamespace().'\Entity'); $this->assertTrue($loader->loadClassMetadata($metadata)); @@ -45,7 +44,7 @@ public function testLoadClassMetadataReturnsTrueIfSuccessful() public function testLoadClassMetadataReturnsFalseIfNotSuccessful() { - $loader = $this->createAnnotationLoader(); + $loader = $this->createAttributeLoader(); $metadata = new ClassMetadata('\stdClass'); $this->assertFalse($loader->loadClassMetadata($metadata)); @@ -53,7 +52,7 @@ public function testLoadClassMetadataReturnsFalseIfNotSuccessful() public function testLoadClassMetadata() { - $loader = $this->createAnnotationLoader(); + $loader = $this->createAttributeLoader(); $namespace = $this->getFixtureNamespace(); $metadata = new ClassMetadata($namespace.'\Entity'); @@ -67,29 +66,29 @@ public function testLoadClassMetadata() $expected->addConstraint(new Sequentially([ new Expression('this.getFirstName() != null'), ])); - $expected->addConstraint(new Callback(['callback' => 'validateMe', 'payload' => 'foo'])); + $expected->addConstraint(new Callback(callback: 'validateMe', payload: 'foo')); $expected->addConstraint(new Callback('validateMeStatic')); $expected->addPropertyConstraint('firstName', new NotNull()); - $expected->addPropertyConstraint('firstName', new Range(['min' => 3])); - $expected->addPropertyConstraint('firstName', new All([new NotNull(), new Range(['min' => 3])])); - $expected->addPropertyConstraint('firstName', new All(['constraints' => [new NotNull(), new Range(['min' => 3])]])); - $expected->addPropertyConstraint('firstName', new Collection([ - 'foo' => [new NotNull(), new Range(['min' => 3])], - 'bar' => new Range(['min' => 5]), + $expected->addPropertyConstraint('firstName', new Range(min: 3)); + $expected->addPropertyConstraint('firstName', new All(constraints: [new NotNull(), new Range(min: 3)])); + $expected->addPropertyConstraint('firstName', new All(constraints: [new NotNull(), new Range(min: 3)])); + $expected->addPropertyConstraint('firstName', new Collection(fields: [ + 'foo' => [new NotNull(), new Range(min: 3)], + 'bar' => new Range(min: 5), 'baz' => new Required([new Email()]), 'qux' => new Optional([new NotBlank()]), - ], null, null, true)); - $expected->addPropertyConstraint('firstName', new Choice([ - 'message' => 'Must be one of %choices%', - 'choices' => ['A', 'B'], - ])); + ], allowExtraFields: true)); + $expected->addPropertyConstraint('firstName', new Choice( + message: 'Must be one of %choices%', + choices: ['A', 'B'], + )); $expected->addPropertyConstraint('firstName', new AtLeastOneOf([ new NotNull(), - new Range(['min' => 3]), + new Range(min: 3), ], null, null, 'foo', null, false)); $expected->addPropertyConstraint('firstName', new Sequentially([ new NotBlank(), - new Range(['min' => 5]), + new Range(min: 5), ])); $expected->addPropertyConstraint('childA', new Valid()); $expected->addPropertyConstraint('childB', new Valid()); @@ -105,11 +104,11 @@ public function testLoadClassMetadata() } /** - * Test MetaData merge with parent annotation. + * Test MetaData merge with parent attribute. */ public function testLoadParentClassMetadata() { - $loader = $this->createAnnotationLoader(); + $loader = $this->createAttributeLoader(); $namespace = $this->getFixtureNamespace(); // Load Parent MetaData @@ -124,11 +123,11 @@ public function testLoadParentClassMetadata() } /** - * Test MetaData merge with parent annotation. + * Test MetaData merge with parent attribute. */ public function testLoadClassMetadataAndMerge() { - $loader = $this->createAnnotationLoader(); + $loader = $this->createAttributeLoader(); $namespace = $this->getFixtureNamespace(); // Load Parent MetaData @@ -153,29 +152,29 @@ public function testLoadClassMetadataAndMerge() $expected->addConstraint(new Sequentially([ new Expression('this.getFirstName() != null'), ])); - $expected->addConstraint(new Callback(['callback' => 'validateMe', 'payload' => 'foo'])); + $expected->addConstraint(new Callback(callback: 'validateMe', payload: 'foo')); $expected->addConstraint(new Callback('validateMeStatic')); $expected->addPropertyConstraint('firstName', new NotNull()); - $expected->addPropertyConstraint('firstName', new Range(['min' => 3])); - $expected->addPropertyConstraint('firstName', new All([new NotNull(), new Range(['min' => 3])])); - $expected->addPropertyConstraint('firstName', new All(['constraints' => [new NotNull(), new Range(['min' => 3])]])); - $expected->addPropertyConstraint('firstName', new Collection([ - 'foo' => [new NotNull(), new Range(['min' => 3])], - 'bar' => new Range(['min' => 5]), + $expected->addPropertyConstraint('firstName', new Range(min: 3)); + $expected->addPropertyConstraint('firstName', new All(constraints: [new NotNull(), new Range(min: 3)])); + $expected->addPropertyConstraint('firstName', new All(constraints: [new NotNull(), new Range(min: 3)])); + $expected->addPropertyConstraint('firstName', new Collection(fields: [ + 'foo' => [new NotNull(), new Range(min: 3)], + 'bar' => new Range(min: 5), 'baz' => new Required([new Email()]), 'qux' => new Optional([new NotBlank()]), - ], null, null, true)); - $expected->addPropertyConstraint('firstName', new Choice([ - 'message' => 'Must be one of %choices%', - 'choices' => ['A', 'B'], - ])); + ], allowExtraFields: true)); + $expected->addPropertyConstraint('firstName', new Choice( + message: 'Must be one of %choices%', + choices: ['A', 'B'], + )); $expected->addPropertyConstraint('firstName', new AtLeastOneOf([ new NotNull(), - new Range(['min' => 3]), + new Range(min: 3), ], null, null, 'foo', null, false)); $expected->addPropertyConstraint('firstName', new Sequentially([ new NotBlank(), - new Range(['min' => 5]), + new Range(min: 5), ])); $expected->addPropertyConstraint('childA', new Valid()); $expected->addPropertyConstraint('childB', new Valid()); @@ -196,9 +195,9 @@ public function testLoadClassMetadataAndMerge() $this->assertInstanceOf(NotNull::class, $otherMetadata[1]->getConstraints()[0]); } - public function testLoadGroupSequenceProviderAnnotation() + public function testLoadGroupSequenceProviderAttribute() { - $loader = $this->createAnnotationLoader(); + $loader = $this->createAttributeLoader(); $namespace = $this->getFixtureNamespace(); $metadata = new ClassMetadata($namespace.'\GroupSequenceProviderEntity'); @@ -211,7 +210,23 @@ public function testLoadGroupSequenceProviderAnnotation() $this->assertEquals($expected, $metadata); } - protected function createAnnotationLoader(): AnnotationLoader + public function testLoadExternalGroupSequenceProvider() + { + $loader = $this->createAttributeLoader(); + $namespace = $this->getFixtureAttributeNamespace(); + + $metadata = new ClassMetadata($namespace.'\GroupProviderDto'); + $loader->loadClassMetadata($metadata); + + $expected = new ClassMetadata($namespace.'\GroupProviderDto'); + $expected->setGroupProvider('Symfony\Component\Validator\Tests\Dummy\DummyGroupProvider'); + $expected->setGroupSequenceProvider(true); + $expected->getReflectionClass(); + + $this->assertEquals($expected, $metadata); + } + + protected function createAttributeLoader(): AttributeLoader { return new AttributeLoader(); } @@ -220,4 +235,9 @@ protected function getFixtureNamespace(): string { return 'Symfony\Component\Validator\Tests\Fixtures\NestedAttribute'; } + + protected function getFixtureAttributeNamespace(): string + { + return 'Symfony\Component\Validator\Tests\Fixtures\Attribute'; + } } diff --git a/Tests/Mapping/Loader/FilesLoaderTest.php b/Tests/Mapping/Loader/FilesLoaderTest.php index f2e84130c..ffb0dd23b 100644 --- a/Tests/Mapping/Loader/FilesLoaderTest.php +++ b/Tests/Mapping/Loader/FilesLoaderTest.php @@ -36,6 +36,13 @@ public function testCallsActualFileLoaderForMetadata() public function getFilesLoader(LoaderInterface $loader) { - return new class([__DIR__.'/constraint-mapping.xml', __DIR__.'/constraint-mapping.yaml', __DIR__.'/constraint-mapping.test', __DIR__.'/constraint-mapping.txt'], $loader) extends FilesLoader {}; + $files = [ + __DIR__.'/constraint-mapping.xml', + __DIR__.'/constraint-mapping.yaml', + __DIR__.'/constraint-mapping.test', + __DIR__.'/constraint-mapping.txt', + ]; + + return new class($files, $loader) extends FilesLoader {}; } } diff --git a/Tests/Mapping/Loader/Fixtures/ConstraintWithoutNamedArguments.php b/Tests/Mapping/Loader/Fixtures/ConstraintWithoutNamedArguments.php new file mode 100644 index 000000000..035a1a837 --- /dev/null +++ b/Tests/Mapping/Loader/Fixtures/ConstraintWithoutNamedArguments.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Tests\Mapping\Loader\Fixtures; + +use Symfony\Component\Validator\Constraint; + +class ConstraintWithoutNamedArguments extends Constraint +{ + public function getTargets(): string + { + return self::CLASS_CONSTRAINT; + } +} diff --git a/Tests/Mapping/Loader/PropertyInfoLoaderTest.php b/Tests/Mapping/Loader/PropertyInfoLoaderTest.php index f6a9e8411..ae5253a3f 100644 --- a/Tests/Mapping/Loader/PropertyInfoLoaderTest.php +++ b/Tests/Mapping/Loader/PropertyInfoLoaderTest.php @@ -12,8 +12,11 @@ namespace Symfony\Component\Validator\Tests\Mapping\Loader; use PHPUnit\Framework\TestCase; -use Symfony\Component\PropertyInfo\PropertyInfoExtractorInterface; -use Symfony\Component\PropertyInfo\Type; +use Symfony\Component\PropertyInfo\PropertyAccessExtractorInterface; +use Symfony\Component\PropertyInfo\PropertyListExtractorInterface; +use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface; +use Symfony\Component\PropertyInfo\Type as LegacyType; +use Symfony\Component\TypeInfo\Type; use Symfony\Component\Validator\Constraints\All; use Symfony\Component\Validator\Constraints\Iban; use Symfony\Component\Validator\Constraints\NotBlank; @@ -35,8 +38,8 @@ class PropertyInfoLoaderTest extends TestCase { public function testLoadClassMetadata() { - $propertyInfoStub = $this->createMock(PropertyInfoExtractorInterface::class); - $propertyInfoStub + $propertyListExtractor = $this->createMock(PropertyListExtractorInterface::class); + $propertyListExtractor ->method('getProperties') ->willReturn([ 'nullableString', @@ -55,25 +58,61 @@ public function testLoadClassMetadata() ]) ; - $propertyInfoStub - ->method('getTypes') - ->willReturn( - [new Type(Type::BUILTIN_TYPE_STRING, true)], - [new Type(Type::BUILTIN_TYPE_STRING)], - [new Type(Type::BUILTIN_TYPE_STRING, true), new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_BOOL)], - [new Type(Type::BUILTIN_TYPE_OBJECT, true, Entity::class)], - [new Type(Type::BUILTIN_TYPE_ARRAY, true, null, true, null, new Type(Type::BUILTIN_TYPE_OBJECT, false, Entity::class))], - [new Type(Type::BUILTIN_TYPE_ARRAY, true, null, true)], - [new Type(Type::BUILTIN_TYPE_FLOAT, true)], // The existing constraint is float - [new Type(Type::BUILTIN_TYPE_STRING, true)], - [new Type(Type::BUILTIN_TYPE_STRING, true)], - [new Type(Type::BUILTIN_TYPE_ARRAY, true, null, true, null, new Type(Type::BUILTIN_TYPE_FLOAT))], - [new Type(Type::BUILTIN_TYPE_STRING)], - [new Type(Type::BUILTIN_TYPE_STRING)] - ) - ; + $propertyTypeExtractor = new class implements PropertyTypeExtractorInterface { + private int $i = 0; + private int $j = 0; + private array $types; + private array $legacyTypes; + + public function getType(string $class, string $property, array $context = []): ?Type + { + $this->types ??= [ + Type::nullable(Type::string()), + Type::string(), + Type::union(Type::string(), Type::int(), Type::bool(), Type::null()), + Type::nullable(Type::object(Entity::class)), + Type::nullable(Type::array(Type::object(Entity::class))), + Type::nullable(Type::array()), + Type::nullable(Type::float()), // The existing constraint is float + Type::nullable(Type::string()), + Type::nullable(Type::string()), + Type::nullable(Type::array(Type::float())), + Type::string(), + Type::string(), + ]; + + $type = $this->types[$this->i]; + ++$this->i; - $propertyInfoStub + return $type; + } + + public function getTypes(string $class, string $property, array $context = []): ?array + { + $this->legacyTypes ??= [ + [new LegacyType('string', true)], + [new LegacyType('string')], + [new LegacyType('string', true), new LegacyType('int'), new LegacyType('bool')], + [new LegacyType('object', true, Entity::class)], + [new LegacyType('array', true, null, true, null, new LegacyType('object', false, Entity::class))], + [new LegacyType('array', true, null, true)], + [new LegacyType('float', true)], // The existing constraint is float + [new LegacyType('string', true)], + [new LegacyType('string', true)], + [new LegacyType('array', true, null, true, null, new LegacyType('float'))], + [new LegacyType('string')], + [new LegacyType('string')], + ]; + + $legacyType = $this->legacyTypes[$this->j]; + ++$this->j; + + return $legacyType; + } + }; + + $propertyAccessExtractor = $this->createMock(PropertyAccessExtractorInterface::class); + $propertyAccessExtractor ->method('isWritable') ->willReturn( true, @@ -87,11 +126,12 @@ public function testLoadClassMetadata() true, true, false, + true, true ) ; - $propertyInfoLoader = new PropertyInfoLoader($propertyInfoStub, $propertyInfoStub, $propertyInfoStub, '{.*}'); + $propertyInfoLoader = new PropertyInfoLoader($propertyListExtractor, $propertyTypeExtractor, $propertyAccessExtractor, '{.*}'); $validator = Validation::createValidatorBuilder() ->enableAttributeMapping() @@ -172,10 +212,9 @@ public function testLoadClassMetadata() $this->assertInstanceOf(TypeConstraint::class, $alreadyPartiallyMappedCollectionConstraints[0]->constraints[0]); $this->assertSame('string', $alreadyPartiallyMappedCollectionConstraints[0]->constraints[0]->type); $this->assertInstanceOf(Iban::class, $alreadyPartiallyMappedCollectionConstraints[0]->constraints[1]); - $this->assertInstanceOf(NotNull::class, $alreadyPartiallyMappedCollectionConstraints[0]->constraints[2]); $readOnlyMetadata = $classMetadata->getPropertyMetadata('readOnly'); - $this->assertEmpty($readOnlyMetadata); + $this->assertSame([], $readOnlyMetadata); /** @var PropertyMetadata[] $noAutoMappingMetadata */ $noAutoMappingMetadata = $classMetadata->getPropertyMetadata('noAutoMapping'); @@ -190,17 +229,27 @@ public function testLoadClassMetadata() */ public function testClassValidator(bool $expected, ?string $classValidatorRegexp = null) { - $propertyInfoStub = $this->createMock(PropertyInfoExtractorInterface::class); - $propertyInfoStub + $propertyListExtractor = $this->createMock(PropertyListExtractorInterface::class); + $propertyListExtractor ->method('getProperties') ->willReturn(['string']) ; - $propertyInfoStub - ->method('getTypes') - ->willReturn([new Type(Type::BUILTIN_TYPE_STRING)]) - ; - $propertyInfoLoader = new PropertyInfoLoader($propertyInfoStub, $propertyInfoStub, $propertyInfoStub, $classValidatorRegexp); + $propertyTypeExtractor = new class implements PropertyTypeExtractorInterface { + public function getType(string $class, string $property, array $context = []): ?Type + { + return Type::string(); + } + + public function getTypes(string $class, string $property, array $context = []): ?array + { + return [new LegacyType('string')]; + } + }; + + $propertyAccessExtractor = $this->createMock(PropertyAccessExtractorInterface::class); + + $propertyInfoLoader = new PropertyInfoLoader($propertyListExtractor, $propertyTypeExtractor, $propertyAccessExtractor, $classValidatorRegexp); $classMetadata = new ClassMetadata(PropertyInfoLoaderEntity::class); $this->assertSame($expected, $propertyInfoLoader->loadClassMetadata($classMetadata)); @@ -216,22 +265,31 @@ public static function regexpProvider(): array ]; } - public function testClassNoAutoMapping() + public function testClassNoAutoMapping(?PropertyTypeExtractorInterface $propertyListExtractor = null) { - $propertyInfoStub = $this->createMock(PropertyInfoExtractorInterface::class); - $propertyInfoStub - ->method('getProperties') - ->willReturn(['string', 'autoMappingExplicitlyEnabled']) - ; + if (null === $propertyListExtractor) { + $propertyListExtractor = $this->createMock(PropertyListExtractorInterface::class); + $propertyListExtractor + ->method('getProperties') + ->willReturn(['string', 'autoMappingExplicitlyEnabled']) + ; - $propertyInfoStub - ->method('getTypes') - ->willReturn( - [new Type(Type::BUILTIN_TYPE_STRING)], - [new Type(Type::BUILTIN_TYPE_BOOL)] - ); + $propertyTypeExtractor = new class implements PropertyTypeExtractorInterface { + public function getType(string $class, string $property, array $context = []): ?Type + { + return Type::string(); + } + + public function getTypes(string $class, string $property, array $context = []): ?array + { + return [new LegacyType('string')]; + } + }; + } + + $propertyAccessExtractor = $this->createMock(PropertyAccessExtractorInterface::class); - $propertyInfoLoader = new PropertyInfoLoader($propertyInfoStub, $propertyInfoStub, $propertyInfoStub, '{.*}'); + $propertyInfoLoader = new PropertyInfoLoader($propertyListExtractor, $propertyTypeExtractor, $propertyAccessExtractor, '{.*}'); $validator = Validation::createValidatorBuilder() ->enableAttributeMapping() ->addLoader($propertyInfoLoader) @@ -240,7 +298,7 @@ public function testClassNoAutoMapping() /** @var ClassMetadata $classMetadata */ $classMetadata = $validator->getMetadataFor(new PropertyInfoLoaderNoAutoMappingEntity()); - $this->assertEmpty($classMetadata->getPropertyMetadata('string')); + $this->assertSame([], $classMetadata->getPropertyMetadata('string')); $this->assertCount(2, $classMetadata->getPropertyMetadata('autoMappingExplicitlyEnabled')[0]->constraints); $this->assertSame(AutoMappingStrategy::ENABLED, $classMetadata->getPropertyMetadata('autoMappingExplicitlyEnabled')[0]->getAutoMappingStrategy()); } diff --git a/Tests/Mapping/Loader/XmlFileLoaderTest.php b/Tests/Mapping/Loader/XmlFileLoaderTest.php index 2385dc888..03757720c 100644 --- a/Tests/Mapping/Loader/XmlFileLoaderTest.php +++ b/Tests/Mapping/Loader/XmlFileLoaderTest.php @@ -12,11 +12,13 @@ namespace Symfony\Component\Validator\Tests\Mapping\Loader; use PHPUnit\Framework\TestCase; +use Symfony\Bridge\PhpUnit\ExpectUserDeprecationMessageTrait; use Symfony\Component\Validator\Constraints\All; use Symfony\Component\Validator\Constraints\Callback; use Symfony\Component\Validator\Constraints\Choice; use Symfony\Component\Validator\Constraints\Collection; use Symfony\Component\Validator\Constraints\IsTrue; +use Symfony\Component\Validator\Constraints\Length; use Symfony\Component\Validator\Constraints\NotNull; use Symfony\Component\Validator\Constraints\Range; use Symfony\Component\Validator\Constraints\Regex; @@ -29,6 +31,7 @@ use Symfony\Component\Validator\Tests\Fixtures\ConstraintA; use Symfony\Component\Validator\Tests\Fixtures\ConstraintB; use Symfony\Component\Validator\Tests\Fixtures\ConstraintWithRequiredArgument; +use Symfony\Component\Validator\Tests\Fixtures\DummyEntityConstraintWithoutNamedArguments; use Symfony\Component\Validator\Tests\Fixtures\Entity_81; use Symfony\Component\Validator\Tests\Fixtures\NestedAttribute\Entity; use Symfony\Component\Validator\Tests\Fixtures\NestedAttribute\GroupSequenceProviderEntity; @@ -37,6 +40,8 @@ class XmlFileLoaderTest extends TestCase { + use ExpectUserDeprecationMessageTrait; + public function testLoadClassMetadataReturnsTrueIfSuccessful() { $loader = new XmlFileLoader(__DIR__.'/constraint-mapping.xml'); @@ -72,18 +77,18 @@ public function testLoadClassMetadata() $expected->addConstraint(new ConstraintWithNamedArguments(['foo', 'bar'])); $expected->addConstraint(new ConstraintWithoutValueWithNamedArguments(['foo'])); $expected->addPropertyConstraint('firstName', new NotNull()); - $expected->addPropertyConstraint('firstName', new Range(['min' => 3])); + $expected->addPropertyConstraint('firstName', new Range(min: 3)); $expected->addPropertyConstraint('firstName', new Choice(['A', 'B'])); - $expected->addPropertyConstraint('firstName', new All([new NotNull(), new Range(['min' => 3])])); - $expected->addPropertyConstraint('firstName', new All(['constraints' => [new NotNull(), new Range(['min' => 3])]])); - $expected->addPropertyConstraint('firstName', new Collection(['fields' => [ - 'foo' => [new NotNull(), new Range(['min' => 3])], - 'bar' => [new Range(['min' => 5])], - ]])); - $expected->addPropertyConstraint('firstName', new Choice([ - 'message' => 'Must be one of %choices%', - 'choices' => ['A', 'B'], + $expected->addPropertyConstraint('firstName', new All(constraints: [new NotNull(), new Range(min: 3)])); + $expected->addPropertyConstraint('firstName', new All(constraints: [new NotNull(), new Range(min: 3)])); + $expected->addPropertyConstraint('firstName', new Collection(fields: [ + 'foo' => [new NotNull(), new Range(min: 3)], + 'bar' => [new Range(min: 5)], ])); + $expected->addPropertyConstraint('firstName', new Choice( + message: 'Must be one of %choices%', + choices: ['A', 'B'], + )); $expected->addGetterConstraint('lastName', new NotNull()); $expected->addGetterConstraint('valid', new IsTrue()); $expected->addGetterConstraint('permissions', new IsTrue()); @@ -99,7 +104,7 @@ public function testLoadClassMetadataWithNonStrings() $loader->loadClassMetadata($metadata); $expected = new ClassMetadata(Entity::class); - $expected->addPropertyConstraint('firstName', new Regex(['pattern' => '/^1/', 'match' => false])); + $expected->addPropertyConstraint('firstName', new Regex(pattern: '/^1/', match: false)); $properties = $metadata->getPropertyMetadata('firstName'); $constraints = $properties[0]->getConstraints(); @@ -171,4 +176,36 @@ public function testDoNotModifyStateIfExceptionIsThrown() $loader->loadClassMetadata($metadata); } } + + /** + * @group legacy + */ + public function testLoadConstraintWithoutNamedArgumentsSupport() + { + $loader = new XmlFileLoader(__DIR__.'/constraint-without-named-arguments-support.xml'); + $metadata = new ClassMetadata(DummyEntityConstraintWithoutNamedArguments::class); + + $this->expectUserDeprecationMessage('Since symfony/validator 7.3: Using constraints not supporting named arguments is deprecated. Try adding the HasNamedArguments attribute to Symfony\Component\Validator\Tests\Mapping\Loader\Fixtures\ConstraintWithoutNamedArguments.'); + + $loader->loadClassMetadata($metadata); + } + + /** + * @group legacy + */ + public function testLengthConstraintValueOptionTriggersDeprecation() + { + $loader = new XmlFileLoader(__DIR__.'/constraint-mapping-exactly-value.xml'); + $metadata = new ClassMetadata(Entity_81::class); + + $this->expectUserDeprecationMessage(\sprintf('Since symfony/validator 7.3: Passing an array of options to configure the "%s" constraint is deprecated, use named arguments instead.', Length::class)); + + $loader->loadClassMetadata($metadata); + $constraints = $metadata->getPropertyMetadata('title')[0]->constraints; + + self::assertCount(1, $constraints); + self::assertInstanceOf(Length::class, $constraints[0]); + self::assertSame(6, $constraints[0]->min); + self::assertSame(6, $constraints[0]->max); + } } diff --git a/Tests/Mapping/Loader/YamlFileLoaderTest.php b/Tests/Mapping/Loader/YamlFileLoaderTest.php index e34e5466e..c3bbcb18e 100644 --- a/Tests/Mapping/Loader/YamlFileLoaderTest.php +++ b/Tests/Mapping/Loader/YamlFileLoaderTest.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Validator\Tests\Mapping\Loader; use PHPUnit\Framework\TestCase; +use Symfony\Bridge\PhpUnit\ExpectUserDeprecationMessageTrait; use Symfony\Component\Validator\Constraints\All; use Symfony\Component\Validator\Constraints\Callback; use Symfony\Component\Validator\Constraints\Choice; @@ -26,6 +27,7 @@ use Symfony\Component\Validator\Tests\Fixtures\ConstraintA; use Symfony\Component\Validator\Tests\Fixtures\ConstraintB; use Symfony\Component\Validator\Tests\Fixtures\ConstraintWithRequiredArgument; +use Symfony\Component\Validator\Tests\Fixtures\DummyEntityConstraintWithoutNamedArguments; use Symfony\Component\Validator\Tests\Fixtures\Entity_81; use Symfony\Component\Validator\Tests\Fixtures\NestedAttribute\Entity; use Symfony\Component\Validator\Tests\Fixtures\NestedAttribute\GroupSequenceProviderEntity; @@ -34,6 +36,8 @@ class YamlFileLoaderTest extends TestCase { + use ExpectUserDeprecationMessageTrait; + public function testLoadClassMetadataReturnsFalseIfEmpty() { $loader = new YamlFileLoader(__DIR__.'/empty-mapping.yml'); @@ -50,10 +54,11 @@ public function testLoadClassMetadataReturnsFalseIfEmpty() */ public function testInvalidYamlFiles($path) { - $this->expectException(\InvalidArgumentException::class); $loader = new YamlFileLoader(__DIR__.'/'.$path); $metadata = new ClassMetadata(Entity::class); + $this->expectException(\InvalidArgumentException::class); + $loader->loadClassMetadata($metadata); } @@ -115,18 +120,18 @@ public function testLoadClassMetadata() $expected->addConstraint(new ConstraintWithNamedArguments('foo')); $expected->addConstraint(new ConstraintWithNamedArguments(['foo', 'bar'])); $expected->addPropertyConstraint('firstName', new NotNull()); - $expected->addPropertyConstraint('firstName', new Range(['min' => 3])); + $expected->addPropertyConstraint('firstName', new Range(min: 3)); $expected->addPropertyConstraint('firstName', new Choice(['A', 'B'])); - $expected->addPropertyConstraint('firstName', new All([new NotNull(), new Range(['min' => 3])])); - $expected->addPropertyConstraint('firstName', new All(['constraints' => [new NotNull(), new Range(['min' => 3])]])); - $expected->addPropertyConstraint('firstName', new Collection(['fields' => [ - 'foo' => [new NotNull(), new Range(['min' => 3])], - 'bar' => [new Range(['min' => 5])], - ]])); - $expected->addPropertyConstraint('firstName', new Choice([ - 'message' => 'Must be one of %choices%', - 'choices' => ['A', 'B'], + $expected->addPropertyConstraint('firstName', new All(constraints: [new NotNull(), new Range(min: 3)])); + $expected->addPropertyConstraint('firstName', new All(constraints: [new NotNull(), new Range(min: 3)])); + $expected->addPropertyConstraint('firstName', new Collection(fields: [ + 'foo' => [new NotNull(), new Range(min: 3)], + 'bar' => [new Range(min: 5)], ])); + $expected->addPropertyConstraint('firstName', new Choice( + message: 'Must be one of %choices%', + choices: ['A', 'B'], + )); $expected->addGetterConstraint('lastName', new NotNull()); $expected->addGetterConstraint('valid', new IsTrue()); $expected->addGetterConstraint('permissions', new IsTrue()); @@ -142,7 +147,7 @@ public function testLoadClassMetadataWithConstants() $loader->loadClassMetadata($metadata); $expected = new ClassMetadata(Entity::class); - $expected->addPropertyConstraint('firstName', new Range(['max' => \PHP_INT_MAX])); + $expected->addPropertyConstraint('firstName', new Range(max: \PHP_INT_MAX)); $this->assertEquals($expected, $metadata); } @@ -186,4 +191,17 @@ public function testLoadGroupProvider() $this->assertEquals($expected, $metadata); } + + /** + * @group legacy + */ + public function testLoadConstraintWithoutNamedArgumentsSupport() + { + $loader = new YamlFileLoader(__DIR__.'/constraint-without-named-arguments-support.yml'); + $metadata = new ClassMetadata(DummyEntityConstraintWithoutNamedArguments::class); + + $this->expectUserDeprecationMessage('Since symfony/validator 7.3: Using constraints not supporting named arguments is deprecated. Try adding the HasNamedArguments attribute to Symfony\Component\Validator\Tests\Mapping\Loader\Fixtures\ConstraintWithoutNamedArguments.'); + + $loader->loadClassMetadata($metadata); + } } diff --git a/Tests/Mapping/Loader/constraint-mapping-exactly-value.xml b/Tests/Mapping/Loader/constraint-mapping-exactly-value.xml new file mode 100644 index 000000000..40e04f3a3 --- /dev/null +++ b/Tests/Mapping/Loader/constraint-mapping-exactly-value.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + diff --git a/Tests/Mapping/Loader/constraint-without-named-arguments-support.xml b/Tests/Mapping/Loader/constraint-without-named-arguments-support.xml new file mode 100644 index 000000000..48321b174 --- /dev/null +++ b/Tests/Mapping/Loader/constraint-without-named-arguments-support.xml @@ -0,0 +1,10 @@ + + + + + + + + diff --git a/Tests/Mapping/Loader/constraint-without-named-arguments-support.yml b/Tests/Mapping/Loader/constraint-without-named-arguments-support.yml new file mode 100644 index 000000000..3e25b78e4 --- /dev/null +++ b/Tests/Mapping/Loader/constraint-without-named-arguments-support.yml @@ -0,0 +1,4 @@ +Symfony\Component\Validator\Tests\Fixtures\DummyEntityConstraintWithoutNamedArguments: + constraints: + - Symfony\Component\Validator\Tests\Mapping\Loader\Fixtures\ConstraintWithoutNamedArguments: + groups: foo diff --git a/Tests/Mapping/MemberMetadataTest.php b/Tests/Mapping/MemberMetadataTest.php index cd429e676..84d047f10 100644 --- a/Tests/Mapping/MemberMetadataTest.php +++ b/Tests/Mapping/MemberMetadataTest.php @@ -84,7 +84,7 @@ public function testSerialize() public function testSerializeCollectionCascaded() { - $this->metadata->addConstraint(new Valid(['traverse' => true])); + $this->metadata->addConstraint(new Valid(traverse: true)); $metadata = unserialize(serialize($this->metadata)); @@ -93,7 +93,7 @@ public function testSerializeCollectionCascaded() public function testSerializeCollectionNotCascaded() { - $this->metadata->addConstraint(new Valid(['traverse' => false])); + $this->metadata->addConstraint(new Valid(traverse: false)); $metadata = unserialize(serialize($this->metadata)); diff --git a/Tests/Test/CompoundConstraintTestCaseTest.php b/Tests/Test/CompoundConstraintTestCaseTest.php new file mode 100644 index 000000000..c59f3bf57 --- /dev/null +++ b/Tests/Test/CompoundConstraintTestCaseTest.php @@ -0,0 +1,88 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Tests\Test; + +use PHPUnit\Framework\ExpectationFailedException; +use Symfony\Component\Validator\Constraints\Compound; +use Symfony\Component\Validator\Constraints\Length; +use Symfony\Component\Validator\Constraints\NotBlank; +use Symfony\Component\Validator\Constraints\Regex; +use Symfony\Component\Validator\Test\CompoundConstraintTestCase; +use Symfony\Component\Validator\Tests\Fixtures\DummyCompoundConstraint; + +/** + * @extends CompoundConstraintTestCase + */ +class CompoundConstraintTestCaseTest extends CompoundConstraintTestCase +{ + protected function createCompound(): Compound + { + return new DummyCompoundConstraint(); + } + + public function testAssertNoViolation() + { + $this->validateValue('ab1'); + + $this->assertNoViolation(); + $this->assertViolationsCount(0); + } + + public function testAssertIsRaisedByCompound() + { + $this->validateValue(''); + + $this->assertViolationsRaisedByCompound(new NotBlank()); + $this->assertViolationsCount(1); + } + + public function testMultipleAssertAreRaisedByCompound() + { + $this->validateValue('1245'); + + $this->assertViolationsRaisedByCompound([ + new Length(max: 3), + new Regex('/[a-z]+/'), + ]); + $this->assertViolationsCount(2); + } + + public function testNoAssertRaisedButExpected() + { + $this->validateValue('azert'); + + $this->expectException(ExpectationFailedException::class); + $this->expectExceptionMessage("Expected violation(s) for constraint(s) Symfony\Component\Validator\Constraints\Length, Symfony\Component\Validator\Constraints\Regex to be raised by compound."); + $this->assertViolationsRaisedByCompound([ + new Length(max: 5), + new Regex('/^[A-Z]+$/'), + ]); + } + + public function testAssertRaisedByCompoundIsNotExactlyTheSame() + { + $this->validateValue('123'); + + $this->expectException(ExpectationFailedException::class); + $this->expectExceptionMessage('Expected violation(s) for constraint(s) Symfony\Component\Validator\Constraints\Regex to be raised by compound.'); + $this->assertViolationsRaisedByCompound(new Regex('/^[a-z]+$/')); + } + + public function testAssertRaisedByCompoundButGotNone() + { + $this->validateValue('123'); + + $this->expectException(ExpectationFailedException::class); + $this->expectExceptionMessage('Expected at least one violation for constraint(s) "Symfony\Component\Validator\Constraints\Length", got none raised.'); + $this->assertViolationsRaisedByCompound(new Length(max: 5)); + } +} diff --git a/Tests/Validator/RecursiveValidatorTest.php b/Tests/Validator/RecursiveValidatorTest.php index e02023ab9..1ae14ba30 100644 --- a/Tests/Validator/RecursiveValidatorTest.php +++ b/Tests/Validator/RecursiveValidatorTest.php @@ -113,10 +113,10 @@ public function testValidate() $context->addViolation('Message %param%', ['%param%' => 'value']); }; - $constraint = new Callback([ - 'callback' => $callback, - 'groups' => 'Group', - ]); + $constraint = new Callback( + callback: $callback, + groups: ['Group'], + ); $violations = $this->validate('Bernhard', $constraint, 'Group'); @@ -149,10 +149,10 @@ public function testClassConstraint() $context->addViolation('Message %param%', ['%param%' => 'value']); }; - $this->metadata->addConstraint(new Callback([ - 'callback' => $callback, - 'groups' => 'Group', - ])); + $this->metadata->addConstraint(new Callback( + callback: $callback, + groups: ['Group'], + )); $violations = $this->validate($entity, null, 'Group'); @@ -188,10 +188,10 @@ public function testPropertyConstraint() $context->addViolation('Message %param%', ['%param%' => 'value']); }; - $this->metadata->addPropertyConstraint('firstName', new Callback([ - 'callback' => $callback, - 'groups' => 'Group', - ])); + $this->metadata->addPropertyConstraint('firstName', new Callback( + callback: $callback, + groups: ['Group'], + )); $violations = $this->validate($entity, null, 'Group'); @@ -227,10 +227,10 @@ public function testGetterConstraint() $context->addViolation('Message %param%', ['%param%' => 'value']); }; - $this->metadata->addGetterConstraint('lastName', new Callback([ - 'callback' => $callback, - 'groups' => 'Group', - ])); + $this->metadata->addGetterConstraint('lastName', new Callback( + callback: $callback, + groups: ['Group'], + )); $violations = $this->validate($entity, null, 'Group'); @@ -264,10 +264,10 @@ public function testArray() $context->addViolation('Message %param%', ['%param%' => 'value']); }; - $this->metadata->addConstraint(new Callback([ - 'callback' => $callback, - 'groups' => 'Group', - ])); + $this->metadata->addConstraint(new Callback( + callback: $callback, + groups: ['Group'], + )); $violations = $this->validate($array, null, 'Group'); @@ -301,10 +301,10 @@ public function testRecursiveArray() $context->addViolation('Message %param%', ['%param%' => 'value']); }; - $this->metadata->addConstraint(new Callback([ - 'callback' => $callback, - 'groups' => 'Group', - ])); + $this->metadata->addConstraint(new Callback( + callback: $callback, + groups: ['Group'], + )); $violations = $this->validate($array, null, 'Group'); @@ -338,10 +338,10 @@ public function testTraversable() $context->addViolation('Message %param%', ['%param%' => 'value']); }; - $this->metadata->addConstraint(new Callback([ - 'callback' => $callback, - 'groups' => 'Group', - ])); + $this->metadata->addConstraint(new Callback( + callback: $callback, + groups: ['Group'], + )); $violations = $this->validate($traversable, null, 'Group'); @@ -377,10 +377,10 @@ public function testRecursiveTraversable() $context->addViolation('Message %param%', ['%param%' => 'value']); }; - $this->metadata->addConstraint(new Callback([ - 'callback' => $callback, - 'groups' => 'Group', - ])); + $this->metadata->addConstraint(new Callback( + callback: $callback, + groups: ['Group'], + )); $violations = $this->validate($traversable, null, 'Group'); @@ -415,10 +415,10 @@ public function testReferenceClassConstraint() }; $this->metadata->addPropertyConstraint('reference', new Valid()); - $this->referenceMetadata->addConstraint(new Callback([ - 'callback' => $callback, - 'groups' => 'Group', - ])); + $this->referenceMetadata->addConstraint(new Callback( + callback: $callback, + groups: ['Group'], + )); $violations = $this->validate($entity, null, 'Group'); @@ -456,10 +456,10 @@ public function testReferencePropertyConstraint() }; $this->metadata->addPropertyConstraint('reference', new Valid()); - $this->referenceMetadata->addPropertyConstraint('value', new Callback([ - 'callback' => $callback, - 'groups' => 'Group', - ])); + $this->referenceMetadata->addPropertyConstraint('value', new Callback( + callback: $callback, + groups: ['Group'], + )); $violations = $this->validate($entity, null, 'Group'); @@ -497,10 +497,10 @@ public function testReferenceGetterConstraint() }; $this->metadata->addPropertyConstraint('reference', new Valid()); - $this->referenceMetadata->addPropertyConstraint('privateValue', new Callback([ - 'callback' => $callback, - 'groups' => 'Group', - ])); + $this->referenceMetadata->addPropertyConstraint('privateValue', new Callback( + callback: $callback, + groups: ['Group'], + )); $violations = $this->validate($entity, null, 'Group'); @@ -531,12 +531,13 @@ public function testsIgnoreNullReference() public function testFailOnScalarReferences() { - $this->expectException(NoSuchMetadataException::class); $entity = new Entity(); $entity->reference = 'string'; $this->metadata->addPropertyConstraint('reference', new Valid()); + $this->expectException(NoSuchMetadataException::class); + $this->validate($entity); } @@ -562,10 +563,10 @@ public function testArrayReference($constraintMethod) }; $this->metadata->$constraintMethod('reference', new Valid()); - $this->referenceMetadata->addConstraint(new Callback([ - 'callback' => $callback, - 'groups' => 'Group', - ])); + $this->referenceMetadata->addConstraint(new Callback( + callback: $callback, + groups: ['Group'], + )); $violations = $this->validate($entity, null, 'Group'); @@ -603,10 +604,10 @@ public function testRecursiveArrayReference($constraintMethod) }; $this->metadata->$constraintMethod('reference', new Valid()); - $this->referenceMetadata->addConstraint(new Callback([ - 'callback' => $callback, - 'groups' => 'Group', - ])); + $this->referenceMetadata->addConstraint(new Callback( + callback: $callback, + groups: ['Group'], + )); $violations = $this->validate($entity, null, 'Group'); @@ -631,14 +632,14 @@ public function testOnlyCascadedArraysAreTraversed() $context->addViolation('Message %param%', ['%param%' => 'value']); }; - $this->metadata->addPropertyConstraint('reference', new Callback([ - 'callback' => function () {}, - 'groups' => 'Group', - ])); - $this->referenceMetadata->addConstraint(new Callback([ - 'callback' => $callback, - 'groups' => 'Group', - ])); + $this->metadata->addPropertyConstraint('reference', new Callback( + callback: function () {}, + groups: ['Group'], + )); + $this->referenceMetadata->addConstraint(new Callback( + callback: $callback, + groups: ['Group'], + )); $violations = $this->validate($entity, null, 'Group'); @@ -658,9 +659,9 @@ public function testArrayTraversalCannotBeDisabled($constraintMethod) $context->addViolation('Message %param%', ['%param%' => 'value']); }; - $this->metadata->$constraintMethod('reference', new Valid([ - 'traverse' => false, - ])); + $this->metadata->$constraintMethod('reference', new Valid( + traverse: false, + )); $this->referenceMetadata->addConstraint(new Callback($callback)); $violations = $this->validate($entity); @@ -681,9 +682,9 @@ public function testRecursiveArrayTraversalCannotBeDisabled($constraintMethod) $context->addViolation('Message %param%', ['%param%' => 'value']); }; - $this->metadata->$constraintMethod('reference', new Valid([ - 'traverse' => false, - ])); + $this->metadata->$constraintMethod('reference', new Valid( + traverse: false, + )); $this->referenceMetadata->addConstraint(new Callback($callback)); @@ -744,10 +745,10 @@ public function testTraversableReference() }; $this->metadata->addPropertyConstraint('reference', new Valid()); - $this->referenceMetadata->addConstraint(new Callback([ - 'callback' => $callback, - 'groups' => 'Group', - ])); + $this->referenceMetadata->addConstraint(new Callback( + callback: $callback, + groups: ['Group'], + )); $violations = $this->validate($entity, null, 'Group'); @@ -773,9 +774,9 @@ public function testDisableTraversableTraversal() }; $this->metadataFactory->addMetadata(new ClassMetadata('ArrayIterator')); - $this->metadata->addPropertyConstraint('reference', new Valid([ - 'traverse' => false, - ])); + $this->metadata->addPropertyConstraint('reference', new Valid( + traverse: false, + )); $this->referenceMetadata->addConstraint(new Callback($callback)); $violations = $this->validate($entity); @@ -786,13 +787,14 @@ public function testDisableTraversableTraversal() public function testMetadataMustExistIfTraversalIsDisabled() { - $this->expectException(NoSuchMetadataException::class); $entity = new Entity(); $entity->reference = new \ArrayIterator(); - $this->metadata->addPropertyConstraint('reference', new Valid([ - 'traverse' => false, - ])); + $this->metadata->addPropertyConstraint('reference', new Valid( + traverse: false, + )); + + $this->expectException(NoSuchMetadataException::class); $this->validate($entity); } @@ -817,13 +819,13 @@ public function testEnableRecursiveTraversableTraversal() $context->addViolation('Message %param%', ['%param%' => 'value']); }; - $this->metadata->addPropertyConstraint('reference', new Valid([ - 'traverse' => true, - ])); - $this->referenceMetadata->addConstraint(new Callback([ - 'callback' => $callback, - 'groups' => 'Group', - ])); + $this->metadata->addPropertyConstraint('reference', new Valid( + traverse: true, + )); + $this->referenceMetadata->addConstraint(new Callback( + callback: $callback, + groups: ['Group'], + )); $violations = $this->validate($entity, null, 'Group'); @@ -864,14 +866,14 @@ public function testValidateProperty() $context->addViolation('Other violation'); }; - $this->metadata->addPropertyConstraint('firstName', new Callback([ - 'callback' => $callback1, - 'groups' => 'Group', - ])); - $this->metadata->addPropertyConstraint('lastName', new Callback([ - 'callback' => $callback2, - 'groups' => 'Group', - ])); + $this->metadata->addPropertyConstraint('firstName', new Callback( + callback: $callback1, + groups: ['Group'], + )); + $this->metadata->addPropertyConstraint('lastName', new Callback( + callback: $callback2, + groups: ['Group'], + )); $violations = $this->validateProperty($entity, 'firstName', 'Group'); @@ -922,14 +924,14 @@ public function testValidatePropertyValue() $context->addViolation('Other violation'); }; - $this->metadata->addPropertyConstraint('firstName', new Callback([ - 'callback' => $callback1, - 'groups' => 'Group', - ])); - $this->metadata->addPropertyConstraint('lastName', new Callback([ - 'callback' => $callback2, - 'groups' => 'Group', - ])); + $this->metadata->addPropertyConstraint('firstName', new Callback( + callback: $callback1, + groups: ['Group'], + )); + $this->metadata->addPropertyConstraint('lastName', new Callback( + callback: $callback2, + groups: ['Group'], + )); $violations = $this->validatePropertyValue( $entity, @@ -971,14 +973,14 @@ public function testValidatePropertyValueWithClassName() $context->addViolation('Other violation'); }; - $this->metadata->addPropertyConstraint('firstName', new Callback([ - 'callback' => $callback1, - 'groups' => 'Group', - ])); - $this->metadata->addPropertyConstraint('lastName', new Callback([ - 'callback' => $callback2, - 'groups' => 'Group', - ])); + $this->metadata->addPropertyConstraint('firstName', new Callback( + callback: $callback1, + groups: ['Group'], + )); + $this->metadata->addPropertyConstraint('lastName', new Callback( + callback: $callback2, + groups: ['Group'], + )); $violations = $this->validatePropertyValue( self::ENTITY_CLASS, @@ -1058,14 +1060,14 @@ public function testValidateSingleGroup() $context->addViolation('Message'); }; - $this->metadata->addConstraint(new Callback([ - 'callback' => $callback, - 'groups' => 'Group 1', - ])); - $this->metadata->addConstraint(new Callback([ - 'callback' => $callback, - 'groups' => 'Group 2', - ])); + $this->metadata->addConstraint(new Callback( + callback: $callback, + groups: ['Group 1'], + )); + $this->metadata->addConstraint(new Callback( + callback: $callback, + groups: ['Group 2'], + )); $violations = $this->validate($entity, null, 'Group 2'); @@ -1081,14 +1083,14 @@ public function testValidateMultipleGroups() $context->addViolation('Message'); }; - $this->metadata->addConstraint(new Callback([ - 'callback' => $callback, - 'groups' => 'Group 1', - ])); - $this->metadata->addConstraint(new Callback([ - 'callback' => $callback, - 'groups' => 'Group 2', - ])); + $this->metadata->addConstraint(new Callback( + callback: $callback, + groups: ['Group 1'], + )); + $this->metadata->addConstraint(new Callback( + callback: $callback, + groups: ['Group 2'], + )); $violations = $this->validate($entity, null, ['Group 1', 'Group 2']); @@ -1107,18 +1109,18 @@ public function testReplaceDefaultGroupByGroupSequenceObject() $context->addViolation('Violation in Group 3'); }; - $this->metadata->addConstraint(new Callback([ - 'callback' => function () {}, - 'groups' => 'Group 1', - ])); - $this->metadata->addConstraint(new Callback([ - 'callback' => $callback1, - 'groups' => 'Group 2', - ])); - $this->metadata->addConstraint(new Callback([ - 'callback' => $callback2, - 'groups' => 'Group 3', - ])); + $this->metadata->addConstraint(new Callback( + callback: function () {}, + groups: ['Group 1'], + )); + $this->metadata->addConstraint(new Callback( + callback: $callback1, + groups: ['Group 2'], + )); + $this->metadata->addConstraint(new Callback( + callback: $callback2, + groups: ['Group 3'], + )); $sequence = new GroupSequence(['Group 1', 'Group 2', 'Group 3', 'Entity']); $this->metadata->setGroupSequence($sequence); @@ -1141,18 +1143,18 @@ public function testReplaceDefaultGroupByGroupSequenceArray() $context->addViolation('Violation in Group 3'); }; - $this->metadata->addConstraint(new Callback([ - 'callback' => function () {}, - 'groups' => 'Group 1', - ])); - $this->metadata->addConstraint(new Callback([ - 'callback' => $callback1, - 'groups' => 'Group 2', - ])); - $this->metadata->addConstraint(new Callback([ - 'callback' => $callback2, - 'groups' => 'Group 3', - ])); + $this->metadata->addConstraint(new Callback( + callback: function () {}, + groups: ['Group 1'], + )); + $this->metadata->addConstraint(new Callback( + callback: $callback1, + groups: ['Group 2'], + )); + $this->metadata->addConstraint(new Callback( + callback: $callback2, + groups: ['Group 3'], + )); $sequence = ['Group 1', 'Group 2', 'Group 3', 'Entity']; $this->metadata->setGroupSequence($sequence); @@ -1177,14 +1179,14 @@ public function testPropagateDefaultGroupToReferenceWhenReplacingDefaultGroup() }; $this->metadata->addPropertyConstraint('reference', new Valid()); - $this->referenceMetadata->addConstraint(new Callback([ - 'callback' => $callback1, - 'groups' => 'Default', - ])); - $this->referenceMetadata->addConstraint(new Callback([ - 'callback' => $callback2, - 'groups' => 'Group 1', - ])); + $this->referenceMetadata->addConstraint(new Callback( + callback: $callback1, + groups: ['Default'], + )); + $this->referenceMetadata->addConstraint(new Callback( + callback: $callback2, + groups: ['Group 1'], + )); $sequence = new GroupSequence(['Group 1', 'Entity']); $this->metadata->setGroupSequence($sequence); @@ -1207,14 +1209,14 @@ public function testValidateCustomGroupWhenDefaultGroupWasReplaced() $context->addViolation('Violation in group sequence'); }; - $this->metadata->addConstraint(new Callback([ - 'callback' => $callback1, - 'groups' => 'Other Group', - ])); - $this->metadata->addConstraint(new Callback([ - 'callback' => $callback2, - 'groups' => 'Group 1', - ])); + $this->metadata->addConstraint(new Callback( + callback: $callback1, + groups: ['Other Group'], + )); + $this->metadata->addConstraint(new Callback( + callback: $callback2, + groups: ['Group 1'], + )); $sequence = new GroupSequence(['Group 1', 'Entity']); $this->metadata->setGroupSequence($sequence); @@ -1241,18 +1243,18 @@ public function testReplaceDefaultGroup($sequence, array $assertViolations) }; $metadata = new ClassMetadata($entity::class); - $metadata->addConstraint(new Callback([ - 'callback' => function () {}, - 'groups' => 'Group 1', - ])); - $metadata->addConstraint(new Callback([ - 'callback' => $callback1, - 'groups' => 'Group 2', - ])); - $metadata->addConstraint(new Callback([ - 'callback' => $callback2, - 'groups' => 'Group 3', - ])); + $metadata->addConstraint(new Callback( + callback: function () {}, + groups: ['Group 1'], + )); + $metadata->addConstraint(new Callback( + callback: $callback1, + groups: ['Group 2'], + )); + $metadata->addConstraint(new Callback( + callback: $callback2, + groups: ['Group 3'], + )); $metadata->setGroupSequenceProvider(true); $this->metadataFactory->addMetadata($metadata); @@ -1346,18 +1348,18 @@ public function testGroupSequenceAbortsAfterFailedGroup() $context->addViolation('Message 2'); }; - $this->metadata->addConstraint(new Callback([ - 'callback' => function () {}, - 'groups' => 'Group 1', - ])); - $this->metadata->addConstraint(new Callback([ - 'callback' => $callback1, - 'groups' => 'Group 2', - ])); - $this->metadata->addConstraint(new Callback([ - 'callback' => $callback2, - 'groups' => 'Group 3', - ])); + $this->metadata->addConstraint(new Callback( + callback: function () {}, + groups: ['Group 1'], + )); + $this->metadata->addConstraint(new Callback( + callback: $callback1, + groups: ['Group 2'], + )); + $this->metadata->addConstraint(new Callback( + callback: $callback2, + groups: ['Group 3'], + )); $sequence = new GroupSequence(['Group 1', 'Group 2', 'Group 3']); $violations = $this->validator->validate($entity, new Valid(), $sequence); @@ -1380,14 +1382,14 @@ public function testGroupSequenceIncludesReferences() }; $this->metadata->addPropertyConstraint('reference', new Valid()); - $this->referenceMetadata->addConstraint(new Callback([ - 'callback' => $callback1, - 'groups' => 'Group 1', - ])); - $this->referenceMetadata->addConstraint(new Callback([ - 'callback' => $callback2, - 'groups' => 'Group 2', - ])); + $this->referenceMetadata->addConstraint(new Callback( + callback: $callback1, + groups: ['Group 1'], + )); + $this->referenceMetadata->addConstraint(new Callback( + callback: $callback2, + groups: ['Group 2'], + )); $sequence = new GroupSequence(['Group 1', 'Entity']); $violations = $this->validator->validate($entity, new Valid(), $sequence); @@ -1440,14 +1442,14 @@ public function testValidateInSeparateContext() $context->addViolation('Message %param%', ['%param%' => 'value']); }; - $this->metadata->addConstraint(new Callback([ - 'callback' => $callback1, - 'groups' => 'Group', - ])); - $this->referenceMetadata->addConstraint(new Callback([ - 'callback' => $callback2, - 'groups' => 'Group', - ])); + $this->metadata->addConstraint(new Callback( + callback: $callback1, + groups: ['Group'], + )); + $this->referenceMetadata->addConstraint(new Callback( + callback: $callback2, + groups: ['Group'], + )); $violations = $this->validator->validate($entity, new Valid(), 'Group'); @@ -1496,14 +1498,14 @@ public function testValidateInContext() $context->addViolation('Message %param%', ['%param%' => 'value']); }; - $this->metadata->addConstraint(new Callback([ - 'callback' => $callback1, - 'groups' => 'Group', - ])); - $this->referenceMetadata->addConstraint(new Callback([ - 'callback' => $callback2, - 'groups' => 'Group', - ])); + $this->metadata->addConstraint(new Callback( + callback: $callback1, + groups: ['Group'], + )); + $this->referenceMetadata->addConstraint(new Callback( + callback: $callback2, + groups: ['Group'], + )); $violations = $this->validator->validate($entity, new Valid(), 'Group'); @@ -1559,14 +1561,14 @@ public function testValidateArrayInContext() $context->addViolation('Message %param%', ['%param%' => 'value']); }; - $this->metadata->addConstraint(new Callback([ - 'callback' => $callback1, - 'groups' => 'Group', - ])); - $this->referenceMetadata->addConstraint(new Callback([ - 'callback' => $callback2, - 'groups' => 'Group', - ])); + $this->metadata->addConstraint(new Callback( + callback: $callback1, + groups: ['Group'], + )); + $this->referenceMetadata->addConstraint(new Callback( + callback: $callback2, + groups: ['Group'], + )); $violations = $this->validator->validate($entity, new Valid(), 'Group'); @@ -1601,10 +1603,10 @@ public function testTraverseTraversableByDefault() }; $this->metadataFactory->addMetadata(new ClassMetadata('ArrayIterator')); - $this->metadata->addConstraint(new Callback([ - 'callback' => $callback, - 'groups' => 'Group', - ])); + $this->metadata->addConstraint(new Callback( + callback: $callback, + groups: ['Group'], + )); $violations = $this->validate($traversable, new Valid(), 'Group'); @@ -1633,10 +1635,10 @@ public function testTraversalEnabledOnClass() $traversableMetadata->addConstraint(new Traverse(true)); $this->metadataFactory->addMetadata($traversableMetadata); - $this->metadata->addConstraint(new Callback([ - 'callback' => $callback, - 'groups' => 'Group', - ])); + $this->metadata->addConstraint(new Callback( + callback: $callback, + groups: ['Group'], + )); $violations = $this->validate($traversable, new Valid(), 'Group'); @@ -1657,10 +1659,10 @@ public function testTraversalDisabledOnClass() $traversableMetadata->addConstraint(new Traverse(false)); $this->metadataFactory->addMetadata($traversableMetadata); - $this->metadata->addConstraint(new Callback([ - 'callback' => $callback, - 'groups' => 'Group', - ])); + $this->metadata->addConstraint(new Callback( + callback: $callback, + groups: ['Group'], + )); $violations = $this->validate($traversable, new Valid(), 'Group'); @@ -1670,12 +1672,11 @@ public function testTraversalDisabledOnClass() public function testExpectTraversableIfTraversalEnabledOnClass() { - $this->expectException(ConstraintDefinitionException::class); - $entity = new Entity(); - $this->metadata->addConstraint(new Traverse(true)); - $this->validator->validate($entity); + $this->expectException(ConstraintDefinitionException::class); + + $this->validator->validate(new Entity()); } public function testReferenceTraversalDisabledOnClass() @@ -1691,10 +1692,10 @@ public function testReferenceTraversalDisabledOnClass() $traversableMetadata->addConstraint(new Traverse(false)); $this->metadataFactory->addMetadata($traversableMetadata); - $this->referenceMetadata->addConstraint(new Callback([ - 'callback' => $callback, - 'groups' => 'Group', - ])); + $this->referenceMetadata->addConstraint(new Callback( + callback: $callback, + groups: ['Group'], + )); $this->metadata->addPropertyConstraint('reference', new Valid()); $violations = $this->validate($entity, new Valid(), 'Group'); @@ -1716,13 +1717,13 @@ public function testReferenceTraversalEnabledOnReferenceDisabledOnClass() $traversableMetadata->addConstraint(new Traverse(false)); $this->metadataFactory->addMetadata($traversableMetadata); - $this->referenceMetadata->addConstraint(new Callback([ - 'callback' => $callback, - 'groups' => 'Group', - ])); - $this->metadata->addPropertyConstraint('reference', new Valid([ - 'traverse' => true, - ])); + $this->referenceMetadata->addConstraint(new Callback( + callback: $callback, + groups: ['Group'], + )); + $this->metadata->addPropertyConstraint('reference', new Valid( + traverse: true, + )); $violations = $this->validate($entity, new Valid(), 'Group'); @@ -1743,13 +1744,13 @@ public function testReferenceTraversalDisabledOnReferenceEnabledOnClass() $traversableMetadata->addConstraint(new Traverse(true)); $this->metadataFactory->addMetadata($traversableMetadata); - $this->referenceMetadata->addConstraint(new Callback([ - 'callback' => $callback, - 'groups' => 'Group', - ])); - $this->metadata->addPropertyConstraint('reference', new Valid([ - 'traverse' => false, - ])); + $this->referenceMetadata->addConstraint(new Callback( + callback: $callback, + groups: ['Group'], + )); + $this->metadata->addPropertyConstraint('reference', new Valid( + traverse: false, + )); $violations = $this->validate($entity, new Valid(), 'Group'); @@ -1766,10 +1767,10 @@ public function testReferenceCascadeDisabledByDefault() $this->fail('Should not be called'); }; - $this->referenceMetadata->addConstraint(new Callback([ - 'callback' => $callback, - 'groups' => 'Group', - ])); + $this->referenceMetadata->addConstraint(new Callback( + callback: $callback, + groups: ['Group'], + )); $violations = $this->validate($entity, new Valid(), 'Group'); @@ -1788,10 +1789,10 @@ public function testReferenceCascadeEnabledIgnoresUntyped() $this->fail('Should not be called'); }; - $this->referenceMetadata->addConstraint(new Callback([ - 'callback' => $callback, - 'groups' => 'Group', - ])); + $this->referenceMetadata->addConstraint(new Callback( + callback: $callback, + groups: ['Group'], + )); $violations = $this->validate($entity, new Valid(), 'Group'); @@ -1815,10 +1816,10 @@ public function testTypedReferenceCascadeEnabled() $cascadingMetadata->addConstraint(new Cascade()); $cascadedMetadata = new ClassMetadata(CascadedChild::class); - $cascadedMetadata->addConstraint(new Callback([ - 'callback' => $callback, - 'groups' => 'Group', - ])); + $cascadedMetadata->addConstraint(new Callback( + callback: $callback, + groups: ['Group'], + )); $this->metadataFactory->addMetadata($cascadingMetadata); $this->metadataFactory->addMetadata($cascadedMetadata); @@ -1867,10 +1868,10 @@ public function testNoDuplicateValidationIfClassConstraintInMultipleGroups() $context->addViolation('Message'); }; - $this->metadata->addConstraint(new Callback([ - 'callback' => $callback, - 'groups' => ['Group 1', 'Group 2'], - ])); + $this->metadata->addConstraint(new Callback( + callback: $callback, + groups: ['Group 1', 'Group 2'], + )); $violations = $this->validator->validate($entity, new Valid(), ['Group 1', 'Group 2']); @@ -1886,10 +1887,10 @@ public function testNoDuplicateValidationIfPropertyConstraintInMultipleGroups() $context->addViolation('Message'); }; - $this->metadata->addPropertyConstraint('firstName', new Callback([ - 'callback' => $callback, - 'groups' => ['Group 1', 'Group 2'], - ])); + $this->metadata->addPropertyConstraint('firstName', new Callback( + callback: $callback, + groups: ['Group 1', 'Group 2'], + )); $violations = $this->validator->validate($entity, new Valid(), ['Group 1', 'Group 2']); @@ -2006,8 +2007,8 @@ public function testNestedObjectIsNotValidatedIfGroupInValidConstraintIsNotValid $reference->value = ''; $entity->childA = $reference; - $this->metadata->addPropertyConstraint('firstName', new NotBlank(['groups' => 'group1'])); - $this->metadata->addPropertyConstraint('childA', new Valid(['groups' => 'group1'])); + $this->metadata->addPropertyConstraint('firstName', new NotBlank(groups: ['group1'])); + $this->metadata->addPropertyConstraint('childA', new Valid(groups: ['group1'])); $this->referenceMetadata->addPropertyConstraint('value', new NotBlank()); $violations = $this->validator->validate($entity, null, []); @@ -2023,9 +2024,9 @@ public function testNestedObjectIsValidatedIfGroupInValidConstraintIsValidated() $reference->value = ''; $entity->childA = $reference; - $this->metadata->addPropertyConstraint('firstName', new NotBlank(['groups' => 'group1'])); - $this->metadata->addPropertyConstraint('childA', new Valid(['groups' => 'group1'])); - $this->referenceMetadata->addPropertyConstraint('value', new NotBlank(['groups' => 'group1'])); + $this->metadata->addPropertyConstraint('firstName', new NotBlank(groups: ['group1'])); + $this->metadata->addPropertyConstraint('childA', new Valid(groups: ['group1'])); + $this->referenceMetadata->addPropertyConstraint('value', new NotBlank(groups: ['group1'])); $violations = $this->validator->validate($entity, null, ['Default', 'group1']); @@ -2043,10 +2044,10 @@ public function testNestedObjectIsValidatedInMultipleGroupsIfGroupInValidConstra $entity->childA = $reference; $this->metadata->addPropertyConstraint('firstName', new NotBlank()); - $this->metadata->addPropertyConstraint('childA', new Valid(['groups' => ['group1', 'group2']])); + $this->metadata->addPropertyConstraint('childA', new Valid(groups: ['group1', 'group2'])); - $this->referenceMetadata->addPropertyConstraint('value', new NotBlank(['groups' => 'group1'])); - $this->referenceMetadata->addPropertyConstraint('value', new NotNull(['groups' => 'group2'])); + $this->referenceMetadata->addPropertyConstraint('value', new NotBlank(groups: ['group1'])); + $this->referenceMetadata->addPropertyConstraint('value', new NotNull(groups: ['group2'])); $violations = $this->validator->validate($entity, null, ['Default', 'group1', 'group2']); @@ -2135,10 +2136,10 @@ public function testRelationBetweenChildAAndChildB() public function testCollectionConstraintValidateAllGroupsForNestedConstraints() { - $this->metadata->addPropertyConstraint('data', new Collection(['fields' => [ - 'one' => [new NotBlank(['groups' => 'one']), new Length(['min' => 2, 'groups' => 'two'])], - 'two' => [new NotBlank(['groups' => 'two'])], - ]])); + $this->metadata->addPropertyConstraint('data', new Collection(fields: [ + 'one' => [new NotBlank(groups: ['one']), new Length(min: 2, groups: ['two'])], + 'two' => [new NotBlank(groups: ['two'])], + ])); $entity = new Entity(); $entity->data = ['one' => 't', 'two' => '']; @@ -2153,9 +2154,9 @@ public function testCollectionConstraintValidateAllGroupsForNestedConstraints() public function testGroupedMethodConstraintValidateInSequence() { $metadata = new ClassMetadata(EntityWithGroupedConstraintOnMethods::class); - $metadata->addPropertyConstraint('bar', new NotNull(['groups' => 'Foo'])); - $metadata->addGetterMethodConstraint('validInFoo', 'isValidInFoo', new IsTrue(['groups' => 'Foo'])); - $metadata->addGetterMethodConstraint('bar', 'getBar', new NotNull(['groups' => 'Bar'])); + $metadata->addPropertyConstraint('bar', new NotNull(groups: ['Foo'])); + $metadata->addGetterMethodConstraint('validInFoo', 'isValidInFoo', new IsTrue(groups: ['Foo'])); + $metadata->addGetterMethodConstraint('bar', 'getBar', new NotNull(groups: ['Bar'])); $this->metadataFactory->addMetadata($metadata); @@ -2196,10 +2197,13 @@ public function testNotNullConstraintOnGetterReturningNull() public function testAllConstraintValidateAllGroupsForNestedConstraints() { - $this->metadata->addPropertyConstraint('data', new All(['constraints' => [ - new NotBlank(['groups' => 'one']), - new Length(['min' => 2, 'groups' => 'two']), - ]])); + $this->metadata->addPropertyConstraint('data', new All(constraints: [ + new NotBlank(groups: ['one']), + new Length( + min: 2, + groups: ['two'], + ), + ])); $entity = new Entity(); $entity->data = ['one' => 't', 'two' => '']; @@ -2329,8 +2333,8 @@ public function testValidateWithExplicitCascade() public function testValidatedConstraintsHashesDoNotCollide() { $metadata = new ClassMetadata(Entity::class); - $metadata->addPropertyConstraint('initialized', new NotNull(['groups' => 'should_pass'])); - $metadata->addPropertyConstraint('initialized', new IsNull(['groups' => 'should_fail'])); + $metadata->addPropertyConstraint('initialized', new NotNull(groups: ['should_pass'])); + $metadata->addPropertyConstraint('initialized', new IsNull(groups: ['should_fail'])); $this->metadataFactory->addMetadata($metadata); diff --git a/Tests/ValidatorBuilderTest.php b/Tests/ValidatorBuilderTest.php index 94b5c97da..ec464b748 100644 --- a/Tests/ValidatorBuilderTest.php +++ b/Tests/ValidatorBuilderTest.php @@ -11,13 +11,9 @@ namespace Symfony\Component\Validator\Tests; -use Doctrine\Common\Annotations\PsrCachedReader; -use Doctrine\Common\Annotations\Reader; use PHPUnit\Framework\TestCase; use Psr\Cache\CacheItemPoolInterface; -use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Symfony\Component\Validator\ConstraintValidatorFactoryInterface; -use Symfony\Component\Validator\Mapping\Loader\AnnotationLoader; use Symfony\Component\Validator\ObjectInitializerInterface; use Symfony\Component\Validator\Validator\RecursiveValidator; use Symfony\Component\Validator\ValidatorBuilder; @@ -25,8 +21,6 @@ class ValidatorBuilderTest extends TestCase { - use ExpectDeprecationTrait; - private ValidatorBuilder $builder; protected function setUp(): void @@ -76,66 +70,6 @@ public function testAddMethodMappings() $this->assertSame($this->builder, $this->builder->addMethodMappings([])); } - /** - * @group legacy - */ - public function testEnableAnnotationMappingWithDefaultDoctrineAnnotationReader() - { - $this->expectDeprecation('Since symfony/validator 6.4: Method "Symfony\Component\Validator\ValidatorBuilder::enableAnnotationMapping()" is deprecated, use "enableAttributeMapping()" instead.'); - $this->assertSame($this->builder, $this->builder->enableAnnotationMapping()); - - $this->expectDeprecation('Since symfony/validator 6.4: Method "Symfony\Component\Validator\ValidatorBuilder::addDefaultDoctrineAnnotationReader()" is deprecated without replacement.'); - $this->assertSame($this->builder, $this->builder->addDefaultDoctrineAnnotationReader()); - - $loaders = $this->builder->getLoaders(); - $this->assertCount(1, $loaders); - $this->assertInstanceOf(AnnotationLoader::class, $loaders[0]); - - $r = new \ReflectionProperty(AnnotationLoader::class, 'reader'); - - $this->assertInstanceOf(PsrCachedReader::class, $r->getValue($loaders[0])); - } - - /** - * @group legacy - */ - public function testEnableAnnotationMappingWithCustomDoctrineAnnotationReader() - { - $reader = $this->createMock(Reader::class); - - $this->expectDeprecation('Since symfony/validator 6.4: Method "Symfony\Component\Validator\ValidatorBuilder::enableAnnotationMapping()" is deprecated, use "enableAttributeMapping()" instead.'); - $this->assertSame($this->builder, $this->builder->enableAnnotationMapping()); - - $this->expectDeprecation('Since symfony/validator 6.4: Method "Symfony\Component\Validator\ValidatorBuilder::setDoctrineAnnotationReader()" is deprecated without replacement.'); - $this->assertSame($this->builder, $this->builder->setDoctrineAnnotationReader($reader)); - - $loaders = $this->builder->getLoaders(); - $this->assertCount(1, $loaders); - $this->assertInstanceOf(AnnotationLoader::class, $loaders[0]); - - $r = new \ReflectionProperty(AnnotationLoader::class, 'reader'); - - $this->assertSame($reader, $r->getValue($loaders[0])); - } - - /** - * @group legacy - */ - public function testExpectDeprecationWhenEnablingAnnotationMapping() - { - $this->expectDeprecation('Since symfony/validator 6.4: Method "Symfony\Component\Validator\ValidatorBuilder::enableAnnotationMapping()" is deprecated, use "enableAttributeMapping()" instead.'); - $this->assertSame($this->builder, $this->builder->enableAnnotationMapping()); - } - - /** - * @group legacy - */ - public function testExpectDeprecationWhenDisablingAnnotationMapping() - { - $this->expectDeprecation('Since symfony/validator 6.4: Method "Symfony\Component\Validator\ValidatorBuilder::disableAnnotationMapping()" is deprecated, use "disableAttributeMapping()" instead.'); - $this->assertSame($this->builder, $this->builder->disableAnnotationMapping()); - } - public function testDisableAttributeMapping() { $this->assertSame($this->builder, $this->builder->disableAttributeMapping()); @@ -165,6 +99,11 @@ public function testSetTranslationDomain() $this->assertSame($this->builder, $this->builder->setTranslationDomain('TRANS_DOMAIN')); } + public function testDisableTranslation() + { + $this->assertSame($this->builder, $this->builder->disableTranslation()); + } + public function testGetValidator() { $this->assertInstanceOf(RecursiveValidator::class, $this->builder->getValidator()); diff --git a/Validation.php b/Validation.php index 45c593003..c33900a47 100644 --- a/Validation.php +++ b/Validation.php @@ -40,7 +40,7 @@ public static function createCallable(Constraint|ValidatorInterface|null $constr /** * Creates a callable that returns true/false instead of throwing validation exceptions. * - * @return callable(mixed $value, ConstraintViolationListInterface &$violations = null): bool + * @return callable(mixed $value, ?ConstraintViolationListInterface &$violations = null): bool */ public static function createIsValidCallable(Constraint|ValidatorInterface|null $constraintOrValidator = null, Constraint ...$constraints): callable { diff --git a/Validator/LazyProperty.php b/Validator/LazyProperty.php index aa76934e2..1826cdb4a 100644 --- a/Validator/LazyProperty.php +++ b/Validator/LazyProperty.php @@ -18,11 +18,9 @@ */ class LazyProperty { - private \Closure $propertyValueCallback; - - public function __construct(\Closure $propertyValueCallback) - { - $this->propertyValueCallback = $propertyValueCallback; + public function __construct( + private \Closure $propertyValueCallback, + ) { } public function getPropertyValue(): mixed diff --git a/Validator/RecursiveContextualValidator.php b/Validator/RecursiveContextualValidator.php index 93c6700f1..9805bdcd4 100644 --- a/Validator/RecursiveContextualValidator.php +++ b/Validator/RecursiveContextualValidator.php @@ -45,28 +45,23 @@ */ class RecursiveContextualValidator implements ContextualValidatorInterface { - private ExecutionContextInterface $context; private string $defaultPropertyPath; private array $defaultGroups; - private MetadataFactoryInterface $metadataFactory; - private ConstraintValidatorFactoryInterface $validatorFactory; - private array $objectInitializers; - private ?ContainerInterface $groupProviderLocator; /** * Creates a validator for the given context. * * @param ObjectInitializerInterface[] $objectInitializers The object initializers */ - public function __construct(ExecutionContextInterface $context, MetadataFactoryInterface $metadataFactory, ConstraintValidatorFactoryInterface $validatorFactory, array $objectInitializers = [], ?ContainerInterface $groupProviderLocator = null) - { - $this->context = $context; + public function __construct( + private ExecutionContextInterface $context, + private MetadataFactoryInterface $metadataFactory, + private ConstraintValidatorFactoryInterface $validatorFactory, + private array $objectInitializers = [], + private ?ContainerInterface $groupProviderLocator = null, + ) { $this->defaultPropertyPath = $context->getPropertyPath(); $this->defaultGroups = [$context->getGroup() ?: Constraint::DEFAULT_GROUP]; - $this->metadataFactory = $metadataFactory; - $this->validatorFactory = $validatorFactory; - $this->objectInitializers = $objectInitializers; - $this->groupProviderLocator = $groupProviderLocator; } public function atPath(string $path): static diff --git a/Validator/RecursiveValidator.php b/Validator/RecursiveValidator.php index 64f311026..12bbd539e 100644 --- a/Validator/RecursiveValidator.php +++ b/Validator/RecursiveValidator.php @@ -29,24 +29,18 @@ */ class RecursiveValidator implements ValidatorInterface { - protected $contextFactory; - protected $metadataFactory; - protected $validatorFactory; - protected $objectInitializers; - protected ?ContainerInterface $groupProviderLocator; - /** * Creates a new validator. * * @param ObjectInitializerInterface[] $objectInitializers The object initializers */ - public function __construct(ExecutionContextFactoryInterface $contextFactory, MetadataFactoryInterface $metadataFactory, ConstraintValidatorFactoryInterface $validatorFactory, array $objectInitializers = [], ?ContainerInterface $groupProviderLocator = null) - { - $this->contextFactory = $contextFactory; - $this->metadataFactory = $metadataFactory; - $this->validatorFactory = $validatorFactory; - $this->objectInitializers = $objectInitializers; - $this->groupProviderLocator = $groupProviderLocator; + public function __construct( + protected ExecutionContextFactoryInterface $contextFactory, + protected MetadataFactoryInterface $metadataFactory, + protected ConstraintValidatorFactoryInterface $validatorFactory, + protected array $objectInitializers = [], + protected ?ContainerInterface $groupProviderLocator = null, + ) { } public function startContext(mixed $root = null): ContextualValidatorInterface diff --git a/Validator/TraceableValidator.php b/Validator/TraceableValidator.php index 4add47fc5..6f9ab5bbc 100644 --- a/Validator/TraceableValidator.php +++ b/Validator/TraceableValidator.php @@ -25,12 +25,12 @@ */ class TraceableValidator implements ValidatorInterface, ResetInterface { - private ValidatorInterface $validator; private array $collectedData = []; - public function __construct(ValidatorInterface $validator) - { - $this->validator = $validator; + public function __construct( + private ValidatorInterface $validator, + protected readonly ?\Closure $disabled = null, + ) { } public function getCollectedData(): array @@ -38,10 +38,7 @@ public function getCollectedData(): array return $this->collectedData; } - /** - * @return void - */ - public function reset() + public function reset(): void { $this->collectedData = []; } @@ -60,6 +57,10 @@ public function validate(mixed $value, Constraint|array|null $constraints = null { $violations = $this->validator->validate($value, $constraints, $groups); + if ($this->disabled?->__invoke()) { + return $violations; + } + $trace = debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS, 7); $file = $trace[0]['file']; diff --git a/ValidatorBuilder.php b/ValidatorBuilder.php index 9ab08e44b..917f1c57a 100644 --- a/ValidatorBuilder.php +++ b/ValidatorBuilder.php @@ -11,18 +11,12 @@ namespace Symfony\Component\Validator; -use Doctrine\Common\Annotations\AnnotationReader; -use Doctrine\Common\Annotations\PsrCachedReader; -use Doctrine\Common\Annotations\Reader; use Psr\Cache\CacheItemPoolInterface; use Psr\Container\ContainerInterface; -use Symfony\Component\Cache\Adapter\ArrayAdapter; use Symfony\Component\Validator\Context\ExecutionContextFactory; -use Symfony\Component\Validator\Exception\LogicException; use Symfony\Component\Validator\Exception\ValidatorException; use Symfony\Component\Validator\Mapping\Factory\LazyLoadingMetadataFactory; use Symfony\Component\Validator\Mapping\Factory\MetadataFactoryInterface; -use Symfony\Component\Validator\Mapping\Loader\AnnotationLoader; use Symfony\Component\Validator\Mapping\Loader\AttributeLoader; use Symfony\Component\Validator\Mapping\Loader\LoaderChain; use Symfony\Component\Validator\Mapping\Loader\LoaderInterface; @@ -50,14 +44,13 @@ class ValidatorBuilder private array $xmlMappings = []; private array $yamlMappings = []; private array $methodMappings = []; - private ?Reader $annotationReader = null; private bool $enableAttributeMapping = false; private ?MetadataFactoryInterface $metadataFactory = null; private ConstraintValidatorFactoryInterface $validatorFactory; private ?ContainerInterface $groupProviderLocator = null; private ?CacheItemPoolInterface $mappingCache = null; private ?TranslatorInterface $translator = null; - private ?string $translationDomain = null; + private string|false|null $translationDomain = null; /** * Adds an object initializer to the validator. @@ -189,18 +182,6 @@ public function addMethodMappings(array $methodNames): static return $this; } - /** - * @deprecated since Symfony 6.4, use "enableAttributeMapping()" instead. - * - * @return $this - */ - public function enableAnnotationMapping(): static - { - trigger_deprecation('symfony/validator', '6.4', 'Method "%s()" is deprecated, use "enableAttributeMapping()" instead.', __METHOD__); - - return $this->enableAttributeMapping(); - } - /** * Enables attribute-based constraint mapping. * @@ -217,18 +198,6 @@ public function enableAttributeMapping(): static return $this; } - /** - * @deprecated since Symfony 6.4, use "disableAttributeMapping()" instead - * - * @return $this - */ - public function disableAnnotationMapping(): static - { - trigger_deprecation('symfony/validator', '6.4', 'Method "%s()" is deprecated, use "disableAttributeMapping()" instead.', __METHOD__); - - return $this->disableAttributeMapping(); - } - /** * Disables attribute-based constraint mapping. * @@ -236,40 +205,11 @@ public function disableAnnotationMapping(): static */ public function disableAttributeMapping(): static { - $this->annotationReader = null; $this->enableAttributeMapping = false; return $this; } - /** - * @deprecated since Symfony 6.4 without replacement - * - * @return $this - */ - public function setDoctrineAnnotationReader(?Reader $reader): static - { - trigger_deprecation('symfony/validator', '6.4', 'Method "%s()" is deprecated without replacement.', __METHOD__); - - $this->annotationReader = $reader; - - return $this; - } - - /** - * @deprecated since Symfony 6.4 without replacement - * - * @return $this - */ - public function addDefaultDoctrineAnnotationReader(): static - { - trigger_deprecation('symfony/validator', '6.4', 'Method "%s()" is deprecated without replacement.', __METHOD__); - - $this->annotationReader = $this->createAnnotationReader(); - - return $this; - } - /** * Sets the class metadata factory used by the validator. * @@ -352,6 +292,16 @@ public function setTranslationDomain(?string $translationDomain): static return $this; } + /** + * @return $this + */ + public function disableTranslation(): static + { + $this->translationDomain = false; + + return $this; + } + /** * @return $this */ @@ -381,9 +331,7 @@ public function getLoaders(): array $loaders[] = new StaticMethodLoader($methodName); } - if ($this->enableAttributeMapping && $this->annotationReader) { - $loaders[] = new AnnotationLoader($this->annotationReader); - } elseif ($this->enableAttributeMapping) { + if ($this->enableAttributeMapping) { $loaders[] = new AttributeLoader(); } @@ -428,17 +376,4 @@ public function getValidator(): ValidatorInterface return new RecursiveValidator($contextFactory, $metadataFactory, $validatorFactory, $this->initializers, $this->groupProviderLocator); } - - private function createAnnotationReader(): Reader - { - if (!class_exists(AnnotationReader::class)) { - throw new LogicException('Enabling annotation based constraint mapping requires the packages doctrine/annotations and symfony/cache to be installed.'); - } - - if (class_exists(ArrayAdapter::class)) { - return new PsrCachedReader(new AnnotationReader(), new ArrayAdapter()); - } - - throw new LogicException('Enabling annotation based constraint mapping requires the packages doctrine/annotations and symfony/cache to be installed.'); - } } diff --git a/Violation/ConstraintViolationBuilder.php b/Violation/ConstraintViolationBuilder.php index e6ce597df..d89932a43 100644 --- a/Violation/ConstraintViolationBuilder.php +++ b/Violation/ConstraintViolationBuilder.php @@ -22,34 +22,27 @@ * * @author Bernhard Schussek * - * @internal since version 2.5. Code against ConstraintViolationBuilderInterface instead. + * @internal */ class ConstraintViolationBuilder implements ConstraintViolationBuilderInterface { - private ConstraintViolationList $violations; - private string|\Stringable $message; - private array $parameters; - private mixed $root; - private mixed $invalidValue; private string $propertyPath; - private TranslatorInterface $translator; - private string|false|null $translationDomain; private ?int $plural = null; - private ?Constraint $constraint; private ?string $code = null; private mixed $cause = null; - public function __construct(ConstraintViolationList $violations, ?Constraint $constraint, string|\Stringable $message, array $parameters, mixed $root, ?string $propertyPath, mixed $invalidValue, TranslatorInterface $translator, string|false|null $translationDomain = null) - { - $this->violations = $violations; - $this->message = $message; - $this->parameters = $parameters; - $this->root = $root; + public function __construct( + private ConstraintViolationList $violations, + private ?Constraint $constraint, + private string|\Stringable $message, + private array $parameters, + private mixed $root, + ?string $propertyPath, + private mixed $invalidValue, + private TranslatorInterface $translator, + private string|false|null $translationDomain = null, + ) { $this->propertyPath = $propertyPath ?? ''; - $this->invalidValue = $invalidValue; - $this->translator = $translator; - $this->translationDomain = $translationDomain; - $this->constraint = $constraint; } public function atPath(string $path): static diff --git a/Violation/ConstraintViolationBuilderInterface.php b/Violation/ConstraintViolationBuilderInterface.php index 02fbeb797..195dec924 100644 --- a/Violation/ConstraintViolationBuilderInterface.php +++ b/Violation/ConstraintViolationBuilderInterface.php @@ -20,8 +20,6 @@ * execution context. * * @author Bernhard Schussek - * - * @method $this disableTranslation() */ interface ConstraintViolationBuilderInterface { @@ -58,6 +56,11 @@ public function setParameter(string $key, string $value): static; */ public function setParameters(array $parameters): static; + /** + * @return $this + */ + public function disableTranslation(): static; + /** * Sets the translation domain which should be used for translating the * violation message. @@ -109,8 +112,6 @@ public function setCause(mixed $cause): static; /** * Adds the violation to the current execution context. - * - * @return void */ - public function addViolation(); + public function addViolation(): void; } diff --git a/composer.json b/composer.json index ece01e2fd..368373f53 100644 --- a/composer.json +++ b/composer.json @@ -16,42 +16,43 @@ } ], "require": { - "php": ">=8.1", - "symfony/deprecation-contracts": "^2.5|^3", + "php": ">=8.2", "symfony/polyfill-ctype": "~1.8", "symfony/polyfill-mbstring": "~1.0", "symfony/polyfill-php83": "^1.27", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/translation-contracts": "^2.5|^3" }, "require-dev": { - "symfony/console": "^5.4|^6.0|^7.0", - "symfony/finder": "^5.4|^6.0|^7.0", - "symfony/http-client": "^5.4|^6.0|^7.0", - "symfony/http-foundation": "^5.4|^6.0|^7.0", - "symfony/http-kernel": "^5.4|^6.0|^7.0", - "symfony/intl": "^5.4|^6.0|^7.0", - "symfony/yaml": "^5.4|^6.0|^7.0", - "symfony/config": "^5.4|^6.0|^7.0", - "symfony/dependency-injection": "^5.4|^6.0|^7.0", - "symfony/expression-language": "^5.4|^6.0|^7.0", - "symfony/cache": "^5.4|^6.0|^7.0", - "symfony/mime": "^5.4|^6.0|^7.0", - "symfony/property-access": "^5.4|^6.0|^7.0", - "symfony/property-info": "^5.4|^6.0|^7.0", - "symfony/translation": "^5.4.35|~6.3.12|^6.4.3|^7.0.3", - "doctrine/annotations": "^1.13|^2", + "symfony/console": "^6.4|^7.0", + "symfony/finder": "^6.4|^7.0", + "symfony/http-client": "^6.4|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/intl": "^6.4|^7.0", + "symfony/yaml": "^6.4|^7.0", + "symfony/config": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/expression-language": "^6.4|^7.0", + "symfony/cache": "^6.4|^7.0", + "symfony/mime": "^6.4|^7.0", + "symfony/property-access": "^6.4|^7.0", + "symfony/property-info": "^6.4|^7.0", + "symfony/string": "^6.4|^7.0", + "symfony/translation": "^6.4.3|^7.0.3", + "symfony/type-info": "^7.1.8", "egulias/email-validator": "^2.1.10|^3|^4" }, "conflict": { - "doctrine/annotations": "<1.13", "doctrine/lexer": "<1.1", - "symfony/dependency-injection": "<5.4", - "symfony/expression-language": "<5.4", - "symfony/http-kernel": "<5.4", - "symfony/intl": "<5.4", - "symfony/property-info": "<5.4", - "symfony/translation": "<5.4.35|>=6.0,<6.3.12|>=6.4,<6.4.3|>=7.0,<7.0.3", - "symfony/yaml": "<5.4" + "symfony/dependency-injection": "<6.4", + "symfony/doctrine-bridge": "<7.0", + "symfony/expression-language": "<6.4", + "symfony/http-kernel": "<6.4", + "symfony/intl": "<6.4", + "symfony/property-info": "<6.4", + "symfony/translation": "<6.4.3|>=7.0,<7.0.3", + "symfony/yaml": "<6.4" }, "autoload": { "psr-4": { "Symfony\\Component\\Validator\\": "" },