Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
193 / 193
100.00% covered (success)
100.00%
54 / 54
CRAP
100.00% covered (success)
100.00%
1 / 1
PredicateSet
100.00% covered (success)
100.00%
193 / 193
100.00% covered (success)
100.00%
54 / 54
119
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
 setParameters
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 addParameters
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 addParameter
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getParameters
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 getParameter
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasParameters
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
 hasParameter
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 extractValues
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
6
 add
100.00% covered (success)
100.00%
53 / 53
100.00% covered (success)
100.00%
1 / 1
18
 resolveExpressionValue
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
5
 bindPlaceholder
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 splitBetweenValue
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 addExpressions
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 and
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 or
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 equalTo
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 notEqualTo
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 greaterThan
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 greaterThanOrEqualTo
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 lessThan
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 lessThanOrEqualTo
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 like
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 notLike
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 between
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 notBetween
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 in
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 notIn
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isNull
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isNotNull
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 exists
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 notExists
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 jsonEqualTo
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 jsonNotEqualTo
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 jsonContains
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 andPredicate
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 orPredicate
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 addPredicate
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
9
 addPredicates
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 addPredicateSet
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 addPredicateSets
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 nest
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 andNest
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 orNest
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setConjunction
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 getConjunction
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setNextConjunction
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 getNextConjunction
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasPredicates
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getPredicates
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasPredicateSets
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getPredicateSets
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 render
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
10
 __toString
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2declare(strict_types=1);
3/**
4 * Pop PHP Framework (https://www.popphp.org/)
5 *
6 * @link       https://github.com/popphp/popphp-framework
7 * @author     Nick Sagona, III <nick@popphp.org>
8 * @copyright  Copyright (c) 2009-2026 Nick Sagona, III
9 * @license    https://www.popphp.org/license     New BSD License
10 */
11
12/**
13 * @namespace
14 */
15namespace Pop\Db\Sql;
16
17use Pop\Db\Sql\Predicate\EqualTo;
18
19/**
20 * Predicate set class
21 *
22 * @category   Pop
23 * @package    Pop\Db
24 * @author     Nick Sagona, III <nick@popphp.org>
25 * @copyright  Copyright (c) 2009-2026 Nick Sagona, III
26 * @license    https://www.popphp.org/license     New BSD License
27 * @version    7.0.0
28 */
29class PredicateSet
30{
31
32    /**
33     * SQL object
34     * @var ?AbstractSql
35     */
36    protected ?AbstractSql $sql = null;
37
38    /**
39     * Predicates
40     * @var array
41     */
42    protected array $predicates = [];
43
44    /**
45     * Nested predicate sets
46     * @var array
47     */
48    protected array $predicateSets = [];
49
50    /**
51     * Conjunction
52     * @var ?string
53     */
54    protected ?string $conjunction = null;
55
56    /**
57     * Next conjunction
58     * @var string
59     */
60    protected string $nextConjunction = 'AND';
61
62    /**
63     * Parameters (for binding)
64     * @var array
65     */
66    protected array $parameters = [];
67
68    /**
69     * Constructor
70     *
71     * Instantiate the predicate set object
72     *
73     * @param  AbstractSql $sql
74     * @param  mixed       $predicates
75     * @param  ?string     $conjunction
76     * @throws Exception
77     */
78    public function __construct(AbstractSql $sql, mixed $predicates = null, ?string $conjunction = null)
79    {
80        $this->sql = $sql;
81
82        if ($predicates !== null) {
83            if (is_array($predicates)) {
84                $this->addPredicates($predicates);
85            } else {
86                $this->addPredicate($predicates);
87            }
88        }
89
90        if ($conjunction !== null) {
91            $this->setConjunction($conjunction);
92        }
93    }
94
95    /**
96     * Set parameters
97     *
98     * @param  array $parameters
99     * @return PredicateSet
100     */
101    public function setParameters(array $parameters): PredicateSet
102    {
103        $this->parameters = $parameters;
104        return $this;
105    }
106
107    /**
108     * Add parameters
109     *
110     * @param  array $parameters
111     * @return PredicateSet
112     */
113    public function addParameters(array $parameters): PredicateSet
114    {
115        foreach ($parameters as $name => $value) {
116            $this->addParameter($name, $value);
117        }
118        return $this;
119    }
120
121    /**
122     * Add parameter
123     *
124     * @param  mixed $name
125     * @param  mixed $value
126     * @return PredicateSet
127     */
128    public function addParameter(mixed $name, mixed $value): PredicateSet
129    {
130        $this->parameters[$name] = $value;
131        return $this;
132    }
133
134    /**
135     * Get parameters
136     *
137     * @return array
138     */
139    public function getParameters(): array
140    {
141        $parameters = $this->parameters;
142
143        foreach ($this->predicateSets as $predicateSet) {
144            $parameters = array_merge($parameters, $predicateSet->getParameters());
145        }
146
147        return $parameters;
148    }
149
150    /**
151     * Get parameter
152     *
153     * @param  mixed $name
154     * @return mixed
155     */
156    public function getParameter(mixed $name): mixed
157    {
158        return $this->parameters[$name] ?? null;
159    }
160
161    /**
162     * Has parameters
163     *
164     * @return bool
165     */
166    public function hasParameters(): bool
167    {
168        if (!empty($this->parameters)) {
169            return true;
170        }
171
172        foreach ($this->predicateSets as $predicateSet) {
173            if ($predicateSet->hasParameters()) {
174                return true;
175            }
176        }
177
178        return false;
179    }
180
181    /**
182     * Has parameter
183     *
184     * @param  mixed $name
185     * @return bool
186     */
187    public function hasParameter(mixed $name): bool
188    {
189        return !empty($this->parameters[$name]);
190    }
191
192    /**
193     * Extract values
194     *
195     * @param  bool $placeholder
196     * @return array
197     */
198    public function extractValues(bool $placeholder = false): array
199    {
200        $values = [];
201
202        foreach ($this->predicates as $i => $predicate) {
203            if (($predicate instanceof EqualTo) && ($predicate->getConjunction() == 'AND')) {
204                [$column, $value] = $predicate->getValues();
205                if ((!$placeholder) && isset($this->parameters[$i])) {
206                    $value = $this->parameters[$i];
207                }
208                $values[$column] = $value;
209            }
210        }
211
212        return $values;
213    }
214
215    /**
216     * Add a predicate from a string expression
217     *
218     * @param  string $expression
219     * @param  mixed  $placeholder
220     * @return PredicateSet
221     */
222    public function add(string $expression, mixed $placeholder = false): PredicateSet
223    {
224        ['column' => $column, 'operator' => $operator, 'value' => $value] = Parser\Expression::parse($expression);
225
226        $value = $this->resolveExpressionValue($column, $value);
227
228        if ($placeholder !== false) {
229            $value = $this->bindPlaceholder($column, $value, $placeholder);
230        }
231
232        switch ($operator) {
233            case '=':
234                $this->equalTo($column, $value);
235                break;
236            case '!=':
237                $this->notEqualTo($column, $value);
238                break;
239            case '>':
240                $this->greaterThan($column, $value);
241                break;
242            case '>=':
243                $this->greaterThanOrEqualTo($column, $value);
244                break;
245            case '<=':
246                $this->lessThanOrEqualTo($column, $value);
247                break;
248            case '<':
249                $this->lessThan($column, $value);
250                break;
251            case 'LIKE':
252                $this->like($column, $value);
253                break;
254            case 'NOT LIKE':
255                $this->notLike($column, $value);
256                break;
257            case 'BETWEEN':
258                $value = self::splitBetweenValue($value);
259                $this->between($column, $value[0], $value[1]);
260                break;
261            case 'NOT BETWEEN':
262                $value = self::splitBetweenValue($value);
263                $this->notBetween($column, $value[0], $value[1]);
264                break;
265            case 'IN':
266                if (is_string($value)) {
267                    $value = [$value, $value];
268                }
269                $this->in($column, $value);
270                break;
271            case 'NOT IN':
272                if (is_string($value)) {
273                    $value = [$value, $value];
274                }
275                $this->notIn($column, $value);
276                break;
277            case 'IS NULL':
278                $this->isNull($column);
279                break;
280            case 'IS NOT NULL':
281                $this->isNotNull($column);
282                break;
283
284        }
285
286        return $this;
287    }
288
289    /**
290     * Resolve a parsed expression value against any bound parameters it references
291     *
292     * @param  string $column
293     * @param  mixed  $value
294     * @return mixed
295     */
296    protected function resolveExpressionValue(string $column, mixed $value): mixed
297    {
298        if (is_array($value)) {
299            foreach ($value as $k => $v) {
300                if ($this->sql->isParameter($v, $column)) {
301                    $value[$k] = $this->sql->getParameter($v, $column);
302                }
303            }
304        } else {
305            if ($this->sql->isParameter($value, $column)) {
306                $value = $this->sql->getParameter($value, $column);
307            }
308        }
309
310        return $value;
311    }
312
313    /**
314     * Bind the resolved value as a parameter and return the placeholder token to render in its place
315     *
316     * @param  string $column
317     * @param  mixed  $value
318     * @param  mixed  $placeholder
319     * @return mixed
320     */
321    protected function bindPlaceholder(string $column, mixed $value, mixed $placeholder): mixed
322    {
323        $this->addParameter($column, $value);
324
325        if ($this->sql->isSqlite()) {
326            return ':' . $column;
327        } else if ($this->sql->isPgsql()) {
328            return '$' . (int)$placeholder;
329        } else {
330            return $this->sql->getPlaceholder();
331        }
332    }
333
334    /**
335     * Split a BETWEEN/NOT BETWEEN value of the form 'value1 AND value2' into its two boundary values
336     *
337     * @param  mixed $value
338     * @return mixed
339     */
340    protected static function splitBetweenValue(mixed $value): mixed
341    {
342        if (is_string($value) && str_contains($value, ' AND ')) {
343            $betweenValues = explode(' AND ', $value);
344            if (count($betweenValues) == 2) {
345                return [str_replace('(', '', $betweenValues[0]), str_replace(')', '', $betweenValues[1])];
346            }
347        }
348
349        return $value;
350    }
351
352    /**
353     * Add a predicates from string expressions
354     *
355     * @param  array $expressions
356     * @return PredicateSet
357     */
358    public function addExpressions(array $expressions): PredicateSet
359    {
360        foreach ($expressions as $expression) {
361            $this->add($expression);
362        }
363
364        return $this;
365    }
366
367    /**
368     * Add an AND predicate from a string expression
369     *
370     * @param  ?string $expression
371     * @throws Exception
372     * @return PredicateSet
373     */
374    public function and(?string $expression = null): PredicateSet
375    {
376        $this->setNextConjunction('AND');
377        if ($expression !== null) {
378            $this->add($expression);
379        }
380        return $this;
381    }
382
383    /**
384     * Add an OR predicate from a string expression
385     *
386     * @param  ?string $expression
387     * @throws Exception
388     * @return PredicateSet
389     */
390    public function or(?string $expression = null): PredicateSet
391    {
392        $this->setNextConjunction('OR');
393        if ($expression !== null) {
394            $this->add($expression);
395        }
396        return $this;
397    }
398
399    /**
400     * Predicate for =
401     *
402     * @param  string $column
403     * @param  mixed  $value
404     * @return PredicateSet
405     */
406    public function equalTo(string $column, mixed $value): PredicateSet
407    {
408        return $this->addPredicate(new Predicate\EqualTo([$column, $value], $this->nextConjunction));
409    }
410
411    /**
412     * Predicate for !=
413     *
414     * @param  string $column
415     * @param  mixed  $value
416     * @return PredicateSet
417     */
418    public function notEqualTo(string $column, mixed $value): PredicateSet
419    {
420        return $this->addPredicate(new Predicate\NotEqualTo([$column, $value], $this->nextConjunction));
421    }
422
423    /**
424     * Predicate for >
425     *
426     * @param  string $column
427     * @param  mixed  $value
428     * @return PredicateSet
429     */
430    public function greaterThan(string $column, mixed $value): PredicateSet
431    {
432        return $this->addPredicate(new Predicate\GreaterThan([$column, $value], $this->nextConjunction));
433    }
434
435    /**
436     * Predicate for >=
437     *
438     * @param  string $column
439     * @param  mixed  $value
440     * @return PredicateSet
441     */
442    public function greaterThanOrEqualTo(string $column, mixed $value): PredicateSet
443    {
444        return $this->addPredicate(new Predicate\GreaterThanOrEqualTo([$column, $value], $this->nextConjunction));
445    }
446
447    /**
448     * Predicate for <
449     *
450     * @param  string $column
451     * @param  mixed  $value
452     * @return PredicateSet
453     */
454    public function lessThan(string $column, mixed $value): PredicateSet
455    {
456        return $this->addPredicate(new Predicate\LessThan([$column, $value], $this->nextConjunction));
457    }
458
459    /**
460     * Predicate for <=
461     *
462     * @param  string $column
463     * @param  mixed  $value
464     * @return PredicateSet
465     */
466    public function lessThanOrEqualTo(string $column, mixed $value): PredicateSet
467    {
468        return $this->addPredicate(new Predicate\LessThanOrEqualTo([$column, $value], $this->nextConjunction));
469    }
470
471    /**
472     * Predicate for LIKE
473     *
474     * @param  string $column
475     * @param  string $value
476     * @return PredicateSet
477     */
478    public function like(string $column, string $value): PredicateSet
479    {
480        return $this->addPredicate(new Predicate\Like([$column, $value], $this->nextConjunction));
481    }
482
483    /**
484     * Predicate for NOT LIKE
485     *
486     * @param  string $column
487     * @param  string $value
488     * @return PredicateSet
489     */
490    public function notLike(string $column, string $value): PredicateSet
491    {
492        return $this->addPredicate(new Predicate\NotLike([$column, $value], $this->nextConjunction));
493    }
494
495    /**
496     * Predicate for BETWEEN
497     *
498     * @param  string $column
499     * @param  mixed  $value1
500     * @param  mixed  $value2
501     * @return PredicateSet
502     */
503    public function between(string $column, mixed $value1, mixed $value2): PredicateSet
504    {
505        return $this->addPredicate(new Predicate\Between([$column, $value1, $value2], $this->nextConjunction));
506    }
507
508    /**
509     * Predicate for NOT BETWEEN
510     *
511     * @param  string $column
512     * @param  mixed  $value1
513     * @param  mixed  $value2
514     * @return PredicateSet
515     */
516    public function notBetween(string $column, mixed $value1, mixed $value2): PredicateSet
517    {
518        return $this->addPredicate(new Predicate\NotBetween([$column, $value1, $value2], $this->nextConjunction));
519    }
520
521    /**
522     * Predicate for IN
523     *
524     * @param  string $column
525     * @param  mixed  $values
526     * @return PredicateSet
527     */
528    public function in(string $column, mixed $values): PredicateSet
529    {
530        return $this->addPredicate(new Predicate\In([$column, $values], $this->nextConjunction));
531    }
532
533    /**
534     * Predicate for NOT IN
535     *
536     * @param  string $column
537     * @param  mixed  $values
538     * @return PredicateSet
539     */
540    public function notIn(string $column, mixed $values): PredicateSet
541    {
542        return $this->addPredicate(new Predicate\NotIn([$column, $values], $this->nextConjunction));
543    }
544
545    /**
546     * Predicate for IS NULL
547     *
548     * @param  string $column
549     * @return PredicateSet
550     */
551    public function isNull(string $column): PredicateSet
552    {
553        return $this->addPredicate(new Predicate\IsNull($column, $this->nextConjunction));
554    }
555
556    /**
557     * Predicate for IS NOT NULL
558     *
559     * @param  string $column
560     * @return PredicateSet
561     */
562    public function isNotNull(string $column): PredicateSet
563    {
564        return $this->addPredicate(new Predicate\IsNotNull($column, $this->nextConjunction));
565    }
566
567    /**
568     * Predicate for EXISTS
569     *
570     * @param  AbstractSql $select
571     * @return PredicateSet
572     */
573    public function exists(AbstractSql $select): PredicateSet
574    {
575        return $this->addPredicate(new Predicate\Exists($select, $this->nextConjunction));
576    }
577
578    /**
579     * Predicate for NOT EXISTS
580     *
581     * @param  AbstractSql $select
582     * @return PredicateSet
583     */
584    public function notExists(AbstractSql $select): PredicateSet
585    {
586        return $this->addPredicate(new Predicate\NotExists($select, $this->nextConjunction));
587    }
588
589    /**
590     * Predicate for JSON path equal to
591     *
592     * @param  string $column
593     * @param  string $path
594     * @param  mixed  $value
595     * @return PredicateSet
596     */
597    public function jsonEqualTo(string $column, string $path, mixed $value): PredicateSet
598    {
599        return $this->addPredicate(new Predicate\JsonEqualTo([$column, $path, $value], $this->nextConjunction));
600    }
601
602    /**
603     * Predicate for JSON path not equal to
604     *
605     * @param  string $column
606     * @param  string $path
607     * @param  mixed  $value
608     * @return PredicateSet
609     */
610    public function jsonNotEqualTo(string $column, string $path, mixed $value): PredicateSet
611    {
612        return $this->addPredicate(new Predicate\JsonNotEqualTo([$column, $path, $value], $this->nextConjunction));
613    }
614
615    /**
616     * Predicate for JSON containment (MySQL/PostgreSQL only)
617     *
618     * @param  string $column
619     * @param  string $path
620     * @param  mixed  $value
621     * @return PredicateSet
622     */
623    public function jsonContains(string $column, string $path, mixed $value): PredicateSet
624    {
625        return $this->addPredicate(new Predicate\JsonContains([$column, $path, $value], $this->nextConjunction));
626    }
627
628    /**
629     * Add AND predicate
630     *
631     * @param  Predicate\AbstractPredicate $predicate
632     * @throws Predicate\Exception
633     * @return PredicateSet
634     */
635    public function andPredicate(Predicate\AbstractPredicate $predicate): PredicateSet
636    {
637        $predicate->setConjunction('AND');
638        return $this->addPredicate($predicate);
639    }
640
641    /**
642     * Add OR predicate
643     *
644     * @param  Predicate\AbstractPredicate $predicate
645     * @throws Predicate\Exception
646     * @return PredicateSet
647     */
648    public function orPredicate(Predicate\AbstractPredicate $predicate): PredicateSet
649    {
650        $predicate->setConjunction('OR');
651        return $this->addPredicate($predicate);
652    }
653
654    /**
655     * Add predicate
656     *
657     * @param  Predicate\AbstractPredicate $predicate
658     * @return PredicateSet
659     */
660    public function addPredicate(Predicate\AbstractPredicate $predicate): PredicateSet
661    {
662        $values = $predicate->getValues();
663
664        if (is_array($values)) {
665            $column = array_shift($values);
666
667            foreach ($values as $key => $value) {
668                // A nested Sql instance (i.e., a subquery) can never be a parameter placeholder
669                // token. Skip the isParameter() check for it, as those comparisons would coerce
670                // the instance to a string and needlessly render the subquery multiple times.
671                if (is_array($value)) {
672                    foreach ($value as $k => $v) {
673                        if ((!($v instanceof AbstractSql)) && $this->sql->isParameter($v, $column)) {
674                            $values[$key][$k] = $this->sql->getParameter($v, $column);
675                        }
676                    }
677                } else {
678                    if ((!($value instanceof AbstractSql)) && $this->sql->isParameter($value, $column)) {
679                        $values[$key] = $this->sql->getParameter($value, $column);
680                    }
681                }
682            }
683
684            $predicate->setValues(array_merge([$column], $values));
685        }
686
687        $this->predicates[] = $predicate;
688        return $this;
689    }
690
691    /**
692     * Add predicates
693     *
694     * @param  array $predicates
695     * @return PredicateSet
696     */
697    public function addPredicates(array $predicates): PredicateSet
698    {
699        foreach ($predicates as $predicate) {
700            $this->addPredicate($predicate);
701        }
702
703        return $this;
704    }
705
706    /**
707     * Add predicate set
708     *
709     * @param  PredicateSet $predicateSet
710     * @return PredicateSet
711     */
712    public function addPredicateSet(PredicateSet $predicateSet): PredicateSet
713    {
714        $this->predicateSets[] = $predicateSet;
715        return $this;
716    }
717
718    /**
719     * Add predicate sets
720     *
721     * @param  array $predicateSets
722     * @return PredicateSet
723     */
724    public function addPredicateSets(array $predicateSets): PredicateSet
725    {
726        foreach ($predicateSets as $predicateSet) {
727            $this->addPredicateSet($predicateSet);
728        }
729
730        return $this;
731    }
732
733    /**
734     * Add a nested predicate set
735     *
736     * @param  string $conjunction
737     * @return PredicateSet
738     */
739    public function nest(string $conjunction = 'AND'): PredicateSet
740    {
741        $predicateSet = new self($this->sql, null, $conjunction);
742        $this->addPredicateSet($predicateSet);
743        return $predicateSet;
744    }
745
746    /**
747     * Add a nested predicate set with the AND conjunction
748     *
749     * @return PredicateSet
750     */
751    public function andNest(): PredicateSet
752    {
753        return $this->nest('AND');
754    }
755
756    /**
757     * Add a nested predicate set with the OR conjunction
758     *
759     * @return PredicateSet
760     */
761    public function orNest(): PredicateSet
762    {
763        return $this->nest('OR');
764    }
765
766    /**
767     * Get the conjunction
768     *
769     * @param  string $conjunction
770     * @throws Exception
771     * @return PredicateSet
772     */
773    public function setConjunction(string $conjunction): PredicateSet
774    {
775        if ((strtoupper($conjunction) != 'OR') && (strtoupper($conjunction) != 'AND')) {
776            throw new Exception("Error: The conjunction must be 'AND' or 'OR'. '" . $conjunction . "' is not allowed.");
777        }
778
779        $this->conjunction = $conjunction;
780
781        return $this;
782    }
783
784    /**
785     * Get the conjunction
786     *
787     * @return ?string
788     */
789    public function getConjunction(): ?string
790    {
791        return $this->conjunction;
792    }
793
794    /**
795     * Get the next conjunction
796     *
797     * @param  string $conjunction
798     * @throws Exception
799     * @return PredicateSet
800     */
801    public function setNextConjunction(string $conjunction): PredicateSet
802    {
803        if ((strtoupper($conjunction) != 'OR') && (strtoupper($conjunction) != 'AND')) {
804            throw new Exception("Error: The conjunction must be 'AND' or 'OR'. '" . $conjunction . "' is not allowed.");
805        }
806
807        $this->nextConjunction = $conjunction;
808
809        return $this;
810    }
811
812    /**
813     * Get the next conjunction
814     *
815     * @return string
816     */
817    public function getNextConjunction(): string
818    {
819        return $this->nextConjunction;
820    }
821
822    /**
823     * Has predicates
824     *
825     * @return bool
826     */
827    public function hasPredicates(): bool
828    {
829        return (count($this->predicates) > 0);
830    }
831
832    /**
833     * Get predicates
834     *
835     * @return array
836     */
837    public function getPredicates(): array
838    {
839        return $this->predicates;
840    }
841
842    /**
843     * Has predicates
844     *
845     * @return bool
846     */
847    public function hasPredicateSets(): bool
848    {
849        return (count($this->predicateSets) > 0);
850    }
851
852    /**
853     * Get predicates
854     *
855     * @return array
856     */
857    public function getPredicateSets(): array
858    {
859        return $this->predicateSets;
860    }
861
862    /**
863     * Predicate set render method
864     *
865     * @throws Exception
866     * @return string
867     */
868    public function render(): string
869    {
870        $predicateString = '';
871
872        foreach ($this->predicates as $i => $predicate) {
873            $predicateString .= ($predicateString === '') ?
874                $predicate->render($this->sql) : ' ' . $predicate->getConjunction() . ' ' . $predicate->render($this->sql);
875        }
876
877        foreach ($this->predicateSets as $i => $predicateSet) {
878            if (empty($predicateSet->getConjunction())) {
879                throw new Exception('Error: The combination conjunction was not set for this predicate set.');
880            }
881            $predicateString .= ($predicateString === '') ?
882                $predicateSet->render() : ' ' . $predicateSet->getConjunction() . ' ' . $predicateSet->render();
883        }
884
885        if (((count($this->predicateSets) > 0) && (count($this->predicates) > 0)) ||
886            (count($this->predicateSets) > 1) || (count($this->predicates) > 1)) {
887            return '(' . $predicateString . ')';
888        } else {
889            return $predicateString;
890        }
891    }
892
893    /**
894     * Return predicate set string
895     *
896     * @return string
897     */
898    public function __toString(): string
899    {
900        return $this->render();
901    }
902
903}