packages/ztd-query-core/src/Sql/LexicalPattern.php
1<?php
2
3declare(strict_types=1);
4
5namespace ZtdQuery\Sql;
6
7use ZtdQuery\Exception\InvalidDefinitionException;
8
9/**
10 * Reads a regular expression against a position in a statement.
11 *
12 * A scanner asks the same two questions over and over — does this pattern
13 * match right here, and does this one character match at all — and both have
14 * an answer that has to be exact: an empty match is no match, because a
15 * scanner that accepts one never advances.
16 */
17final class LexicalPattern
18{
19 /**
20 * Answers what a pattern matches at a position, if anything.
21 *
22 * @param string|null $pattern Pattern to read, or null when the dialect has none
23 * @param string $subject Statement being scanned
24 * @param int $offset Position to read from
25 *
26 * @return string|null What it matched, or null when it matched nothing
27 */
28 public function matchAt(?string $pattern, string $subject, int $offset): ?string
29 {
30 if ($pattern === null || preg_match($pattern, substr($subject, $offset), $matches) !== 1) {
31 return null;
32 }
33
34 return $matches[0] === '' ? null : $matches[0];
35 }
36
37 /**
38 * Reports whether one character matches a pattern.
39 *
40 * @param string $pattern Pattern to read
41 * @param string $character Character to test
42 *
43 * @return bool True when it matches, and false for no character at all
44 */
45 public function matchesCharacter(string $pattern, string $character): bool
46 {
47 return $character !== '' && preg_match($pattern, $character) === 1;
48 }
49
50 /**
51 * Refuses a pattern that is not one preg can read.
52 *
53 * preg reports a bad pattern as a warning rather than by raising, so the
54 * warning is turned into the refusal here; a pattern kept and used later
55 * would fail at every position instead of once, at the point it was given.
56 *
57 * @param string|null $pattern Pattern to check, or null when the dialect has none
58 *
59 * @throws InvalidDefinitionException When the pattern is empty or preg will not read it
60 */
61 public function assertValid(?string $pattern): void
62 {
63 if ($pattern === null) {
64 return;
65 }
66 set_error_handler(static function (): never {
67 throw new InvalidDefinitionException('A lexical pattern must be a valid non-empty regular expression.');
68 });
69 try {
70 $valid = $pattern !== '' && preg_match($pattern, '') !== false;
71 } finally {
72 restore_error_handler();
73 }
74 if (!$valid) {
75 throw new InvalidDefinitionException('A lexical pattern must be a valid non-empty regular expression.');
76 }
77 }
78}
79