diff --git a/src/Symfony/Component/Validator/CHANGELOG.md b/src/Symfony/Component/Validator/CHANGELOG.md index e7bb1c2654..f980454e22 100644 --- a/src/Symfony/Component/Validator/CHANGELOG.md +++ b/src/Symfony/Component/Validator/CHANGELOG.md @@ -29,6 +29,7 @@ CHANGELOG * }) */ ``` + * added the `Isin` constraint and validator 5.1.0 ----- diff --git a/src/Symfony/Component/Validator/Constraints/Isin.php b/src/Symfony/Component/Validator/Constraints/Isin.php new file mode 100644 index 0000000000..586ea829d2 --- /dev/null +++ b/src/Symfony/Component/Validator/Constraints/Isin.php @@ -0,0 +1,38 @@ + + * + * 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; + +/** + * @Annotation + * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) + * + * @author Laurent Masforné + */ +class Isin extends Constraint +{ + const VALIDATION_LENGTH = 12; + const VALIDATION_PATTERN = '/[A-Z]{2}[A-Z0-9]{9}[0-9]{1}/'; + + const INVALID_LENGTH_ERROR = '88738dfc-9ed5-ba1e-aebe-402a2a9bf58e'; + const INVALID_PATTERN_ERROR = '3d08ce0-ded9-a93d-9216-17ac21265b65e'; + const INVALID_CHECKSUM_ERROR = '32089b-0ee1-93ba-399e-aa232e62f2d29d'; + + protected static $errorNames = [ + self::INVALID_LENGTH_ERROR => 'INVALID_LENGTH_ERROR', + self::INVALID_PATTERN_ERROR => 'INVALID_PATTERN_ERROR', + self::INVALID_CHECKSUM_ERROR => 'INVALID_CHECKSUM_ERROR', + ]; + + public $message = 'This is not a valid International Securities Identification Number (ISIN).'; +} diff --git a/src/Symfony/Component/Validator/Constraints/IsinValidator.php b/src/Symfony/Component/Validator/Constraints/IsinValidator.php new file mode 100644 index 0000000000..9ae31acb14 --- /dev/null +++ b/src/Symfony/Component/Validator/Constraints/IsinValidator.php @@ -0,0 +1,92 @@ + + * + * 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\Validator\Validator\ValidatorInterface; + +/** + * @author Laurent Masforné + * + * @see https://en.wikipedia.org/wiki/International_Securities_Identification_Number + */ +class IsinValidator extends ConstraintValidator +{ + /** + * @var ValidatorInterface + */ + private $validator; + + public function __construct(ValidatorInterface $validator) + { + $this->validator = $validator; + } + + /** + * {@inheritdoc} + */ + public function validate($value, Constraint $constraint) + { + if (!$constraint instanceof Isin) { + throw new UnexpectedTypeException($constraint, Isin::class); + } + + if (null === $value || '' === $value) { + return; + } + + if (!is_scalar($value) && !(\is_object($value) && method_exists($value, '__toString'))) { + throw new UnexpectedValueException($value, 'string'); + } + + $value = strtoupper($value); + + if (Isin::VALIDATION_LENGTH !== \strlen($value)) { + $this->context->buildViolation($constraint->message) + ->setParameter('{{ value }}', $this->formatValue($value)) + ->setCode(Isin::INVALID_LENGTH_ERROR) + ->addViolation(); + + return; + } + + if (!preg_match(Isin::VALIDATION_PATTERN, $value)) { + $this->context->buildViolation($constraint->message) + ->setParameter('{{ value }}', $this->formatValue($value)) + ->setCode(Isin::INVALID_PATTERN_ERROR) + ->addViolation(); + + return; + } + + if (!$this->isCorrectChecksum($value)) { + $this->context->buildViolation($constraint->message) + ->setParameter('{{ value }}', $this->formatValue($value)) + ->setCode(Isin::INVALID_CHECKSUM_ERROR) + ->addViolation(); + } + } + + private function isCorrectChecksum(string $input): bool + { + $characters = str_split($input); + foreach ($characters as $i => $char) { + $characters[$i] = \intval($char, 36); + } + $number = implode('', $characters); + + return 0 === $this->validator->validate($number, new Luhn())->count(); + } +} diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.en.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.en.xlf index 674ccf5c30..ecc73e48aa 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.en.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.en.xlf @@ -382,6 +382,10 @@ Each element of this collection should satisfy its own set of constraints. Each element of this collection should satisfy its own set of constraints. + + This value is not a valid International Securities Identification Number (ISIN). + This value is not a valid International Securities Identification Number (ISIN). + diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.fr.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.fr.xlf index c44ade69e0..a4dd54295b 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.fr.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.fr.xlf @@ -382,6 +382,10 @@ Each element of this collection should satisfy its own set of constraints. Chaque élément de cette collection doit satisfaire à son propre jeu de contraintes. + + This value is not a valid International Securities Identification Number (ISIN). + Cette valeur n'est pas un code international de sécurité valide (ISIN). + diff --git a/src/Symfony/Component/Validator/Tests/Constraints/IsinValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/IsinValidatorTest.php new file mode 100644 index 0000000000..0822fb5ad6 --- /dev/null +++ b/src/Symfony/Component/Validator/Tests/Constraints/IsinValidatorTest.php @@ -0,0 +1,135 @@ +getValidator()); + } + + public function testNullIsValid() + { + $this->validator->validate(null, new Isin()); + + $this->assertNoViolation(); + } + + public function testEmptyStringIsValid() + { + $this->validator->validate('', new Isin()); + + $this->assertNoViolation(); + } + + /** + * @dataProvider getValidIsin + */ + public function testValidIsin($isin) + { + $this->validator->validate($isin, new Isin()); + $this->assertNoViolation(); + } + + public function getValidIsin() + { + return [ + ['XS2125535901'], // Goldman Sachs International + ['DE000HZ8VA77'], // UniCredit Bank AG + ['CH0528261156'], // Leonteq Securities AG [Guernsey] + ['US0378331005'], // Apple, Inc. + ['AU0000XVGZA3'], // TREASURY CORP VICTORIA 5 3/4% 2005-2016 + ['GB0002634946'], // BAE Systems + ['CH0528261099'], // Leonteq Securities AG [Guernsey] + ['XS2155672814'], // OP Corporate Bank plc + ['XS2155687259'], // Orbian Financial Services III, LLC + ['XS2155696672'], // Sheffield Receivables Company LLC + ]; + } + + /** + * @dataProvider getIsinWithInvalidLenghFormat + */ + public function testIsinWithInvalidFormat($isin) + { + $this->assertViolationRaised($isin, Isin::INVALID_LENGTH_ERROR); + } + + public function getIsinWithInvalidLenghFormat() + { + return [ + ['X'], + ['XS'], + ['XS2'], + ['XS21'], + ['XS215'], + ['XS2155'], + ['XS21556'], + ['XS215569'], + ['XS2155696'], + ['XS21556966'], + ['XS215569667'], + ]; + } + + /** + * @dataProvider getIsinWithInvalidPattern + */ + public function testIsinWithInvalidPattern($isin) + { + $this->assertViolationRaised($isin, Isin::INVALID_PATTERN_ERROR); + } + + public function getIsinWithInvalidPattern() + { + return [ + ['X12155696679'], + ['123456789101'], + ['XS215569667E'], + ['XS215E69667A'], + ]; + } + + /** + * @dataProvider getIsinWithValidFormatButIncorrectChecksum + */ + public function testIsinWithValidFormatButIncorrectChecksum($isin) + { + $this->assertViolationRaised($isin, Isin::INVALID_CHECKSUM_ERROR); + } + + public function getIsinWithValidFormatButIncorrectChecksum() + { + return [ + ['XS2112212144'], + ['DE013228VA77'], + ['CH0512361156'], + ['XS2125660123'], + ['XS2012587408'], + ['XS2012380102'], + ['XS2012239364'], + ]; + } + + private function assertViolationRaised($isin, $code) + { + $constraint = new Isin([ + 'message' => 'myMessage', + ]); + + $this->validator->validate($isin, $constraint); + + $this->buildViolation('myMessage') + ->setParameter('{{ value }}', '"'.$isin.'"') + ->setCode($code) + ->assertRaised(); + } +}