Skip to content

Commit

Permalink
Merge pull request laminas#322 from gsteel/v3/refactor-iban
Browse files Browse the repository at this point in the history
Refactor `Iban` validator
  • Loading branch information
gsteel committed Jul 3, 2024
2 parents 50404d7 + 916aba9 commit 2c2d75a
Show file tree
Hide file tree
Showing 4 changed files with 55 additions and 141 deletions.
25 changes: 18 additions & 7 deletions docs/book/v3/validators/iban.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ The following options are supported for `Laminas\Validator\Iban`:

- `country_code`: Sets the country code which is used to get the IBAN format
for validation.
- `allow_non_sepa`: A boolean that limits allowable account numbers to SEPA countries when `false`

## IBAN validation

Expand All @@ -19,13 +20,12 @@ to use `Laminas\Validator\Iban`.

### Ungreedy IBAN validation

Sometime it is useful just to validate if the given value is a IBAN number or
Sometimes it is useful just to validate if the given value is a IBAN number or
not. This means that you don't want to validate it against a defined country.
This can be done by using `false` as locale.
This can be done by omitting the `country_code` option.

```php
$validator = new Laminas\Validator\Iban(['country_code' => false]);
// Note: you can also provide FALSE as the sole parameter
$validator = new Laminas\Validator\Iban();

if ($validator->isValid('AT611904300234573201')) {
// IBAN appears to be valid
Expand All @@ -40,9 +40,8 @@ country!

### Region aware IBAN validation

To validate against a defined country, you just provide a country code. You can
do this during instaniation via the option `country_code`, or afterwards by
using `setCountryCode()`.
To validate against a defined country, you must provide a country code. You can
do this during instantiation via the option `country_code`.

```php
$validator = new Laminas\Validator\Iban(['country_code' => 'AT']);
Expand All @@ -53,3 +52,15 @@ if ($validator->isValid('AT611904300234573201')) {
// IBAN is not valid
}
```

### Restrict to SEPA Countries

To only accept bank accounts from within the Single Euro Payments Area (SEPA), you can set the option `allow_non_sepa` to `false`:

```php
$validator = new Laminas\Validator\Iban(['allow_non_sepa' => false]);

$validator->isValid('AT611904300234573201'); // true
$validator->isValid('BA391290079401028494'); // false

```
13 changes: 0 additions & 13 deletions psalm-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -989,19 +989,6 @@
<code><![CDATA[$regexChar]]></code>
</MixedAssignment>
</file>
<file src="src/Iban.php">
<MixedArgument>
<code><![CDATA[$options['allow_non_sepa']]]></code>
<code><![CDATA[$options['country_code']]]></code>
</MixedArgument>
<PossiblyInvalidArgument>
<code><![CDATA[$options]]></code>
</PossiblyInvalidArgument>
<RedundantCastGivenDocblockType>
<code><![CDATA[(bool) $allowNonSepa]]></code>
<code><![CDATA[(string) $countryCode]]></code>
</RedundantCastGivenDocblockType>
</file>
<file src="src/IsJsonString.php">
<InvalidArgument>
<code><![CDATA[$this->maxDepth]]></code>
Expand Down
97 changes: 19 additions & 78 deletions src/Iban.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@

namespace Laminas\Validator;

use Laminas\Stdlib\ArrayUtils;
use Traversable;
use Laminas\Validator\Exception\InvalidArgumentException;

use function array_key_exists;
use function in_array;
Expand All @@ -19,6 +18,11 @@

/**
* Validates IBAN Numbers (International Bank Account Numbers)
*
* @psalm-type OptionsArgument = array{
* country_code?: string,
* allow_non_sepa?: bool,
* }
*/
final class Iban extends AbstractValidator
{
Expand All @@ -41,17 +45,13 @@ final class Iban extends AbstractValidator

/**
* Optional country code by ISO 3166-1
*
* @var string|null
*/
protected $countryCode;
private readonly ?string $countryCode;

/**
* Optionally allow IBAN codes from non-SEPA countries. Defaults to true
*
* @var bool
*/
protected $allowNonSepa = true;
private readonly bool $allowNonSepa;

/**
* SEPA ISO 3166-1country codes
Expand Down Expand Up @@ -168,83 +168,24 @@ final class Iban extends AbstractValidator
'VG' => 'VG[0-9]{2}[A-Z]{4}[0-9]{16}',
];

/**
* Sets validator options
*
* @param array|Traversable $options OPTIONAL
*/
public function __construct($options = [])
/** @param OptionsArgument $options */
public function __construct(array $options = [])
{
if ($options instanceof Traversable) {
$options = ArrayUtils::iteratorToArray($options);
$countryCode = $options['country_code'] ?? null;
if ($countryCode !== null && ! isset(self::IBAN_REGEX[$countryCode])) {
throw new InvalidArgumentException(
"Country code '{$countryCode}' invalid by ISO 3166-1 or not supported"
);
}

if (array_key_exists('country_code', $options)) {
$this->setCountryCode($options['country_code']);
}
$this->countryCode = $countryCode;
$this->allowNonSepa = $options['allow_non_sepa'] ?? true;

if (array_key_exists('allow_non_sepa', $options)) {
$this->setAllowNonSepa($options['allow_non_sepa']);
}
unset($options['country_code'], $options['allow_non_sepa']);

parent::__construct($options);
}

/**
* Returns the optional country code by ISO 3166-1
*
* @return string|null
*/
public function getCountryCode()
{
return $this->countryCode;
}

/**
* Sets an optional country code by ISO 3166-1
*
* @param string|null $countryCode
* @return $this provides a fluent interface
* @throws Exception\InvalidArgumentException
*/
public function setCountryCode($countryCode = null)
{
if ($countryCode !== null) {
$countryCode = (string) $countryCode;

if (! isset(self::IBAN_REGEX[$countryCode])) {
throw new Exception\InvalidArgumentException(
"Country code '{$countryCode}' invalid by ISO 3166-1 or not supported"
);
}
}

$this->countryCode = $countryCode;
return $this;
}

/**
* Returns the optional allow non-sepa countries setting
*
* @return bool
*/
public function allowNonSepa()
{
return $this->allowNonSepa;
}

/**
* Sets the optional allow non-sepa countries setting
*
* @param bool $allowNonSepa
* @return $this provides a fluent interface
*/
public function setAllowNonSepa($allowNonSepa)
{
$this->allowNonSepa = (bool) $allowNonSepa;
return $this;
}

/**
* Returns true if $value is a valid IBAN
*/
Expand All @@ -258,7 +199,7 @@ public function isValid(mixed $value): bool
$value = str_replace(' ', '', strtoupper($value));
$this->setValue($value);

$countryCode = $this->getCountryCode();
$countryCode = $this->countryCode;
if ($countryCode === null) {
$countryCode = substr($value, 0, 2);
}
Expand Down
61 changes: 18 additions & 43 deletions test/IbanTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -110,29 +110,12 @@ public function testBasic(string $iban, bool $expected): void
);
}

public function testSettingAndGettingCountryCode(): void
public function testInvalidCountryCodeIsExceptional(): void
{
$validator = new IbanValidator();
$validator->setCountryCode('DE');

self::assertSame('DE', $validator->getCountryCode());

$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('ISO 3166-1');

$validator->setCountryCode('foo');
}

public function testInstanceWithCountryCode(): void
{
$validator = new IbanValidator(['country_code' => 'AT']);

self::assertSame('AT', $validator->getCountryCode());

$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('ISO 3166-1');

new IbanValidator(['country_code' => 'BAR']);
new IbanValidator(['country_code' => 'foo']);
}

public function testSepaNotSupportedCountryCode(): void
Expand All @@ -141,13 +124,11 @@ public function testSepaNotSupportedCountryCode(): void

self::assertTrue($validator->isValid('DO17552081023122561803924090'));

$validator->setAllowNonSepa(false);
$validator = new IbanValidator([
'allow_non_sepa' => false,
]);

self::assertFalse($validator->isValid('DO17552081023122561803924090'));

$validator->setAllowNonSepa(true);

self::assertTrue($validator->isValid('DO17552081023122561803924090'));
}

public function testIbanNotSupportedCountryCode(): void
Expand Down Expand Up @@ -181,37 +162,31 @@ public function testEqualsMessageTemplates(): void
self::assertSame($validator->getOption('messageTemplates'), $validator->getMessageTemplates());
}

public function testConstructorAllowsSettingOptionsViaOptionsArray(): void
{
$validator = new IbanValidator(['country_code' => 'AT', 'allow_non_sepa' => false]);

self::assertSame('AT', $validator->getCountryCode());
self::assertFalse($validator->allowNonSepa());
}

/**
* @psalm-return array<string, array{0: mixed}>
* @psalm-return array<string, array{0: mixed, 1: string}>
*/
public static function invalidValues(): array
{
return [
'null' => [null],
'true' => [true],
'false' => [false],
'zero' => [0],
'int' => [1],
'zero-float' => [0.0],
'float' => [1.1],
'array' => [['foo']],
'object' => [(object) []],
'null' => [null, IbanValidator::FALSEFORMAT],
'true' => [true, IbanValidator::FALSEFORMAT],
'false' => [false, IbanValidator::FALSEFORMAT],
'zero' => [0, IbanValidator::FALSEFORMAT],
'int' => [1, IbanValidator::FALSEFORMAT],
'zero-float' => [0.0, IbanValidator::FALSEFORMAT],
'float' => [1.1, IbanValidator::FALSEFORMAT],
'array' => [['foo'], IbanValidator::FALSEFORMAT],
'object' => [(object) [], IbanValidator::FALSEFORMAT],
'Not match regex' => ['GB123', IbanValidator::FALSEFORMAT],
];
}

#[DataProvider('invalidValues')]
public function testIsValidReturnsFalseForNonStringValue(mixed $value): void
public function testIsValidReturnsFalseForNonStringValue(mixed $value, string $errorKey): void
{
$validator = new IbanValidator();

self::assertFalse($validator->isValid($value));
self::assertArrayHasKey($errorKey, $validator->getMessages());
}
}

0 comments on commit 2c2d75a

Please sign in to comment.