Completed
Push — master ( 84dee3...084275 )
by Michal
36:03
created

Query::_transformQuery()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 14
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
dl 0
loc 14
rs 9.2
c 1
b 0
f 0
cc 4
eloc 8
nc 4
nop 0
1
<?php
2
/**
3
 * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
4
 * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
5
 *
6
 * Licensed under The MIT License
7
 * For full copyright and license information, please see the LICENSE.txt
8
 * Redistributions of files must retain the above copyright notice.
9
 *
10
 * @copyright     Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
11
 * @link          http://cakephp.org CakePHP(tm) Project
12
 * @since         3.0.0
13
 * @license       http://www.opensource.org/licenses/mit-license.php MIT License
14
 */
15
namespace Cake\ORM;
16
17
use ArrayObject;
18
use Cake\Database\ExpressionInterface;
19
use Cake\Database\Query as DatabaseQuery;
20
use Cake\Database\ValueBinder;
21
use Cake\Datasource\QueryInterface;
22
use Cake\Datasource\QueryTrait;
23
use JsonSerializable;
24
use RuntimeException;
25
26
/**
27
 * Extends the base Query class to provide new methods related to association
28
 * loading, automatic fields selection, automatic type casting and to wrap results
29
 * into a specific iterator that will be responsible for hydrating results if
30
 * required.
31
 *
32
 */
33
class Query extends DatabaseQuery implements JsonSerializable, QueryInterface
34
{
35
36
    use QueryTrait {
37
        cache as private _cache;
38
        all as private _all;
39
        _decorateResults as private _applyDecorators;
40
        __call as private _call;
41
    }
42
43
    /**
44
     * Indicates that the operation should append to the list
45
     *
46
     * @var int
47
     */
48
    const APPEND = 0;
49
50
    /**
51
     * Indicates that the operation should prepend to the list
52
     *
53
     * @var int
54
     */
55
    const PREPEND = 1;
56
57
    /**
58
     * Indicates that the operation should overwrite the list
59
     *
60
     * @var bool
61
     */
62
    const OVERWRITE = true;
63
64
    /**
65
     * Whether the user select any fields before being executed, this is used
66
     * to determined if any fields should be automatically be selected.
67
     *
68
     * @var bool
69
     */
70
    protected $_hasFields;
71
72
    /**
73
     * Tracks whether or not the original query should include
74
     * fields from the top level table.
75
     *
76
     * @var bool
77
     */
78
    protected $_autoFields;
79
80
    /**
81
     * Whether to hydrate results into entity objects
82
     *
83
     * @var bool
84
     */
85
    protected $_hydrate = true;
86
87
    /**
88
     * A callable function that can be used to calculate the total amount of
89
     * records this query will match when not using `limit`
90
     *
91
     * @var callable
92
     */
93
    protected $_counter;
94
95
    /**
96
     * Instance of a class responsible for storing association containments and
97
     * for eager loading them when this query is executed
98
     *
99
     * @var \Cake\ORM\EagerLoader
100
     */
101
    protected $_eagerLoader;
102
103
    /**
104
     * True if the beforeFind event has already been triggered for this query
105
     *
106
     * @var bool
107
     */
108
    protected $_beforeFindFired = false;
109
110
    /**
111
     * The COUNT(*) for the query.
112
     *
113
     * When set, count query execution will be bypassed.
114
     *
115
     * @var int
116
     */
117
    protected $_resultsCount;
118
119
    /**
120
     * Constructor
121
     *
122
     * @param \Cake\Database\Connection $connection The connection object
123
     * @param \Cake\ORM\Table $table The table this query is starting on
124
     */
125
    public function __construct($connection, $table)
126
    {
127
        parent::__construct($connection);
128
        $this->repository($table);
129
130
        if ($this->_repository) {
131
            $this->addDefaultTypes($this->_repository);
0 ignored issues
show
Compatibility introduced by
$this->_repository of type object<Cake\Datasource\RepositoryInterface> is not a sub-type of object<Cake\ORM\Table>. It seems like you assume a concrete implementation of the interface Cake\Datasource\RepositoryInterface to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
132
        }
133
    }
134
135
    /**
136
     * {@inheritDoc}
137
     *
138
     * If you pass an instance of a `Cake\ORM\Table` or `Cake\ORM\Association` class,
139
     * all the fields in the schema of the table or the association will be added to
140
     * the select clause.
141
     *
142
     * @param array|ExpressionInterface|string|\Cake\ORM\Table|\Cake\ORM\Association $fields fields
143
     * to be added to the list.
144
     * @param bool $overwrite whether to reset fields with passed list or not
145
     */
146
    public function select($fields = [], $overwrite = false)
147
    {
148
        if ($fields instanceof Association) {
149
            $fields = $fields->target();
150
        }
151
152
        if ($fields instanceof Table) {
153
            $fields = $this->aliasFields($fields->schema()->columns(), $fields->alias());
154
        }
155
156
        return parent::select($fields, $overwrite);
157
    }
158
159
    /**
160
     * Hints this object to associate the correct types when casting conditions
161
     * for the database. This is done by extracting the field types from the schema
162
     * associated to the passed table object. This prevents the user from repeating
163
     * himself when specifying conditions.
164
     *
165
     * This method returns the same query object for chaining.
166
     *
167
     * @param \Cake\ORM\Table $table The table to pull types from
168
     * @return $this
169
     */
170
    public function addDefaultTypes(Table $table)
171
    {
172
        $alias = $table->alias();
173
        $map = $table->schema()->typeMap();
174
        $fields = [];
175
        foreach ($map as $f => $type) {
176
            $fields[$f] = $fields[$alias . '.' . $f] = $type;
177
        }
178
        $this->typeMap()->addDefaults($fields);
0 ignored issues
show
Bug introduced by
The method addDefaults does only exist in Cake\Database\TypeMap, but not in Cake\Database\TypeMapTrait.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
179
180
        return $this;
181
    }
182
183
    /**
184
     * Sets the instance of the eager loader class to use for loading associations
185
     * and storing containments. If called with no arguments, it will return the
186
     * currently configured instance.
187
     *
188
     * @param \Cake\ORM\EagerLoader $instance The eager loader to use. Pass null
189
     *   to get the current eagerloader.
190
     * @return \Cake\ORM\EagerLoader|$this
191
     */
192
    public function eagerLoader(EagerLoader $instance = null)
193
    {
194
        if ($instance === null) {
195
            if ($this->_eagerLoader === null) {
196
                $this->_eagerLoader = new EagerLoader;
197
            }
198
            return $this->_eagerLoader;
199
        }
200
        $this->_eagerLoader = $instance;
201
        return $this;
202
    }
203
204
    /**
205
     * Sets the list of associations that should be eagerly loaded along with this
206
     * query. The list of associated tables passed must have been previously set as
207
     * associations using the Table API.
208
     *
209
     * ### Example:
210
     *
211
     * ```
212
     *  // Bring articles' author information
213
     *  $query->contain('Author');
214
     *
215
     *  // Also bring the category and tags associated to each article
216
     *  $query->contain(['Category', 'Tag']);
217
     * ```
218
     *
219
     * Associations can be arbitrarily nested using dot notation or nested arrays,
220
     * this allows this object to calculate joins or any additional queries that
221
     * must be executed to bring the required associated data.
222
     *
223
     * ### Example:
224
     *
225
     * ```
226
     *  // Eager load the product info, and for each product load other 2 associations
227
     *  $query->contain(['Product' => ['Manufacturer', 'Distributor']);
228
     *
229
     *  // Which is equivalent to calling
230
     *  $query->contain(['Products.Manufactures', 'Products.Distributors']);
231
     *
232
     *  // For an author query, load his region, state and country
233
     *  $query->contain('Regions.States.Countries');
234
     * ```
235
     *
236
     * It is possible to control the conditions and fields selected for each of the
237
     * contained associations:
238
     *
239
     * ### Example:
240
     *
241
     * ```
242
     *  $query->contain(['Tags' => function ($q) {
243
     *      return $q->where(['Tags.is_popular' => true]);
244
     *  }]);
245
     *
246
     *  $query->contain(['Products.Manufactures' => function ($q) {
247
     *      return $q->select(['name'])->where(['Manufactures.active' => true]);
248
     *  }]);
249
     * ```
250
     *
251
     * Each association might define special options when eager loaded, the allowed
252
     * options that can be set per association are:
253
     *
254
     * - foreignKey: Used to set a different field to match both tables, if set to false
255
     *   no join conditions will be generated automatically. `false` can only be used on
256
     *   joinable associations and cannot be used with hasMany or belongsToMany associations.
257
     * - fields: An array with the fields that should be fetched from the association
258
     * - queryBuilder: Equivalent to passing a callable instead of an options array
259
     *
260
     * ### Example:
261
     *
262
     * ```
263
     * // Set options for the hasMany articles that will be eagerly loaded for an author
264
     * $query->contain([
265
     *   'Articles' => [
266
     *     'fields' => ['title', 'author_id']
267
     *   ]
268
     * ]);
269
     * ```
270
     *
271
     * When containing associations, it is important to include foreign key columns.
272
     * Failing to do so will trigger exceptions.
273
     *
274
     * ```
275
     * // Use special join conditions for getting an Articles's belongsTo 'authors'
276
     * $query->contain([
277
     *   'Authors' => [
278
     *     'foreignKey' => false,
279
     *     'queryBuilder' => function ($q) {
280
     *       return $q->where(...); // Add full filtering conditions
281
     *     }
282
     *   ]
283
     * ]);
284
     * ```
285
     *
286
     * If called with no arguments, this function will return an array with
287
     * with the list of previously configured associations to be contained in the
288
     * result.
289
     *
290
     * If called with an empty first argument and $override is set to true, the
291
     * previous list will be emptied.
292
     *
293
     * @param array|string $associations list of table aliases to be queried
294
     * @param bool $override whether override previous list with the one passed
295
     * defaults to merging previous list with the new one.
296
     * @return array|$this
297
     */
298
    public function contain($associations = null, $override = false)
299
    {
300
        $loader = $this->eagerLoader();
301
        if ($override === true) {
302
            $loader->clearContain();
0 ignored issues
show
Bug introduced by
The method clearContain does only exist in Cake\ORM\EagerLoader, but not in Cake\ORM\Query.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
303
            $this->_dirty();
304
        }
305
306
        if ($associations === null) {
307
            return $loader->contain();
308
        }
309
310
        $result = $loader->contain($associations);
311
        $this->_addAssociationsToTypeMap($this->repository(), $this->typeMap(), $result);
0 ignored issues
show
Bug introduced by
It seems like $this->typeMap() targeting Cake\Database\TypeMapTrait::typeMap() can also be of type object<Cake\Database\TypeMapTrait>; however, Cake\ORM\Query::_addAssociationsToTypeMap() does only seem to accept object<Cake\Database\TypeMap>, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
312
        return $this;
313
    }
314
315
    /**
316
     * Used to recursively add contained association column types to
317
     * the query.
318
     *
319
     * @param \Cake\ORM\Table $table The table instance to pluck associations from.
320
     * @param \Cake\Database\TypeMap $typeMap The typemap to check for columns in.
321
     *   This typemap is indirectly mutated via Cake\ORM\Query::addDefaultTypes()
322
     * @param array $associations The nested tree of associations to walk.
323
     * @return void
324
     */
325
    protected function _addAssociationsToTypeMap($table, $typeMap, $associations)
326
    {
327
        foreach ($associations as $name => $nested) {
328
            $association = $table->association($name);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $association is correct as $table->association($name) (which targets Cake\ORM\Table::association()) seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
329
            if (!$association) {
330
                continue;
331
            }
332
            $target = $association->target();
333
            $primary = (array)$target->primaryKey();
334
            if (empty($primary) || $typeMap->type($target->aliasField($primary[0])) === null) {
335
                $this->addDefaultTypes($target);
336
            }
337
            if (!empty($nested)) {
338
                $this->_addAssociationsToTypeMap($target, $typeMap, $nested);
339
            }
340
        }
341
    }
342
343
    /**
344
     * Adds filtering conditions to this query to only bring rows that have a relation
345
     * to another from an associated table, based on conditions in the associated table.
346
     *
347
     * This function will add entries in the `contain` graph.
348
     *
349
     * ### Example:
350
     *
351
     * ```
352
     *  // Bring only articles that were tagged with 'cake'
353
     *  $query->matching('Tags', function ($q) {
354
     *      return $q->where(['name' => 'cake']);
355
     *  );
356
     * ```
357
     *
358
     * It is possible to filter by deep associations by using dot notation:
359
     *
360
     * ### Example:
361
     *
362
     * ```
363
     *  // Bring only articles that were commented by 'markstory'
364
     *  $query->matching('Comments.Users', function ($q) {
365
     *      return $q->where(['username' => 'markstory']);
366
     *  );
367
     * ```
368
     *
369
     * As this function will create `INNER JOIN`, you might want to consider
370
     * calling `distinct` on this query as you might get duplicate rows if
371
     * your conditions don't filter them already. This might be the case, for example,
372
     * of the same user commenting more than once in the same article.
373
     *
374
     * ### Example:
375
     *
376
     * ```
377
     *  // Bring unique articles that were commented by 'markstory'
378
     *  $query->distinct(['Articles.id'])
379
     *  ->matching('Comments.Users', function ($q) {
380
     *      return $q->where(['username' => 'markstory']);
381
     *  );
382
     * ```
383
     *
384
     * Please note that the query passed to the closure will only accept calling
385
     * `select`, `where`, `andWhere` and `orWhere` on it. If you wish to
386
     * add more complex clauses you can do it directly in the main query.
387
     *
388
     * @param string $assoc The association to filter by
389
     * @param callable $builder a function that will receive a pre-made query object
390
     * that can be used to add custom conditions or selecting some fields
391
     * @return $this
392
     */
393
    public function matching($assoc, callable $builder = null)
394
    {
395
        $this->eagerLoader()->matching($assoc, $builder);
396
        $this->_dirty();
397
        return $this;
398
    }
399
400
    /**
401
     * Creates a LEFT JOIN with the passed association table while preserving
402
     * the foreign key matching and the custom conditions that were originally set
403
     * for it.
404
     *
405
     * This function will add entries in the `contain` graph.
406
     *
407
     * ### Example:
408
     *
409
     * ```
410
     *  // Get the count of articles per user
411
     *  $usersQuery
412
     *      ->select(['total_articles' => $query->func()->count('Articles.id')])
413
     *      ->leftJoinWith('Articles')
414
     *      ->group(['Users.id'])
415
     *      ->autoFields(true);
416
     * ```
417
     *
418
     * You can also customize the conditions passed to the LEFT JOIN:
419
     *
420
     * ```
421
     *  // Get the count of articles per user with at least 5 votes
422
     *  $usersQuery
423
     *      ->select(['total_articles' => $query->func()->count('Articles.id')])
424
     *      ->leftJoinWith('Articles', function ($q) {
425
     *          return $q->where(['Articles.votes >=' => 5]);
426
     *      })
427
     *      ->group(['Users.id'])
428
     *      ->autoFields(true);
429
     * ```
430
     *
431
     * This will create the following SQL:
432
     *
433
     * ```
434
     *  SELECT COUNT(Articles.id) AS total_articles, Users.*
435
     *  FROM users Users
436
     *  LEFT JOIN articles Articles ON Articles.user_id = Users.id AND Articles.votes >= 5
437
     *  GROUP BY USers.id
438
     * ```
439
     *
440
     * It is possible to left join deep associations by using dot notation
441
     *
442
     * ### Example:
443
     *
444
     * ```
445
     *  // Total comments in articles by 'markstory'
446
     *  $query
447
     *   ->select(['total_comments' => $query->func()->count('Comments.id')])
448
     *   ->leftJoinWith('Comments.Users', function ($q) {
449
     *      return $q->where(['username' => 'markstory']);
450
     *  )
451
     *  ->group(['Users.id']);
452
     * ```
453
     *
454
     * Please note that the query passed to the closure will only accept calling
455
     * `select`, `where`, `andWhere` and `orWhere` on it. If you wish to
456
     * add more complex clauses you can do it directly in the main query.
457
     *
458
     * @param string $assoc The association to join with
459
     * @param callable $builder a function that will receive a pre-made query object
460
     * that can be used to add custom conditions or selecting some fields
461
     * @return $this
462
     */
463 View Code Duplication
    public function leftJoinWith($assoc, callable $builder = null)
464
    {
465
        $this->eagerLoader()->matching($assoc, $builder, [
0 ignored issues
show
Unused Code introduced by
The call to Query::matching() has too many arguments starting with array('joinType' => 'LEFT', 'fields' => false).

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
466
            'joinType' => 'LEFT',
467
            'fields' => false
468
        ]);
469
        $this->_dirty();
470
        return $this;
471
    }
472
473
    /**
474
     * Creates an INNER JOIN with the passed association table while preserving
475
     * the foreign key matching and the custom conditions that were originally set
476
     * for it.
477
     *
478
     * This function will add entries in the `contain` graph.
479
     *
480
     * ### Example:
481
     *
482
     * ```
483
     *  // Bring only articles that were tagged with 'cake'
484
     *  $query->innerJoinWith('Tags', function ($q) {
485
     *      return $q->where(['name' => 'cake']);
486
     *  );
487
     * ```
488
     *
489
     * This will create the following SQL:
490
     *
491
     * ```
492
     *  SELECT Articles.*
493
     *  FROM articles Articles
494
     *  INNER JOIN tags Tags ON Tags.name = 'cake'
495
     *  INNER JOIN articles_tags ArticlesTags ON ArticlesTags.tag_id = Tags.id
496
     *    AND ArticlesTags.articles_id = Articles.id
497
     * ```
498
     *
499
     * This function works the same as `matching()` with the difference that it
500
     * will select no fields from the association.
501
     *
502
     * @param string $assoc The association to join with
503
     * @param callable $builder a function that will receive a pre-made query object
504
     * that can be used to add custom conditions or selecting some fields
505
     * @return $this
506
     * @see \Cake\ORM\Query::matching()
507
     */
508 View Code Duplication
    public function innerJoinWith($assoc, callable $builder = null)
509
    {
510
        $this->eagerLoader()->matching($assoc, $builder, [
0 ignored issues
show
Unused Code introduced by
The call to Query::matching() has too many arguments starting with array('joinType' => 'INNER', 'fields' => false).

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
511
            'joinType' => 'INNER',
512
            'fields' => false
513
        ]);
514
        $this->_dirty();
515
        return $this;
516
    }
517
518
    /**
519
     * Adds filtering conditions to this query to only bring rows that have no match
520
     * to another from an associated table, based on conditions in the associated table.
521
     *
522
     * This function will add entries in the `contain` graph.
523
     *
524
     * ### Example:
525
     *
526
     * ```
527
     *  // Bring only articles that were not tagged with 'cake'
528
     *  $query->notMatching('Tags', function ($q) {
529
     *      return $q->where(['name' => 'cake']);
530
     *  );
531
     * ```
532
     *
533
     * It is possible to filter by deep associations by using dot notation:
534
     *
535
     * ### Example:
536
     *
537
     * ```
538
     *  // Bring only articles that weren't commented by 'markstory'
539
     *  $query->notMatching('Comments.Users', function ($q) {
540
     *      return $q->where(['username' => 'markstory']);
541
     *  );
542
     * ```
543
     *
544
     * As this function will create a `LEFT JOIN`, you might want to consider
545
     * calling `distinct` on this query as you might get duplicate rows if
546
     * your conditions don't filter them already. This might be the case, for example,
547
     * of the same article having multiple comments.
548
     *
549
     * ### Example:
550
     *
551
     * ```
552
     *  // Bring unique articles that were commented by 'markstory'
553
     *  $query->distinct(['Articles.id'])
554
     *  ->notMatching('Comments.Users', function ($q) {
555
     *      return $q->where(['username' => 'markstory']);
556
     *  );
557
     * ```
558
     *
559
     * Please note that the query passed to the closure will only accept calling
560
     * `select`, `where`, `andWhere` and `orWhere` on it. If you wish to
561
     * add more complex clauses you can do it directly in the main query.
562
     *
563
     * @param string $assoc The association to filter by
564
     * @param callable $builder a function that will receive a pre-made query object
565
     * that can be used to add custom conditions or selecting some fields
566
     * @return $this
567
     */
568 View Code Duplication
    public function notMatching($assoc, callable $builder = null)
569
    {
570
        $this->eagerLoader()->matching($assoc, $builder, [
0 ignored issues
show
Unused Code introduced by
The call to Query::matching() has too many arguments starting with array('joinType' => 'LEF... 'negateMatch' => true).

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
571
            'joinType' => 'LEFT',
572
            'fields' => false,
573
            'negateMatch' => true
574
        ]);
575
        $this->_dirty();
576
        return $this;
577
    }
578
579
    /**
580
     * {@inheritDoc}
581
     *
582
     * Populates or adds parts to current query clauses using an array.
583
     * This is handy for passing all query clauses at once. The option array accepts:
584
     *
585
     * - fields: Maps to the select method
586
     * - conditions: Maps to the where method
587
     * - limit: Maps to the limit method
588
     * - order: Maps to the order method
589
     * - offset: Maps to the offset method
590
     * - group: Maps to the group method
591
     * - having: Maps to the having method
592
     * - contain: Maps to the contain options for eager loading
593
     * - join: Maps to the join method
594
     * - page: Maps to the page method
595
     *
596
     * ### Example:
597
     *
598
     * ```
599
     * $query->applyOptions([
600
     *   'fields' => ['id', 'name'],
601
     *   'conditions' => [
602
     *     'created >=' => '2013-01-01'
603
     *   ],
604
     *   'limit' => 10
605
     * ]);
606
     * ```
607
     *
608
     * Is equivalent to:
609
     *
610
     * ```
611
     *  $query
612
     *  ->select(['id', 'name'])
613
     *  ->where(['created >=' => '2013-01-01'])
614
     *  ->limit(10)
615
     * ```
616
     */
617
    public function applyOptions(array $options)
618
    {
619
        $valid = [
620
            'fields' => 'select',
621
            'conditions' => 'where',
622
            'join' => 'join',
623
            'order' => 'order',
624
            'limit' => 'limit',
625
            'offset' => 'offset',
626
            'group' => 'group',
627
            'having' => 'having',
628
            'contain' => 'contain',
629
            'page' => 'page',
630
        ];
631
632
        ksort($options);
633
        foreach ($options as $option => $values) {
634
            if (isset($valid[$option], $values)) {
635
                $this->{$valid[$option]}($values);
636
            } else {
637
                $this->_options[$option] = $values;
638
            }
639
        }
640
641
        return $this;
642
    }
643
644
    /**
645
     * Creates a copy of this current query, triggers beforeFind and resets some state.
646
     *
647
     * The following state will be cleared:
648
     *
649
     * - autoFields
650
     * - limit
651
     * - offset
652
     * - map/reduce functions
653
     * - result formatters
654
     * - order
655
     * - containments
656
     *
657
     * This method creates query clones that are useful when working with subqueries.
658
     *
659
     * @return \Cake\ORM\Query
660
     */
661
    public function cleanCopy()
662
    {
663
        $clone = clone $this;
664
        $clone->triggerBeforeFind();
665
        $clone->autoFields(false);
666
        $clone->limit(null);
667
        $clone->order([], true);
668
        $clone->offset(null);
669
        $clone->mapReduce(null, null, true);
670
        $clone->formatResults(null, true);
671
        return $clone;
672
    }
673
674
    /**
675
     * Object clone hook.
676
     *
677
     * Destroys the clones inner iterator and clones the value binder, and eagerloader instances.
678
     *
679
     * @return void
680
     */
681
    public function __clone()
682
    {
683
        parent::__clone();
684
        if ($this->_eagerLoader) {
685
            $this->_eagerLoader = clone $this->_eagerLoader;
686
        }
687
    }
688
689
    /**
690
     * {@inheritDoc}
691
     *
692
     * Returns the COUNT(*) for the query. If the query has not been
693
     * modified, and the count has already been performed the cached
694
     * value is returned
695
     */
696
    public function count()
697
    {
698
        if ($this->_resultsCount === null) {
699
            $this->_resultsCount = $this->_performCount();
700
        }
701
702
        return $this->_resultsCount;
703
    }
704
705
    /**
706
     * Performs and returns the COUNT(*) for the query.
707
     *
708
     * @return int
709
     */
710
    protected function _performCount()
711
    {
712
        $query = $this->cleanCopy();
713
        $counter = $this->_counter;
714
        if ($counter) {
715
            $query->counter(null);
716
            return (int)$counter($query);
717
        }
718
719
        $complex = (
720
            $query->clause('distinct') ||
721
            count($query->clause('group')) ||
722
            count($query->clause('union')) ||
723
            $query->clause('having')
724
        );
725
726
        if (!$complex) {
727
            // Expression fields could have bound parameters.
728
            foreach ($query->clause('select') as $field) {
729
                if ($field instanceof ExpressionInterface) {
730
                    $complex = true;
731
                    break;
732
                }
733
            }
734
        }
735
736
        if (!$complex && $this->_valueBinder !== null) {
737
            $order = $this->clause('order');
738
            $complex = $order === null ? false : $order->hasNestedExpression();
739
        }
740
741
        $count = ['count' => $query->func()->count('*')];
742
743
        if (!$complex) {
744
            $query->eagerLoader()->autoFields(false);
745
            $statement = $query
746
                ->select($count, true)
747
                ->autoFields(false)
748
                ->execute();
749
        } else {
750
            $statement = $this->connection()->newQuery()
751
                ->select($count)
752
                ->from(['count_source' => $query])
753
                ->execute();
754
        }
755
756
        $result = $statement->fetch('assoc')['count'];
757
        $statement->closeCursor();
758
        return (int)$result;
759
    }
760
761
    /**
762
     * Registers a callable function that will be executed when the `count` method in
763
     * this query is called. The return value for the function will be set as the
764
     * return value of the `count` method.
765
     *
766
     * This is particularly useful when you need to optimize a query for returning the
767
     * count, for example removing unnecessary joins, removing group by or just return
768
     * an estimated number of rows.
769
     *
770
     * The callback will receive as first argument a clone of this query and not this
771
     * query itself.
772
     *
773
     * If the first param is a null value, the built-in counter function will be called
774
     * instead
775
     *
776
     * @param callable|null $counter The counter value
777
     * @return $this
778
     */
779
    public function counter($counter)
780
    {
781
        $this->_counter = $counter;
782
        return $this;
783
    }
784
785
    /**
786
     * Toggle hydrating entities.
787
     *
788
     * If set to false array results will be returned
789
     *
790
     * @param bool|null $enable Use a boolean to set the hydration mode.
791
     *   Null will fetch the current hydration mode.
792
     * @return bool|$this A boolean when reading, and $this when setting the mode.
793
     */
794
    public function hydrate($enable = null)
795
    {
796
        if ($enable === null) {
797
            return $this->_hydrate;
798
        }
799
800
        $this->_dirty();
801
        $this->_hydrate = (bool)$enable;
802
        return $this;
803
    }
804
805
    /**
806
     * {@inheritDoc}
807
     *
808
     * @return $this
809
     * @throws \RuntimeException When you attempt to cache a non-select query.
810
     */
811
    public function cache($key, $config = 'default')
812
    {
813
        if ($this->_type !== 'select' && $this->_type !== null) {
814
            throw new RuntimeException('You cannot cache the results of non-select queries.');
815
        }
816
        return $this->_cache($key, $config);
817
    }
818
819
    /**
820
     * {@inheritDoc}
821
     *
822
     * @throws \RuntimeException if this method is called on a non-select Query.
823
     */
824
    public function all()
825
    {
826
        if ($this->_type !== 'select' && $this->_type !== null) {
827
            throw new RuntimeException(
828
                'You cannot call all() on a non-select query. Use execute() instead.'
829
            );
830
        }
831
        return $this->_all();
832
    }
833
834
    /**
835
     * Trigger the beforeFind event on the query's repository object.
836
     *
837
     * Will not trigger more than once, and only for select queries.
838
     *
839
     * @return void
840
     */
841
    public function triggerBeforeFind()
842
    {
843
        if (!$this->_beforeFindFired && $this->_type === 'select') {
844
            $table = $this->repository();
845
            $this->_beforeFindFired = true;
846
            $table->dispatchEvent('Model.beforeFind', [
847
                $this,
848
                new ArrayObject($this->_options),
849
                !$this->eagerLoaded()
850
            ]);
851
        }
852
    }
853
854
    /**
855
     * {@inheritDoc}
856
     */
857
    public function sql(ValueBinder $binder = null)
858
    {
859
        $this->triggerBeforeFind();
860
861
        $this->_transformQuery();
862
        $sql = parent::sql($binder);
863
        return $sql;
864
    }
865
866
    /**
867
     * Executes this query and returns a ResultSet object containing the results.
868
     * This will also setup the correct statement class in order to eager load deep
869
     * associations.
870
     *
871
     * @return \Cake\ORM\ResultSet
872
     */
873
    protected function _execute()
874
    {
875
        $this->triggerBeforeFind();
876
        if ($this->_results) {
877
            $decorator = $this->_decoratorClass();
878
            return new $decorator($this->_results);
879
        }
880
        $statement = $this->eagerLoader()->loadExternal($this, $this->execute());
0 ignored issues
show
Bug introduced by
The method loadExternal does only exist in Cake\ORM\EagerLoader, but not in Cake\ORM\Query.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
881
        return new ResultSet($this, $statement);
882
    }
883
884
    /**
885
     * Applies some defaults to the query object before it is executed.
886
     *
887
     * Specifically add the FROM clause, adds default table fields if none are
888
     * specified and applies the joins required to eager load associations defined
889
     * using `contain`
890
     *
891
     * @see \Cake\Database\Query::execute()
892
     * @return void
893
     */
894
    protected function _transformQuery()
895
    {
896
        if (!$this->_dirty) {
897
            return;
898
        }
899
900
        if ($this->_type === 'select') {
901
            if (empty($this->_parts['from'])) {
902
                $this->from([$this->_repository->alias() => $this->_repository->table()]);
903
            }
904
            $this->_addDefaultFields();
905
            $this->eagerLoader()->attachAssociations($this, $this->_repository, !$this->_hasFields);
0 ignored issues
show
Compatibility introduced by
$this->_repository of type object<Cake\Datasource\RepositoryInterface> is not a sub-type of object<Cake\ORM\Table>. It seems like you assume a concrete implementation of the interface Cake\Datasource\RepositoryInterface to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
Bug introduced by
The method attachAssociations does only exist in Cake\ORM\EagerLoader, but not in Cake\ORM\Query.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
906
        }
907
    }
908
909
    /**
910
     * Inspects if there are any set fields for selecting, otherwise adds all
911
     * the fields for the default table.
912
     *
913
     * @return void
914
     */
915
    protected function _addDefaultFields()
916
    {
917
        $select = $this->clause('select');
918
        $this->_hasFields = true;
919
920
        if (!count($select) || $this->_autoFields === true) {
921
            $this->_hasFields = false;
922
            $this->select($this->repository()->schema()->columns());
923
            $select = $this->clause('select');
924
        }
925
926
        $aliased = $this->aliasFields($select, $this->repository()->alias());
0 ignored issues
show
Bug introduced by
The method alias does only exist in Cake\Datasource\RepositoryInterface, but not in Cake\Datasource\QueryTrait.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
927
        $this->select($aliased, true);
928
    }
929
930
    /**
931
     * {@inheritDoc}
932
     *
933
     * @see \Cake\ORM\Table::find()
934
     */
935
    public function find($finder, array $options = [])
936
    {
937
        return $this->repository()->callFinder($finder, $this, $options);
938
    }
939
940
    /**
941
     * Marks a query as dirty, removing any preprocessed information
942
     * from in memory caching such as previous results
943
     *
944
     * @return void
945
     */
946
    protected function _dirty()
947
    {
948
        $this->_results = null;
949
        $this->_resultsCount = null;
950
        parent::_dirty();
951
    }
952
953
    /**
954
     * Create an update query.
955
     *
956
     * This changes the query type to be 'update'.
957
     * Can be combined with set() and where() methods to create update queries.
958
     *
959
     * @param string|null $table Unused parameter.
960
     * @return $this
961
     */
962
    public function update($table = null)
963
    {
964
        $table = $table ?: $this->repository()->table();
965
        return parent::update($table);
966
    }
967
968
    /**
969
     * Create a delete query.
970
     *
971
     * This changes the query type to be 'delete'.
972
     * Can be combined with the where() method to create delete queries.
973
     *
974
     * @param string|null $table Unused parameter.
975
     * @return $this
976
     */
977
    public function delete($table = null)
978
    {
979
        $repo = $this->repository();
980
        $this->from([$repo->alias() => $repo->table()]);
0 ignored issues
show
Bug introduced by
The method alias does only exist in Cake\Datasource\RepositoryInterface, but not in Cake\Datasource\QueryTrait.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
981
        return parent::delete();
982
    }
983
984
    /**
985
     * Create an insert query.
986
     *
987
     * This changes the query type to be 'insert'.
988
     * Note calling this method will reset any data previously set
989
     * with Query::values()
990
     *
991
     * Can be combined with the where() method to create delete queries.
992
     *
993
     * @param array $columns The columns to insert into.
994
     * @param array $types A map between columns & their datatypes.
995
     * @return $this
996
     */
997
    public function insert(array $columns, array $types = [])
998
    {
999
        $table = $this->repository()->table();
1000
        $this->into($table);
1001
        return parent::insert($columns, $types);
1002
    }
1003
1004
    /**
1005
     * {@inheritDoc}
1006
     *
1007
     * @throws \BadMethodCallException if the method is called for a non-select query
1008
     */
1009
    public function __call($method, $arguments)
1010
    {
1011
        if ($this->type() === 'select') {
1012
            return $this->_call($method, $arguments);
1013
        }
1014
1015
        throw new \BadMethodCallException(
1016
            sprintf('Cannot call method "%s" on a "%s" query', $method, $this->type())
1017
        );
1018
    }
1019
1020
    /**
1021
     * {@inheritDoc}
1022
     */
1023
    public function __debugInfo()
1024
    {
1025
        $eagerLoader = $this->eagerLoader();
1026
        return parent::__debugInfo() + [
1027
            'hydrate' => $this->_hydrate,
1028
            'buffered' => $this->_useBufferedResults,
1029
            'formatters' => count($this->_formatters),
1030
            'mapReducers' => count($this->_mapReduce),
1031
            'contain' => $eagerLoader ? $eagerLoader->contain() : [],
1032
            'matching' => $eagerLoader ? $eagerLoader->matching() : [],
0 ignored issues
show
Bug introduced by
The call to matching() misses a required argument $assoc.

This check looks for function calls that miss required arguments.

Loading history...
1033
            'extraOptions' => $this->_options,
1034
            'repository' => $this->_repository
1035
        ];
1036
    }
1037
1038
    /**
1039
     * Executes the query and converts the result set into JSON.
1040
     *
1041
     * Part of JsonSerializable interface.
1042
     *
1043
     * @return \Cake\Datasource\ResultSetInterface The data to convert to JSON.
1044
     */
1045
    public function jsonSerialize()
1046
    {
1047
        return $this->all();
1048
    }
1049
1050
    /**
1051
     * Get/Set whether or not the ORM should automatically append fields.
1052
     *
1053
     * By default calling select() will disable auto-fields. You can re-enable
1054
     * auto-fields with this method.
1055
     *
1056
     * @param bool|null $value The value to set or null to read the current value.
1057
     * @return bool|$this Either the current value or the query object.
1058
     */
1059
    public function autoFields($value = null)
1060
    {
1061
        if ($value === null) {
1062
            return $this->_autoFields;
1063
        }
1064
        $this->_autoFields = (bool)$value;
1065
        return $this;
1066
    }
1067
1068
    /**
1069
     * Decorates the results iterator with MapReduce routines and formatters
1070
     *
1071
     * @param \Traversable $result Original results
1072
     * @return \Cake\Datasource\ResultSetInterface
1073
     */
1074
    protected function _decorateResults($result)
1075
    {
1076
        $result = $this->_applyDecorators($result);
1077
1078
        if (!($result instanceof ResultSet) && $this->bufferResults()) {
1079
            $class = $this->_decoratorClass();
1080
            $result = new $class($result->buffered());
1081
        }
1082
1083
        return $result;
1084
    }
1085
}
1086