Skip to content

Commit c9c7fa0

Browse files
lmasforneYaFou
authored andcommitted
Feature #36362 add Isin validator constraint
Feature #36362 typo Fix PR feedbacks Fix coding standard ticket 36362 fix PR feedbacks Update src/Symfony/Component/Validator/Constraints/IsinValidator.php Co-Authored-By: Yannis Foucher <33806646+YaFou@users.noreply.github.com>
1 parent ef19a03 commit c9c7fa0

File tree

6 files changed

+304
-0
lines changed

6 files changed

+304
-0
lines changed

src/Symfony/Component/Validator/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ CHANGELOG
2828
* })
2929
*/
3030
```
31+
* added `Isin` constraints
3132

3233
5.1.0
3334
-----
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: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
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+
19+
/**
20+
* @author Laurent Masforné <l.masforne@gmail.com>
21+
*
22+
* @see https://en.wikipedia.org/wiki/International_Securities_Identification_Number
23+
*/
24+
class IsinValidator extends ConstraintValidator
25+
{
26+
/**
27+
* {@inheritdoc}
28+
*/
29+
public function validate($value, Constraint $constraint)
30+
{
31+
if (!$constraint instanceof Isin) {
32+
throw new UnexpectedTypeException($constraint, Isin::class);
33+
}
34+
35+
if (null === $value || '' === $value) {
36+
return;
37+
}
38+
39+
if (!is_scalar($value) && !(\is_object($value) && method_exists($value, '__toString'))) {
40+
throw new UnexpectedValueException($value, 'string');
41+
}
42+
43+
$value = strtoupper($value);
44+
45+
if (Isin::VALIDATION_LENGTH !== \strlen($value)) {
46+
$this->context->buildViolation($constraint->message)
47+
->setParameter('{{ value }}', $this->formatValue($value))
48+
->setCode(Isin::INVALID_LENGTH_ERROR)
49+
->addViolation();
50+
51+
return;
52+
}
53+
54+
if (!preg_match(Isin::VALIDATION_PATTERN, $value)) {
55+
$this->context->buildViolation($constraint->message)
56+
->setParameter('{{ value }}', $this->formatValue($value))
57+
->setCode(Isin::INVALID_PATTERN_ERROR)
58+
->addViolation();
59+
60+
return;
61+
}
62+
63+
if (!$this->isCorrectChecksum($value)) {
64+
$this->context->buildViolation($constraint->message)
65+
->setParameter('{{ value }}', $this->formatValue($value))
66+
->setCode(Isin::INVALID_CHECKSUM_ERROR)
67+
->addViolation();
68+
}
69+
}
70+
71+
private function isCorrectChecksum(string $input): bool
72+
{
73+
$characters = str_split($input);
74+
foreach ($characters as $i => $char) {
75+
$characters[$i] = \intval($char, 36);
76+
}
77+
$checkDigit = array_pop($characters);
78+
$number = implode('', $characters);
79+
$expectedCheckDigit = $this->getCheckDigit($number);
80+
81+
return $checkDigit === $expectedCheckDigit;
82+
}
83+
84+
/**
85+
* This method performs the luhn algorithm
86+
* to obtain a check digit.
87+
*/
88+
private function getCheckDigit(string $input): int
89+
{
90+
// first split up the string
91+
$numbers = str_split($input);
92+
93+
// calculate the positional value.
94+
// when there is an even number of digits the second group will be multiplied, so p starts on 0
95+
// when there is an odd number of digits the first group will be multiplied, so p starts on 1
96+
$p = \count($numbers) % 2;
97+
// run through each number
98+
foreach ($numbers as $i => $num) {
99+
$num = (int) $num;
100+
// every positional number needs to be multiplied by 2
101+
if ($p % 2) {
102+
$num = $num * 2;
103+
// if the result was more than 9
104+
// add the individual digits
105+
$num = array_sum(str_split($num));
106+
}
107+
$numbers[$i] = $num;
108+
++$p;
109+
}
110+
111+
// get the total value of all the digits
112+
$sum = array_sum($numbers);
113+
114+
// get the remainder when dividing by 10
115+
$mod = $sum % 10;
116+
117+
// subtract from 10
118+
$rem = 10 - $mod;
119+
120+
// mod from 10 to catch if the result was 0
121+
$digit = $rem % 10;
122+
123+
return $digit;
124+
}
125+
}

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>Ce n'est pas un code international de sécurité valide (ISIN).</target>
388+
</trans-unit>
385389
</body>
386390
</file>
387391
</xliff>
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
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+
9+
class IsinValidatorTest extends ConstraintValidatorTestCase
10+
{
11+
protected function createValidator()
12+
{
13+
return new IsinValidator();
14+
}
15+
16+
public function testNullIsValid()
17+
{
18+
$this->validator->validate(null, new Isin());
19+
20+
$this->assertNoViolation();
21+
}
22+
23+
public function testEmptyStringIsValid()
24+
{
25+
$this->validator->validate('', new Isin());
26+
27+
$this->assertNoViolation();
28+
}
29+
30+
/**
31+
* @dataProvider getValidIsin
32+
*/
33+
public function testValidIsin($isin)
34+
{
35+
$this->validator->validate($isin, new Isin());
36+
$this->assertNoViolation();
37+
}
38+
39+
public function getValidIsin()
40+
{
41+
return [
42+
['XS2125535901'], // Goldman Sachs International MTN HKD HKD 126d
43+
['DE000HZ8VA77'], // UniCredit Bank AG Bond EUR EUR 94d
44+
['CH0528261156'], // Leonteq Securities AG [Guernsey] Bond GBP GBP 3y
45+
['US0378331005'], // Apple, Inc.
46+
['AU0000XVGZA3'], // TREASURY CORP VICTORIA 5 3/4% 2005-2016
47+
['GB0002634946'], // BAE Systems
48+
['CH0528261099'], // Leonteq Securities AG [Guernsey] Bond EUR EUR 3y
49+
['XS2155672814'], // OP Corporate Bank plc CP GBP GBP 93d
50+
['XS2155687259'], // Orbian Financial Services III, LLC CP USD USD 206d
51+
['XS2155696672'], // Sheffield Receivables Company LLC CP EUR EUR 7d//Vatican City State
52+
];
53+
}
54+
55+
/**
56+
* @dataProvider getIsinWithInvalidLenghFormat
57+
*/
58+
public function testIsinWithInvalidFormat($isin)
59+
{
60+
$this->assertViolationRaised($isin, Isin::INVALID_LENGTH_ERROR);
61+
}
62+
63+
public function getIsinWithInvalidLenghFormat()
64+
{
65+
return [
66+
['X'],
67+
['XS'],
68+
['XS2'],
69+
['XS21'],
70+
['XS215'],
71+
['XS2155'],
72+
['XS21556'],
73+
['XS215569'],
74+
['XS2155696'],
75+
['XS21556966'],
76+
['XS215569667'],
77+
];
78+
}
79+
80+
/**
81+
* @dataProvider getIsinWithInvalidPattern
82+
*/
83+
public function testIsinWithInvalidPattern($isin)
84+
{
85+
$this->assertViolationRaised($isin, Isin::INVALID_PATTERN_ERROR);
86+
}
87+
88+
public function getIsinWithInvalidPattern()
89+
{
90+
return [
91+
['X12155696679'],
92+
['123456789101'],
93+
['XS215569667E'],
94+
['XS215E69667A'],
95+
];
96+
}
97+
98+
/**
99+
* @dataProvider getIsinWithValidFormatButIncorrectChecksum
100+
*/
101+
public function testIsinWithValidFormatButIncorrectChecksum($isin)
102+
{
103+
$this->assertViolationRaised($isin, Isin::INVALID_CHECKSUM_ERROR);
104+
}
105+
106+
public function getIsinWithValidFormatButIncorrectChecksum()
107+
{
108+
return [
109+
['XS2112212144'],
110+
['DE013228VA77'],
111+
['CH0512361156'],
112+
['XS2125660123'],
113+
['XS2012587408'],
114+
['XS2012380102'],
115+
['XS2012239364'],
116+
];
117+
}
118+
119+
private function assertViolationRaised($isin, $code)
120+
{
121+
$constraint = new Isin([
122+
'message' => 'myMessage',
123+
]);
124+
125+
$this->validator->validate($isin, $constraint);
126+
127+
$this->buildViolation('myMessage')
128+
->setParameter('{{ value }}', '"'.$isin.'"')
129+
->setCode($code)
130+
->assertRaised();
131+
}
132+
}

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