From e6b7d0630823000a14b3759652205addf7d4404e Mon Sep 17 00:00:00 2001 From: Sukhwinder Dhillon Date: Wed, 28 Dec 2022 15:57:04 +0100 Subject: [PATCH 1/6] WIP: Add `RegexMatchValidator` --- src/RegexMatchValidator.php | 74 +++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 src/RegexMatchValidator.php diff --git a/src/RegexMatchValidator.php b/src/RegexMatchValidator.php new file mode 100644 index 0000000..36c1a6c --- /dev/null +++ b/src/RegexMatchValidator.php @@ -0,0 +1,74 @@ +pattern = $pattern['pattern']; + $this->notMatchMessage = $pattern['notMatchMessage'] ?? null; + } else { + $this->pattern = $pattern; + } + } + + public function isValid($value): bool + { + // Multiple isValid() calls must not stack validation messages + $this->clearMessages(); + + $status = @preg_match($this->pattern, $value); + if ($status === false) { + $this->addMessage(sprintf( + "There was an internal error while using the pattern '%s'", + $this->pattern + )); + + return false; + } + + if ($status === 0) { + if (empty($this->notMatchMessage)) { + $this->addMessage(sprintf( + $this->translate("'%s' does not match against pattern '%s'"), + $value, + $this->pattern + )); + } else { + $this->addMessage($this->notMatchMessage); + } + + return false; + } + + return true; + } +} From c723b402d5aa4dea7412f321a876384693aa06ea Mon Sep 17 00:00:00 2001 From: "Alexander A. Klimov" Date: Mon, 16 Feb 2026 13:33:58 +0100 Subject: [PATCH 2/6] `RegexMatchValidator#__construct()`: document parameter array shape --- src/RegexMatchValidator.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/RegexMatchValidator.php b/src/RegexMatchValidator.php index 36c1a6c..073dac2 100644 --- a/src/RegexMatchValidator.php +++ b/src/RegexMatchValidator.php @@ -22,7 +22,7 @@ class RegexMatchValidator extends BaseValidator /** * Create a RegexMatchValidator * - * @param string|array $pattern + * @param string|array{pattern: string, notMatchMessage?: string|null} $pattern * * @throws Exception If the given parameter is an array and does not contain the `pattern` option */ From 49314b902ee585387ebf68a22209803cfa32c67b Mon Sep 17 00:00:00 2001 From: "Alexander A. Klimov" Date: Mon, 16 Feb 2026 14:47:51 +0100 Subject: [PATCH 3/6] `RegexMatchValidator#__construct()`: reject invalid pattern --- src/RegexMatchValidator.php | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/src/RegexMatchValidator.php b/src/RegexMatchValidator.php index 073dac2..c901273 100644 --- a/src/RegexMatchValidator.php +++ b/src/RegexMatchValidator.php @@ -24,7 +24,7 @@ class RegexMatchValidator extends BaseValidator * * @param string|array{pattern: string, notMatchMessage?: string|null} $pattern * - * @throws Exception If the given parameter is an array and does not contain the `pattern` option + * @throws Exception If the pattern is missing or invalid */ public function __construct(string|array $pattern) { @@ -38,6 +38,12 @@ public function __construct(string|array $pattern) } else { $this->pattern = $pattern; } + + $rsv = new RegexSyntaxValidator(); + + if (! $rsv->isValid($this->pattern)) { + throw new Exception($rsv->getMessages()[0]); + } } public function isValid($value): bool @@ -45,17 +51,7 @@ public function isValid($value): bool // Multiple isValid() calls must not stack validation messages $this->clearMessages(); - $status = @preg_match($this->pattern, $value); - if ($status === false) { - $this->addMessage(sprintf( - "There was an internal error while using the pattern '%s'", - $this->pattern - )); - - return false; - } - - if ($status === 0) { + if (! preg_match($this->pattern, $value)) { if (empty($this->notMatchMessage)) { $this->addMessage(sprintf( $this->translate("'%s' does not match against pattern '%s'"), From 2711f11f99d74e834771d9681aab2af751d95883 Mon Sep 17 00:00:00 2001 From: "Alexander A. Klimov" Date: Wed, 4 Mar 2026 10:17:56 +0100 Subject: [PATCH 4/6] `RegexMatchValidator`: reject whitespace-only `notMatchMessage` --- src/RegexMatchValidator.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/RegexMatchValidator.php b/src/RegexMatchValidator.php index c901273..7a1c589 100644 --- a/src/RegexMatchValidator.php +++ b/src/RegexMatchValidator.php @@ -24,7 +24,7 @@ class RegexMatchValidator extends BaseValidator * * @param string|array{pattern: string, notMatchMessage?: string|null} $pattern * - * @throws Exception If the pattern is missing or invalid + * @throws Exception If the notMatchMessage consists only of whitespace or the pattern is missing or invalid */ public function __construct(string|array $pattern) { @@ -35,6 +35,10 @@ public function __construct(string|array $pattern) $this->pattern = $pattern['pattern']; $this->notMatchMessage = $pattern['notMatchMessage'] ?? null; + + if ($this->notMatchMessage !== null && empty(trim($this->notMatchMessage))) { + throw new Exception("Option 'notMatchMessage' consists only of whitespace"); + } } else { $this->pattern = $pattern; } From 928a1db56e4aad7f7b22fe4ef81e9cbf3ef6c37f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Aleksandrovi=C4=8D=20Klimov?= Date: Tue, 3 Mar 2026 10:09:03 +0100 Subject: [PATCH 5/6] Apply suggestions from code review Co-authored-by: Eric Lippmann --- src/RegexMatchValidator.php | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/src/RegexMatchValidator.php b/src/RegexMatchValidator.php index 7a1c589..9d6793a 100644 --- a/src/RegexMatchValidator.php +++ b/src/RegexMatchValidator.php @@ -2,7 +2,7 @@ namespace ipl\Validator; -use Exception; +use InvalidArgumentException; /** * Validates value with a given regex pattern @@ -24,29 +24,27 @@ class RegexMatchValidator extends BaseValidator * * @param string|array{pattern: string, notMatchMessage?: string|null} $pattern * - * @throws Exception If the notMatchMessage consists only of whitespace or the pattern is missing or invalid + * @throws InvalidArgumentException If the notMatchMessage consists only of whitespace + * @throws InvalidArgumentException If the pattern is missing or invalid */ public function __construct(string|array $pattern) { if (is_array($pattern)) { - if (! isset($pattern['pattern'])) { - throw new Exception("Missing option 'pattern'"); - } - - $this->pattern = $pattern['pattern']; + $this->pattern = $pattern['pattern'] ?? throw new InvalidArgumentException("Missing option 'pattern'"); $this->notMatchMessage = $pattern['notMatchMessage'] ?? null; - if ($this->notMatchMessage !== null && empty(trim($this->notMatchMessage))) { - throw new Exception("Option 'notMatchMessage' consists only of whitespace"); + if ($this->notMatchMessage !== null && trim($this->notMatchMessage) === '') { + throw new InvalidArgumentException( + "Option 'notMatchMessage' must not be an empty or whitespace-only string" + ); } } else { $this->pattern = $pattern; } - $rsv = new RegexSyntaxValidator(); - - if (! $rsv->isValid($this->pattern)) { - throw new Exception($rsv->getMessages()[0]); + $syntax = new RegexSyntaxValidator(); + if (! $syntax->isValid($this->pattern)) { + throw new InvalidArgumentException($syntax->getMessages()[0]); } } @@ -56,7 +54,7 @@ public function isValid($value): bool $this->clearMessages(); if (! preg_match($this->pattern, $value)) { - if (empty($this->notMatchMessage)) { + if ($this->notMatchMessage === null) { $this->addMessage(sprintf( $this->translate("'%s' does not match against pattern '%s'"), $value, From dd7f7c3792120c01943dfc346d1046fa87b80dd7 Mon Sep 17 00:00:00 2001 From: "Alexander A. Klimov" Date: Tue, 10 Mar 2026 10:29:45 +0100 Subject: [PATCH 6/6] Test `RegexMatchValidator` --- src/RegexMatchValidator.php | 10 +- tests/RegexMatchValidatorTest.php | 155 ++++++++++++++++++++++++++++++ 2 files changed, 164 insertions(+), 1 deletion(-) create mode 100644 tests/RegexMatchValidatorTest.php diff --git a/src/RegexMatchValidator.php b/src/RegexMatchValidator.php index 9d6793a..df1b130 100644 --- a/src/RegexMatchValidator.php +++ b/src/RegexMatchValidator.php @@ -53,7 +53,15 @@ public function isValid($value): bool // Multiple isValid() calls must not stack validation messages $this->clearMessages(); - if (! preg_match($this->pattern, $value)) { + $result = preg_match($this->pattern, $value); + + if ($result === false) { + $this->addMessage(preg_last_error_msg()); + + return false; + } + + if (! $result) { if ($this->notMatchMessage === null) { $this->addMessage(sprintf( $this->translate("'%s' does not match against pattern '%s'"), diff --git a/tests/RegexMatchValidatorTest.php b/tests/RegexMatchValidatorTest.php new file mode 100644 index 0000000..f4af34f --- /dev/null +++ b/tests/RegexMatchValidatorTest.php @@ -0,0 +1,155 @@ +assertTrue($validator->isValid('abc')); + $this->assertTrue($validator->isValid('xyz')); + } + + public function testInvalidPatternMatch(): void + { + $validator = new RegexMatchValidator('/^[a-z]+$/'); + + $this->assertFalse($validator->isValid('ABC')); + $this->assertFalse($validator->isValid('123')); + $this->assertFalse($validator->isValid('abc123')); + } + + public function testConstructorWithStringPattern(): void + { + $validator = new RegexMatchValidator('/^\d{3}-\d{4}$/'); + + $this->assertTrue($validator->isValid('123-4567')); + $this->assertFalse($validator->isValid('12-4567')); + $this->assertFalse($validator->isValid('abc-defg')); + } + + public function testConstructorWithArrayPattern(): void + { + $validator = new RegexMatchValidator(['pattern' => '/^test/']); + + $this->assertTrue($validator->isValid('test123')); + $this->assertTrue($validator->isValid('testing')); + $this->assertFalse($validator->isValid('notest')); + } + + public function testCustomNotMatchMessage(): void + { + $customMessage = 'This value does not match the required pattern'; + $validator = new RegexMatchValidator([ + 'pattern' => '/^[0-9]+$/', + 'notMatchMessage' => $customMessage + ]); + + $this->assertFalse($validator->isValid('abc')); + + $messages = $validator->getMessages(); + $this->assertCount(1, $messages); + $this->assertSame($customMessage, $messages[0]); + } + + public function testDefaultNotMatchMessage(): void + { + $validator = new RegexMatchValidator('/^[0-9]+$/'); + + $this->assertFalse($validator->isValid('abc')); + + $messages = $validator->getMessages(); + $this->assertCount(1, $messages); + $this->assertStringContainsString("'abc'", $messages[0]); + $this->assertStringContainsString('/^[0-9]+$/', $messages[0]); + } + + public function testWhitespaceOnlyNotMatchMessageThrowsException(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage("Option 'notMatchMessage' must not be an empty or whitespace-only string"); + + new RegexMatchValidator(['pattern' => '/^test/', 'notMatchMessage' => " \t\n "]); + } + + public function testMissingPatternOptionThrowsException(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage("Missing option 'pattern'"); + + new RegexMatchValidator(['notMatchMessage' => 'Test message']); + } + + public function testInvalidRegexPattern(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('failed to compile'); + + new RegexMatchValidator('/[/'); // Unclosed character class + } + + public function testPregMatchErrorYieldsErrorMessage(): void + { + $validator = new RegexMatchValidator('/^.$/u'); + + $this->assertFalse($validator->isValid("\xFF")); + + $messages = $validator->getMessages(); + + $this->assertCount(1, $messages); + $this->assertStringContainsString('UTF-8', $messages[0]); + } + + public function testMultipleIsValidCalls(): void + { + $validator = new RegexMatchValidator('/^[a-z]+$/'); + + // First validation - should fail + $this->assertFalse($validator->isValid('123')); + $this->assertCount(1, $validator->getMessages()); + + // Second validation - should succeed and clear previous messages + $this->assertTrue($validator->isValid('abc')); + $this->assertCount(0, $validator->getMessages()); + + // Third validation - should fail with new message + $this->assertFalse($validator->isValid('XYZ')); + $this->assertCount(1, $validator->getMessages()); + } + + public function testHexColorPattern(): void + { + $validator = new RegexMatchValidator('/^#[0-9A-Fa-f]{6}$/'); + + $this->assertTrue($validator->isValid('#FF5733')); + $this->assertTrue($validator->isValid('#000000')); + $this->assertTrue($validator->isValid('#ffffff')); + $this->assertFalse($validator->isValid('#FFF')); + $this->assertFalse($validator->isValid('FF5733')); + $this->assertFalse($validator->isValid('#GGGGGG')); + } + + public function testPhoneNumberPattern(): void + { + $validator = new RegexMatchValidator('/^\+?[1-9]\d{1,14}$/'); + + $this->assertTrue($validator->isValid('+1234567890')); + $this->assertTrue($validator->isValid('1234567890')); + $this->assertFalse($validator->isValid('+0123456789')); + $this->assertFalse($validator->isValid('abc')); + } +}