Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
99.24% covered (success)
99.24%
130 / 131
92.86% covered (success)
92.86%
13 / 14
CRAP
0.00% covered (danger)
0.00%
0 / 1
QueryBuilderBase
99.24% covered (success)
99.24%
130 / 131
92.86% covered (success)
92.86%
13 / 14
62
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 __destruct
n/a
0 / 0
n/a
0 / 0
1
 __call
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 resetQuery
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 _select
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 _getCompile
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 _like
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
4
 _having
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
4
 _where
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
3
 _whereString
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
6
 _whereIn
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 _run
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
5
 _appendQuery
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
4
 _compileType
n/a
0 / 0
n/a
0 / 0
8
 _compile
95.24% covered (success)
95.24%
20 / 21
0.00% covered (danger)
0.00%
0 / 1
6
 _compileReturning
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
10
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 function regexInArray;
19
20use BadMethodCallException;
21use PDO;
22use PDOStatement;
23use Query\Drivers\DriverInterface;
24
25/**
26 * @method affectedRows(): int
27 * @method beginTransaction(): bool
28 * @method commit(): bool
29 * @method errorCode(): string
30 * @method errorInfo(): array
31 * @method exec(string $statement): int
32 * @method getAttribute(int $attribute)
33 * @method getColumns(string $table): array | null
34 * @method getDbs(): array | null
35 * @method getFks(string $table): array | null
36 * @method getIndexes(string $table): array | null
37 * @method getLastQuery(): string
38 * @method getSchemas(): array | null
39 * @method getSequences(): array | null
40 * @method getSystemTables(): array | null
41 * @method getTables(): array
42 * @method getTypes(): array | null
43 * @method getUtil(): \Query\Drivers\AbstractUtil
44 * @method getVersion(): string
45 * @method getViews(): array | null
46 * @method inTransaction(): bool
47 * @method lastInsertId(string $name = NULL): string
48 * @method numRows(): int | null
49 * @method prepare(string $statement, array $driver_options = []): PDOStatement
50 * @method prepareExecute(string $sql, array $params): PDOStatement
51 * @method prepareQuery(string $sql, array $data): PDOStatement
52 * @method query(string $statement): PDOStatement
53 * @method quote(string $string, int $parameter_type = PDO::PARAM_STR): string
54 * @method rollback(): bool
55 * @method setAttribute(int $attribute, $value): bool
56 * @method setTablePrefix(string $prefix): void
57 * @method truncate(string $table): PDOStatement
58 */
59class QueryBuilderBase {
60
61    /**
62     * Convenience property for connection management
63     */
64    public string $connName = '';
65
66    /**
67     * List of queries executed
68     */
69    public array $queries = [
70        'total_time' => 0
71    ];
72
73    /**
74     * Whether to do only an explain on the query
75     */
76    protected bool $explain = FALSE;
77
78    /**
79     * Whether to return data from a modification query
80     */
81    protected bool $returning = FALSE;
82
83    /**
84     * Query Builder state
85     */
86    protected State $state;
87
88    // --------------------------------------------------------------------------
89    // ! Methods
90    // --------------------------------------------------------------------------
91    /**
92     * Constructor
93     */
94    public function __construct(protected ?DriverInterface $driver, protected QueryParser $parser)
95    {
96        // Create new State object
97        $this->state = new State();
98    }
99
100    /**
101     * Destructor
102     * @codeCoverageIgnore
103     */
104    public function __destruct()
105    {
106        $this->driver = NULL;
107    }
108
109    /**
110     * Calls a function further down the inheritance chain.
111     * 'Implements' methods on the driver object
112     *
113     * @return mixed
114     * @throws BadMethodCallException
115     */
116    public function __call(string $name, array $params)
117    {
118        if (method_exists($this->driver, $name))
119        {
120            return $this->driver->$name(...$params);
121        }
122
123        throw new BadMethodCallException('Method does not exist');
124    }
125
126    /**
127     * Clear out the class variables, so the next query can be run
128     */
129    public function resetQuery(): void
130    {
131        $this->state = new State();
132        $this->explain = FALSE;
133        $this->returning = FALSE;
134    }
135
136    /**
137     * Method to simplify select_ methods
138     */
139    protected function _select(string $field, bool|string $as = FALSE): string
140    {
141        // Escape the identifiers
142        $field = $this->driver->quoteIdent($field);
143
144        if ( ! \is_string($as))
145        {
146            // @codeCoverageIgnoreStart
147            return $field;
148            // @codeCoverageIgnoreEnd
149        }
150
151        $as = $this->driver->quoteIdent($as);
152        return "({$field}) AS {$as} ";
153    }
154
155    /**
156     * Helper function for returning sql strings
157     */
158    protected function _getCompile(QueryType $type, string $table, bool $reset): string
159    {
160        $sql = $this->_compile($type, $table);
161
162        // Reset the query builder for the next query
163        if ($reset)
164        {
165            $this->resetQuery();
166        }
167
168        return $sql;
169    }
170
171    /**
172     * Simplify 'like' methods
173     */
174    protected function _like(string $field, mixed $val, LikeType $pos, string $like = 'LIKE', string $conj = 'AND'): self
175    {
176        $field = $this->driver->quoteIdent($field);
177
178        // Add the like string into the order map
179        $like = $field . " {$like} ?";
180
181        if ($pos === LikeType::BEFORE)
182        {
183            $val = "%{$val}";
184        }
185        elseif ($pos === LikeType::AFTER)
186        {
187            $val = "{$val}%";
188        }
189        else
190        {
191            $val = "%{$val}%";
192        }
193
194        $conj = empty($this->state->getQueryMap()) ? ' WHERE ' : " {$conj} ";
195        $this->state->appendMap($conj, $like, MapType::LIKE);
196
197        // Add to the values array
198        $this->state->appendWhereValues($val);
199
200        return $this;
201    }
202
203    /**
204     * Simplify building having clauses
205     */
206    protected function _having(mixed $key, mixed $values = [], string $conj = 'AND'): self
207    {
208        $where = $this->_where($key, $values);
209
210        // Create key/value placeholders
211        foreach ($where as $f => $val)
212        {
213            // Split each key by spaces, in case there
214            // is an operator such as >, <, !=, etc.
215            $fArray = explode(' ', trim($f));
216
217            $item = $this->driver->quoteIdent($fArray[0]);
218
219            // Simple key value, or an operator
220            $item .= (count($fArray) === 1) ? '=?' : " {$fArray[1]} ?";
221
222            // Put in the having map
223            $this->state->appendHavingMap([
224                'conjunction' => empty($this->state->getHavingMap())
225                    ? ' HAVING '
226                    : " {$conj} ",
227                'string' => $item
228            ]);
229        }
230
231        return $this;
232    }
233
234    /**
235     * Do all the redundant stuff for where/having type methods
236     */
237    protected function _where(mixed $key, mixed $val = []): array
238    {
239        $where = [];
240        $pairs = [];
241
242        if (is_scalar($key))
243        {
244            $pairs[$key] = $val;
245        } else
246        {
247            $pairs = $key;
248        }
249
250        foreach ($pairs as $k => $v)
251        {
252            $where[$k] = $v;
253            $this->state->appendWhereValues($v);
254        }
255
256        return $where;
257    }
258
259    /**
260     * Simplify generating where string
261     */
262    protected function _whereString(mixed $key, mixed $values = [], string $defaultConj = 'AND'): self
263    {
264        // Create key/value placeholders
265        foreach ($this->_where($key, $values) as $f => $val)
266        {
267            $queryMap = $this->state->getQueryMap();
268
269            // Split each key by spaces, in case there
270            // is an operator such as >, <, !=, etc.
271            $fArray = explode(' ', trim($f));
272
273            $item = $this->driver->quoteIdent($fArray[0]);
274
275            // Simple key value, or an operator
276            $item .= (count($fArray) === 1) ? '=?' : " {$fArray[1]} ?";
277            $lastItem = end($queryMap);
278
279            // Determine the correct conjunction
280            $conjunctionList = array_column($queryMap, 'conjunction');
281            if (empty($queryMap) || ( ! regexInArray($conjunctionList, "/^ ?\n?WHERE/i")))
282            {
283                $conj = "\nWHERE ";
284            }
285            elseif ($lastItem['type'] === MapType::GROUP_START)
286            {
287                $conj = '';
288            }
289            else
290            {
291                $conj = " {$defaultConj} ";
292            }
293
294            $this->state->appendMap($conj, $item, MapType::WHERE);
295        }
296
297        return $this;
298    }
299
300    /**
301     * Simplify where_in methods
302     *
303     * @param mixed $key
304     * @param mixed $val
305     * @param string $in - The (not) in fragment
306     * @param string $conj - The where in conjunction
307     */
308    protected function _whereIn(mixed $key, mixed $val = [], string $in = 'IN', string $conj = 'AND'): self
309    {
310        $key = $this->driver->quoteIdent($key);
311        $params = array_fill(0, is_countable($val) ? count($val) : 0, '?');
312        $this->state->appendWhereValues($val);
313
314        $conjunction = empty($this->state->getQueryMap()) ? ' WHERE ' : " {$conj} ";
315        $str = $key . " {$in} (" . implode(',', $params) . ') ';
316
317        $this->state->appendMap($conjunction, $str, MapType::WHERE_IN);
318
319        return $this;
320    }
321
322    /**
323     * Executes the compiled query
324     *
325     * @param array|null $vals
326     */
327    protected function _run(QueryType $type, string $table, string $sql = NULL, array $vals = NULL, bool $reset = TRUE): PDOStatement
328    {
329        if ($sql === NULL)
330        {
331            $sql = $this->_compile($type, $table);
332        }
333
334        if ($vals === NULL)
335        {
336            $vals = array_merge($this->state->getValues(), $this->state->getWhereValues());
337        }
338
339        $startTime = microtime(TRUE);
340
341        $res = empty($vals)
342            ? $this->driver->query($sql)
343            : $this->driver->prepareExecute($sql, $vals);
344
345        $endTime = microtime(TRUE);
346        $totalTime = number_format($endTime - $startTime, 5);
347
348        // Add this query to the list of executed queries
349        $this->_appendQuery($vals, $sql, (int)$totalTime);
350
351        // Reset class state for next query
352        if ($reset)
353        {
354            $this->resetQuery();
355        }
356
357        return $res;
358    }
359
360    /**
361     * Convert the prepared statement into readable sql
362     */
363    protected function _appendQuery(array $values, string $sql, int $totalTime): void
364    {
365        $evals = is_array($values) ? $values : [];
366        $esql = str_replace('?', '%s', $sql);
367
368        // Quote string values
369        foreach ($evals as &$v)
370        {
371            $v = ( is_numeric($v))
372                ? $v
373                : htmlentities($this->driver->quote($v), ENT_NOQUOTES, 'utf-8');
374        }
375        unset($v);
376
377        // Add the query onto the array of values to pass
378        // as arguments to sprintf
379        array_unshift($evals, $esql);
380
381        // Add the interpreted query to the list of executed queries
382        $this->queries[] = [