Passed
Pull Request — master (#20)
by Glynn
02:17
created

QueryBuilderHandler::whereYear()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 4
c 0
b 0
f 0
dl 0
loc 8
rs 10
cc 2
nc 2
nop 3
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 function mb_strlen;
13
14
use Pixie\QueryBuilder\Raw;
15
use Pixie\Hydration\Hydrator;
16
use Pixie\QueryBuilder\JoinBuilder;
17
use Pixie\QueryBuilder\QueryObject;
18
use Pixie\QueryBuilder\Transaction;
19
use Pixie\QueryBuilder\WPDBAdapter;
20
21
class QueryBuilderHandler
22
{
23
    /**
24
     * @var \Viocon\Container
25
     */
26
    protected $container;
27
28
    /**
29
     * @var Connection
30
     */
31
    protected $connection;
32
33
    /**
34
     * @var array<string, mixed[]|mixed>
35
     */
36
    protected $statements = [];
37
38
    /**
39
     * @var wpdb
40
     */
41
    protected $dbInstance;
42
43
    /**
44
     * @var string|string[]|null
45
     */
46
    protected $sqlStatement = null;
47
48
    /**
49
     * @var string|null
50
     */
51
    protected $tablePrefix = null;
52
53
    /**
54
     * @var WPDBAdapter
55
     */
56
    protected $adapterInstance;
57
58
    /**
59
     * The mode to return results as.
60
     * Accepts WPDB constants or class names.
61
     *
62
     * @var string
63
     */
64
    protected $fetchMode;
65
66
    /**
67
     * Custom args used to construct models for hydrator
68
     *
69
     * @var array<int, mixed>|null
70
     */
71
    protected $hydratorConstructorArgs;
72
73
    /**
74
     * @param \Pixie\Connection|null $connection
75
     * @param string $fetchMode
76
     * @param mixed[] $hydratorConstructorArgs
77
     *
78
     * @throws Exception if no connection passed and not previously established
79
     */
80
    final public function __construct(
81
        Connection $connection = null,
82
        string $fetchMode = \OBJECT,
83
        ?array $hydratorConstructorArgs = null
84
    ) {
85
        if (is_null($connection)) {
86
            // throws if connection not already established.
87
            $connection = Connection::getStoredConnection();
88
        }
89
90
        // Set all dependencies from connection.
91
        $this->connection = $connection;
92
        $this->container  = $this->connection->getContainer();
93
        $this->dbInstance = $this->connection->getDbInstance();
94
        $this->setAdapterConfig($this->connection->getAdapterConfig());
95
96
        // Set up optional hydration details.
97
        $this->setFetchMode($fetchMode);
98
        $this->hydratorConstructorArgs = $hydratorConstructorArgs;
99
100
        // Query builder adapter instance
101
        $this->adapterInstance = $this->container->build(
102
            WPDBAdapter::class,
103
            [$this->connection]
104
        );
105
    }
106
107
    /**
108
     * Sets the config for WPDB
109
     *
110
     * @param array<string, mixed> $adapterConfig
111
     *
112
     * @return void
113
     */
114
    protected function setAdapterConfig(array $adapterConfig): void
115
    {
116
        if (isset($adapterConfig['prefix'])) {
117
            $this->tablePrefix = $adapterConfig['prefix'];
118
        }
119
    }
120
121
    /**
122
     * Set the fetch mode
123
     *
124
     * @param string $mode
125
     * @param array<int, mixed>|null $constructorArgs
126
     *
127
     * @return static
128
     */
129
    public function setFetchMode(string $mode, ?array $constructorArgs = null): self
130
    {
131
        $this->fetchMode               = $mode;
132
        $this->hydratorConstructorArgs = $constructorArgs;
133
134
        return $this;
135
    }
136
137
    /**
138
     * @param Connection|null $connection
139
     *
140
     * @return static
141
     *
142
     * @throws Exception
143
     */
144
    public function newQuery(Connection $connection = null): self
145
    {
146
        if (is_null($connection)) {
147
            $connection = $this->connection;
148
        }
149
150
        $newQuery = $this->constructCurrentBuilderClass($connection);
151
        $newQuery->setFetchMode($this->getFetchMode(), $this->hydratorConstructorArgs);
152
153
        return $newQuery;
154
    }
155
156
    /**
157
     * Returns a new instance of the current, with the passed connection.
158
     *
159
     * @param \Pixie\Connection $connection
160
     *
161
     * @return static
162
     */
163
    protected function constructCurrentBuilderClass(Connection $connection): self
164
    {
165
        return new static($connection);
166
    }
167
168
    /**
169
     * Interpolates a query
170
     *
171
     * @param string $query
172
     * @param array<mixed> $bindings
173
     * @return string
174
     */
175
    public function interpolateQuery(string $query, array $bindings = []): string
176
    {
177
        return $this->adapterInstance->interpolateQuery($query, $bindings);
178
    }
179
180
    /**
181
     * @param string           $sql
182
     * @param array<int,mixed> $bindings
183
     *
184
     * @return static
185
     */
186
    public function query($sql, $bindings = []): self
187
    {
188
        list($this->sqlStatement) = $this->statement($sql, $bindings);
189
190
        return $this;
191
    }
192
193
    /**
194
     * @param string           $sql
195
     * @param array<int,mixed> $bindings
196
     *
197
     * @return array{0:string, 1:float}
198
     */
199
    public function statement(string $sql, $bindings = []): array
200
    {
201
        $start        = microtime(true);
202
        $sqlStatement = empty($bindings) ? $sql : $this->interpolateQuery($sql, $bindings);
203
204
        if (!is_string($sqlStatement)) {
0 ignored issues
show
introduced by
The condition is_string($sqlStatement) is always true.
Loading history...
205
            throw new Exception('Could not interpolate query', 1);
206
        }
207
208
        return [$sqlStatement, microtime(true) - $start];
209
    }
210
211
    /**
212
     * Get all rows
213
     *
214
     * @return array<mixed,mixed>|null
215
     *
216
     * @throws Exception
217
     */
218
    public function get()
219
    {
220
        $eventResult = $this->fireEvents('before-select');
221
        if (!is_null($eventResult)) {
222
            return $eventResult;
223
        }
224
        $executionTime = 0;
225
        if (is_null($this->sqlStatement)) {
226
            $queryObject = $this->getQuery('select');
227
            $statement   = $this->statement(
228
                $queryObject->getSql(),
229
                $queryObject->getBindings()
230
            );
231
232
            $this->sqlStatement = $statement[0];
233
            $executionTime      = $statement[1];
234
        }
235
236
        $start  = microtime(true);
237
        $result = $this->dbInstance()->get_results(
238
            is_array($this->sqlStatement) ? (end($this->sqlStatement) ?: '') : $this->sqlStatement,
239
            // If we are using the hydrator, return as OBJECT and let the hydrator map the correct model.
240
            $this->useHydrator() ? OBJECT : $this->getFetchMode()
241
        );
242
        $executionTime += microtime(true) - $start;
243
        $this->sqlStatement = null;
244
245
        // Ensure we have an array of results.
246
        if (!is_array($result) && null !== $result) {
247
            $result = [$result];
248
        }
249
250
        // Maybe hydrate the results.
251
        if (null !== $result && $this->useHydrator()) {
252
            $result = $this->getHydrator()->fromMany($result);
253
        }
254
255
        $this->fireEvents('after-select', $result, $executionTime);
256
257
        return $result;
258
    }
259
260
    /**
261
     * Returns a populated instance of the Hydrator.
262
     *
263
     * @return Hydrator
264
     */
265
    protected function getHydrator(): Hydrator /* @phpstan-ignore-line */
266
    {
267
        $hydrator = new Hydrator($this->getFetchMode(), $this->hydratorConstructorArgs ?? []); /* @phpstan-ignore-line */
268
269
        return $hydrator;
270
    }
271
272
    /**
273
     * Checks if the results should be mapped via the hydrator
274
     *
275
     * @return bool
276
     */
277
    protected function useHydrator(): bool
278
    {
279
        return !in_array($this->getFetchMode(), [\ARRAY_A, \ARRAY_N, \OBJECT, \OBJECT_K]);
280
    }
281
282
    /**
283
     * Find all matching a simple where condition.
284
     *
285
     * Shortcut of ->where('key','=','value')->limit(1)->get();
286
     *
287
     * @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...
288
     */
289
    public function first()
290
    {
291
        $this->limit(1);
292
        $result = $this->get();
293
294
        return empty($result) ? null : $result[0];
295
    }
296
297
    /**
298
     * Find all matching a simple where condition.
299
     *
300
     * Shortcut of ->where('key','=','value')->get();
301
     *
302
     * @param string $fieldName
303
     * @param mixed $value
304
     *
305
     * @return array<mixed,mixed>|null Can return any object using hydrator
306
     */
307
    public function findAll($fieldName, $value)
308
    {
309
        $this->where($fieldName, '=', $value);
310
311
        return $this->get();
312
    }
313
314
    /**
315
     * @param string $fieldName
316
     * @param mixed $value
317
     *
318
     * @return \stdClass\array<mixed,mixed>|object|null Can return any object using hydrator
319
     */
320
    public function find($value, $fieldName = 'id')
321
    {
322
        $this->where($fieldName, '=', $value);
323
324
        return $this->first();
325
    }
326
327
    /**
328
     * @param string $fieldName
329
     * @param mixed $value
330
     *
331
     * @return \stdClass\array<mixed,mixed>|object Can return any object using hydrator
332
     * @throws Exception If fails to find
333
     */
334
    public function findOrFail($value, $fieldName = 'id')
335
    {
336
        $result = $this->find($value, $fieldName);
337
        if (null === $result) {
338
            throw new Exception("Failed to find {$fieldName}={$value}", 1);
339
        }
340
        return $result;
341
    }
342
343
    /**
344
     * Used to handle all aggregation method.
345
     *
346
     * @see Taken from the pecee-pixie library - https://github.com/skipperbent/pecee-pixie/
347
     *
348
     * @param string $type
349
     * @param string $field
350
     *
351
     * @return float
352
     */
353
    protected function aggregate(string $type, string $field = '*'): float
354
    {
355
        // Verify that field exists
356
        if ('*' !== $field && true === isset($this->statements['selects']) && false === \in_array($field, $this->statements['selects'], true)) {
357
            throw new \Exception(sprintf('Failed %s query - the column %s hasn\'t been selected in the query.', $type, $field));
358
        }
359
360
        if (false === isset($this->statements['tables'])) {
361
            throw new Exception('No table selected');
362
        }
363
364
        $count = $this
365
            ->table($this->subQuery($this, 'count'))
366
            ->select([$this->raw(sprintf('%s(%s) AS field', strtoupper($type), $field))])
367
            ->first();
368
369
        return true === isset($count->field) ? (float)$count->field : 0;
370
    }
371
372
    /**
373
     * Get count of all the rows for the current query
374
     *
375
     * @see Taken from the pecee-pixie library - https://github.com/skipperbent/pecee-pixie/
376
     *
377
     * @param string $field
378
     *
379
     * @return int
380
     *
381
     * @throws Exception
382
     */
383
    public function count(string $field = '*'): int
384
    {
385
        return (int)$this->aggregate('count', $field);
386
    }
387
388
    /**
389
     * Get the sum for a field in the current query
390
     *
391
     * @see Taken from the pecee-pixie library - https://github.com/skipperbent/pecee-pixie/
392
     *
393
     * @param string $field
394
     *
395
     * @return float
396
     *
397
     * @throws Exception
398
     */
399
    public function sum(string $field): float
400
    {
401
        return $this->aggregate('sum', $field);
402
    }
403
404
    /**
405
     * Get the average for a field in the current query
406
     *
407
     * @see Taken from the pecee-pixie library - https://github.com/skipperbent/pecee-pixie/
408
     *
409
     * @param string $field
410
     *
411
     * @return float
412
     *
413
     * @throws Exception
414
     */
415
    public function average(string $field): float
416
    {
417
        return $this->aggregate('avg', $field);
418
    }
419
420
    /**
421
     * Get the minimum for a field in the current query
422
     *
423
     * @see Taken from the pecee-pixie library - https://github.com/skipperbent/pecee-pixie/
424
     *
425
     * @param string $field
426
     *
427
     * @return float
428
     *
429
     * @throws Exception
430
     */
431
    public function min(string $field): float
432
    {
433
        return $this->aggregate('min', $field);
434
    }
435
436
    /**
437
     * Get the maximum for a field in the current query
438
     *
439
     * @see Taken from the pecee-pixie library - https://github.com/skipperbent/pecee-pixie/
440
     *
441
     * @param string $field
442
     *
443
     * @return float
444
     *
445
     * @throws Exception
446
     */
447
    public function max(string $field): float
448
    {
449
        return $this->aggregate('max', $field);
450
    }
451
452
    /**
453
     * @param string $type
454
     * @param bool|array<mixed, mixed> $dataToBePassed
455
     *
456
     * @return mixed
457
     *
458
     * @throws Exception
459
     */
460
    public function getQuery(string $type = 'select', $dataToBePassed = [])
461
    {
462
        $allowedTypes = ['select', 'insert', 'insertignore', 'replace', 'delete', 'update', 'criteriaonly'];
463
        if (!in_array(strtolower($type), $allowedTypes)) {
464
            throw new Exception($type . ' is not a known type.', 2);
465
        }
466
467
        $queryArr = $this->adapterInstance->$type($this->statements, $dataToBePassed);
468
469
        return $this->container->build(
470
            QueryObject::class,
471
            [$queryArr['sql'], $queryArr['bindings'], $this->dbInstance]
472
        );
473
    }
474
475
    /**
476
     * @param QueryBuilderHandler $queryBuilder
477
     * @param string|null $alias
478
     *
479
     * @return Raw
480
     */
481
    public function subQuery(QueryBuilderHandler $queryBuilder, ?string $alias = null)
482
    {
483
        $sql = '(' . $queryBuilder->getQuery()->getRawSql() . ')';
484
        if (is_string($alias) && 0 !== mb_strlen($alias)) {
485
            $sql = $sql . ' as ' . $alias;
486
        }
487
488
        return $queryBuilder->raw($sql);
489
    }
490
491
    /**
492
     * Handles the various insert operations based on the type.
493
     *
494
     * @param array<int|string, mixed|mixed[]> $data
495
     * @param string $type
496
     *
497
     * @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
498
     */
499
    private function doInsert(array $data, string $type)
500
    {
501
        $eventResult = $this->fireEvents('before-insert');
502
        if (!is_null($eventResult)) {
503
            return $eventResult;
504
        }
505
506
        // If first value is not an array () not a batch insert)
507
        if (!is_array(current($data))) {
508
            $queryObject = $this->getQuery($type, $data);
509
510
            list($preparedQuery, $executionTime) = $this->statement($queryObject->getSql(), $queryObject->getBindings());
511
            $this->dbInstance->get_results($preparedQuery);
512
513
            // Check we have a result.
514
            $return = 1 === $this->dbInstance->rows_affected ? $this->dbInstance->insert_id : null;
515
        } else {
516
            // Its a batch insert
517
            $return        = [];
518
            $executionTime = 0;
519
            foreach ($data as $subData) {
520
                $queryObject = $this->getQuery($type, $subData);
521
522
                list($preparedQuery, $time) = $this->statement($queryObject->getSql(), $queryObject->getBindings());
523
                $this->dbInstance->get_results($preparedQuery);
524
                $executionTime += $time;
525
526
                if (1 === $this->dbInstance->rows_affected) {
527
                    $return[] = $this->dbInstance->insert_id;
528
                }
529
            }
530
        }
531
532
        $this->fireEvents('after-insert', $return, $executionTime);
533
534
        return $return;
535
    }
536
537
    /**
538
     * @param array<int|string, mixed|mixed[]> $data either key=>value array for single or array of arrays for bulk
539
     *
540
     * @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
541
     */
542
    public function insert($data)
543
    {
544
        return $this->doInsert($data, 'insert');
545
    }
546
547
    /**
548
     *
549
     * @param array<int|string, mixed|mixed[]> $data either key=>value array for single or array of arrays for bulk
550
     *
551
     * @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
552
     */
553
    public function insertIgnore($data)
554
    {
555
        return $this->doInsert($data, 'insertignore');
556
    }
557
558
    /**
559
     *
560
     * @param array<int|string, mixed|mixed[]> $data either key=>value array for single or array of arrays for bulk
561
     *
562
     * @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
563
     */
564
    public function replace($data)
565
    {
566
        return $this->doInsert($data, 'replace');
567
    }
568
569
    /**
570
     * @param array<string, mixed> $data
571
     *
572
     * @return int|null
573
     */
574
    public function update($data)
575
    {
576
        $eventResult = $this->fireEvents('before-update');
577
        if (!is_null($eventResult)) {
578
            return $eventResult;
579
        }
580
        $queryObject                         = $this->getQuery('update', $data);
581
        list($preparedQuery, $executionTime) = $this->statement($queryObject->getSql(), $queryObject->getBindings());
582
583
        $this->dbInstance()->get_results($preparedQuery);
584
        $this->fireEvents('after-update', $queryObject, $executionTime);
585
586
        return 0 !== $this->dbInstance()->rows_affected
587
            ? $this->dbInstance()->rows_affected
588
            : null;
589
    }
590
591
    /**
592
     * @param array<string, mixed> $data
593
     *
594
     * @return int|null will return row id for insert and bool for success/fail on update
595
     */
596
    public function updateOrInsert($data)
597
    {
598
        if ($this->first()) {
599
            return $this->update($data);
600
        }
601
602
        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...
603
    }
604
605
    /**
606
     * @param array<string, mixed> $data
607
     *
608
     * @return static
609
     */
610
    public function onDuplicateKeyUpdate($data)
611
    {
612
        $this->addStatement('onduplicate', $data);
613
614
        return $this;
615
    }
616
617
    /**
618
     * @return int number of rows effected
619
     */
620
    public function delete(): int
621
    {
622
        $eventResult = $this->fireEvents('before-delete');
623
        if (!is_null($eventResult)) {
624
            return $eventResult;
625
        }
626
627
        $queryObject = $this->getQuery('delete');
628
629
        list($preparedQuery, $executionTime) = $this->statement($queryObject->getSql(), $queryObject->getBindings());
630
        $this->dbInstance()->get_results($preparedQuery);
631
        $this->fireEvents('after-delete', $queryObject, $executionTime);
632
633
        return $this->dbInstance()->rows_affected;
634
    }
635
636
    /**
637
     * @param string|Raw ...$tables Single table or array of tables
638
     *
639
     * @return static
640
     *
641
     * @throws Exception
642
     */
643
    public function table(...$tables): QueryBuilderHandler
644
    {
645
        $instance =  $this->constructCurrentBuilderClass($this->connection);
646
        $this->setFetchMode($this->getFetchMode(), $this->hydratorConstructorArgs);
647
        $tables = $this->addTablePrefix($tables, false);
648
        $instance->addStatement('tables', $tables);
649
650
        return $instance;
651
    }
652
653
    /**
654
     * @param string|Raw ...$tables Single table or array of tables
655
     *
656
     * @return static
657
     */
658
    public function from(...$tables): self
659
    {
660
        $tables = $this->addTablePrefix($tables, false);
661
        $this->addStatement('tables', $tables);
662
663
        return $this;
664
    }
665
666
    /**
667
     * @param string|string[]|Raw[]|array<string, string> $fields
668
     *
669
     * @return static
670
     */
671
    public function select($fields): self
672
    {
673
        if (!is_array($fields)) {
674
            $fields = func_get_args();
675
        }
676
677
        $fields = $this->addTablePrefix($fields);
678
        $this->addStatement('selects', $fields);
679
680
        return $this;
681
    }
682
683
    /**
684
     * @param string|string[]|Raw[]|array<string, string> $fields
685
     *
686
     * @return static
687
     */
688
    public function selectDistinct($fields)
689
    {
690
        $this->select($fields);
691
        $this->addStatement('distinct', true);
692
693
        return $this;
694
    }
695
696
    /**
697
     * @param string|string[] $field either the single field or an array of fields
698
     *
699
     * @return static
700
     */
701
    public function groupBy($field): self
702
    {
703
        $field = $this->addTablePrefix($field);
704
        $this->addStatement('groupBys', $field);
705
706
        return $this;
707
    }
708
709
    /**
710
     * @param string|array<string|Raw, mixed> $fields
711
     * @param string          $defaultDirection
712
     *
713
     * @return static
714
     */
715
    public function orderBy($fields, string $defaultDirection = 'ASC'): self
716
    {
717
        if (!is_array($fields)) {
718
            $fields = [$fields];
719
        }
720
721
        foreach ($fields as $key => $value) {
722
            $field = $key;
723
            $type  = $value;
724
            if (is_int($key)) {
725
                $field = $value;
726
                $type  = $defaultDirection;
727
            }
728
            if (!$field instanceof Raw) {
729
                $field = $this->addTablePrefix($field);
730
            }
731
            $this->statements['orderBys'][] = compact('field', 'type');
732
        }
733
734
        return $this;
735
    }
736
737
    /**
738
     * @param int $limit
739
     *
740
     * @return static
741
     */
742
    public function limit(int $limit): self
743
    {
744
        $this->statements['limit'] = $limit;
745
746
        return $this;
747
    }
748
749
    /**
750
     * @param int $offset
751
     *
752
     * @return static
753
     */
754
    public function offset(int $offset): self
755
    {
756
        $this->statements['offset'] = $offset;
757
758
        return $this;
759
    }
760
761
    /**
762
     * @param string|string[]|Raw|Raw[]       $key
763
     * @param string $operator
764
     * @param mixed $value
765
     * @param string $joiner
766
     *
767
     * @return static
768
     */
769
    public function having($key, string $operator, $value, string $joiner = 'AND')
770
    {
771
        $key                           = $this->addTablePrefix($key);
772
        $this->statements['havings'][] = compact('key', 'operator', 'value', 'joiner');
773
774
        return $this;
775
    }
776
777
    /**
778
     * @param string|string[]|Raw|Raw[]       $key
779
     * @param string $operator
780
     * @param mixed $value
781
     *
782
     * @return static
783
     */
784
    public function orHaving($key, $operator, $value)
785
    {
786
        return $this->having($key, $operator, $value, 'OR');
787
    }
788
789
    /**
790
     * @param string|Raw $key
791
     * @param string|mixed|null $operator Can be used as value, if 3rd arg not passed
792
     * @param mixed|null $value
793
     *
794
     * @return static
795
     */
796
    public function where($key, $operator = null, $value = null): self
797
    {
798
        // If two params are given then assume operator is =
799
        if (2 == func_num_args()) {
800
            $value    = $operator;
801
            $operator = '=';
802
        }
803
804
        return $this->whereHandler($key, $operator, $value);
805
    }
806
807
    /**
808
     * @param string|Raw $key
809
     * @param string|mixed|null $operator Can be used as value, if 3rd arg not passed
810
     * @param mixed|null $value
811
     *
812
     * @return static
813
     */
814
    public function orWhere($key, $operator = null, $value = null): self
815
    {
816
        // If two params are given then assume operator is =
817
        if (2 == func_num_args()) {
818
            $value    = $operator;
819
            $operator = '=';
820
        }
821
822
        return $this->whereHandler($key, $operator, $value, 'OR');
823
    }
824
825
    /**
826
     * @param string|Raw $key
827
     * @param string|mixed|null $operator Can be used as value, if 3rd arg not passed
828
     * @param mixed|null $value
829
     *
830
     * @return static
831
     */
832
    public function whereNot($key, $operator = null, $value = null): self
833
    {
834
        // If two params are given then assume operator is =
835
        if (2 == func_num_args()) {
836
            $value    = $operator;
837
            $operator = '=';
838
        }
839
840
        return $this->whereHandler($key, $operator, $value, 'AND NOT');
841
    }
842
843
    /**
844
     * @param string|Raw $key
845
     * @param string|mixed|null $operator Can be used as value, if 3rd arg not passed
846
     * @param mixed|null $value
847
     *
848
     * @return static
849
     */
850
    public function orWhereNot($key, $operator = null, $value = null)
851
    {
852
        // If two params are given then assume operator is =
853
        if (2 == func_num_args()) {
854
            $value    = $operator;
855
            $operator = '=';
856
        }
857
858
        return $this->whereHandler($key, $operator, $value, 'OR NOT');
859
    }
860
861
    /**
862
     * @param string|Raw $key
863
     * @param mixed[]|string|Raw $values
864
     *
865
     * @return static
866
     */
867
    public function whereIn($key, $values): self
868
    {
869
        return $this->whereHandler($key, 'IN', $values, 'AND');
870
    }
871
872
    /**
873
     * @param string|Raw $key
874
     * @param mixed[]|string|Raw $values
875
     *
876
     * @return static
877
     */
878
    public function whereNotIn($key, $values): self
879
    {
880
        return $this->whereHandler($key, 'NOT IN', $values, 'AND');
881
    }
882
883
    /**
884
     * @param string|Raw $key
885
     * @param mixed[]|string|Raw $values
886
     *
887
     * @return static
888
     */
889
    public function orWhereIn($key, $values): self
890
    {
891
        return $this->whereHandler($key, 'IN', $values, 'OR');
892
    }
893
894
    /**
895
     * @param string|Raw $key
896
     * @param mixed[]|string|Raw $values
897
     *
898
     * @return static
899
     */
900
    public function orWhereNotIn($key, $values): self
901
    {
902
        return $this->whereHandler($key, 'NOT IN', $values, 'OR');
903
    }
904
905
    /**
906
     * @param string|Raw $key
907
     * @param mixed $valueFrom
908
     * @param mixed $valueTo
909
     *
910
     * @return static
911
     */
912
    public function whereBetween($key, $valueFrom, $valueTo): self
913
    {
914
        return $this->whereHandler($key, 'BETWEEN', [$valueFrom, $valueTo], 'AND');
915
    }
916
917
    /**
918
     * @param string|Raw $key
919
     * @param mixed $valueFrom
920
     * @param mixed $valueTo
921
     *
922
     * @return static
923
     */
924
    public function orWhereBetween($key, $valueFrom, $valueTo): self
925
    {
926
        return $this->whereHandler($key, 'BETWEEN', [$valueFrom, $valueTo], 'OR');
927
    }
928
929
    /**
930
     * Handles all function call based where conditions
931
     *
932
     * @param string|Raw $key
933
     * @param string $function
934
     * @param string|mixed|null $operator Can be used as value, if 3rd arg not passed
935
     * @param mixed|null $value
936
     * @return static
937
     */
938
    protected function whereFunctionCallHandler($key, $function, $operator, $value): self
939
    {
940
        $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

940
        $key = \sprintf('%s(%s)', $function, /** @scrutinizer ignore-type */ $this->addTablePrefix($key));
Loading history...
941
        return $this->where($key, $operator, $value);
942
    }
943
944
    /**
945
     * @param string|Raw $key
946
     * @param string|mixed|null $operator Can be used as value, if 3rd arg not passed
947
     * @param mixed|null $value
948
     * @return self
949
     */
950
    public function whereMonth($key, $operator = null, $value = null): self
951
    {
952
        // If two params are given then assume operator is =
953
        if (2 == func_num_args()) {
954
            $value    = $operator;
955
            $operator = '=';
956
        }
957
        return $this->whereFunctionCallHandler($key, 'MONTH', $operator, $value);
958
    }
959
960
    /**
961
     * @param string|Raw $key
962
     * @param string|mixed|null $operator Can be used as value, if 3rd arg not passed
963
     * @param mixed|null $value
964
     * @return self
965
     */
966
    public function whereDay($key, $operator = null, $value = null): self
967
    {
968
        // If two params are given then assume operator is =
969
        if (2 == func_num_args()) {
970
            $value    = $operator;
971
            $operator = '=';
972
        }
973
        return $this->whereFunctionCallHandler($key, 'DAY', $operator, $value);
974
    }
975
976
    /**
977
     * @param string|Raw $key
978
     * @param string|mixed|null $operator Can be used as value, if 3rd arg not passed
979
     * @param mixed|null $value
980
     * @return self
981
     */
982
    public function whereYear($key, $operator = null, $value = null): self
983
    {
984
        // If two params are given then assume operator is =
985
        if (2 == func_num_args()) {
986
            $value    = $operator;
987
            $operator = '=';
988
        }
989
        return $this->whereFunctionCallHandler($key, 'YEAR', $operator, $value);
990
    }
991
992
    /**
993
     * @param string|Raw $key
994
     * @param string|mixed|null $operator Can be used as value, if 3rd arg not passed
995
     * @param mixed|null $value
996
     * @return self
997
     */
998
    public function whereDate($key, $operator = null, $value = null): self
999
    {
1000
        // If two params are given then assume operator is =
1001
        if (2 == func_num_args()) {
1002
            $value    = $operator;
1003
            $operator = '=';
1004
        }
1005
        return $this->whereFunctionCallHandler($key, 'DATE', $operator, $value);
1006
    }
1007
1008
    /**
1009
     * @param string|Raw $key
1010
     *
1011
     * @return static
1012
     */
1013
    public function whereNull($key): self
1014
    {
1015
        return $this->whereNullHandler($key);
1016
    }
1017
1018
    /**
1019
     * @param string|Raw $key
1020
     *
1021
     * @return static
1022
     */
1023
    public function whereNotNull($key): self
1024
    {
1025
        return $this->whereNullHandler($key, 'NOT');
1026
    }
1027
1028
    /**
1029
     * @param string|Raw $key
1030
     *
1031
     * @return static
1032
     */
1033
    public function orWhereNull($key): self
1034
    {
1035
        return $this->whereNullHandler($key, '', 'or');
1036
    }
1037
1038
    /**
1039
     * @param string|Raw $key
1040
     *
1041
     * @return static
1042
     */
1043
    public function orWhereNotNull($key): self
1044
    {
1045
        return $this->whereNullHandler($key, 'NOT', 'or');
1046
    }
1047
1048
    /**
1049
     * @param string|Raw $key
1050
     * @param string $prefix
1051
     * @param string $operator
1052
     *
1053
     * @return static
1054
     */
1055
    protected function whereNullHandler($key, string $prefix = '', $operator = ''): self
1056
    {
1057
        $prefix = 0 === mb_strlen($prefix) ? '' : " {$prefix}";
1058
1059
        if ($key instanceof Raw) {
1060
            $key = $this->adapterInstance->parseRaw($key);
1061
        }
1062
1063
        $key = $this->adapterInstance->wrapSanitizer($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 $value of Pixie\QueryBuilder\WPDBAdapter::wrapSanitizer() 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

1063
        $key = $this->adapterInstance->wrapSanitizer(/** @scrutinizer ignore-type */ $this->addTablePrefix($key));
Loading history...
1064
        if ($key instanceof Closure) {
1065
            throw new Exception('Key used for whereNull condition must be a string or raw exrpession.', 1);
1066
        }
1067
1068
        return $this->{$operator . 'Where'}($this->raw("{$key} IS{$prefix} NULL"));
1069
    }
1070
1071
    /**
1072
     * @param string|Raw $table
1073
     * @param string|Raw|Closure $key
1074
     * @param string|null $operator
1075
     * @param mixed $value
1076
     * @param string $type
1077
     *
1078
     * @return static
1079
     */
1080
    public function join($table, $key, ?string $operator = null, $value = null, $type = 'inner')
1081
    {
1082
        if (!$key instanceof Closure) {
1083
            $key = function ($joinBuilder) use ($key, $operator, $value) {
1084
                $joinBuilder->on($key, $operator, $value);
1085
            };
1086
        }
1087
1088
        // Build a new JoinBuilder class, keep it by reference so any changes made
1089
        // in the closure should reflect here
1090
        $joinBuilder = $this->container->build(JoinBuilder::class, [$this->connection]);
1091
        $joinBuilder = &$joinBuilder;
1092
        // Call the closure with our new joinBuilder object
1093
        $key($joinBuilder);
1094
        $table = $this->addTablePrefix($table, false);
1095
        // Get the criteria only query from the joinBuilder object
1096
        $this->statements['joins'][] = compact('type', 'table', 'joinBuilder');
1097
        return $this;
1098
    }
1099
1100
    /**
1101
     * Runs a transaction
1102
     *
1103
     * @param \Closure(Transaction):void $callback
1104
     *
1105
     * @return static
1106
     */
1107
    public function transaction(Closure $callback): self
1108
    {
1109
        try {
1110
            // Begin the transaction
1111
            $this->dbInstance->query('START TRANSACTION');
1112
1113
            // Get the Transaction class
1114
            $transaction = $this->container->build(Transaction::class, [$this->connection]);
1115
1116
            $this->handleTransactionCall($callback, $transaction);
1117
1118
            // If no errors have been thrown or the transaction wasn't completed within
1119
            $this->dbInstance->query('COMMIT');
1120
1121
            return $this;
1122
        } catch (TransactionHaltException $e) {
1123
            // Commit or rollback behavior has been handled in the closure, so exit
1124
            return $this;
1125
        } catch (\Exception $e) {
1126
            // something happened, rollback changes
1127
            $this->dbInstance->query('ROLLBACK');
1128
1129
            return $this;
1130
        }
1131
    }
1132
1133
    /**
1134
     * Handles the transaction call.
1135
     *
1136
     * Catches any WP Errors (printed)
1137
     *
1138
     * @param Closure    $callback
1139
     * @param Transaction $transaction
1140
     *
1141
     * @return void
1142
     *
1143
     * @throws Exception
1144
     */
1145
    protected function handleTransactionCall(Closure $callback, Transaction $transaction): void
1146
    {
1147
        try {
1148
            ob_start();
1149
            $callback($transaction);
1150
            $output = ob_get_clean() ?: '';
1151
        } catch (Throwable $th) {
1152
            ob_end_clean();
1153
            throw $th;
1154
        }
1155
1156
        // If we caught an error, throw an exception.
1157
        if (0 !== mb_strlen($output)) {
1158
            throw new Exception($output);
1159
        }
1160
    }
1161
1162
    /**
1163
     * @param string|Raw $table
1164
     * @param string|Raw|Closure $key
1165
     * @param string|null $operator
1166
     * @param mixed $value
1167
     *
1168
     * @return static
1169
     */
1170
    public function leftJoin($table, $key, $operator = null, $value = null)
1171
    {
1172
        return $this->join($table, $key, $operator, $value, 'left');
1173
    }
1174
1175
    /**
1176
     * @param string|Raw $table
1177
     * @param string|Raw|Closure $key
1178
     * @param string|null $operator
1179
     * @param mixed $value
1180
     *
1181
     * @return static
1182
     */
1183
    public function rightJoin($table, $key, $operator = null, $value = null)
1184
    {
1185
        return $this->join($table, $key, $operator, $value, 'right');
1186
    }
1187
1188
    /**
1189
     * @param string|Raw $table
1190
     * @param string|Raw|Closure $key
1191
     * @param string|null $operator
1192
     * @param mixed $value
1193
     *
1194
     * @return static
1195
     */
1196
    public function innerJoin($table, $key, $operator = null, $value = null)
1197
    {
1198
        return $this->join($table, $key, $operator, $value, 'inner');
1199
    }
1200
1201
    /**
1202
     * @param string|Raw $table
1203
     * @param string|Raw|Closure $key
1204
     * @param string|null $operator
1205
     * @param mixed $value
1206
     *
1207
     * @return static
1208
     */
1209
    public function crossJoin($table, $key, $operator = null, $value = null)
1210
    {
1211
        return $this->join($table, $key, $operator, $value, 'cross');
1212
    }
1213
1214
    /**
1215
     * @param string|Raw $table
1216
     * @param string|Raw|Closure $key
1217
     * @param string|null $operator
1218
     * @param mixed $value
1219
     *
1220
     * @return static
1221
     */
1222
    public function outerJoin($table, $key, $operator = null, $value = null)
1223
    {
1224
        return $this->join($table, $key, $operator, $value, 'outer');
1225
    }
1226
1227
    /**
1228
     * Shortcut to join 2 tables on the same key name with equals
1229
     *
1230
     * @param string $table
1231
     * @param string $key
1232
     * @param string $type
1233
     * @return self
1234
     * @throws Exception If base table is set as more than 1 or 0
1235
     */
1236
    public function joinUsing(string $table, string $key, string $type = 'INNER'): self
1237
    {
1238
        if (!array_key_exists('tables', $this->statements) || count($this->statements['tables']) !== 1) {
1239
            throw new Exception("JoinUsing can only be used with a single table set as the base of the query", 1);
1240
        }
1241
        $baseTable = end($this->statements['tables']);
1242
1243
        $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...
1244
        $localKey = $table = $this->addTablePrefix("{$baseTable}.{$key}", true);
1245
        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

1245
        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

1245
        return $this->join(/** @scrutinizer ignore-type */ $table, $remoteKey, '=', $localKey, $type);
Loading history...
1246
    }
1247
1248
    /**
1249
     * Add a raw query
1250
     *
1251
     * @param string|Raw $value
1252
     * @param mixed|mixed[] $bindings
1253
     *
1254
     * @return Raw
1255
     */
1256
    public function raw($value, $bindings = []): Raw
1257
    {
1258
        return new Raw($value, $bindings);
1259
    }
1260
1261
    /**
1262
     * Return wpdb instance
1263
     *
1264
     * @return wpdb
1265
     */
1266
    public function dbInstance(): wpdb
1267
    {
1268
        return $this->dbInstance;
1269
    }
1270
1271
    /**
1272
     * @param Connection $connection
1273
     *
1274
     * @return static
1275
     */
1276
    public function setConnection(Connection $connection): self
1277
    {
1278
        $this->connection = $connection;
1279
1280
        return $this;
1281
    }
1282
1283
    /**
1284
     * @return Connection
1285
     */
1286
    public function getConnection()
1287
    {
1288
        return $this->connection;
1289
    }
1290
1291
    /**
1292
     * @param string|Raw|Closure $key
1293
     * @param string|null      $operator
1294
     * @param mixed|null       $value
1295
     * @param string $joiner
1296
     *
1297
     * @return static
1298
     */
1299
    protected function whereHandler($key, $operator = null, $value = null, $joiner = 'AND')
1300
    {
1301
        $key                          = $this->addTablePrefix($key);
1302
        $this->statements['wheres'][] = compact('key', 'operator', 'value', 'joiner');
1303
        return $this;
1304
    }
1305
1306
    /**
1307
     * Add table prefix (if given) on given string.
1308
     *
1309
     * @param array<string|int, string|int|float|bool|Raw|Closure>|string|int|float|bool|Raw|Closure     $values
1310
     * @param bool $tableFieldMix If we have mixes of field and table names with a "."
1311
     *
1312
     * @return mixed|mixed[]
1313
     */
1314
    public function addTablePrefix($values, bool $tableFieldMix = true)
1315
    {
1316
        if (is_null($this->tablePrefix)) {
1317
            return $values;
1318
        }
1319
1320
        // $value will be an array and we will add prefix to all table names
1321
1322
        // If supplied value is not an array then make it one
1323
        $single = false;
1324
        if (!is_array($values)) {
1325
            $values = [$values];
1326
            // We had single value, so should return a single value
1327
            $single = true;
1328
        }
1329
1330
        $return = [];
1331
1332
        foreach ($values as $key => $value) {
1333
            // It's a raw query, just add it to our return array and continue next
1334
            if ($value instanceof Raw || $value instanceof Closure) {
1335
                $return[$key] = $value;
1336
                continue;
1337
            }
1338
1339
            // If key is not integer, it is likely a alias mapping,
1340
            // so we need to change prefix target
1341
            $target = &$value;
1342
            if (!is_int($key)) {
1343
                $target = &$key;
1344
            }
1345
1346
            if (!$tableFieldMix || (is_string($target) && false !== strpos($target, '.'))) {
1347
                $target = $this->tablePrefix . $target;
1348
            }
1349
1350
            $return[$key] = $value;
1351
        }
1352
1353
        // If we had single value then we should return a single value (end value of the array)
1354
        return true === $single ? end($return) : $return;
1355
    }
1356
1357
    /**
1358
     * @param string $key
1359
     * @param mixed|mixed[]|bool $value
1360
     *
1361
     * @return void
1362
     */
1363
    protected function addStatement($key, $value)
1364
    {
1365
        if (!is_array($value)) {
1366
            $value = [$value];
1367
        }
1368
1369
        if (!array_key_exists($key, $this->statements)) {
1370
            $this->statements[$key] = $value;
1371
        } else {
1372
            $this->statements[$key] = array_merge($this->statements[$key], $value);
1373
        }
1374
    }
1375
1376
    /**
1377
     * @param string $event
1378
     * @param string|Raw $table
1379
     *
1380
     * @return callable|null
1381
     */
1382
    public function getEvent(string $event, $table = ':any'): ?callable
1383
    {
1384
        return $this->connection->getEventHandler()->getEvent($event, $table);
1385
    }
1386
1387
    /**
1388
     * @param string $event
1389
     * @param string|Raw $table
1390
     * @param Closure $action
1391
     *
1392
     * @return void
1393
     */
1394
    public function registerEvent($event, $table, Closure $action): void
1395
    {
1396
        $table = $table ?: ':any';
1397
1398
        if (':any' != $table) {
1399
            $table = $this->addTablePrefix($table, false);
1400
        }
1401
1402
        $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

1402
        $this->connection->getEventHandler()->registerEvent($event, /** @scrutinizer ignore-type */ $table, $action);
Loading history...
1403
    }
1404
1405
    /**
1406
     * @param string $event
1407
     * @param string|Raw $table
1408
     *
1409
     * @return void
1410
     */
1411
    public function removeEvent(string $event, $table = ':any')
1412
    {
1413
        if (':any' != $table) {
1414
            $table = $this->addTablePrefix($table, false);
1415
        }
1416
1417
        $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

1417
        $this->connection->getEventHandler()->removeEvent($event, /** @scrutinizer ignore-type */ $table);
Loading history...
1418
    }
1419
1420
    /**
1421
     * @param string $event
1422
     *
1423
     * @return mixed
1424
     */
1425
    public function fireEvents(string $event)
1426
    {
1427
        $params = func_get_args(); // @todo Replace this with an easier to read alteratnive
1428
        array_unshift($params, $this);
1429
1430
        return call_user_func_array([$this->connection->getEventHandler(), 'fireEvents'], $params);
1431
    }
1432
1433
    /**
1434
     * @return array<string, mixed[]>
1435
     */
1436
    public function getStatements()
1437
    {
1438
        return $this->statements;
1439
    }
1440
1441
    /**
1442
     * @return string will return WPDB Fetch mode
1443
     */
1444
    public function getFetchMode()
1445
    {
1446
        return null !== $this->fetchMode
1447
            ? $this->fetchMode
1448
            : \OBJECT;
1449
    }
1450
}
1451