Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
3 / 3
CRAP
100.00% covered (success)
100.00%
1 / 1
QueryParser
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
3 / 3
10
100.00% covered (success)
100.00%
1 / 1
 __construct
n/a
0 / 0
n/a
0 / 0
1
 parseJoin
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 compileJoin
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
5
 filterArray
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
1<?php declare(strict_types=1);
2/**
3 * Query
4 *
5 * SQL Query Builder / Database Abstraction Layer
6 *
7 * PHP version 8.1
8 *
9 * @package     Query
10 * @author      Timothy J. Warren <tim@timshomepage.net>
11 * @copyright   2012 - 2022 Timothy J. Warren
12 * @license     http://www.opensource.org/licenses/mit-license.html  MIT License
13 * @link        https://git.timshomepage.net/aviat/Query
14 * @version     4.0.0
15 */
16namespace Query;
17
18use Query\Drivers\DriverInterface;
19
20/**
21 * Utility Class to parse sql clauses for properly escaping identifiers
22 */
23class QueryParser {
24
25    /**
26     * Regex patterns for various syntax components
27     */
28    private array $matchPatterns = [
29        'function' => '([a-zA-Z0-9_]+\((.*?)\))',
30        'identifier' => '([a-zA-Z0-9_-]+\.?)+',
31        'operator' => '=|AND|&&?|~|\|\|?|\^|/|>=?|<=?|-|%|OR|\+|NOT|\!=?|<>|XOR'
32    ];
33
34    /**
35     * Regex matches
36     */
37    public array $matches = [
38        'functions' => [],
39        'identifiers' => [],
40        'operators' => [],
41        'combined' => [],
42    ];
43
44    /**
45     * Constructor/entry point into parser
46     */
47    public function __construct(private readonly DriverInterface $db)
48    {
49    }
50
51    /**
52     * Parser method for setting the parse string
53     *
54     * @return array[]
55     */
56    public function parseJoin(string $sql): array
57    {
58        // Get sql clause components
59        preg_match_all('`'.$this->matchPatterns['function'].'`', $sql, $this->matches['functions'], PREG_SET_ORDER);
60        preg_match_all('`'.$this->matchPatterns['identifier'].'`', $sql, $this->matches['identifiers'], PREG_SET_ORDER);
61        preg_match_all('`'.$this->matchPatterns['operator'].'`', $sql, $this->matches['operators'], PREG_SET_ORDER);
62
63        // Get everything at once for ordering
64        $fullPattern = '`'.$this->matchPatterns['function'].'+|'.$this->matchPatterns['identifier'].'|('.$this->matchPatterns['operator'].')+`i';
65        preg_match_all($fullPattern, $sql, $this->matches['combined'], PREG_SET_ORDER);
66
67        // Go through the matches, and get the most relevant matches
68        $this->matches = array_map([$this, 'filterArray'], $this->matches);
69
70        return $this->matches;
71    }
72
73    /**
74     * Compiles a join condition after parsing
75     */
76    public function compileJoin(string $condition): string
77    {
78        $parts = $this->parseJoin($condition);
79        $count = is_countable($parts['identifiers']) ? count($parts['identifiers']) : 0;
80
81        // Go through and quote the identifiers
82        for($i=0; $i <= $count; $i++)
83        {
84            if (in_array($parts['combined'][$i], $parts['identifiers']) && ! is_numeric($parts['combined'][$i]))
85            {
86                $parts['combined'][$i] = $this->db->quoteIdent($parts['combined'][$i]);
87            }
88        }
89
90        return implode('', $parts['combined']);
91    }
92
93    /**
94     * Returns a more useful match array
95     *
96     * @return array
97     */
98    protected function filterArray(array $array): array
99    {
100        $newArray = [];
101
102        foreach($array as $row)
103        {
104            $newArray[] =  (is_array($row)) ? $row[0] : $row;
105        }
106
107        return $newArray;
108    }
109}