diff --git a/src/Symfony/Component/Validator/Constraints/Bic.php b/src/Symfony/Component/Validator/Constraints/Bic.php index 625fb7213241..22640bedfc79 100644 --- a/src/Symfony/Component/Validator/Constraints/Bic.php +++ b/src/Symfony/Component/Validator/Constraints/Bic.php @@ -15,6 +15,7 @@ use Symfony\Component\PropertyAccess\PropertyAccess; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Exception\ConstraintDefinitionException; +use Symfony\Component\Validator\Exception\InvalidArgumentException; use Symfony\Component\Validator\Exception\LogicException; /** @@ -27,6 +28,14 @@ #[\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'; /** @@ -49,18 +58,34 @@ class Bic extends Constraint public string $ibanMessage = 'This Business Identifier Code (BIC) is not associated with IBAN {{ iban }}.'; public ?string $iban = null; public ?string $ibanPropertyPath = null; + public ?string $mode = self::VALIDATION_MODE_STRICT; /** * @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 string|null $mode The mode used to validate the BIC; pass null to use the default 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) - { + 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.'); + } parent::__construct($options, $groups, $payload); @@ -68,6 +93,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/src/Symfony/Component/Validator/Constraints/BicValidator.php b/src/Symfony/Component/Validator/Constraints/BicValidator.php index 19b88b68937a..d0d1b5084185 100644 --- a/src/Symfony/Component/Validator/Constraints/BicValidator.php +++ b/src/Symfony/Component/Validator/Constraints/BicValidator.php @@ -100,6 +100,9 @@ public function validate(mixed $value, Constraint $constraint): void } $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)) @@ -109,8 +112,8 @@ public function validate(mixed $value, Constraint $constraint): void 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/src/Symfony/Component/Validator/Tests/Constraints/BicValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/BicValidatorTest.php index 8b3c815423a4..b71c7dac5272 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/BicValidatorTest.php +++ b/src/Symfony/Component/Validator/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; @@ -301,6 +302,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/src/Symfony/Component/Validator/Tests/Constraints/IbanValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/IbanValidatorTest.php index 212d5329f44a..390f106df564 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/IbanValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/IbanValidatorTest.php @@ -53,6 +53,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