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