diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b26191f..9d082d4f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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: diff --git a/composer.json b/composer.json index 6a7504ef..ebbeaaef 100644 --- a/composer.json +++ b/composer.json @@ -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": { diff --git a/docs/rules/Phone.md b/docs/rules/Phone.md index 32719903..e9978761 100644 --- a/docs/rules/Phone.md +++ b/docs/rules/Phone.md @@ -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 *** diff --git a/library/Exceptions/PhoneException.php b/library/Exceptions/PhoneException.php index 459e1d98..ea708452 100644 --- a/library/Exceptions/PhoneException.php +++ b/library/Exceptions/PhoneException.php @@ -9,6 +9,8 @@ declare(strict_types=1); namespace Respect\Validation\Exceptions; +use Respect\Validation\Helpers\CountryInfo; + /** * @author Danilo Correa * @author Henrique Moody @@ -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; + } } diff --git a/library/Exceptions/ValidationException.php b/library/Exceptions/ValidationException.php index 416f3f81..ccce42c2 100644 --- a/library/Exceptions/ValidationException.php +++ b/library/Exceptions/ValidationException.php @@ -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; diff --git a/library/Helpers/Subdivisions.php b/library/Helpers/CountryInfo.php similarity index 97% rename from library/Helpers/Subdivisions.php rename to library/Helpers/CountryInfo.php index f1bd78f8..f74d3f24 100644 --- a/library/Helpers/Subdivisions.php +++ b/library/Helpers/CountryInfo.php @@ -16,7 +16,7 @@ use function file_get_contents; use function json_decode; use function sprintf; -final class Subdivisions +final class CountryInfo { /** * @var mixed[] diff --git a/library/Rules/Phone.php b/library/Rules/Phone.php index 02a7d834..5dae4d05 100644 --- a/library/Rules/Phone.php +++ b/library/Rules/Phone.php @@ -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 - * @author Graham Campbell - * @author Henrique Moody + * @author Alexandre Gomes Gaigalas */ 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; + } } } diff --git a/library/Rules/SubdivisionCode.php b/library/Rules/SubdivisionCode.php index dde6386c..bd6618b2 100644 --- a/library/Rules/SubdivisionCode.php +++ b/library/Rules/SubdivisionCode.php @@ -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; } } diff --git a/tests/integration/issue-727.phpt b/tests/integration/issue-727.phpt index a7d4bc96..91b7721c 100644 --- a/tests/integration/issue-727.phpt +++ b/tests/integration/issue-727.phpt @@ -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; diff --git a/tests/integration/rules/phone.phpt b/tests/integration/rules/phone.phpt index 2c230c45..ddbaa26b 100644 --- a/tests/integration/rules/phone.phpt +++ b/tests/integration/rules/phone.phpt @@ -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 diff --git a/tests/unit/Rules/PhoneTest.php b/tests/unit/Rules/PhoneTest.php index ad4728d4..623f622a 100644 --- a/tests/unit/Rules/PhoneTest.php +++ b/tests/unit/Rules/PhoneTest.php @@ -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'); + } } diff --git a/tests/unit/Rules/ResourceTypeTest.php b/tests/unit/Rules/ResourceTypeTest.php index e7be4aa2..15f23c5b 100644 --- a/tests/unit/Rules/ResourceTypeTest.php +++ b/tests/unit/Rules/ResourceTypeTest.php @@ -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} */