Skip to content

Commit f76ac74

Browse files
committed
feature #37565 [Validator] Add Isin validator constraint (lmasforne)
This PR was merged into the 5.2-dev branch. Discussion ---------- [Validator] Add Isin validator constraint Co-Authored-By: Yannis Foucher <33806646+YaFou@users.noreply.github.com> | Q | A | ------------- | --- | Branch? | master | Bug fix? | no | New feature? | yes | Deprecations? | no | Tickets | Fix #36362 | License | MIT | Doc PR | symfony/symfony-docs#13960 Rebase of #36368 I asked him by mail and he didn't have time to finish the PR and allowed me to do it. Commits ------- 8e1ffc8 Feature #36362 add Isin validator constraint
2 parents 1889ba8 + 8e1ffc8 commit f76ac74

File tree

6 files changed

+274
-0
lines changed

6 files changed

+274
-0
lines changed

src/Symfony/Component/Validator/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ CHANGELOG
2929
* })
3030
*/
3131
```
32+
* added the `Isin` constraint and validator
3233

3334
5.1.0
3435
-----
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Validator\Constraints;
13+
14+
use Symfony\Component\Validator\Constraint;
15+
16+
/**
17+
* @Annotation
18+
* @Target({"PROPERTY", "METHOD", "ANNOTATION"})
19+
*
20+
* @author Laurent Masforné <l.masforne@gmail.com>
21+
*/
22+
class Isin extends Constraint
23+
{
24+
const VALIDATION_LENGTH = 12;
25+
const VALIDATION_PATTERN = '/[A-Z]{2}[A-Z0-9]{9}[0-9]{1}/';
26+
27+
const INVALID_LENGTH_ERROR = '88738dfc-9ed5-ba1e-aebe-402a2a9bf58e';
28+
const INVALID_PATTERN_ERROR = '3d08ce0-ded9-a93d-9216-17ac21265b65e';
29+
const INVALID_CHECKSUM_ERROR = '32089b-0ee1-93ba-399e-aa232e62f2d29d';
30+
31+
protected static $errorNames = [
32+
self::INVALID_LENGTH_ERROR => 'INVALID_LENGTH_ERROR',
33+
self::INVALID_PATTERN_ERROR => 'INVALID_PATTERN_ERROR',
34+
self::INVALID_CHECKSUM_ERROR => 'INVALID_CHECKSUM_ERROR',
35+
];
36+
37+
public $message = 'This is not a valid International Securities Identification Number (ISIN).';
38+
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Validator\Constraints;
13+
14+
use Symfony\Component\Validator\Constraint;
15+
use Symfony\Component\Validator\ConstraintValidator;
16+
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
17+
use Symfony\Component\Validator\Exception\UnexpectedValueException;
18+
use Symfony\Component\Validator\Validator\ValidatorInterface;
19+
20+
/**
21+
* @author Laurent Masforné <l.masforne@gmail.com>
22+
*
23+
* @see https://en.wikipedia.org/wiki/International_Securities_Identification_Number
24+
*/
25+
class IsinValidator extends ConstraintValidator
26+
{
27+
/**
28+
* @var ValidatorInterface
29+
*/
30+
private $validator;
31+
32+
public function __construct(ValidatorInterface $validator)
33+
{
34+
$this->validator = $validator;
35+
}
36+
37+
/**
38+
* {@inheritdoc}
39+
*/
40+
public function validate($value, Constraint $constraint)
41+
{
42+
if (!$constraint instanceof Isin) {
43+
throw new UnexpectedTypeException($constraint, Isin::class);
44+
}
45+
46+
if (null === $value || '' === $value) {
47+
return;
48+
}
49+
50+
if (!is_scalar($value) && !(\is_object($value) && method_exists($value, '__toString'))) {
51+
throw new UnexpectedValueException($value, 'string');
52+
}
53+
54+
$value = strtoupper($value);
55+
56+
if (Isin::VALIDATION_LENGTH !== \strlen($value)) {
57+
$this->context->buildViolation($constraint->message)
58+
->setParameter('{{ value }}', $this->formatValue($value))
59+
->setCode(Isin::INVALID_LENGTH_ERROR)
60+
->addViolation();
61+
62+
return;
63+
}
64+
65+
if (!preg_match(Isin::VALIDATION_PATTERN, $value)) {
66+
$this->context->buildViolation($constraint->message)
67+
->setParameter('{{ value }}', $this->formatValue($value))
68+
->setCode(Isin::INVALID_PATTERN_ERROR)
69+
->addViolation();
70+
71+
return;
72+
}
73+
74+
if (!$this->isCorrectChecksum($value)) {
75+
$this->context->buildViolation($constraint->message)
76+
->setParameter('{{ value }}', $this->formatValue($value))
77+
->setCode(Isin::INVALID_CHECKSUM_ERROR)
78+
->addViolation();
79+
}
80+
}
81+
82+
private function isCorrectChecksum(string $input): bool
83+
{
84+
$characters = str_split($input);
85+
foreach ($characters as $i => $char) {
86+
$characters[$i] = \intval($char, 36);
87+
}
88+
$number = implode('', $characters);
89+
90+
return 0 === $this->validator->validate($number, new Luhn())->count();
91+
}
92+
}

src/Symfony/Component/Validator/Resources/translations/validators.en.xlf

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,10 @@
382382
<source>Each element of this collection should satisfy its own set of constraints.</source>
383383
<target>Each element of this collection should satisfy its own set of constraints.</target>
384384
</trans-unit>
385+
<trans-unit id="99">
386+
<source>This value is not a valid International Securities Identification Number (ISIN).</source>
387+
<target>This value is not a valid International Securities Identification Number (ISIN).</target>
388+
</trans-unit>
385389
</body>
386390
</file>
387391
</xliff>

src/Symfony/Component/Validator/Resources/translations/validators.fr.xlf

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,10 @@
382382
<source>Each element of this collection should satisfy its own set of constraints.</source>
383383
<target>Chaque élément de cette collection doit satisfaire à son propre jeu de contraintes.</target>
384384
</trans-unit>
385+
<trans-unit id="99">
386+
<source>This value is not a valid International Securities Identification Number (ISIN).</source>
387+
<target>Cette valeur n'est pas un code international de sécurité valide (ISIN).</target>
388+
</trans-unit>
385389
</body>
386390
</file>
387391
</xliff>
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
<?php
2+
3+
namespace Symfony\Component\Validator\Tests\Constraints;
4+
5+
use Symfony\Component\Validator\Constraints\Isin;
6+
use Symfony\Component\Validator\Constraints\IsinValidator;
7+
use Symfony\Component\Validator\Test\ConstraintValidatorTestCase;
8+
use Symfony\Component\Validator\ValidatorBuilder;
9+
10+
class IsinValidatorTest extends ConstraintValidatorTestCase
11+
{
12+
protected function createValidator()
13+
{
14+
$validatorBuilder = new ValidatorBuilder();
15+
16+
return new IsinValidator($validatorBuilder->getValidator());
17+
}
18+
19+
public function testNullIsValid()
20+
{
21+
$this->validator->validate(null, new Isin());
22+
23+
$this->assertNoViolation();
24+
}
25+
26+
public function testEmptyStringIsValid()
27+
{
28+
$this->validator->validate('', new Isin());
29+
30+
$this->assertNoViolation();
31+
}
32+
33+
/**
34+
* @dataProvider getValidIsin
35+
*/
36+
public function testValidIsin($isin)
37+
{
38+
$this->validator->validate($isin, new Isin());
39+
$this->assertNoViolation();
40+
}
41+
42+
public function getValidIsin()
43+
{
44+
return [
45+
['XS2125535901'], // Goldman Sachs International
46+
['DE000HZ8VA77'], // UniCredit Bank AG
47+
['CH0528261156'], // Leonteq Securities AG [Guernsey]
48+
['US0378331005'], // Apple, Inc.
49+
['AU0000XVGZA3'], // TREASURY CORP VICTORIA 5 3/4% 2005-2016
50+
['GB0002634946'], // BAE Systems
51+
['CH0528261099'], // Leonteq Securities AG [Guernsey]
52+
['XS2155672814'], // OP Corporate Bank plc
53+
['XS2155687259'], // Orbian Financial Services III, LLC
54+
['XS2155696672'], // Sheffield Receivables Company LLC
55+
];
56+
}
57+
58+
/**
59+
* @dataProvider getIsinWithInvalidLenghFormat
60+
*/
61+
public function testIsinWithInvalidFormat($isin)
62+
{
63+
$this->assertViolationRaised($isin, Isin::INVALID_LENGTH_ERROR);
64+
}
65+
66+
public function getIsinWithInvalidLenghFormat()
67+
{
68+
return [
69+
['X'],
70+
['XS'],
71+
['XS2'],
72+
['XS21'],
73+
['XS215'],
74+
['XS2155'],
75+
['XS21556'],
76+
['XS215569'],
77+
['XS2155696'],
78+
['XS21556966'],
79+
['XS215569667'],
80+
];
81+
}
82+
83+
/**
84+
* @dataProvider getIsinWithInvalidPattern
85+
*/
86+
public function testIsinWithInvalidPattern($isin)
87+
{
88+
$this->assertViolationRaised($isin, Isin::INVALID_PATTERN_ERROR);
89+
}
90+
91+
public function getIsinWithInvalidPattern()
92+
{
93+
return [
94+
['X12155696679'],
95+
['123456789101'],
96+
['XS215569667E'],
97+
['XS215E69667A'],
98+
];
99+
}
100+
101+
/**
102+
* @dataProvider getIsinWithValidFormatButIncorrectChecksum
103+
*/
104+
public function testIsinWithValidFormatButIncorrectChecksum($isin)
105+
{
106+
$this->assertViolationRaised($isin, Isin::INVALID_CHECKSUM_ERROR);
107+
}
108+
109+
public function getIsinWithValidFormatButIncorrectChecksum()
110+
{
111+
return [
112+
['XS2112212144'],
113+
['DE013228VA77'],
114+
['CH0512361156'],
115+
['XS2125660123'],
116+
['XS2012587408'],
117+
['XS2012380102'],
118+
['XS2012239364'],
119+
];
120+
}
121+
122+
private function assertViolationRaised($isin, $code)
123+
{
124+
$constraint = new Isin([
125+
'message' => 'myMessage',
126+
]);
127+
128+
$this->validator->validate($isin, $constraint);
129+
130+
$this->buildViolation('myMessage')
131+
->setParameter('{{ value }}', '"'.$isin.'"')
132+
->setCode($code)
133+
->assertRaised();
134+
}
135+
}

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy