diff --git a/src/Psl/Internal/Loader.php b/src/Psl/Internal/Loader.php index 901f116f3..681569367 100644 --- a/src/Psl/Internal/Loader.php +++ b/src/Psl/Internal/Loader.php @@ -251,11 +251,13 @@ final class Loader 'Psl\Result\wrap', 'Psl\Regex\split', 'Psl\Regex\matches', + 'Psl\Regex\matching', 'Psl\Regex\replace', 'Psl\Regex\replace_by', 'Psl\Regex\replace_every', 'Psl\Regex\Internal\get_prec_error', 'Psl\Regex\Internal\call_preg', + 'Psl\Regex\Type\capture_groups', 'Psl\SecureRandom\bytes', 'Psl\SecureRandom\float', 'Psl\SecureRandom\int', diff --git a/src/Psl/Regex/Type/capture_groups.php b/src/Psl/Regex/Type/capture_groups.php new file mode 100644 index 000000000..094f32458 --- /dev/null +++ b/src/Psl/Regex/Type/capture_groups.php @@ -0,0 +1,32 @@ + $groups + * + * @return TypeInterface> + * + * @psalm-suppress MixedReturnTypeCoercion - Psalm uses track of the types: Trust the return type above. + */ +function capture_groups(array $groups): TypeInterface +{ + return shape( + from_keys( + unique([0, ...$groups]), + /** + * @return TypeInterface + */ + static fn() => string() + ) + ); +} diff --git a/src/Psl/Regex/matching.php b/src/Psl/Regex/matching.php new file mode 100644 index 000000000..e32922060 --- /dev/null +++ b/src/Psl/Regex/matching.php @@ -0,0 +1,46 @@ + $capture_groups What shape does the matching items have? + * + * @return T + * + * @throws Exception\RuntimeException If an internal error accord. + * @throws CoercionException If the capture groups don't match the pattern matches + * @throws Exception\InvalidPatternException If $pattern is invalid. + */ +function matching(string $subject, string $pattern, TypeInterface $capture_groups, int $offset = 0): array +{ + $matching = Internal\call_preg( + 'preg_match', + static function () use ($subject, $pattern, $offset): array { + $matching = []; + $matches = preg_match($pattern, $subject, $matching, 0, $offset); + + if ($matches === 0) { + throw new RuntimeException( + 'Could not match the pattern ' . $pattern . ' to the given input: ' . $subject . '.' + ); + } + + return $matching; + } + ); + + return $capture_groups->coerce($matching); +} diff --git a/tests/Psl/Regex/MatchingTest.php b/tests/Psl/Regex/MatchingTest.php new file mode 100644 index 000000000..95840cc2e --- /dev/null +++ b/tests/Psl/Regex/MatchingTest.php @@ -0,0 +1,103 @@ +expectException(Regex\Exception\RuntimeException::class); + $this->expectErrorMessage('Could not match the pattern ' . $pattern . ' to the given input: ' . $subject . '.'); + + Regex\matching($subject, $pattern, capture_groups([]), $offset); + } + + public function testMatchingWithInvalidPattern(): void + { + $this->expectException(Regex\Exception\InvalidPatternException::class); + $this->expectExceptionMessage("No ending delimiter '/' found"); + + Regex\matching('hello', '/hello', capture_groups([])); + } + + public function provideMatchingData(): iterable + { + yield [ + [ + 0 => 'PHP', + 1 => 'PHP', + ], + 'PHP is the web scripting language of choice.', + '/(php)/i', + capture_groups([0, 1]) + ]; + yield [ + [ + 0 => 'Hello world', + 1 => 'Hello', + ], + 'Hello world is the web scripting language of choice.', + '/(hello) world/i', + capture_groups([0, 1]) + ]; + yield [ + [ + 0 => 'web', + 1 => 'web', + ], + 'PHP is the web scripting language of choice.', + '/(\bweb\b)/i', + capture_groups([0, 1]) + ]; + yield [ + [ + 0 => 'PHP', + 'language' => 'PHP', + ], + 'PHP is the web scripting language of choice.', + '/(?PPHP)/', + capture_groups([0, 'language']) + ]; + yield [ + [ + 0 => 'http://www.php.net', + 1 => 'www.php.net' + ], + 'http://www.php.net/index.html', + '@^(?:http://)?([^/]+)@i', + capture_groups([1]) + ]; + } + + public function provideNonMatchingData(): iterable + { + yield ['PHP is the web scripting language of choice.', '/php/']; + yield ['PHP is the website scripting language of choice.', '/\bweb\b/i']; + yield ['php is the web scripting language of choice.', '/PHP/']; + yield ['hello', '/[^.]+\.[^.]+$/']; + } +} diff --git a/tests/Psl/Regex/Type/CaptureGroupsTest.php b/tests/Psl/Regex/Type/CaptureGroupsTest.php new file mode 100644 index 000000000..4c5cde522 --- /dev/null +++ b/tests/Psl/Regex/Type/CaptureGroupsTest.php @@ -0,0 +1,20 @@ + 'Hello', 1 => 'World']; + $shape = capture_groups([1]); + $actual = $shape->coerce($data); + + static::assertSame($actual, $data); + } +}