Passed
Push — master ( b7b77b...565dd3 )
by Glynn
05:12 queued 03:02
created

QueryBuilderHandler::aggregate()   B

Complexity

Conditions 8
Paths 16

Size

Total Lines 28
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

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

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

1347
        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

1347
        return $this->join(/** @scrutinizer ignore-type */ $table, $remoteKey, '=', $localKey, $type);
Loading history...
1348
    }
1349
1350
    /**
1351
     * Add a raw query
1352
     *
1353
     * @param string|Raw $value
1354
     * @param mixed|mixed[] $bindings
1355
     *
1356
     * @return Raw
1357
     */
1358
    public function raw($value, $bindings = []): Raw
1359
    {
1360
        return new Raw($value, $bindings);
1361
    }
1362
1363
    /**
1364
     * Return wpdb instance
1365
     *
1366
     * @return wpdb
1367
     */
1368
    public function dbInstance(): wpdb
1369
    {
1370
        return $this->dbInstance;
1371
    }
1372
1373
    /**
1374
     * @param Connection $connection
1375
     *
1376
     * @return static
1377
     */
1378
    public function setConnection(Connection $connection): self
1379
    {
1380
        $this->connection = $connection;
1381
1382
        return $this;
1383
    }
1384
1385
    /**
1386
     * @return Connection
1387
     */
1388
    public function getConnection(): Connection
1389
    {
1390
        return $this->connection;
1391
    }
1392
1393
    /**
1394
     * @param string|Raw|Closure $key
1395
     * @param string|null      $operator
1396
     * @param mixed|null       $value
1397
     * @param string $joiner
1398
     *
1399
     * @return static
1400
     */
1401
    protected function whereHandler($key, $operator = null, $value = null, $joiner = 'AND')
1402
    {
1403
        $key = $this->addTablePrefix($key);
1404
        if ($key instanceof Raw) {
1405
            $key = $this->adapterInstance->parseRaw($key);
1406
        }
1407
1408
        if ($this->jsonHandler->isJsonSelector($key)) {
1409
            $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

1409
            $key = $this->jsonHandler->extractAndUnquoteFromJsonSelector(/** @scrutinizer ignore-type */ $key);
Loading history...
1410
        }
1411
1412
        $this->statements['wheres'][] = compact('key', 'operator', 'value', 'joiner');
1413
        return $this;
1414
    }
1415
1416
1417
1418
    /**
1419
     * @param string $key
1420
     * @param mixed|mixed[]|bool $value
1421
     *
1422
     * @return void
1423
     */
1424
    protected function addStatement($key, $value)
1425
    {
1426
        if (!is_array($value)) {
1427
            $value = [$value];
1428
        }
1429
1430
        if (!array_key_exists($key, $this->statements)) {
1431
            $this->statements[$key] = $value;
1432
        } else {
1433
            $this->statements[$key] = array_merge($this->statements[$key], $value);
1434
        }
1435
    }
1436
1437
    /**
1438
     * @param string $event
1439
     * @param string|Raw $table
1440
     *
1441
     * @return callable|null
1442
     */
1443
    public function getEvent(string $event, $table = ':any'): ?callable
1444
    {
1445
        return $this->connection->getEventHandler()->getEvent($event, $table);
1446
    }
1447
1448
    /**
1449
     * @param string $event
1450
     * @param string|Raw $table
1451
     * @param Closure $action
1452
     *
1453
     * @return void
1454
     */
1455
    public function registerEvent($event, $table, Closure $action): void
1456
    {
1457
        $table = $table ?: ':any';
1458
1459
        if (':any' != $table) {
1460
            $table = $this->addTablePrefix($table, false);
1461
        }
1462
1463
        $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

1463
        $this->connection->getEventHandler()->registerEvent($event, /** @scrutinizer ignore-type */ $table, $action);
Loading history...
1464
    }
1465
1466
    /**
1467
     * @param string $event
1468
     * @param string|Raw $table
1469
     *
1470
     * @return void
1471
     */
1472
    public function removeEvent(string $event, $table = ':any')
1473
    {
1474
        if (':any' != $table) {
1475
            $table = $this->addTablePrefix($table, false);
1476
        }
1477
1478
        $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

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