Passed
Push — master ( 500a63...c8d25d )
by Glynn
03:58 queued 01:28
created

QueryBuilderHandler::whereNull()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 1
c 1
b 0
f 0
dl 0
loc 3
rs 10
cc 1
nc 1
nop 1
1
<?php
2
3
namespace Pixie\QueryBuilder;
4
5
use wpdb;
6
use Closure;
7
use Throwable;
8
use Pixie\Binding;
9
use Pixie\Exception;
10
use Pixie\Connection;
11
12
use Pixie\HasConnection;
13
14
use Pixie\JSON\JsonHandler;
15
use Pixie\QueryBuilder\Raw;
16
use Pixie\Hydration\Hydrator;
17
use Pixie\JSON\JsonSelectorHandler;
18
use Pixie\QueryBuilder\JoinBuilder;
19
use Pixie\QueryBuilder\QueryObject;
20
use Pixie\QueryBuilder\Transaction;
21
use Pixie\QueryBuilder\WPDBAdapter;
22
use Pixie\QueryBuilder\TablePrefixer;
23
use function mb_strlen;
24
25
class QueryBuilderHandler implements HasConnection
26
{
27
    /**
28
     * @method add
29
     */
30
    use TablePrefixer;
31
32
    /**
33
     * @var \Viocon\Container
34
     */
35
    protected $container;
36
37
    /**
38
     * @var Connection
39
     */
40
    protected $connection;
41
42
    /**
43
     * @var array<string, mixed[]|mixed>
44
     */
45
    protected $statements = [];
46
47
    /**
48
     * @var wpdb
49
     */
50
    protected $dbInstance;
51
52
    /**
53
     * @var string|string[]|null
54
     */
55
    protected $sqlStatement = null;
56
57
    /**
58
     * @var string|null
59
     */
60
    protected $tablePrefix = null;
61
62
    /**
63
     * @var WPDBAdapter
64
     */
65
    protected $adapterInstance;
66
67
    /**
68
     * The mode to return results as.
69
     * Accepts WPDB constants or class names.
70
     *
71
     * @var string
72
     */
73
    protected $fetchMode;
74
75
    /**
76
     * Custom args used to construct models for hydrator
77
     *
78
     * @var array<int, mixed>|null
79
     */
80
    protected $hydratorConstructorArgs;
81
82
    /**
83
     * Handler for Json Selectors
84
     *
85
     * @var JsonHandler
86
     */
87
    protected $jsonHandler;
88
89
    /**
90
     * @param \Pixie\Connection|null $connection
91
     * @param string $fetchMode
92
     * @param mixed[] $hydratorConstructorArgs
93
     *
94
     * @throws Exception if no connection passed and not previously established
95
     */
96
    final public function __construct(
97
        Connection $connection = null,
98
        string $fetchMode = \OBJECT,
99
        ?array $hydratorConstructorArgs = null
100
    ) {
101
        if (is_null($connection)) {
102
            // throws if connection not already established.
103
            $connection = Connection::getStoredConnection();
104
        }
105
106
        // Set all dependencies from connection.
107
        $this->connection = $connection;
108
        $this->container  = $this->connection->getContainer();
109
        $this->dbInstance = $this->connection->getDbInstance();
110
        $this->setAdapterConfig($this->connection->getAdapterConfig());
111
112
        // Set up optional hydration details.
113
        $this->setFetchMode($fetchMode);
114
        $this->hydratorConstructorArgs = $hydratorConstructorArgs;
115
116
        // Query builder adapter instance
117
        $this->adapterInstance = $this->container->build(
118
            WPDBAdapter::class,
119
            [$this->connection]
120
        );
121
122
        // Setup JSON Selector handler.
123
        $this->jsonHandler = new JsonHandler($connection);
124
    }
125
126
    /**
127
     * Sets the config for WPDB
128
     *
129
     * @param array<string, mixed> $adapterConfig
130
     *
131
     * @return void
132
     */
133
    protected function setAdapterConfig(array $adapterConfig): void
134
    {
135
        if (isset($adapterConfig[Connection::PREFIX])) {
136
            $this->tablePrefix = $adapterConfig[Connection::PREFIX];
137
        }
138
    }
139
140
    /**
141
     * Fetch query results as object of specified type
142
     *
143
     * @param string $className
144
     * @param array<int, mixed> $constructorArgs
145
     * @return static
146
     */
147
    public function asObject($className, $constructorArgs = array()): self
148
    {
149
        return $this->setFetchMode($className, $constructorArgs);
150
    }
151
152
    /**
153
     * Set the fetch mode
154
     *
155
     * @param string $mode
156
     * @param array<int, mixed>|null $constructorArgs
157
     *
158
     * @return static
159
     */
160
    public function setFetchMode(string $mode, ?array $constructorArgs = null): self
161
    {
162
        $this->fetchMode               = $mode;
163
        $this->hydratorConstructorArgs = $constructorArgs;
164
165
        return $this;
166
    }
167
168
    /**
169
     * @param Connection|null $connection
170
     *
171
     * @return static
172
     *
173
     * @throws Exception
174
     */
175
    public function newQuery(Connection $connection = null): self
176
    {
177
        if (is_null($connection)) {
178
            $connection = $this->connection;
179
        }
180
181
        $newQuery = $this->constructCurrentBuilderClass($connection);
182
        $newQuery->setFetchMode($this->getFetchMode(), $this->hydratorConstructorArgs);
183
184
        return $newQuery;
185
    }
186
187
    /**
188
     * Returns a new instance of the current, with the passed connection.
189
     *
190
     * @param \Pixie\Connection $connection
191
     *
192
     * @return static
193
     */
194
    protected function constructCurrentBuilderClass(Connection $connection): self
195
    {
196
        return new static($connection);
197
    }
198
199
    /**
200
     * Interpolates a query
201
     *
202
     * @param string $query
203
     * @param array<mixed> $bindings
204
     * @return string
205
     */
206
    public function interpolateQuery(string $query, array $bindings = []): string
207
    {
208
        return $this->adapterInstance->interpolateQuery($query, $bindings);
209
    }
210
211
    /**
212
     * @param string           $sql
213
     * @param array<int,mixed> $bindings
214
     *
215
     * @return static
216
     */
217
    public function query($sql, $bindings = []): self
218
    {
219
        list($this->sqlStatement) = $this->statement($sql, $bindings);
220
221
        return $this;
222
    }
223
224
    /**
225
     * @param string           $sql
226
     * @param array<int,mixed> $bindings
227
     *
228
     * @return array{0:string, 1:float}
229
     */
230
    public function statement(string $sql, $bindings = []): array
231
    {
232
        $start        = microtime(true);
233
        $sqlStatement = empty($bindings) ? $sql : $this->interpolateQuery($sql, $bindings);
234
235
        if (!is_string($sqlStatement)) {
0 ignored issues
show
introduced by
The condition is_string($sqlStatement) is always true.
Loading history...
236
            throw new Exception('Could not interpolate query', 1);
237
        }
238
239
        return [$sqlStatement, microtime(true) - $start];
240
    }
241
242
    /**
243
     * Get all rows
244
     *
245
     * @return array<mixed,mixed>|null
246
     *
247
     * @throws Exception
248
     */
249
    public function get()
250
    {
251
        $eventResult = $this->fireEvents('before-select');
252
        if (!is_null($eventResult)) {
253
            return $eventResult;
254
        }
255
        $executionTime = 0;
256
        if (is_null($this->sqlStatement)) {
257
            $queryObject = $this->getQuery('select');
258
            $statement   = $this->statement(
259
                $queryObject->getSql(),
260
                $queryObject->getBindings()
261
            );
262
263
            $this->sqlStatement = $statement[0];
264
            $executionTime      = $statement[1];
265
        }
266
267
        $start  = microtime(true);
268
        $result = $this->dbInstance()->get_results(
269
            is_array($this->sqlStatement) ? (end($this->sqlStatement) ?: '') : $this->sqlStatement,
270
            // If we are using the hydrator, return as OBJECT and let the hydrator map the correct model.
271
            $this->useHydrator() ? OBJECT : $this->getFetchMode()
272
        );
273
        $executionTime += microtime(true) - $start;
274
        $this->sqlStatement = null;
275
276
        // Ensure we have an array of results.
277
        if (!is_array($result) && null !== $result) {
278
            $result = [$result];
279
        }
280
281
        // Maybe hydrate the results.
282
        if (null !== $result && $this->useHydrator()) {
283
            $result = $this->getHydrator()->fromMany($result);
284
        }
285
286
        $this->fireEvents('after-select', $result, $executionTime);
287
288
        return $result;
289
    }
290
291
    /**
292
     * Returns a populated instance of the Hydrator.
293
     *
294
     * @return Hydrator
295
     */
296
    protected function getHydrator(): Hydrator /* @phpstan-ignore-line */
297
    {
298
        $hydrator = new Hydrator($this->getFetchMode(), $this->hydratorConstructorArgs ?? []); /* @phpstan-ignore-line */
299
300
        return $hydrator;
301
    }
302
303
    /**
304
     * Checks if the results should be mapped via the hydrator
305
     *
306
     * @return bool
307
     */
308
    protected function useHydrator(): bool
309
    {
310
        return !in_array($this->getFetchMode(), [\ARRAY_A, \ARRAY_N, \OBJECT, \OBJECT_K]);
311
    }
312
313
    /**
314
     * Find all matching a simple where condition.
315
     *
316
     * Shortcut of ->where('key','=','value')->limit(1)->get();
317
     *
318
     * @return \stdClass\array<mixed,mixed>|object|null Can return any object using hydrator
0 ignored issues
show
Bug introduced by
The type stdClass\array was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
319
     */
320
    public function first()
321
    {
322
        $this->limit(1);
323
        $result = $this->get();
324
325
        return empty($result) ? null : $result[0];
326
    }
327
328
    /**
329
     * Find all matching a simple where condition.
330
     *
331
     * Shortcut of ->where('key','=','value')->get();
332
     *
333
     * @param string $fieldName
334
     * @param mixed $value
335
     *
336
     * @return array<mixed,mixed>|null Can return any object using hydrator
337
     */
338
    public function findAll($fieldName, $value)
339
    {
340
        $this->where($fieldName, '=', $value);
341
342
        return $this->get();
343
    }
344
345
    /**
346
     * @param string $fieldName
347
     * @param mixed $value
348
     *
349
     * @return \stdClass\array<mixed,mixed>|object|null Can return any object using hydrator
350
     */
351
    public function find($value, $fieldName = 'id')
352
    {
353
        $this->where($fieldName, '=', $value);
354
355
        return $this->first();
356
    }
357
358
    /**
359
     * @param string $fieldName
360
     * @param mixed $value
361
     *
362
     * @return \stdClass\array<mixed,mixed>|object Can return any object using hydrator
363
     * @throws Exception If fails to find
364
     */
365
    public function findOrFail($value, $fieldName = 'id')
366
    {
367
        $result = $this->find($value, $fieldName);
368
        if (null === $result) {
369
            throw new Exception("Failed to find {$fieldName}={$value}", 1);
370
        }
371
        return $result;
372
    }
373
374
    /**
375
     * Used to handle all aggregation method.
376
     *
377
     * @see Taken from the pecee-pixie library - https://github.com/skipperbent/pecee-pixie/
378
     *
379
     * @param string $type
380
     * @param string $field
381
     *
382
     * @return float
383
     */
384
    protected function aggregate(string $type, string $field = '*'): float
385
    {
386
        // Verify that field exists
387
        if ('*' !== $field && true === isset($this->statements['selects']) && false === \in_array($field, $this->statements['selects'], true)) {
388
            throw new \Exception(sprintf('Failed %s query - the column %s hasn\'t been selected in the query.', $type, $field));
389
        }
390
391
        if (false === isset($this->statements['tables'])) {
392
            throw new Exception('No table selected');
393
        }
394
395
        $count = $this
396
            ->table($this->subQuery($this, 'count'))
397
            ->select([$this->raw(sprintf('%s(%s) AS field', strtoupper($type), $field))])
398
            ->first();
399
400
        return true === isset($count->field) ? (float)$count->field : 0;
401
    }
402
403
    /**
404
     * Get count of all the rows for the current query
405
     *
406
     * @see Taken from the pecee-pixie library - https://github.com/skipperbent/pecee-pixie/
407
     *
408
     * @param string $field
409
     *
410
     * @return int
411
     *
412
     * @throws Exception
413
     */
414
    public function count(string $field = '*'): int
415
    {
416
        return (int)$this->aggregate('count', $field);
417
    }
418
419
    /**
420
     * Get the sum for a field in the current query
421
     *
422
     * @see Taken from the pecee-pixie library - https://github.com/skipperbent/pecee-pixie/
423
     *
424
     * @param string $field
425
     *
426
     * @return float
427
     *
428
     * @throws Exception
429
     */
430
    public function sum(string $field): float
431
    {
432
        return $this->aggregate('sum', $field);
433
    }
434
435
    /**
436
     * Get the average for a field in the current query
437
     *
438
     * @see Taken from the pecee-pixie library - https://github.com/skipperbent/pecee-pixie/
439
     *
440
     * @param string $field
441
     *
442
     * @return float
443
     *
444
     * @throws Exception
445
     */
446
    public function average(string $field): float
447
    {
448
        return $this->aggregate('avg', $field);
449
    }
450
451
    /**
452
     * Get the minimum for a field in the current query
453
     *
454
     * @see Taken from the pecee-pixie library - https://github.com/skipperbent/pecee-pixie/
455
     *
456
     * @param string $field
457
     *
458
     * @return float
459
     *
460
     * @throws Exception
461
     */
462
    public function min(string $field): float
463
    {
464
        return $this->aggregate('min', $field);
465
    }
466
467
    /**
468
     * Get the maximum for a field in the current query
469
     *
470
     * @see Taken from the pecee-pixie library - https://github.com/skipperbent/pecee-pixie/
471
     *
472
     * @param string $field
473
     *
474
     * @return float
475
     *
476
     * @throws Exception
477
     */
478
    public function max(string $field): float
479
    {
480
        return $this->aggregate('max', $field);
481
    }
482
483
    /**
484
     * @param string $type
485
     * @param bool|array<mixed, mixed> $dataToBePassed
486
     *
487
     * @return mixed
488
     *
489
     * @throws Exception
490
     */
491
    public function getQuery(string $type = 'select', $dataToBePassed = [])
492
    {
493
        $allowedTypes = ['select', 'insert', 'insertignore', 'replace', 'delete', 'update', 'criteriaonly'];
494
        if (!in_array(strtolower($type), $allowedTypes)) {
495
            throw new Exception($type . ' is not a known type.', 2);
496
        }
497
498
        $queryArr = $this->adapterInstance->$type($this->statements, $dataToBePassed);
499
500
        return $this->container->build(
501
            QueryObject::class,
502
            [$queryArr['sql'], $queryArr['bindings'], $this->dbInstance]
503
        );
504
    }
505
506
    /**
507
     * @param QueryBuilderHandler $queryBuilder
508
     * @param string|null $alias
509
     *
510
     * @return Raw
511
     */
512
    public function subQuery(QueryBuilderHandler $queryBuilder, ?string $alias = null)
513
    {
514
        $sql = '(' . $queryBuilder->getQuery()->getRawSql() . ')';
515
        if (is_string($alias) && 0 !== mb_strlen($alias)) {
516
            $sql = $sql . ' as ' . $alias;
517
        }
518
519
        return $queryBuilder->raw($sql);
520
    }
521
522
    /**
523
     * Handles the various insert operations based on the type.
524
     *
525
     * @param array<int|string, mixed|mixed[]> $data
526
     * @param string $type
527
     *
528
     * @return int|int[]|mixed|null can return a single row id, array of row ids, null (for failed) or any other value short circuited from event
529
     */
530
    private function doInsert(array $data, string $type)
531
    {
532
        $eventResult = $this->fireEvents('before-insert');
533
        if (!is_null($eventResult)) {
534
            return $eventResult;
535
        }
536
537
        // If first value is not an array () not a batch insert)
538
        if (!is_array(current($data))) {
539
            $queryObject = $this->getQuery($type, $data);
540
541
            list($preparedQuery, $executionTime) = $this->statement($queryObject->getSql(), $queryObject->getBindings());
542
            $this->dbInstance->get_results($preparedQuery);
543
544
            // Check we have a result.
545
            $return = 1 === $this->dbInstance->rows_affected ? $this->dbInstance->insert_id : null;
546
        } else {
547
            // Its a batch insert
548
            $return        = [];
549
            $executionTime = 0;
550
            foreach ($data as $subData) {
551
                $queryObject = $this->getQuery($type, $subData);
552
553
                list($preparedQuery, $time) = $this->statement($queryObject->getSql(), $queryObject->getBindings());
554
                $this->dbInstance->get_results($preparedQuery);
555
                $executionTime += $time;
556
557
                if (1 === $this->dbInstance->rows_affected) {
558
                    $return[] = $this->dbInstance->insert_id;
559
                }
560
            }
561
        }
562
563
        $this->fireEvents('after-insert', $return, $executionTime);
564
565
        return $return;
566
    }
567
568
    /**
569
     * @param array<int|string, mixed|mixed[]> $data either key=>value array for single or array of arrays for bulk
570
     *
571
     * @return int|int[]|mixed|null can return a single row id, array of row ids, null (for failed) or any other value short circuited from event
572
     */
573
    public function insert($data)
574
    {
575
        return $this->doInsert($data, 'insert');
576
    }
577
578
    /**
579
     *
580
     * @param array<int|string, mixed|mixed[]> $data either key=>value array for single or array of arrays for bulk
581
     *
582
     * @return int|int[]|mixed|null can return a single row id, array of row ids, null (for failed) or any other value short circuited from event
583
     */
584
    public function insertIgnore($data)
585
    {
586
        return $this->doInsert($data, 'insertignore');
587
    }
588
589
    /**
590
     *
591
     * @param array<int|string, mixed|mixed[]> $data either key=>value array for single or array of arrays for bulk
592
     *
593
     * @return int|int[]|mixed|null can return a single row id, array of row ids, null (for failed) or any other value short circuited from event
594
     */
595
    public function replace($data)
596
    {
597
        return $this->doInsert($data, 'replace');
598
    }
599
600
    /**
601
     * @param array<string, mixed> $data
602
     *
603
     * @return int|null
604
     */
605
    public function update($data)
606
    {
607
        $eventResult = $this->fireEvents('before-update');
608
        if (!is_null($eventResult)) {
609
            return $eventResult;
610
        }
611
        $queryObject                         = $this->getQuery('update', $data);
612
        list($preparedQuery, $executionTime) = $this->statement($queryObject->getSql(), $queryObject->getBindings());
613
614
        $this->dbInstance()->get_results($preparedQuery);
615
        $this->fireEvents('after-update', $queryObject, $executionTime);
616
617
        return 0 !== $this->dbInstance()->rows_affected
618
            ? $this->dbInstance()->rows_affected
619
            : null;
620
    }
621
622
    /**
623
     * @param array<string, mixed> $data
624
     *
625
     * @return int|null will return row id for insert and bool for success/fail on update
626
     */
627
    public function updateOrInsert($data)
628
    {
629
        if ($this->first()) {
630
            return $this->update($data);
631
        }
632
633
        return $this->insert($data);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->insert($data) also could return the type integer[] which is incompatible with the documented return type integer|null.
Loading history...
634
    }
635
636
    /**
637
     * @param array<string, mixed> $data
638
     *
639
     * @return static
640
     */
641
    public function onDuplicateKeyUpdate($data)
642
    {
643
        $this->addStatement('onduplicate', $data);
644
645
        return $this;
646
    }
647
648
    /**
649
     * @return int number of rows effected
650
     */
651
    public function delete(): int
652
    {
653
        $eventResult = $this->fireEvents('before-delete');
654
        if (!is_null($eventResult)) {
655
            return $eventResult;
656
        }
657
658
        $queryObject = $this->getQuery('delete');
659
660
        list($preparedQuery, $executionTime) = $this->statement($queryObject->getSql(), $queryObject->getBindings());
661
        $this->dbInstance()->get_results($preparedQuery);
662
        $this->fireEvents('after-delete', $queryObject, $executionTime);
663
664
        return $this->dbInstance()->rows_affected;
665
    }
666
667
    /**
668
     * @param string|Raw ...$tables Single table or array of tables
669
     *
670
     * @return static
671
     *
672
     * @throws Exception
673
     */
674
    public function table(...$tables)
675
    {
676
        $instance =  $this->constructCurrentBuilderClass($this->connection);
677
        $this->setFetchMode($this->getFetchMode(), $this->hydratorConstructorArgs);
678
        $tables = $this->addTablePrefix($tables, false);
679
        $instance->addStatement('tables', $tables);
680
681
        return $instance;
682
    }
683
684
    /**
685
     * @param string|Raw ...$tables Single table or array of tables
686
     *
687
     * @return static
688
     */
689
    public function from(...$tables): self
690
    {
691
        $tables = $this->addTablePrefix($tables, false);
692
        $this->addStatement('tables', $tables);
693
694
        return $this;
695
    }
696
697
    /**
698
     * @param string|string[]|Raw[]|array<string, string> $fields
699
     *
700
     * @return static
701
     */
702
    public function select($fields): self
703
    {
704
        if (!is_array($fields)) {
705
            $fields = func_get_args();
706
        }
707
708
        foreach ($fields as $field => $alias) {
709
            // If we have a JSON expression
710
            if ($this->jsonHandler->isJsonSelector($field)) {
711
                $field = $this->jsonHandler->extractAndUnquoteFromJsonSelector($field);
712
            }
713
714
            // If no alias passed, but field is for JSON. thrown an exception.
715
            if (is_numeric($field) && is_string($alias) && $this->jsonHandler->isJsonSelector($alias)) {
716
                throw new Exception("An alias must be used if you wish to select from JSON Object", 1);
717
            }
718
719
            // Treat each array as a single table, to retain order added
720
            $field = is_numeric($field)
721
                ? $field = $alias // If single colum
0 ignored issues
show
Unused Code introduced by
The assignment to $field is dead and can be removed.
Loading history...
722
                : $field = [$field => $alias]; // Has alias
723
724
            $field = $this->addTablePrefix($field);
725
            $this->addStatement('selects', $field);
726
        }
727
728
        return $this;
729
    }
730
731
    /**
732
     * @param string|string[]|Raw[]|array<string, string> $fields
733
     *
734
     * @return static
735
     */
736
    public function selectDistinct($fields)
737
    {
738
        $this->select($fields);
739
        $this->addStatement('distinct', true);
740
741
        return $this;
742
    }
743
744
    /**
745
     * @param string|string[] $field either the single field or an array of fields
746
     *
747
     * @return static
748
     */
749
    public function groupBy($field): self
750
    {
751
        $field = $this->addTablePrefix($field);
752
        $this->addStatement('groupBys', $field);
753
754
        return $this;
755
    }
756
757
    /**
758
     * @param string|array<string|int, mixed> $fields
759
     * @param string          $defaultDirection
760
     *
761
     * @return static
762
     */
763
    public function orderBy($fields, string $defaultDirection = 'ASC'): self
764
    {
765
        if (!is_array($fields)) {
766
            $fields = [$fields];
767
        }
768
769
        foreach ($fields as $key => $value) {
770
            $field = $key;
771
            $type  = $value;
772
            if (is_int($key)) {
773
                $field = $value;
774
                $type  = $defaultDirection;
775
            }
776
777
            if ($this->jsonHandler->isJsonSelector($field)) {
778
                $field = $this->jsonHandler->extractAndUnquoteFromJsonSelector($field);
779
            }
780
781
            if (!$field instanceof Raw) {
782
                $field = $this->addTablePrefix($field);
783
            }
784
            $this->statements['orderBys'][] = compact('field', 'type');
785
        }
786
787
        return $this;
788
    }
789
790
    /**
791
     * @param string|Raw $key The database column which holds the JSON value
792
     * @param string|Raw|string[] $jsonKey The json key/index to search
793
     * @param string $defaultDirection
794
     * @return static
795
     */
796
    public function orderByJson($key, $jsonKey, string $defaultDirection = 'ASC'): self
797
    {
798
        $key = $this->jsonHandler->jsonExpressionFactory()->extractAndUnquote($key, $jsonKey);
799
        return $this->orderBy($key, $defaultDirection);
800
    }
801
802
    /**
803
     * @param int $limit
804
     *
805
     * @return static
806
     */
807
    public function limit(int $limit): self
808
    {
809
        $this->statements['limit'] = $limit;
810
811
        return $this;
812
    }
813
814
    /**
815
     * @param int $offset
816
     *
817
     * @return static
818
     */
819
    public function offset(int $offset): self
820
    {
821
        $this->statements['offset'] = $offset;
822
823
        return $this;
824
    }
825
826
    /**
827
     * @param string|string[]|Raw|Raw[]       $key
828
     * @param string $operator
829
     * @param mixed $value
830
     * @param string $joiner
831
     *
832
     * @return static
833
     */
834
    public function having($key, string $operator, $value, string $joiner = 'AND')
835
    {
836
        $key                           = $this->addTablePrefix($key);
837
        $this->statements['havings'][] = compact('key', 'operator', 'value', 'joiner');
838
839
        return $this;
840
    }
841
842
    /**
843
     * @param string|string[]|Raw|Raw[]       $key
844
     * @param string $operator
845
     * @param mixed $value
846
     *
847
     * @return static
848
     */
849
    public function orHaving($key, $operator, $value)
850
    {
851
        return $this->having($key, $operator, $value, 'OR');
852
    }
853
854
    /**
855
     * @param string|Raw $key
856
     * @param string|mixed|null $operator Can be used as value, if 3rd arg not passed
857
     * @param mixed|null $value
858
     *
859
     * @return static
860
     */
861
    public function where($key, $operator = null, $value = null): self
862
    {
863
        // If two params are given then assume operator is =
864
        if (2 === func_num_args()) {
865
            $value    = $operator;
866
            $operator = '=';
867
        }
868
869
        return $this->whereHandler($key, $operator, $value);
870
    }
871
872
    /**
873
     * @param string|Raw|\Closure(QueryBuilderHandler):void $key
874
     * @param string|mixed|null $operator Can be used as value, if 3rd arg not passed
875
     * @param mixed|null $value
876
     *
877
     * @return static
878
     */
879
    public function orWhere($key, $operator = null, $value = null): self
880
    {
881
        // If two params are given then assume operator is =
882
        if (2 === func_num_args()) {
883
            $value    = $operator;
884
            $operator = '=';
885
        }
886
887
        return $this->whereHandler($key, $operator, $value, 'OR');
888
    }
889
890
    /**
891
     * @param string|Raw $key
892
     * @param string|mixed|null $operator Can be used as value, if 3rd arg not passed
893
     * @param mixed|null $value
894
     *
895
     * @return static
896
     */
897
    public function whereNot($key, $operator = null, $value = null): self
898
    {
899
        // If two params are given then assume operator is =
900
        if (2 === func_num_args()) {
901
            $value    = $operator;
902
            $operator = '=';
903
        }
904
905
        return $this->whereHandler($key, $operator, $value, 'AND NOT');
906
    }
907
908
    /**
909
     * @param string|Raw $key
910
     * @param string|mixed|null $operator Can be used as value, if 3rd arg not passed
911
     * @param mixed|null $value
912
     *
913
     * @return static
914
     */
915
    public function orWhereNot($key, $operator = null, $value = null)
916
    {
917
        // If two params are given then assume operator is =
918
        if (2 === func_num_args()) {
919
            $value    = $operator;
920
            $operator = '=';
921
        }
922
923
        return $this->whereHandler($key, $operator, $value, 'OR NOT');
924
    }
925
926
    /**
927
     * @param string|Raw $key
928
     * @param mixed[]|string|Raw $values
929
     *
930
     * @return static
931
     */
932
    public function whereIn($key, $values): self
933
    {
934
        return $this->whereHandler($key, 'IN', $values, 'AND');
935
    }
936
937
    /**
938
     * @param string|Raw $key
939
     * @param mixed[]|string|Raw $values
940
     *
941
     * @return static
942
     */
943
    public function whereNotIn($key, $values): self
944
    {
945
        return $this->whereHandler($key, 'NOT IN', $values, 'AND');
946
    }
947
948
    /**
949
     * @param string|Raw $key
950
     * @param mixed[]|string|Raw $values
951
     *
952
     * @return static
953
     */
954
    public function orWhereIn($key, $values): self
955
    {
956
        return $this->whereHandler($key, 'IN', $values, 'OR');
957
    }
958
959
    /**
960
     * @param string|Raw $key
961
     * @param mixed[]|string|Raw $values
962
     *
963
     * @return static
964
     */
965
    public function orWhereNotIn($key, $values): self
966
    {
967
        return $this->whereHandler($key, 'NOT IN', $values, 'OR');
968
    }
969
970
    /**
971
     * @param string|Raw $key
972
     * @param mixed $valueFrom
973
     * @param mixed $valueTo
974
     *
975
     * @return static
976
     */
977
    public function whereBetween($key, $valueFrom, $valueTo): self
978
    {
979
        return $this->whereHandler($key, 'BETWEEN', [$valueFrom, $valueTo], 'AND');
980
    }
981
982
    /**
983
     * @param string|Raw $key
984
     * @param mixed $valueFrom
985
     * @param mixed $valueTo
986
     *
987
     * @return static
988
     */
989
    public function orWhereBetween($key, $valueFrom, $valueTo): self
990
    {
991
        return $this->whereHandler($key, 'BETWEEN', [$valueFrom, $valueTo], 'OR');
992
    }
993
994
    /**
995
     * Handles all function call based where conditions
996
     *
997
     * @param string|Raw $key
998
     * @param string $function
999
     * @param string|mixed|null $operator Can be used as value, if 3rd arg not passed
1000
     * @param mixed|null $value
1001
     * @return static
1002
     */
1003
    protected function whereFunctionCallHandler($key, $function, $operator, $value): self
1004
    {
1005
        $key = \sprintf('%s(%s)', $function, $this->addTablePrefix($key));
0 ignored issues
show
Bug introduced by
It seems like $this->addTablePrefix($key) can also be of type array<mixed,mixed>; however, parameter $values of sprintf() does only seem to accept double|integer|string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1005
        $key = \sprintf('%s(%s)', $function, /** @scrutinizer ignore-type */ $this->addTablePrefix($key));
Loading history...
1006
        return $this->where($key, $operator, $value);
1007
    }
1008
1009
    /**
1010
     * @param string|Raw $key
1011
     * @param string|mixed|null $operator Can be used as value, if 3rd arg not passed
1012
     * @param mixed|null $value
1013
     * @return self
1014
     */
1015
    public function whereMonth($key, $operator = null, $value = null): self
1016
    {
1017
        // If two params are given then assume operator is =
1018
        if (2 === func_num_args()) {
1019
            $value    = $operator;
1020
            $operator = '=';
1021
        }
1022
        return $this->whereFunctionCallHandler($key, 'MONTH', $operator, $value);
1023
    }
1024
1025
    /**
1026
     * @param string|Raw $key
1027
     * @param string|mixed|null $operator Can be used as value, if 3rd arg not passed
1028
     * @param mixed|null $value
1029
     * @return self
1030
     */
1031
    public function whereDay($key, $operator = null, $value = null): self
1032
    {
1033
        // If two params are given then assume operator is =
1034
        if (2 === func_num_args()) {
1035
            $value    = $operator;
1036
            $operator = '=';
1037
        }
1038
        return $this->whereFunctionCallHandler($key, 'DAY', $operator, $value);
1039
    }
1040
1041
    /**
1042
     * @param string|Raw $key
1043
     * @param string|mixed|null $operator Can be used as value, if 3rd arg not passed
1044
     * @param mixed|null $value
1045
     * @return self
1046
     */
1047
    public function whereYear($key, $operator = null, $value = null): self
1048
    {
1049
        // If two params are given then assume operator is =
1050
        if (2 === func_num_args()) {
1051
            $value    = $operator;
1052
            $operator = '=';
1053
        }
1054
        return $this->whereFunctionCallHandler($key, 'YEAR', $operator, $value);
1055
    }
1056
1057
    /**
1058
     * @param string|Raw $key
1059
     * @param string|mixed|null $operator Can be used as value, if 3rd arg not passed
1060
     * @param mixed|null $value
1061
     * @return self
1062
     */
1063
    public function whereDate($key, $operator = null, $value = null): self
1064
    {
1065
        // If two params are given then assume operator is =
1066
        if (2 === func_num_args()) {
1067
            $value    = $operator;
1068
            $operator = '=';
1069
        }
1070
        return $this->whereFunctionCallHandler($key, 'DATE', $operator, $value);
1071
    }
1072
1073
    /**
1074
     * @param string|Raw $key
1075
     *
1076
     * @return static
1077
     */
1078
    public function whereNull($key): self
1079
    {
1080
        return $this->whereNullHandler($key);
1081
    }
1082
1083
    /**
1084
     * @param string|Raw $key
1085
     *
1086
     * @return static
1087
     */
1088
    public function whereNotNull($key): self
1089
    {
1090
        return $this->whereNullHandler($key, 'NOT');
1091
    }
1092
1093
    /**
1094
     * @param string|Raw $key
1095
     *
1096
     * @return static
1097
     */
1098
    public function orWhereNull($key): self
1099
    {
1100
        return $this->whereNullHandler($key, '', 'or');
1101
    }
1102
1103
    /**
1104
     * @param string|Raw $key
1105
     *
1106
     * @return static
1107
     */
1108
    public function orWhereNotNull($key): self
1109
    {
1110
        return $this->whereNullHandler($key, 'NOT', 'or');
1111
    }
1112
1113
    /**
1114
     * @param string|Raw $key
1115
     * @param string $prefix
1116
     * @param string $operator
1117
     *
1118
     * @return static
1119
     */
1120
    protected function whereNullHandler($key, string $prefix = '', $operator = ''): self
1121
    {
1122
        $prefix = 0 === mb_strlen($prefix) ? '' : " {$prefix}";
1123
1124
        if ($key instanceof Raw) {
1125
            $key = $this->adapterInstance->parseRaw($key);
1126
        }
1127
1128
        $key = $this->addTablePrefix($key);
1129
        if ($key instanceof Closure) {
1130
            throw new Exception('Key used for whereNull condition must be a string or raw exrpession.', 1);
1131
        }
1132
1133
        return $this->{$operator . 'Where'}($this->raw("{$key} IS{$prefix} NULL"));
1134
    }
1135
1136
1137
    /**
1138
     * Runs a transaction
1139
     *
1140
     * @param \Closure(Transaction):void $callback
1141
     *
1142
     * @return static
1143
     */
1144
    public function transaction(Closure $callback): self
1145
    {
1146
        try {
1147
            // Begin the transaction
1148
            $this->dbInstance->query('START TRANSACTION');
1149
1150
            // Get the Transaction class
1151
            $transaction = $this->container->build(Transaction::class, [$this->connection]);
1152
1153
            $this->handleTransactionCall($callback, $transaction);
1154
1155
            // If no errors have been thrown or the transaction wasn't completed within
1156
            $this->dbInstance->query('COMMIT');
1157
1158
            return $this;
1159
        } catch (TransactionHaltException $e) {
1160
            // Commit or rollback behavior has been handled in the closure, so exit
1161
            return $this;
1162
        } catch (\Exception $e) {
1163
            // something happened, rollback changes
1164
            $this->dbInstance->query('ROLLBACK');
1165
1166
            return $this;
1167
        }
1168
    }
1169
1170
    /**
1171
     * Handles the transaction call.
1172
     * Catches any WPDB Errors (printed)
1173
     *
1174
     * @param Closure    $callback
1175
     * @param Transaction $transaction
1176
     *
1177
     * @return void
1178
     * @throws Exception
1179
     */
1180
    protected function handleTransactionCall(Closure $callback, Transaction $transaction): void
1181
    {
1182
        try {
1183
            ob_start();
1184
            $callback($transaction);
1185
            $output = ob_get_clean() ?: '';
1186
        } catch (Throwable $th) {
1187
            ob_end_clean();
1188
            throw $th;
1189
        }
1190
1191
        // If we caught an error, throw an exception.
1192
        if (0 !== mb_strlen($output)) {
1193
            throw new Exception($output);
1194
        }
1195
    }
1196
1197
    /*************************************************************************/
1198
    /*************************************************************************/
1199
    /*************************************************************************/
1200
    /**                              JOIN JOIN                              **/
1201
    /**                                 JOIN                                **/
1202
    /**                              JOIN JOIN                              **/
1203
    /*************************************************************************/
1204
    /*************************************************************************/
1205
    /*************************************************************************/
1206
1207
    /**
1208
     * @param string|Raw $table
1209
     * @param string|Raw|Closure $key
1210
     * @param string|null $operator
1211
     * @param mixed $value
1212
     * @param string $type
1213
     *
1214
     * @return static
1215
     */
1216
    public function join($table, $key, ?string $operator = null, $value = null, $type = 'inner')
1217
    {
1218
        // Potentially cast key from JSON
1219
        if ($this->jsonHandler->isJsonSelector($key)) {
1220
            /** @var string $key */
1221
            $key = $this->jsonHandler->extractAndUnquoteFromJsonSelector($key); /** @phpstan-ignore-line */
1222
        }
1223
1224
        // Potentially cast value from json
1225
        if ($this->jsonHandler->isJsonSelector($value)) {
1226
            /** @var string $value */
1227
            $value = $this->jsonHandler->extractAndUnquoteFromJsonSelector($value);
1228
        }
1229
1230
        if (!$key instanceof Closure) {
1231
            $key = function ($joinBuilder) use ($key, $operator, $value) {
1232
                $joinBuilder->on($key, $operator, $value);
1233
            };
1234
        }
1235
1236
        // Build a new JoinBuilder class, keep it by reference so any changes made
1237
        // in the closure should reflect here
1238
        $joinBuilder = $this->container->build(JoinBuilder::class, [$this->connection]);
1239
        $joinBuilder = &$joinBuilder;
1240
        // Call the closure with our new joinBuilder object
1241
        $key($joinBuilder);
1242
        $table = $this->addTablePrefix($table, false);
1243
        // Get the criteria only query from the joinBuilder object
1244
        $this->statements['joins'][] = compact('type', 'table', 'joinBuilder');
1245
        return $this;
1246
    }
1247
1248
1249
1250
    /**
1251
     * @param string|Raw $table
1252
     * @param string|Raw|Closure $key
1253
     * @param string|null $operator
1254
     * @param mixed $value
1255
     *
1256
     * @return static
1257
     */
1258
    public function leftJoin($table, $key, $operator = null, $value = null)
1259
    {
1260
        return $this->join($table, $key, $operator, $value, 'left');
1261
    }
1262
1263
    /**
1264
     * @param string|Raw $table
1265
     * @param string|Raw|Closure $key
1266
     * @param string|null $operator
1267
     * @param mixed $value
1268
     *
1269
     * @return static
1270
     */
1271
    public function rightJoin($table, $key, $operator = null, $value = null)
1272
    {
1273
        return $this->join($table, $key, $operator, $value, 'right');
1274
    }
1275
1276
    /**
1277
     * @param string|Raw $table
1278
     * @param string|Raw|Closure $key
1279
     * @param string|null $operator
1280
     * @param mixed $value
1281
     *
1282
     * @return static
1283
     */
1284
    public function innerJoin($table, $key, $operator = null, $value = null)
1285
    {
1286
        return $this->join($table, $key, $operator, $value, 'inner');
1287
    }
1288
1289
    /**
1290
     * @param string|Raw $table
1291
     * @param string|Raw|Closure $key
1292
     * @param string|null $operator
1293
     * @param mixed $value
1294
     *
1295
     * @return static
1296
     */
1297
    public function crossJoin($table, $key, $operator = null, $value = null)
1298
    {
1299
        return $this->join($table, $key, $operator, $value, 'cross');
1300
    }
1301
1302
    /**
1303
     * @param string|Raw $table
1304
     * @param string|Raw|Closure $key
1305
     * @param string|null $operator
1306
     * @param mixed $value
1307
     *
1308
     * @return static
1309
     */
1310
    public function outerJoin($table, $key, $operator = null, $value = null)
1311
    {
1312
        return $this->join($table, $key, $operator, $value, 'outer');
1313
    }
1314
1315
    /**
1316
     * Shortcut to join 2 tables on the same key name with equals
1317
     *
1318
     * @param string $table
1319
     * @param string $key
1320
     * @param string $type
1321
     * @return self
1322
     * @throws Exception If base table is set as more than 1 or 0
1323
     */
1324
    public function joinUsing(string $table, string $key, string $type = 'INNER'): self
1325
    {
1326
        if (!array_key_exists('tables', $this->statements) || count($this->statements['tables']) !== 1) {
1327
            throw new Exception("JoinUsing can only be used with a single table set as the base of the query", 1);
1328
        }
1329
        $baseTable = end($this->statements['tables']);
1330
1331
        // Potentialy cast key from JSON
1332
        if ($this->jsonHandler->isJsonSelector($key)) {
1333
            $key = $this->jsonHandler->extractAndUnquoteFromJsonSelector($key);
1334
        }
1335
1336
        $remoteKey = $table = $this->addTablePrefix("{$table}.{$key}", true);
0 ignored issues
show
Unused Code introduced by
The assignment to $table is dead and can be removed.
Loading history...
1337
        $localKey = $table = $this->addTablePrefix("{$baseTable}.{$key}", true);
1338
        return $this->join($table, $remoteKey, '=', $localKey, $type);
0 ignored issues
show
Bug introduced by
It seems like $remoteKey can also be of type array<mixed,mixed>; however, parameter $key of Pixie\QueryBuilder\QueryBuilderHandler::join() does only seem to accept Closure|Pixie\QueryBuilder\Raw|string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1338
        return $this->join($table, /** @scrutinizer ignore-type */ $remoteKey, '=', $localKey, $type);
Loading history...
Bug introduced by
It seems like $table can also be of type array<mixed,mixed>; however, parameter $table of Pixie\QueryBuilder\QueryBuilderHandler::join() does only seem to accept Pixie\QueryBuilder\Raw|string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1338
        return $this->join(/** @scrutinizer ignore-type */ $table, $remoteKey, '=', $localKey, $type);
Loading history...
1339
    }
1340
1341
    /**
1342
     * Add a raw query
1343
     *
1344
     * @param string|Raw $value
1345
     * @param mixed|mixed[] $bindings
1346
     *
1347
     * @return Raw
1348
     */
1349
    public function raw($value, $bindings = []): Raw
1350
    {
1351
        return new Raw($value, $bindings);
1352
    }
1353
1354
    /**
1355
     * Return wpdb instance
1356
     *
1357
     * @return wpdb
1358
     */
1359
    public function dbInstance(): wpdb
1360
    {
1361
        return $this->dbInstance;
1362
    }
1363
1364
    /**
1365
     * @param Connection $connection
1366
     *
1367
     * @return static
1368
     */
1369
    public function setConnection(Connection $connection): self
1370
    {
1371
        $this->connection = $connection;
1372
1373
        return $this;
1374
    }
1375
1376
    /**
1377
     * @return Connection
1378
     */
1379
    public function getConnection()
1380
    {
1381
        return $this->connection;
1382
    }
1383
1384
    /**
1385
     * @param string|Raw|Closure $key
1386
     * @param string|null      $operator
1387
     * @param mixed|null       $value
1388
     * @param string $joiner
1389
     *
1390
     * @return static
1391
     */
1392
    protected function whereHandler($key, $operator = null, $value = null, $joiner = 'AND')
1393
    {
1394
        $key = $this->addTablePrefix($key);
1395
        if ($key instanceof Raw) {
1396
            $key = $this->adapterInstance->parseRaw($key);
1397
        }
1398
1399
        if ($this->jsonHandler->isJsonSelector($key)) {
1400
            $key = $this->jsonHandler->extractAndUnquoteFromJsonSelector($key);
0 ignored issues
show
Bug introduced by
It seems like $key can also be of type array<mixed,mixed>; however, parameter $selector of Pixie\JSON\JsonHandler::...quoteFromJsonSelector() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1400
            $key = $this->jsonHandler->extractAndUnquoteFromJsonSelector(/** @scrutinizer ignore-type */ $key);
Loading history...
1401
        }
1402
1403
        $this->statements['wheres'][] = compact('key', 'operator', 'value', 'joiner');
1404
        return $this;
1405
    }
1406
1407
1408
1409
    /**
1410
     * @param string $key
1411
     * @param mixed|mixed[]|bool $value
1412
     *
1413
     * @return void
1414
     */
1415
    protected function addStatement($key, $value)
1416
    {
1417
        if (!is_array($value)) {
1418
            $value = [$value];
1419
        }
1420
1421
        if (!array_key_exists($key, $this->statements)) {
1422
            $this->statements[$key] = $value;
1423
        } else {
1424
            $this->statements[$key] = array_merge($this->statements[$key], $value);
1425
        }
1426
    }
1427
1428
    /**
1429
     * @param string $event
1430
     * @param string|Raw $table
1431
     *
1432
     * @return callable|null
1433
     */
1434
    public function getEvent(string $event, $table = ':any'): ?callable
1435
    {
1436
        return $this->connection->getEventHandler()->getEvent($event, $table);
1437
    }
1438
1439
    /**
1440
     * @param string $event
1441
     * @param string|Raw $table
1442
     * @param Closure $action
1443
     *
1444
     * @return void
1445
     */
1446
    public function registerEvent($event, $table, Closure $action): void
1447
    {
1448
        $table = $table ?: ':any';
1449
1450
        if (':any' != $table) {
1451
            $table = $this->addTablePrefix($table, false);
1452
        }
1453
1454
        $this->connection->getEventHandler()->registerEvent($event, $table, $action);
0 ignored issues
show
Bug introduced by
It seems like $table can also be of type array<mixed,mixed>; however, parameter $table of Pixie\EventHandler::registerEvent() does only seem to accept null|string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1454
        $this->connection->getEventHandler()->registerEvent($event, /** @scrutinizer ignore-type */ $table, $action);
Loading history...
1455
    }
1456
1457
    /**
1458
     * @param string $event
1459
     * @param string|Raw $table
1460
     *
1461
     * @return void
1462
     */
1463
    public function removeEvent(string $event, $table = ':any')
1464
    {
1465
        if (':any' != $table) {
1466
            $table = $this->addTablePrefix($table, false);
1467
        }
1468
1469
        $this->connection->getEventHandler()->removeEvent($event, $table);
0 ignored issues
show
Bug introduced by
It seems like $table can also be of type array<mixed,mixed>; however, parameter $table of Pixie\EventHandler::removeEvent() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1469
        $this->connection->getEventHandler()->removeEvent($event, /** @scrutinizer ignore-type */ $table);
Loading history...
1470
    }
1471
1472
    /**
1473
     * @param string $event
1474
     *
1475
     * @return mixed
1476
     */
1477
    public function fireEvents(string $event)
1478
    {
1479
        $params = func_get_args(); // @todo Replace this with an easier to read alteratnive
1480
        array_unshift($params, $this);
1481
1482
        return call_user_func_array([$this->connection->getEventHandler(), 'fireEvents'], $params);
1483
    }
1484
1485
    /**
1486
     * @return array<string, mixed[]>
1487
     */
1488
    public function getStatements()
1489
    {
1490
        return $this->statements;
1491
    }
1492
1493
    /**
1494
     * @return string will return WPDB Fetch mode
1495
     */
1496
    public function getFetchMode()
1497
    {
1498
        return null !== $this->fetchMode
1499
            ? $this->fetchMode
1500
            : \OBJECT;
1501
    }
1502
1503
    /**
1504
     * Returns an NEW instance of the JSON builder populated with the same connection and hydrator details.
1505
     *
1506
     * @return JsonQueryBuilder
1507
     */
1508
    public function jsonBuilder(): JsonQueryBuilder
1509
    {
1510
        return new JsonQueryBuilder($this->getConnection(), $this->getFetchMode(), $this->hydratorConstructorArgs);
1511
    }
1512
}
1513