Parser.php
70 lines
| 1 | <?php |
| 2 | namespace MailPoetVendor\Doctrine\DBAL\SQL; |
| 3 | if (!defined('ABSPATH')) exit; |
| 4 | use MailPoetVendor\Doctrine\DBAL\SQL\Parser\Exception; |
| 5 | use MailPoetVendor\Doctrine\DBAL\SQL\Parser\Exception\RegularExpressionError; |
| 6 | use MailPoetVendor\Doctrine\DBAL\SQL\Parser\Visitor; |
| 7 | use function array_merge; |
| 8 | use function implode; |
| 9 | use function preg_last_error; |
| 10 | use function preg_match; |
| 11 | use function sprintf; |
| 12 | use function strlen; |
| 13 | use const PREG_NO_ERROR; |
| 14 | final class Parser |
| 15 | { |
| 16 | private const SPECIAL_CHARS = ':\\?\'"`\\[\\-\\/'; |
| 17 | private const BACKTICK_IDENTIFIER = '`[^`]*`'; |
| 18 | private const BRACKET_IDENTIFIER = '(?<!\\b(?i:ARRAY))\\[(?:[^\\]])*\\]'; |
| 19 | private const MULTICHAR = ':{2,}'; |
| 20 | private const NAMED_PARAMETER = ':[a-zA-Z0-9_]+'; |
| 21 | private const POSITIONAL_PARAMETER = '(?<!\\?)\\?(?!\\?)'; |
| 22 | private const ONE_LINE_COMMENT = '--[^\\r\\n]*'; |
| 23 | private const MULTI_LINE_COMMENT = '/\\*([^*]+|\\*+[^/*])*\\**\\*/'; |
| 24 | private const SPECIAL = '[' . self::SPECIAL_CHARS . ']'; |
| 25 | private const OTHER = '[^' . self::SPECIAL_CHARS . ']+'; |
| 26 | private string $sqlPattern; |
| 27 | private string $tokenPattern; |
| 28 | public function __construct(bool $mySQLStringEscaping) |
| 29 | { |
| 30 | if ($mySQLStringEscaping) { |
| 31 | $patterns = [$this->getMySQLStringLiteralPattern("'"), $this->getMySQLStringLiteralPattern('"')]; |
| 32 | } else { |
| 33 | $patterns = [$this->getAnsiSQLStringLiteralPattern("'"), $this->getAnsiSQLStringLiteralPattern('"')]; |
| 34 | } |
| 35 | $patterns = array_merge($patterns, [self::BACKTICK_IDENTIFIER, self::BRACKET_IDENTIFIER, self::MULTICHAR, self::ONE_LINE_COMMENT, self::MULTI_LINE_COMMENT, self::OTHER]); |
| 36 | $this->sqlPattern = sprintf('(%s)', implode('|', $patterns)); |
| 37 | $this->tokenPattern = '~\\G' . '(?P<named>' . self::NAMED_PARAMETER . ')' . '|(?P<positional>' . self::POSITIONAL_PARAMETER . ')' . '|(?P<other>' . $this->sqlPattern . '|' . self::SPECIAL . ')' . '~s'; |
| 38 | } |
| 39 | public function parse(string $sql, Visitor $visitor) : void |
| 40 | { |
| 41 | $offset = 0; |
| 42 | $length = strlen($sql); |
| 43 | while ($offset < $length) { |
| 44 | if (preg_match($this->tokenPattern, $sql, $matches, 0, $offset) === 1) { |
| 45 | $match = $matches[0]; |
| 46 | if ($matches['named'] !== '') { |
| 47 | $visitor->acceptNamedParameter($match); |
| 48 | } elseif ($matches['positional'] !== '') { |
| 49 | $visitor->acceptPositionalParameter($match); |
| 50 | } else { |
| 51 | $visitor->acceptOther($match); |
| 52 | } |
| 53 | $offset += strlen($match); |
| 54 | } elseif (preg_last_error() !== PREG_NO_ERROR) { |
| 55 | // @codeCoverageIgnoreStart |
| 56 | throw RegularExpressionError::new(); |
| 57 | // @codeCoverageIgnoreEnd |
| 58 | } |
| 59 | } |
| 60 | } |
| 61 | private function getMySQLStringLiteralPattern(string $delimiter) : string |
| 62 | { |
| 63 | return $delimiter . '((\\\\.)|(?![' . $delimiter . '\\\\]).)*' . $delimiter; |
| 64 | } |
| 65 | private function getAnsiSQLStringLiteralPattern(string $delimiter) : string |
| 66 | { |
| 67 | return $delimiter . '[^' . $delimiter . ']*' . $delimiter; |
| 68 | } |
| 69 | } |
| 70 |