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
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
Meta:

View file

@ -24,6 +24,7 @@
},
"require-dev": {
"egulias/email-validator": "^3.0",
"giggsey/libphonenumber-for-php-lite": "^8.13",
"malukenho/docheader": "^1.0",
"mikey179/vfsstream": "^1.6",
"phpstan/phpstan": "^1.9",
@ -38,7 +39,8 @@
"ext-bcmath": "Arbitrary Precision Mathematics",
"ext-fileinfo": "File Information",
"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": {
"psr-4": {

View file

@ -2,19 +2,15 @@
- `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
- 555 555 5555
- +5(555)555.5555
- 33(1)22 22 22 22
- +33(1)22 22 22 22
- +33(020)7777 7777
- 03-6106666
```php
v::phone()->validate('+1 650 253 00 00'); // true
v::phone('BR')->validate('+55 11 91111 1111'); // true
v::phone('BR')->validate('11 91111 1111'); // false
```
## Categorization
@ -24,6 +20,7 @@ space or dashed notations) such as:
Version | Description
--------|-------------
2.3.0 | Updated to use external validator
0.5.0 | Created
***

View file

@ -9,6 +9,8 @@ declare(strict_types=1);
namespace Respect\Validation\Exceptions;
use Respect\Validation\Helpers\CountryInfo;
/**
* @author Danilo Correa <danilosilva87@gmail.com>
* @author Henrique Moody <henriquemoody@gmail.com>
@ -16,15 +18,37 @@ namespace Respect\Validation\Exceptions;
*/
final class PhoneException extends ValidationException
{
public const FOR_COUNTRY = 'for_country';
public const INTERNATIONAL = 'international';
/**
* {@inheritDoc}
*/
protected $defaultTemplates = [
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::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;
}
public function setParam(string $name, mixed $value): void
{
$this->params[$name] = $value;
}
public function updateMode(string $mode): void
{
$this->mode = $mode;

View file

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

View file

@ -9,23 +9,50 @@ declare(strict_types=1);
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 preg_match;
use function sprintf;
/**
* Validates whether the input is a valid phone number.
*
* 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)
* Validates an international or country-specific telephone number
*
* @author Danilo Correa <danilosilva87@gmail.com>
* @author Graham Campbell <graham@mineuk.com>
* @author Henrique Moody <henriquemoody@gmail.com>
* @author Alexandre Gomes Gaigalas <alganet@gmail.com>
*/
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}
*/
@ -35,16 +62,12 @@ final class Phone extends AbstractRule
return false;
}
return preg_match($this->getPregFormat(), (string) $input) > 0;
}
private function getPregFormat(): string
{
return sprintf(
'/^\+?(%1$s)? ?(?(?=\()(\(%2$s\) ?%3$s)|([. -]?(%2$s[. -]*)?%3$s))$/',
'\d{0,3}',
'\d{1,3}',
'((\d{3,5})[. -]?(\d{4})|(\d{2}[. -]?){4})'
);
try {
return PhoneNumberUtil::getInstance()->isValidNumber(
PhoneNumberUtil::getInstance()->parse((string) $input, $this->countryCode)
);
} catch (NumberParseException $e) {
return false;
}
}
}

View file

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

View file

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

View file

@ -18,7 +18,7 @@ try {
}
try {
v::not(v::phone())->check('11977777777');
v::not(v::phone())->check('+1 650 253 00 00');
} catch (PhoneException $exception) {
echo $exception->getMessage() . PHP_EOL;
}
@ -30,13 +30,13 @@ try {
}
try {
v::not(v::phone())->assert('+5 555 555 5555');
v::not(v::phone())->assert('+55 11 91111 1111');
} catch (NestedValidationException $exception) {
echo $exception->getFullMessage() . PHP_EOL;
}
?>
--EXPECT--
"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
- "+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;
use Respect\Validation\Exceptions\PhoneException;
use Respect\Validation\Test\RuleTestCase;
/**
@ -29,55 +30,12 @@ final class PhoneTest extends RuleTestCase
*/
public function providerForValidInput(): array
{
$sut = new Phone();
return [
[$sut, '+5-555-555-5555'],
[$sut, '+5 555 555 5555'],
[$sut, '+5.555.555.5555'],
[$sut, '5-555-555-5555'],
[$sut, '5.555.555.5555'],
[$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'],
[new Phone(), '+1 650 253 00 00'],
[new Phone('BR'), '+55 11 91111 1111'],
[new Phone('BR'), '11 91111 1111'], // no international prefix
[new Phone('BR'), '+5511911111111'], // no whitespace
[new Phone('BR'), '11911111111'], // no prefix, no whitespace
];
}
@ -86,38 +44,29 @@ final class PhoneTest extends RuleTestCase
*/
public function providerForInvalidInput(): array
{
$sut = new Phone();
return [
[$sut, ''],
[$sut, '123'],
[$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, []],
[new Phone(), '+1-650-253-00-0'],
[new Phone('BR'), '+1 11 91111 1111'], // invalid + code for BR
];
}
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 tmpfile;
use function xml_parser_create;
/**
* @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}
*/