Use libphonenumber

Doing regex on phone numbers is not a great idea. This is a breaking
change, but a good one. Phone validation is now much stricter, and
allows choosing the country.
This commit is contained in:
Alexandre Gomes Gaigalas 2023-02-18 18:09:26 -03:00
parent fc8230acef
commit cc3bf86b2f
12 changed files with 135 additions and 136 deletions

View file

@ -12,6 +12,18 @@ Deprecations:
- Symfony façade validators are no longer supported and were - Symfony façade validators are no longer supported and were
removed. removed.
Fixes:
- `KeySet` now reports which extra keys are causing the rule to fail.
Changes:
- You can no longer wrap `KeySet` in `Not`.
- `Phone` now uses `giggsey/libphonenumber-for-php`, this package needs
to be installed if you want to use this validator.
- `Phone` now supports the parameter `$countryCode` to validate phones
of a specific country.
## 2.2.4 ## 2.2.4
Meta: Meta:

View file

@ -24,6 +24,7 @@
}, },
"require-dev": { "require-dev": {
"egulias/email-validator": "^3.0", "egulias/email-validator": "^3.0",
"giggsey/libphonenumber-for-php-lite": "^8.13",
"malukenho/docheader": "^1.0", "malukenho/docheader": "^1.0",
"mikey179/vfsstream": "^1.6", "mikey179/vfsstream": "^1.6",
"phpstan/phpstan": "^1.9", "phpstan/phpstan": "^1.9",
@ -38,7 +39,8 @@
"ext-bcmath": "Arbitrary Precision Mathematics", "ext-bcmath": "Arbitrary Precision Mathematics",
"ext-fileinfo": "File Information", "ext-fileinfo": "File Information",
"ext-mbstring": "Multibyte String Functions", "ext-mbstring": "Multibyte String Functions",
"egulias/email-validator": "Strict (RFC compliant) email validation" "egulias/email-validator": "Improves the Email rule if available",
"giggsey/libphonenumber-for-php-lite": "Enables the phone rule if available"
}, },
"autoload": { "autoload": {
"psr-4": { "psr-4": {

View file

@ -2,19 +2,15 @@
- `Phone()` - `Phone()`
Validates whether the input is a valid phone number. Validates whether the input is a valid phone number. This rule requires
the `giggsey/libphonenumber-for-php-lite` package.
Validates a valid 7, 10, 11 digit phone number (North America, Europe and most
Asian and Middle East countries), supporting country and area codes (in dot,
space or dashed notations) such as:
- (555)555-5555 ```php
- 555 555 5555 v::phone()->validate('+1 650 253 00 00'); // true
- +5(555)555.5555 v::phone('BR')->validate('+55 11 91111 1111'); // true
- 33(1)22 22 22 22 v::phone('BR')->validate('11 91111 1111'); // false
- +33(1)22 22 22 22 ```
- +33(020)7777 7777
- 03-6106666
## Categorization ## Categorization
@ -24,6 +20,7 @@ space or dashed notations) such as:
Version | Description Version | Description
--------|------------- --------|-------------
2.3.0 | Updated to use external validator
0.5.0 | Created 0.5.0 | Created
*** ***

View file

@ -9,6 +9,8 @@ declare(strict_types=1);
namespace Respect\Validation\Exceptions; namespace Respect\Validation\Exceptions;
use Respect\Validation\Helpers\CountryInfo;
/** /**
* @author Danilo Correa <danilosilva87@gmail.com> * @author Danilo Correa <danilosilva87@gmail.com>
* @author Henrique Moody <henriquemoody@gmail.com> * @author Henrique Moody <henriquemoody@gmail.com>
@ -16,15 +18,37 @@ namespace Respect\Validation\Exceptions;
*/ */
final class PhoneException extends ValidationException final class PhoneException extends ValidationException
{ {
public const FOR_COUNTRY = 'for_country';
public const INTERNATIONAL = 'international';
/** /**
* {@inheritDoc} * {@inheritDoc}
*/ */
protected $defaultTemplates = [ protected $defaultTemplates = [
self::MODE_DEFAULT => [ self::MODE_DEFAULT => [
self::STANDARD => '{{name}} must be a valid telephone number', self::INTERNATIONAL => '{{name}} must be a valid telephone number',
self::FOR_COUNTRY => '{{name}} must be a valid telephone number for country {{countryName}}',
], ],
self::MODE_NEGATIVE => [ self::MODE_NEGATIVE => [
self::STANDARD => '{{name}} must not be a valid telephone number', self::INTERNATIONAL => '{{name}} must not be a valid telephone number',
self::FOR_COUNTRY => '{{name}} must not be a valid telephone number for country {{countryName}}',
], ],
]; ];
/**
* {@inheritDoc}
*/
protected function chooseTemplate(): string
{
$countryCode = $this->getParam('countryCode');
if (!$countryCode) {
return self::INTERNATIONAL;
}
$countryInfo = new CountryInfo($countryCode);
$this->setParam('countryName', $countryInfo->getCountry());
return self::FOR_COUNTRY;
}
} }

View file

@ -106,6 +106,11 @@ class ValidationException extends InvalidArgumentException implements Exception
return $this->params[$name] ?? null; return $this->params[$name] ?? null;
} }
public function setParam(string $name, mixed $value): void
{
$this->params[$name] = $value;
}
public function updateMode(string $mode): void public function updateMode(string $mode): void
{ {
$this->mode = $mode; $this->mode = $mode;

View file

@ -16,7 +16,7 @@ use function file_get_contents;
use function json_decode; use function json_decode;
use function sprintf; use function sprintf;
final class Subdivisions final class CountryInfo
{ {
/** /**
* @var mixed[] * @var mixed[]

View file

@ -9,23 +9,50 @@ declare(strict_types=1);
namespace Respect\Validation\Rules; namespace Respect\Validation\Rules;
use libphonenumber\NumberParseException;
use libphonenumber\PhoneNumberUtil;
use Respect\Validation\Exceptions\ComponentException;
use function class_exists;
use function is_null;
use function is_scalar; use function is_scalar;
use function preg_match;
use function sprintf; use function sprintf;
/** /**
* Validates whether the input is a valid phone number. * Validates whether the input is a valid phone number.
* *
* Validates a valid 7, 10, 11 digit phone number (North America, Europe and * Validates an international or country-specific telephone number
* most Asian and Middle East countries), supporting country and area codes (in
* dot,space or dashed notations)
* *
* @author Danilo Correa <danilosilva87@gmail.com> * @author Alexandre Gomes Gaigalas <alganet@gmail.com>
* @author Graham Campbell <graham@mineuk.com>
* @author Henrique Moody <henriquemoody@gmail.com>
*/ */
final class Phone extends AbstractRule final class Phone extends AbstractRule
{ {
/**
* @var ?string
*/
private $countryCode;
/**
* {@inheritDoc}
*/
public function __construct(?string $countryCode = null)
{
$this->countryCode = $countryCode;
if (!is_null($countryCode) && !(new CountryCode())->validate($countryCode)) {
throw new ComponentException(
sprintf(
'Invalid country code %s',
$countryCode
)
);
}
if (!class_exists(PhoneNumberUtil::class)) {
throw new ComponentException('The phone validator requires giggsey/libphonenumber-for-php');
}
}
/** /**
* {@inheritDoc} * {@inheritDoc}
*/ */
@ -35,16 +62,12 @@ final class Phone extends AbstractRule
return false; return false;
} }
return preg_match($this->getPregFormat(), (string) $input) > 0; try {
} return PhoneNumberUtil::getInstance()->isValidNumber(
PhoneNumberUtil::getInstance()->parse((string) $input, $this->countryCode)
private function getPregFormat(): string );
{ } catch (NumberParseException $e) {
return sprintf( return false;
'/^\+?(%1$s)? ?(?(?=\()(\(%2$s\) ?%3$s)|([. -]?(%2$s[. -]*)?%3$s))$/', }
'\d{0,3}',
'\d{1,3}',
'((\d{3,5})[. -]?(\d{4})|(\d{2}[. -]?){4})'
);
} }
} }

View file

@ -9,7 +9,7 @@ declare(strict_types=1);
namespace Respect\Validation\Rules; namespace Respect\Validation\Rules;
use Respect\Validation\Helpers\Subdivisions; use Respect\Validation\Helpers\CountryInfo;
use function array_keys; use function array_keys;
@ -32,14 +32,14 @@ final class SubdivisionCode extends AbstractSearcher
/** /**
* @var string[] * @var string[]
*/ */
private $subdivisions; private $countryInfo;
public function __construct(string $countryCode) public function __construct(string $countryCode)
{ {
$subdivisions = new Subdivisions($countryCode); $countryInfo = new CountryInfo($countryCode);
$this->countryName = $subdivisions->getCountry(); $this->countryName = $countryInfo->getCountry();
$this->subdivisions = array_keys($subdivisions->getSubdivisions()); $this->countryInfo = array_keys($countryInfo->getSubdivisions());
} }
/** /**
@ -47,6 +47,6 @@ final class SubdivisionCode extends AbstractSearcher
*/ */
protected function getDataSource(): array protected function getDataSource(): array
{ {
return $this->subdivisions; return $this->countryInfo;
} }
} }

View file

@ -16,7 +16,7 @@ $work->countryCode = 61;
$work->primary = true; $work->primary = true;
$personal = new stdClass(); $personal = new stdClass();
$personal->number = '+61.0406 464 890'; $personal->number = '123';
$personal->country = 61; $personal->country = 61;
$personal->primary = false; $personal->primary = false;

View file

@ -18,7 +18,7 @@ try {
} }
try { try {
v::not(v::phone())->check('11977777777'); v::not(v::phone())->check('+1 650 253 00 00');
} catch (PhoneException $exception) { } catch (PhoneException $exception) {
echo $exception->getMessage() . PHP_EOL; echo $exception->getMessage() . PHP_EOL;
} }
@ -30,13 +30,13 @@ try {
} }
try { try {
v::not(v::phone())->assert('+5 555 555 5555'); v::not(v::phone())->assert('+55 11 91111 1111');
} catch (NestedValidationException $exception) { } catch (NestedValidationException $exception) {
echo $exception->getFullMessage() . PHP_EOL; echo $exception->getFullMessage() . PHP_EOL;
} }
?> ?>
--EXPECT-- --EXPECT--
"123" must be a valid telephone number "123" must be a valid telephone number
"11977777777" must not be a valid telephone number "+1 650 253 00 00" must not be a valid telephone number
- "(555)5555 555" must be a valid telephone number - "(555)5555 555" must be a valid telephone number
- "+5 555 555 5555" must not be a valid telephone number - "+55 11 91111 1111" must not be a valid telephone number

View file

@ -9,6 +9,7 @@ declare(strict_types=1);
namespace Respect\Validation\Rules; namespace Respect\Validation\Rules;
use Respect\Validation\Exceptions\PhoneException;
use Respect\Validation\Test\RuleTestCase; use Respect\Validation\Test\RuleTestCase;
/** /**
@ -29,55 +30,12 @@ final class PhoneTest extends RuleTestCase
*/ */
public function providerForValidInput(): array public function providerForValidInput(): array
{ {
$sut = new Phone();
return [ return [
[$sut, '+5-555-555-5555'], [new Phone(), '+1 650 253 00 00'],
[$sut, '+5 555 555 5555'], [new Phone('BR'), '+55 11 91111 1111'],
[$sut, '+5.555.555.5555'], [new Phone('BR'), '11 91111 1111'], // no international prefix
[$sut, '5-555-555-5555'], [new Phone('BR'), '+5511911111111'], // no whitespace
[$sut, '5.555.555.5555'], [new Phone('BR'), '11911111111'], // no prefix, no whitespace
[$sut, '5 555 555 5555'],
[$sut, '555.555.5555'],
[$sut, '555 555 5555'],
[$sut, '555-555-5555'],
[$sut, '555-5555555'],
[$sut, '5(555)555.5555'],
[$sut, '+5(555)555.5555'],
[$sut, '+5(555)555 5555'],
[$sut, '+5(555)555-5555'],
[$sut, '+5(555)5555555'],
[$sut, '(555)5555555'],
[$sut, '(555)555.5555'],
[$sut, '(555)555-5555'],
[$sut, '(555) 555 5555'],
[$sut, '55555555555'],
[$sut, '5555555555'],
[$sut, '+33(1)2222222'],
[$sut, '+33(1)222 2222'],
[$sut, '+33(1)222.2222'],
[$sut, '+33(1)22 22 22 22'],
[$sut, '33(1)2222222'],
[$sut, '33(1)22222222'],
[$sut, '33(1)22 22 22 22'],
[$sut, '(020) 7476 4026'],
[$sut, '33(020) 7777 7777'],
[$sut, '33(020)7777 7777'],
[$sut, '+33(020) 7777 7777'],
[$sut, '+33(020)7777 7777'],
[$sut, '03-6106666'],
[$sut, '036106666'],
[$sut, '+33(11) 97777 7777'],
[$sut, '+3311977777777'],
[$sut, '11977777777'],
[$sut, '11 97777 7777'],
[$sut, '(11) 97777 7777'],
[$sut, '(11) 97777-7777'],
[$sut, '555-5555'],
[$sut, '5555555'],
[$sut, '555.5555'],
[$sut, '555 5555'],
[$sut, '+1 (555) 555 5555'],
]; ];
} }
@ -86,38 +44,29 @@ final class PhoneTest extends RuleTestCase
*/ */
public function providerForInvalidInput(): array public function providerForInvalidInput(): array
{ {
$sut = new Phone();
return [ return [
[$sut, ''], [new Phone(), '+1-650-253-00-0'],
[$sut, '123'], [new Phone('BR'), '+1 11 91111 1111'], // invalid + code for BR
[$sut, '(11- 97777-7777'],
[$sut, '-11) 97777-7777'],
[$sut, 's555-5555'],
[$sut, '555-555'],
[$sut, '555555'],
[$sut, '555+5555'],
[$sut, '(555)555555'],
[$sut, '(555)55555'],
[$sut, '+(555)555 555'],
[$sut, '+5(555)555 555'],
[$sut, '+5(555)555 555 555'],
[$sut, '555)555 555'],
[$sut, '+5(555)5555 555'],
[$sut, '(555)55 555'],
[$sut, '(555)5555 555'],
[$sut, '+5(555)555555'],
[$sut, '5(555)55 55555'],
[$sut, '(5)555555'],
[$sut, '+55(5)55 5 55 55'],
[$sut, '+55(5)55 55 55 5'],
[$sut, '+55(5)55 55 55'],
[$sut, '+55(5)5555 555'],
[$sut, '+55()555 5555'],
[$sut, '03610666-5'],
[$sut, 'text'],
[$sut, "555\n5555"],
[$sut, []],
]; ];
} }
public function testThrowsExceptionWithCountryName(): void
{
$phoneValidator = new Phone('BR');
$this->expectException(PhoneException::class);
$this->expectExceptionMessage('"abc" must be a valid telephone number for country "Brazil"');
$phoneValidator->assert('abc');
}
public function testThrowsExceptionForInternationalNumbers(): void
{
$phoneValidator = new Phone();
$this->expectException(PhoneException::class);
$this->expectExceptionMessage('"abc" must be a valid telephone number');
$phoneValidator->assert('abc');
}
} }

View file

@ -14,7 +14,6 @@ use stdClass;
use function stream_context_create; use function stream_context_create;
use function tmpfile; use function tmpfile;
use function xml_parser_create;
/** /**
* @group rule * @group rule
@ -39,18 +38,6 @@ final class ResourceTypeTest extends RuleTestCase
]; ];
} }
/**
* @test
*
* @requires PHP < 8.0
*/
public function itShouldTestXmlResource(): void
{
$rule = new ResourceType();
self::assertValidInput($rule, xml_parser_create());
}
/** /**
* {@inheritDoc} * {@inheritDoc}
*/ */