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