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