• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

ICanBoogie / ActiveRecord / 8366801652

20 Mar 2024 10:35PM UTC coverage: 84.036% (-1.7%) from 85.731%
8366801652

push

github

olvlvl
Remove query method forwarding and ArrayAccessor implementation

3 of 3 new or added lines in 3 files covered. (100.0%)

93 existing lines in 5 files now uncovered.

1395 of 1660 relevant lines covered (84.04%)

21.2 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

68.38
/lib/ActiveRecord/Query.php
1
<?php
2

3
/*
4
 * This file is part of the ICanBoogie package.
5
 *
6
 * (c) Olivier Laviale <olivier.laviale@gmail.com>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11

12
namespace ICanBoogie\ActiveRecord;
13

14
use ArrayIterator;
15
use DateTimeInterface;
16
use ICanBoogie\ActiveRecord;
17
use ICanBoogie\DateTime;
18
use ICanBoogie\Prototype\MethodNotDefined;
19
use ICanBoogie\PrototypeTrait;
20
use InvalidArgumentException;
21
use IteratorAggregate;
22
use JetBrains\PhpStorm\Deprecated;
23
use LogicException;
24
use PDO;
25
use ReflectionClass;
26
use ReflectionMethod;
27
use Traversable;
28

29
use function array_combine;
30
use function array_fill;
31
use function array_map;
32
use function array_merge;
33
use function array_shift;
34
use function count;
35
use function explode;
36
use function func_get_arg;
37
use function func_get_args;
38
use function func_num_args;
39
use function get_class;
40
use function implode;
41
use function is_array;
42
use function is_numeric;
43
use function is_string;
44
use function preg_replace;
45
use function preg_replace_callback;
46
use function reset;
47
use function strpos;
48
use function substr;
49

50
use const PHP_INT_MAX;
51

52
/**
53
 * The class offers many features to compose model queries. Most query related
54
 * methods of the {@link Model} class create a {@link Query} object that is returned for
55
 * further specification, such as filters or limits.
56
 *
57
 * @method Query and ($conditions, $conditions_args = null, $_ = null) Alias to where().
58
 *
59
 * @property-read array $all An array with all the records matching the query.
60
 * @property-read mixed $one The first record matching the query.
61
 * @property-read array $pairs An array of key/value pairs.
62
 * @property-read array $rc The first column of the first row matching the query.
63
 * @property-read int $count The number of records matching the query.
64
 * @property-read bool|array $exists `true` if a record matching the query exists, `false`
65
 * otherwise. If there is multiple records, the property is an array of booleans.
66
 *
67
 * @property-read Model $model The target model of the query.
68
 * @property-read array $joints The joints collection from {@link join()}.
69
 * @property-read array $joints_args The arguments to the joints.
70
 * @property-read array $conditions The collected conditions.
71
 * @property-read array $conditions_args The arguments to the conditions.
72
 * @property-read array $having_args The arguments to the `HAVING` clause.
73
 * @property-read array $args Returns the arguments to the query.
74
 * @property-read Query $prepared Return a prepared query.
75
 *
76
 * @template TRecord of ActiveRecord
77
 *
78
 * @implements IteratorAggregate<TRecord>
79
 */
80
class Query implements IteratorAggregate
81
{
82
    use PrototypeTrait {
83
        PrototypeTrait::__call as private __prototype_call;
84
    }
85

86
    public const LIMIT_MAX = PHP_INT_MAX;
87

88
    /**
89
     * Part of the `SELECT` clause.
90
     *
91
     * @var string
92
     */
93
    private $select;
94

95
    /**
96
     * `JOIN` clauses.
97
     *
98
     * @var array
99
     * @uses get_joints
100
     */
101
    private $joints = [];
102

103
    private function get_joints(): array
104
    {
105
        return $this->joints;
2✔
106
    }
107

108
    /**
109
     * Joints arguments.
110
     *
111
     * @var array
112
     * @uses get_joints_args
113
     * @uses get_args
114
     */
115
    private $joints_args = [];
116

117
    private function get_joints_args(): array
118
    {
119
        return $this->joints_args;
1✔
120
    }
121

122
    /**
123
     * Collected conditions.
124
     *
125
     * @var array
126
     * @uses get_conditions
127
     */
128
    private $conditions = [];
129

130
    private function get_conditions(): array
131
    {
132
        return $this->conditions;
1✔
133
    }
134

135
    /**
136
     * Arguments for the conditions.
137
     *
138
     * @var array
139
     * @uses get_conditions_args
140
     * @uses get_args
141
     */
142
    private $conditions_args = [];
143

144
    private function get_conditions_args(): array
145
    {
146
        return $this->conditions_args;
2✔
147
    }
148

149
    /**
150
     * Part of the `HAVING` clause.
151
     *
152
     * @var string
153
     */
154
    private $having;
155

156
    /**
157
     * Arguments to the `HAVING` clause.
158
     *
159
     * @var array
160
     * @uses get_having_args
161
     * @uses get_args
162
     */
163
    private $having_args = [];
164

165
    private function get_having_args(): array
166
    {
UNCOV
167
        return $this->having_args;
×
168
    }
169

170
    /**
171
     * Returns the arguments to the query, which include joints arguments, conditions arguments,
172
     * and _having_ arguments.
173
     *
174
     * @return array
175
     */
176
    private function get_args(): array
177
    {
178
        return array_merge($this->joints_args, $this->conditions_args, $this->having_args);
23✔
179
    }
180

181
    /**
182
     * Part of the `GROUP BY` clause.
183
     *
184
     * @var string
185
     */
186
    private $group;
187

188
    /**
189
     * Part of the `ORDER BY` clause.
190
     *
191
     * @var mixed
192
     */
193
    private $order;
194

195
    /**
196
     * The number of records the skip before fetching.
197
     *
198
     * @var int
199
     */
200
    private $offset;
201

202
    /**
203
     * The maximum number of records to fetch.
204
     *
205
     * @var int
206
     */
207
    private $limit;
208

209
    /**
210
     * Fetch mode.
211
     *
212
     * @var mixed
213
     */
214
    private $mode;
215

216
    /**
217
     * The target model of the query.
218
     *
219
     * @var Model
220
     * @uses get_model
221
     */
222
    private $model;
223

224
    private function get_model(): Model
225
    {
226
        return $this->model;
4✔
227
    }
228

229
    /**
230
     * @param Model $model The model to query.
231
     */
232
    public function __construct(Model $model)
233
    {
234
        $this->model = $model;
35✔
235
    }
236

237
    /**
238
     * Override the method to handle magic 'filter_by_' methods.
239
     *
240
     * @inheritdoc
241
     */
242
    public function __call($method, $arguments)
243
    {
244
        if ($method === 'and') {
2✔
245
            return $this->where(...$arguments);
1✔
246
        }
247

248
        if (strpos($method, 'filter_by_') === 0) {
2✔
249
            return $this->dynamic_filter(substr($method, 10), $arguments); // 10 is for: strlen('filter_by_')
2✔
250
        }
251

252
        try {
UNCOV
253
            return self::__prototype_call($method, $arguments);
×
UNCOV
254
        } catch (MethodNotDefined $e) {
×
UNCOV
255
            throw new ScopeNotDefined($method, $this->model, $e);
×
256
        }
257
    }
258

259
    /*
260
     * Rendering
261
     */
262

263
    /**
264
     * Convert the query into a string.
265
     *
266
     * @return string
267
     */
268
    public function __toString(): string
269
    {
270
        return $this->resolve_statement(
28✔
271
            $this->render_select() . ' ' .
28✔
272
            $this->render_from() .
28✔
273
            $this->render_main()
28✔
274
        );
28✔
275
    }
276

277
    /**
278
     * Render the `SELECT` clause.
279
     *
280
     * @return string
281
     */
282
    private function render_select(): string
283
    {
284
        return 'SELECT ' . ($this->select ? $this->select : '*');
28✔
285
    }
286

287
    /**
288
     * Render the `FROM` clause.
289
     *
290
     * The rendered `FROM` clause might include some JOINS too.
291
     *
292
     * @return string
293
     */
294
    private function render_from(): string
295
    {
296
        return 'FROM {self_and_related}';
28✔
297
    }
298

299
    /**
300
     * Renders the `JOIN` clauses.
301
     *
302
     * @return string
303
     */
304
    private function render_joints(): string
305
    {
306
        return implode(' ', $this->joints);
6✔
307
    }
308

309
    /**
310
     * Render the main body of the query, without the `SELECT` and `FROM` clauses.
311
     *
312
     * @return string
313
     */
314
    private function render_main(): string
315
    {
316
        $query = '';
28✔
317

318
        if ($this->joints) {
28✔
319
            $query = ' ' . $this->render_joints();
6✔
320
        }
321

322
        $conditions = $this->conditions;
28✔
323

324
        if ($conditions) {
28✔
325
            $query .= ' WHERE ' . implode(' AND ', $conditions);
20✔
326
        }
327

328
        $group = $this->group;
28✔
329

330
        if ($group) {
28✔
331
            $query .= ' GROUP BY ' . $group;
2✔
332

333
            $having = $this->having;
2✔
334

335
            if ($having) {
2✔
UNCOV
336
                $query .= ' HAVING ' . $having;
×
337
            }
338
        }
339

340
        $order = $this->order;
28✔
341

342
        if ($order) {
28✔
343
            $query .= ' ' . $this->render_order($order);
6✔
344
        }
345

346
        $offset = $this->offset;
28✔
347
        $limit = $this->limit;
28✔
348

349
        if ($offset || $limit) {
28✔
350
            $query .= ' ' . $this->render_offset_and_limit($offset, $limit);
12✔
351
        }
352

353
        return $query;
28✔
354
    }
355

356
    /**
357
     * Render the `ORDER` clause.
358
     */
359
    private function render_order(array $order): string
360
    {
361
        if (count($order) == 1) {
6✔
362
            $raw = $order[0];
5✔
363
            $rendered = preg_replace(
5✔
364
                '/-([a-zA-Z0-9_]+)/',
5✔
365
                '$1 DESC',
5✔
366
                $raw
5✔
367
            );
5✔
368

369
            return 'ORDER BY ' . $rendered;
5✔
370
        }
371

372
        $connection = $this->model->connection;
1✔
373

374
        $field = array_shift($order);
1✔
375
        $field_values = is_array($order[0]) ? $order[0] : $order;
1✔
376
        $field_values = array_map(function ($v) use ($connection) {
1✔
377
            return $connection->pdo->quote($v);
1✔
378
        }, $field_values);
1✔
379

380
        return "ORDER BY FIELD($field, " . implode(', ', $field_values) . ")";
1✔
381
    }
382

383
    /**
384
     * Render the `LIMIT` and `OFFSET` clauses.
385
     *
386
     * @param int $offset
387
     * @param int $limit
388
     *
389
     * @return string
390
     */
391
    private function render_offset_and_limit($offset, $limit): string
392
    {
393
        if ($offset && $limit) {
12✔
UNCOV
394
            return "LIMIT $offset, $limit";
×
395
        } else {
396
            if ($offset) {
12✔
UNCOV
397
                return "LIMIT $offset, " . self::LIMIT_MAX;
×
398
            } else {
399
                if ($limit) {
12✔
400
                    return "LIMIT $limit";
12✔
401
                }
402
            }
403
        }
404

UNCOV
405
        return '';
×
406
    }
407

408
    /*
409
     *
410
     */
411

412
    /**
413
     * Resolve the placeholders of a statement.
414
     *
415
     * Note: Currently, the method simply forwards the statement to the model's
416
     * resolve_statement() method.
417
     *
418
     * @param string $statement
419
     *
420
     * @return string
421
     */
422
    private function resolve_statement(string $statement): string
423
    {
424
        return $this->model->resolve_statement($statement);
28✔
425
    }
426

427
    /**
428
     * Define the `SELECT` clause.
429
     *
430
     * @param string $expression The expression of the `SELECT` clause. e.g. 'nid, title'.
431
     *
432
     * @return $this
433
     */
434
    public function select($expression): self
435
    {
436
        $this->select = $expression;
7✔
437

438
        return $this;
7✔
439
    }
440

441
    /**
442
     * Add a `JOIN` clause.
443
     *
444
     * @param ?string $expression
445
     *     A raw `JOIN` clause.
446
     * @param ?Query<ActiveRecord> $query
447
     *     A {@link Query} instance, it is rendered as a string and used as a subquery of the `JOIN` clause.
448
     *     The `$options` parameter can be used to customize the output.
449
     * @param ?class-string<ActiveRecord> $with
450
     * @param ?class-string<Model> $model_class
451
     *     A model class.
452
     * @param ?Model $model
453
     *     A model.
454
     * @param ?non-empty-string $mode
455
     *     Join mode. Default: "INNER"
456
     * @param ?non-empty-string $as
457
     *     The alias of the subquery. Default: The query's model alias.
458
     * @param ?non-empty-string $on
459
     *     The column on which to joint is created. Default: The query's model primary key.
460
     *
461
     * @return $this
462
     *
463
     * <pre>
464
     * <?php
465
     *
466
     * # using a model identifier
467
     *
468
     * $query->join(model_id: 'nodes');
469
     *
470
     * # using a subquery
471
     *
472
     * $subquery = get_model('updates')
473
     * ->select('updated_at, $subscriber_id, update_hash')
474
     * ->order('updated_at DESC')
475
     *
476
     * $query->join(query: $subquery, on: 'subscriber_id');
477
     *
478
     * # using a raw clause
479
     *
480
     * $query->join(expression: "INNER JOIN `articles` USING(`nid`)");
481
     * </pre>
482
     */
483
    public function join(
484
        string $expression = null,
485
        Query $query = null,
486
        string $with = null,
487
        string $mode = 'INNER',
488
        string $as = null,
489
        string $on = null,
490
    ): self {
491
        if ($expression) {
7✔
492
            $this->joints[] = $expression;
4✔
493

494
            return $this;
4✔
495
        }
496

497
        if ($query) {
3✔
498
            $this->join_with_query($query, mode: $mode, as: $as, on: $on);
2✔
499

500
            return $this;
2✔
501
        }
502

503
        if ($with) {
1✔
504
            $model = $this->model->models->model_for_record($with);
1✔
505

506
            $this->join_with_model($model, mode: $mode, as: $as, on: $on);
1✔
507

508
            return $this;
1✔
509
        }
510

UNCOV
511
        throw new LogicException("One of [ expression, query, record ] needs to be defined");
×
512
    }
513

514
    /**
515
     * Join a subquery to the query.
516
     *
517
     * @param Query<ActiveRecord> $query
518
     * @param string $mode
519
     *     Join mode. Default: "INNER".
520
     * @param ?string $as
521
     *     The alias of the subquery. Default: The query's model alias.
522
     * @param ?string $on
523
     *     The column on which the joint is created. Default: The query's model primary key.
524
     */
525
    private function join_with_query(
526
        Query $query,
527
        string $mode = 'INNER',
528
        string $as = null,
529
        string $on = null,
530
    ): void {
531
        $as ??= $query->model->alias;
2✔
532
        $on ??= $query->model->primary;
2✔
533

534
        if ($on) {
2✔
535
            assert(is_string($on));
536

537
            $on = $this->render_join_on($on, $as, $query);
2✔
538
        }
539

540
        if ($on) {
2✔
541
            $on = ' ' . $on;
2✔
542
        }
543

544
        $this->joints[] = "$mode JOIN($query) `$as`{$on}";
2✔
545
        $this->joints_args = array_merge($this->joints_args, $query->args);
2✔
546
    }
547

548
    /**
549
     * Join a model to the query.
550
     *
551
     * @param Model<int|string|string[], ActiveRecord> $model
552
     * @param non-empty-string $mode
553
     *     Join mode.
554
     * @param ?non-empty-string $as
555
     *     The alias of the model. Default: The model's alias.
556
     * @param ?non-empty-string $on
557
     *     The column on which the joint is created, or an _ON_ expression. Default: The model's primary key. @todo
558
     */
559
    private function join_with_model( // @phpstan-ignore-line
560
        Model $model,
561
        string $mode = 'INNER',
562
        string $as = null,
563
        string $on = null,
564
    ): void {
565
        $as ??= $model->alias;
1✔
566
        //phpcs:disable PSR2.Methods.FunctionCallSignature.SpaceBeforeOpenBracket
567
        $on ??= (function () use ($model): string {
1✔
568
            $primary = $this->model->primary;
1✔
569
            $model_schema = $model->extended_schema;
1✔
570

571
            assert(is_array($primary) || is_string($primary));
572

573
            if (is_array($primary)) {
1✔
UNCOV
574
                foreach ($primary as $column) {
×
UNCOV
575
                    if ($model_schema->has_column($column)) {
×
UNCOV
576
                        return $column;
×
577
                    }
578
                }
579
            } elseif (!$model_schema->has_column($primary)) {
1✔
580
                $primary = $model_schema->primary;
1✔
581

582
                if (is_array($primary)) {
1✔
UNCOV
583
                    $primary = reset($primary);
×
584
                }
585
            }
586

587
            assert(is_string($primary));
588

589
            return $primary;
1✔
590
        }) ();
1✔
591

592
        $this->joints[] = "$mode JOIN `$model->name` AS `$as` USING(`$on`)";
1✔
593
    }
594

595
    /**
596
     * Render the `on` join option.
597
     *
598
     * The method tries to determine the best solution between `ON` and `USING`.
599
     *
600
     * @param string $column
601
     * @param string $as
602
     * @param Query<ActiveRecord> $query
603
     */
604
    private function render_join_on(string $column, string $as, Query $query): string
605
    {
606
        if ($query->model->schema->has_column($column) && $this->model->schema->has_column($column)) {
2✔
607
            return "USING(`$column`)";
2✔
608
        }
609

UNCOV
610
        $target = $this->model;
×
611

UNCOV
612
        while ($target) {
×
UNCOV
613
            if ($target->schema->has_column($column)) {
×
UNCOV
614
                break;
×
615
            }
616

UNCOV
617
            $target = $target->parent_model;
×
618
        }
619

UNCOV
620
        if (!$target) {
×
UNCOV
621
            $model_class = $this->model::class;
×
622

UNCOV
623
            throw new InvalidArgumentException("Unable to resolve column `$column` from model {$model_class}");
×
624
        }
625

UNCOV
626
        return "ON `$as`.`$column` = `{$target->alias}`.`$column`";
×
627
    }
628

629
    /**
630
     * Parse the conditions for the {@link where()} and {@link having()} methods.
631
     *
632
     * {@link DateTimeInterface} conditions are converted to strings.
633
     *
634
     * @param $conditions_and_args
635
     *
636
     * @return array An array made of the condition string and its arguments.
637
     */
638
    private function deferred_parse_conditions(...$conditions_and_args): array
639
    {
640
        $conditions = array_shift($conditions_and_args);
24✔
641
        $args = $conditions_and_args;
24✔
642

643
        if (is_array($conditions)) {
24✔
644
            $c = '';
19✔
645
            $conditions_args = [];
19✔
646

647
            foreach ($conditions as $column => $arg) {
19✔
648
                if (is_array($arg) || $arg instanceof self) {
19✔
649
                    $joined = '';
4✔
650

651
                    if (is_array($arg)) {
4✔
652
                        foreach ($arg as $value) {
4✔
653
                            $joined .= ',' . (is_numeric($value) ? $value : $this->model->quote($value));
4✔
654
                        }
655

656
                        $joined = substr($joined, 1);
4✔
657
                    } else {
UNCOV
658
                        $joined = (string)$arg;
×
UNCOV
659
                        $conditions_args = array_merge($conditions_args, $arg->args);
×
660
                    }
661

662
                    $c .= ' AND `' . ($column[0] == '!' ? substr($column, 1) . '` NOT' : $column . '`')
4✔
663
                        . ' IN(' . $joined . ')';
4✔
664
                } else {
665
                    $conditions_args[] = $arg;
15✔
666

667
                    $c .= ' AND `' . ($column[0] == '!' ? substr($column, 1) . '` !' : $column . '` ')
15✔
668
                        . '= ?';
15✔
669
                }
670
            }
671

672
            $conditions = substr($c, 5);
19✔
673
        } else {
674
            $conditions_args = [];
8✔
675

676
            if ($args) {
8✔
677
                if (is_array($args[0])) {
5✔
UNCOV
678
                    $conditions_args = $args[0];
×
679
                } else {
680
                    #
681
                    # We dereference values otherwise the caller would get a corrupted array.
682
                    #
683

684
                    foreach ($args as $key => $value) {
5✔
685
                        $conditions_args[$key] = $value;
5✔
686
                    }
687
                }
688
            }
689
        }
690

691
        foreach ($conditions_args as &$value) {
24✔
692
            if ($value instanceof DateTimeInterface) {
17✔
693
                $value = DateTime::from($value)->utc->as_db;
1✔
694
            }
695
        }
696

697
        return [ $conditions ? '(' . $conditions . ')' : null, $conditions_args ];
24✔
698
    }
699

700
    /**
701
     * Handles dynamic filters.
702
     *
703
     * @param string $filter
704
     * @param array $conditions_args
705
     *
706
     * @return $this
707
     */
708
    private function dynamic_filter(string $filter, array $conditions_args = []): self
709
    {
710
        $conditions = explode('_and_', $filter);
2✔
711

712
        return $this->where(array_combine($conditions, $conditions_args));
2✔
713
    }
714

715
    /**
716
     * Add conditions to the SQL statement.
717
     *
718
     * Conditions can either be specified as string or array.
719
     *
720
     * 1. Pure string conditions
721
     *
722
     * If you'de like to add conditions to your statement, you could just specify them in there,
723
     * just like `$model->where('order_count = 2');`. This will find all the entries, where the
724
     * `order_count` field's value is 2.
725
     *
726
     * 2. Array conditions
727
     *
728
     * Now what if that number could vary, say as an argument from somewhere, or perhaps from the
729
     * user’s level status somewhere? The find then becomes something like:
730
     *
731
     * `$model->where('order_count = ?', 2);`
732
     *
733
     * or
734
     *
735
     * `$model->where([ 'order_count' => 2 ]);`
736
     *
737
     * Or if you want to specify two conditions, you can do it like:
738
     *
739
     * `$model->where('order_count = ? AND locked = ?', 2, false);`
740
     *
741
     * or
742
     *
743
     * `$model->where([ 'order_count' => 2, 'locked' => false ]);`
744
     *
745
     * Or if you want to specify subset conditions:
746
     *
747
     * `$model->where([ 'order_id' => [ 123, 456, 789 ] ]);`
748
     *
749
     * This will return the orders with the `order_id` 123, 456 or 789.
750
     *
751
     * 3. Modifiers
752
     *
753
     * When using the "identifier" => "value" notation, you can switch the comparison method by
754
     * prefixing the identifier with a bang "!"
755
     *
756
     * `$model->where([ '!order_id' => [ 123, 456, 789 ]]);`
757
     *
758
     * This will return the orders with the `order_id` different than 123, 456 and 789.
759
     *
760
     * `$model->where([ '!order_count' => 2 ];`
761
     *
762
     * This will return the orders with the `order_count` different than 2.
763
     *
764
     * @param mixed ...$conditions_and_args
765
     *
766
     * @return $this
767
     */
768
    public function where(...$conditions_and_args): self
769
    {
770
        [ $conditions, $conditions_args ] = $this->deferred_parse_conditions(...$conditions_and_args);
24✔
771

772
        if ($conditions) {
24✔
773
            $this->conditions[] = $conditions;
23✔
774

775
            if ($conditions_args) {
23✔
776
                $this->conditions_args = array_merge($this->conditions_args, $conditions_args);
17✔
777
            }
778
        }
779

780
        return $this;
24✔
781
    }
782

783
    /**
784
     * Defines the `ORDER` clause.
785
     *
786
     * @param string $order_or_field_name The order for the `ORDER` clause e.g.
787
     * 'weight, date DESC', or field to order with, in which case `$field_values` is required.
788
     * @param array $field_values Values of the field specified by `$order_or_field_name`.
789
     *
790
     * @return $this
791
     */
792
    public function order($order_or_field_name, $field_values = null)
793
    {
794
        $this->order = func_get_args();
6✔
795

796
        return $this;
6✔
797
    }
798

799
    /**
800
     * Defines the `GROUP` clause.
801
     *
802
     * @param string $group
803
     *
804
     * @return $this
805
     */
806
    public function group($group)
807
    {
808
        $this->group = $group;
2✔
809

810
        return $this;
2✔
811
    }
812

813
    /**
814
     * Defines the `HAVING` clause.
815
     *
816
     * @param mixed ...$conditions_and_args
817
     *
818
     * @return $this
819
     */
820
    public function having(...$conditions_and_args)
821
    {
UNCOV
822
        list($having, $having_args) = $this->deferred_parse_conditions(...$conditions_and_args);
×
823

UNCOV
824
        $this->having = $having;
×
UNCOV
825
        $this->having_args = $having_args;
×
826

UNCOV
827
        return $this;
×
828
    }
829

830
    /**
831
     * Define the offset of the `LIMIT` clause.
832
     *
833
     * @param $offset
834
     *
835
     * @return $this
836
     */
837
    public function offset($offset)
838
    {
UNCOV
839
        $this->offset = (int)$offset;
×
840

UNCOV
841
        return $this;
×
842
    }
843

844
    /**
845
     * Apply the limit and/or offset to the SQL fired.
846
     *
847
     * You can use the limit to specify the number of records to be retrieved, ad use the offset to
848
     * specify the number of records to skip before starting to return records:
849
     *
850
     *     $model->limit(10);
851
     *
852
     * Will return a maximum of 10 clients and because ti specifies no offset it will return the
853
     * first 10 in the table:
854
     *
855
     *     $model->limit(5, 10);
856
     *
857
     * Will return a maximum of 10 clients beginning with the 5th.
858
     *
859
     * @param int $limit
860
     *
861
     * @return $this
862
     */
863
    public function limit($limit)
864
    {
865
        $offset = null;
1✔
866

867
        if (func_num_args() == 2) {
1✔
UNCOV
868
            $offset = $limit;
×
UNCOV
869
            $limit = func_get_arg(1);
×
870
        }
871

872
        $this->offset = (int)$offset;
1✔
873
        $this->limit = (int)$limit;
1✔
874

875
        return $this;
1✔
876
    }
877

878
    /**
879
     * Set the fetch mode for the query.
880
     *
881
     * @param mixed ...$mode
882
     *
883
     * @return $this
884
     *
885
     * @see http://www.php.net/manual/en/pdostatement.setfetchmode.php
886
     */
887
    public function mode(...$mode): self
888
    {
889
        $this->mode = $mode;
3✔
890

891
        return $this;
3✔
892
    }
893

894
    /**
895
     * Prepare the query.
896
     *
897
     * We use the connection's prepare() method because the statement has already been resolved
898
     * during the __toString() method and we don't want for the statement to be parsed twice.
899
     *
900
     * @return Statement
901
     */
902
    private function prepare(): Statement
903
    {
904
        return $this->model->connection->prepare((string)$this);
17✔
905
    }
906

907
    /**
908
     * Return a prepared query.
909
     *
910
     * @return Statement
911
     */
912
    protected function get_prepared(): Statement
913
    {
UNCOV
914
        return $this->prepare();
×
915
    }
916

917
    /**
918
     * Prepare and executes the query.
919
     *
920
     * @return Statement
921
     */
922
    public function query(): Statement
923
    {
924
        $statement = $this->prepare();
17✔
925
        $statement->execute($this->args);
17✔
926

927
        return $statement;
17✔
928
    }
929

930
    /*
931
     * FINISHER
932
     */
933

934
    /**
935
     * Resolves fetch mode.
936
     *
937
     * @param mixed ...$mode
938
     *
939
     * @return array<mixed>
940
     */
941
    private function resolve_fetch_mode(...$mode): array
942
    {
943
        if ($mode) {
17✔
UNCOV
944
            $args = $mode;
×
945
        } elseif ($this->mode) {
17✔
946
            $args = $this->mode;
1✔
947
        } elseif ($this->select) {
17✔
UNCOV
948
            $args = [ PDO::FETCH_ASSOC ];
×
949
        } elseif ($this->model->activerecord_class) {
17✔
950
            $args = [ PDO::FETCH_CLASS, $this->model->activerecord_class, [ $this->model ] ];
17✔
951
        } else {
UNCOV
952
            $args = [ PDO::FETCH_CLASS, ActiveRecord::class, [ $this->model ] ];
×
953
        }
954

955
        return $args;
17✔
956
    }
957

958
    /**
959
     * Execute the query and returns an array of records.
960
     *
961
     * @param mixed ...$mode Fetch mode.
962
     *
963
     * @return array<mixed>
964
     */
965
    public function all(...$mode): array
966
    {
967
        return $this->query()->pdo_statement->fetchAll(...$this->resolve_fetch_mode(...$mode));
8✔
968
    }
969

970
    /**
971
     * Getter for the {@link $all} magic property.
972
     *
973
     * @return array<mixed>
974
     */
975
    protected function get_all(): array
976
    {
977
        return $this->all();
7✔
978
    }
979

980
    /**
981
     * Return the first result of the query and close the cursor.
982
     *
983
     * @param mixed ...$mode Fetch node.
984
     *
985
     * @return mixed The return value of this function on success depends on the fetch mode. In
986
     * all cases, FALSE is returned on failure.
987
     */
988
    public function one(...$mode)
989
    {
990
        $query = clone $this;
11✔
991
        $query->limit = 1;
11✔
992
        $statement = $query->query();
11✔
993
        $args = $query->resolve_fetch_mode(...$mode);
11✔
994

995
        if (count($args) > 1 && $args[0] == PDO::FETCH_CLASS) {
11✔
996
            array_shift($args);
11✔
997

998
            $rc = $statement->pdo_statement->fetchObject(...$args);
11✔
999

1000
            $statement->pdo_statement->closeCursor();
11✔
1001

1002
            return $rc;
11✔
1003
        }
1004

UNCOV
1005
        return $statement->one(...$args);
×
1006
    }
1007

1008
    /**
1009
     * Getter for the {@link $one} magic property.
1010
     *
1011
     * @return mixed
1012
     *
1013
     * @see one()
1014
     */
1015
    protected function get_one()
1016
    {
1017
        return $this->one();
11✔
1018
    }
1019

1020
    /**
1021
     * Execute que query and return an array of key/value pairs, where the key is the value of
1022
     * the first column and the value of the key the value of the second column.
1023
     *
1024
     * @return array
1025
     */
1026
    protected function get_pairs(): array
1027
    {
UNCOV
1028
        return $this->all(PDO::FETCH_KEY_PAIR);
×
1029
    }
1030

1031
    /**
1032
     * Return the value of the first column of the first row.
1033
     *
1034
     * @return mixed
1035
     */
1036
    protected function get_rc()
1037
    {
1038
        $previous_limit = $this->limit;
1✔
1039

1040
        $this->limit = 1;
1✔
1041

1042
        $statement = $this->query();
1✔
1043

1044
        $this->limit = $previous_limit;
1✔
1045

1046
        return $statement->rc;
1✔
1047
    }
1048

1049
    /**
1050
     * Check the existence of records in the model.
1051
     *
1052
     * $model->exists;
1053
     * $model->where('name = "max"')->exists;
1054
     * $model->exists(1);
1055
     * $model->exists(1, 2);
1056
     * $model->exists([ 1, 2 ]);
1057
     *
1058
     * @param mixed $key
1059
     *
1060
     * @return bool|array
1061
     */
1062
    public function exists($key = null)
1063
    {
1064
        if ($key !== null && func_num_args() > 1) {
1✔
UNCOV
1065
            $key = func_get_args();
×
1066
        }
1067

1068
        $query = clone $this;
1✔
1069

1070
        #
1071
        # Checking if the query matches any record.
1072
        #
1073

1074
        if ($key === null) {
1✔
1075
            return !!$query
1✔
1076
                ->select('1')
1✔
1077
                ->limit(1)
1✔
1078
                ->rc;
1✔
1079
        }
1080

1081
        #
1082
        # Checking if the query matches the specified record keys.
1083
        #
1084

UNCOV
1085
        $rc = $query
×
UNCOV
1086
            ->select('`{primary}`')
×
UNCOV
1087
            ->and([ '{primary}' => $key ])
×
UNCOV
1088
            ->limit(0, 0)
×
UNCOV
1089
            ->all(PDO::FETCH_COLUMN);
×
1090

UNCOV
1091
        if ($rc && is_array($key)) {
×
UNCOV
1092
            $exists = array_combine($key, array_fill(0, count($key), false));
×
1093

UNCOV
1094
            foreach ($rc as $key) {
×
UNCOV
1095
                $exists[$key] = true;
×
1096
            }
1097

UNCOV
1098
            foreach ($exists as $v) {
×
UNCOV
1099
                if (!$v) {
×
UNCOV
1100
                    return $exists;
×
1101
                }
1102
            }
1103

1104
            # all true
1105

UNCOV
1106
            return true;
×
1107
        }
1108

UNCOV
1109
        return !empty($rc);
×
1110
    }
1111

1112
    /**
1113
     * Getter for the {@link $exists} magic property.
1114
     *
1115
     * @return bool|array
1116
     *
1117
     * @see exists()
1118
     */
1119
    protected function get_exists()
1120
    {
1121
        return $this->exists();
1✔
1122
    }
1123

1124
    /**
1125
     * Handle all the computations.
1126
     *
1127
     * @param string $method
1128
     * @param string|null $column
1129
     *
1130
     * @return int|array
1131
     */
1132
    private function compute(string $method, string $column = null)
1133
    {
UNCOV
1134
        $query = 'SELECT ';
×
1135

UNCOV
1136
        if ($column) {
×
UNCOV
1137
            if ($method == 'COUNT') {
×
UNCOV
1138
                $query .= "`$column`, $method(`$column`)";
×
1139

UNCOV
1140
                $this->group($column);
×
1141
            } else {
UNCOV
1142
                $query .= "$method(`$column`)";
×
1143
            }
1144
        } else {
UNCOV
1145
            $query .= $method . '(*)';
×
1146
        }
1147

UNCOV
1148
        $query .= ' AS count ' . $this->render_from() . $this->render_main();
×
UNCOV
1149
        $statement = ($this->model)($query, $this->args);
×
1150

UNCOV
1151
        if ($method == 'COUNT' && $column) {
×
UNCOV
1152
            return $statement->pairs;
×
1153
        }
1154

UNCOV
1155
        return (int)$statement->rc;
×
1156
    }
1157

1158
    /**
1159
     * Implement the 'COUNT' computation.
1160
     *
1161
     * @param string|null $column The name of the column to count.
1162
     */
1163
    public function count(string $column = null): int|array
1164
    {
UNCOV
1165
        return $this->compute('COUNT', $column);
×
1166
    }
1167

1168
    /**
1169
     * Getter for the {@link $count} magic property.
1170
     *
1171
     * @return int
1172
     */
1173
    protected function get_count(): int
1174
    {
UNCOV
1175
        return $this->count();
×
1176
    }
1177

1178
    /**
1179
     * Implement the 'AVG' computation.
1180
     *
1181
     * @param string $column
1182
     *
1183
     * @return int
1184
     */
1185
    public function average(string $column)
1186
    {
UNCOV
1187
        return $this->compute('AVG', $column);
×
1188
    }
1189

1190
    /**
1191
     * Implement the 'MIN' computation.
1192
     *
1193
     * @param string $column
1194
     *
1195
     * @return mixed
1196
     */
1197
    public function minimum(string $column)
1198
    {
UNCOV
1199
        return $this->compute('MIN', $column);
×
1200
    }
1201

1202
    /**
1203
     * Implement the 'MAX' computation.
1204
     *
1205
     * @param string $column
1206
     *
1207
     * @return mixed
1208
     */
1209
    public function maximum(string $column)
1210
    {
UNCOV
1211
        return $this->compute('MAX', $column);
×
1212
    }
1213

1214
    /**
1215
     * Implement the 'SUM' computation.
1216
     *
1217
     * @param string $column
1218
     *
1219
     * @return mixed
1220
     */
1221
    public function sum(string $column)
1222
    {
UNCOV
1223
        return $this->compute('SUM', $column);
×
1224
    }
1225

1226
    /**
1227
     * Delete the records matching the conditions and limits of the query.
1228
     *
1229
     * @param string $tables When using a JOIN, `$tables` is used to specify the tables in which
1230
     * records should be deleted. Default: The alias of queried model, only if at least one join
1231
     * clause has been defined using the {@link join()} method.
1232
     *
1233
     * @return bool The result of the operation.
1234
     *
1235
     * @todo-20140901: reflect on join to add the required tables by default, discarding tables
1236
     * joined with the LEFT mode.
1237
     */
1238
    public function delete($tables = null)
1239
    {
UNCOV
1240
        if (!$tables && $this->joints) {
×
1241
            $tables = "`{alias}`";
×
1242
        }
1243

UNCOV
1244
        if ($tables) {
×
UNCOV
1245
            $query = "DELETE {$tables} FROM {self} AS `{alias}`";
×
1246
        } else {
UNCOV
1247
            $query = "DELETE FROM {self}";
×
1248
        }
1249

UNCOV
1250
        $query .= $this->render_main();
×
1251

UNCOV
1252
        return $this->model->execute($query, $this->args);
×
1253
    }
1254

1255
    /**
1256
     * Return an iterator for the query.
1257
     *
1258
     * @return Traversable<TRecord>
1259
     */
1260
    public function getIterator(): Traversable
1261
    {
UNCOV
1262
        return new ArrayIterator($this->all());
×
1263
    }
1264
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc