Issues (114)

Security Analysis    no request data  

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

src/System/Query.php (14 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
namespace Analogue\ORM\System;
4
5
use Closure;
6
use Exception;
7
use Analogue\ORM\EntityCollection;
8
use Analogue\ORM\Relationships\Relationship;
9
use Analogue\ORM\Exceptions\EntityNotFoundException;
10
use Analogue\ORM\Drivers\DBAdapter;
11
use Illuminate\Pagination\Paginator;
12
use Illuminate\Pagination\LengthAwarePaginator;
13
use Illuminate\Database\Query\Expression;
14
15
/**
16
 * Analogue Query builder.
17
 *
18
 * @mixin QueryAdapter|\Analogue\ORM\Drivers\IlluminateQueryAdapter
19
 */
20
class Query
21
{
22
    /**
23
     * Mapper Instance
24
     *
25
     * @var \Analogue\ORM\System\Mapper
26
     */
27
    protected $mapper;
28
29
    /**
30
     * DB Adatper
31
     *
32
     * @var \Analogue\ORM\Drivers\DBAdapter
33
     */
34
    protected $adapter;
35
36
    /**
37
     * Query Builder Instance
38
     *
39
     * @var \Analogue\ORM\Drivers\QueryAdapter|\Analogue\ORM\Drivers\IlluminateQueryAdapter
40
     */
41
    protected $query;
42
43
    /**
44
     * Entity Map Instance
45
     *
46
     * @var \Analogue\ORM\EntityMap
47
     */
48
    protected $entityMap;
49
    
50
    /**
51
     * The relationships that should be eager loaded.
52
     *
53
     * @var array
54
     */
55
    protected $eagerLoad = [];
56
57
    /**
58
     * All of the registered builder macros.
59
     *
60
     * @var array
61
     */
62
    protected $macros = [];
63
64
    /**
65
     * The methods that should be returned from query builder.
66
     *
67
     * @var array
68
     */
69
    protected $passthru = [
70
        'toSql',
71
        'lists',
72
        'pluck',
73
        'count',
74
        'min',
75
        'max',
76
        'avg',
77
        'sum',
78
        'exists',
79
        'getBindings',
80
    ];
81
82
    /**
83
     * Query Builder Blacklist
84
     */
85
    protected $blacklist = [
86
        'insert',
87
        'insertGetId',
88
        'lock',
89
        'lockForUpdate',
90
        'sharedLock',
91
        'update',
92
        'increment',
93
        'decrement',
94
        'delete',
95
        'truncate',
96
        'raw',
97
    ];
98
99
    /**
100
     * Create a new Analogue Query Builder instance.
101
     *
102
     * @param  Mapper    $mapper
103
     * @param  DBAdapter $adapter
104
     */
105
    public function __construct(Mapper $mapper, DBAdapter $adapter)
106
    {
107
        $this->mapper = $mapper;
108
109
        $this->adapter = $adapter;
110
111
        $this->entityMap = $mapper->getEntityMap();
112
113
        // Specify the table to work on
114
        $this->query = $adapter->getQuery()->from($this->entityMap->getTable());
115
116
        $this->with($this->entityMap->getEagerloadedRelationships());
117
    }
118
119
    /**
120
     * Run the query and return the result
121
     *
122
     * @param  array $columns
123
     * @return \Analogue\ORM\EntityCollection
124
     */
125
    public function get($columns = ['*'])
126
    {
127
        $entities = $this->getEntities($columns);
128
129
        // If we actually found models we will also eager load any relationships that
130
        // have been specified as needing to be eager loaded, which will solve the
131
        // n+1 query issue for the developers to avoid running a lot of queries.
132
133
        if (count($entities) > 0) {
134
            $entities = $this->eagerLoadRelations($entities);
135
        }
136
137
        return $this->entityMap->newCollection($entities);
138
    }
139
140
    /**
141
     * Find an entity by its primary key
142
     *
143
     * @param  string|integer $id
144
     * @param  array          $columns
145
     * @return \Analogue\ORM\Mappable
146
     */
147
    public function find($id, $columns = ['*'])
148
    {
149
        if (is_array($id)) {
150
            return $this->findMany($id, $columns);
151
        }
152
153
        $this->query->where($this->entityMap->getQualifiedKeyName(), '=', $id);
154
155
        return $this->first($columns);
156
    }
157
158
    /**
159
     * Find many entities by their primary keys.
160
     *
161
     * @param  array $id
162
     * @param  array $columns
163
     * @return EntityCollection
164
     */
165
    public function findMany($id, $columns = ['*'])
166
    {
167
        if (empty($id)) {
168
            return new EntityCollection;
169
        }
170
171
        $this->query->whereIn($this->entityMap->getQualifiedKeyName(), $id);
172
173
        return $this->get($columns);
174
    }
175
176
    /**
177
     * Find a model by its primary key or throw an exception.
178
     *
179
     * @param  mixed $id
180
     * @param  array $columns
181
     * @throws \Analogue\ORM\Exceptions\EntityNotFoundException
182
     * @return mixed|self
183
     */
184 View Code Duplication
    public function findOrFail($id, $columns = ['*'])
0 ignored issues
show
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
185
    {
186
        if (!is_null($entity = $this->find($id, $columns))) {
187
            return $entity;
188
        }
189
190
        throw (new EntityNotFoundException)->setEntity(get_class($this->entityMap));
191
    }
192
193
194
    /**
195
     * Execute the query and get the first result.
196
     *
197
     * @param  array $columns
198
     * @return \Analogue\ORM\Entity
199
     */
200
    public function first($columns = ['*'])
201
    {
202
        return $this->take(1)->get($columns)->first();
0 ignored issues
show
Documentation Bug introduced by
The method take does not exist on object<Analogue\ORM\System\Query>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
203
    }
204
205
    /**
206
     * Execute the query and get the first result or throw an exception.
207
     *
208
     * @param  array $columns
209
     * @throws EntityNotFoundException
210
     * @return \Analogue\ORM\Entity
211
     */
212 View Code Duplication
    public function firstOrFail($columns = ['*'])
0 ignored issues
show
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
213
    {
214
        if (!is_null($entity = $this->first($columns))) {
215
            return $entity;
216
        }
217
218
        throw (new EntityNotFoundException)->setEntity(get_class($this->entityMap));
219
    }
220
221
    /**
222
     * Pluck a single column from the database.
223
     *
224
     * @param  string $column
225
     * @return mixed
226
     */
227
    public function pluck($column)
228
    {
229
        $result = $this->first([$column]);
230
231
        if ($result) {
232
            return $result->{$column};
233
        }
234
    }
235
236
    /**
237
     * Chunk the results of the query.
238
     *
239
     * @param  int      $count
240
     * @param  callable $callback
241
     * @return void
242
     */
243
    public function chunk($count, callable $callback)
244
    {
245
        $results = $this->forPage($page = 1, $count)->get();
0 ignored issues
show
Documentation Bug introduced by
The method forPage does not exist on object<Analogue\ORM\System\Query>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
246
247
        while (count($results) > 0) {
248
            // On each chunk result set, we will pass them to the callback and then let the
249
            // developer take care of everything within the callback, which allows us to
250
            // keep the memory low for spinning through large result sets for working.
251
            call_user_func($callback, $results);
252
253
            $page++;
254
255
            $results = $this->forPage($page, $count)->get();
0 ignored issues
show
Documentation Bug introduced by
The method forPage does not exist on object<Analogue\ORM\System\Query>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
256
        }
257
    }
258
    
259
    /**
260
     * Get an array with the values of a given column.
261
     *
262
     * @param  string $column
263
     * @param  string $key
264
     * @return array
265
     */
266
    public function lists($column, $key = null)
267
    {
268
        return $this->query->lists($column, $key);
269
    }
270
271
    /**
272
     * Get a paginator for the "select" statement.
273
     *
274
     * @param  int   $perPage
275
     * @param  array $columns
276
     * @return LengthAwarePaginator
277
     */
278
    public function paginate($perPage = null, $columns = ['*'])
279
    {
280
        $total = $this->query->getCountForPagination();
281
282
        $this->query->forPage(
283
            $page = Paginator::resolveCurrentPage(),
284
            $perPage = $perPage ?: $this->entityMap->getPerPage()
285
        );
286
287
        return new LengthAwarePaginator($this->get($columns)->all(), $total, $perPage, $page, [
288
            'path' => Paginator::resolveCurrentPath()
289
        ]);
290
    }
291
292
    /**
293
     * Get a paginator for a grouped statement.
294
     *
295
     * @param  \Illuminate\Pagination\Factory $paginator
296
     * @param  int                            $perPage
297
     * @param  array                          $columns
298
     * @return \Illuminate\Pagination\Paginator
299
     */
300
    protected function groupedPaginate($paginator, $perPage, $columns)
301
    {
302
        $results = $this->get($columns)->all();
303
304
        return $this->query->buildRawPaginator($paginator, $results, $perPage);
305
    }
306
307
    /**
308
     * Get a paginator for an ungrouped statement.
309
     *
310
     * @param  \Illuminate\Pagination\Factory $paginator
311
     * @param  int                            $perPage
312
     * @param  array                          $columns
313
     * @return \Illuminate\Pagination\Paginator
314
     */
315
    protected function ungroupedPaginate($paginator, $perPage, $columns)
316
    {
317
        $total = $this->query->getPaginationCount();
318
319
        // Once we have the paginator we need to set the limit and offset values for
320
        // the query so we can get the properly paginated items. Once we have an
321
        // array of items we can create the paginator instances for the items.
322
        $page = $paginator->getCurrentPage($total);
323
324
        $this->query->forPage($page, $perPage);
325
326
        return $paginator->make($this->get($columns)->all(), $total, $perPage);
327
    }
328
329
    /**
330
     * Paginate the given query into a simple paginator.
331
     *
332
     * @param  int   $perPage
333
     * @param  array $columns
334
     * @return \Illuminate\Contracts\Pagination\Paginator
335
     */
336
    public function simplePaginate($perPage = null, $columns = ['*'])
337
    {
338
        $page = Paginator::resolveCurrentPage();
339
340
        $perPage = $perPage ?: $this->entityMap->getPerPage();
341
342
        $this->skip(($page - 1) * $perPage)->take($perPage + 1);
0 ignored issues
show
Documentation Bug introduced by
The method skip does not exist on object<Analogue\ORM\System\Query>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
343
344
        return new Paginator($this->get($columns)->all(), $perPage, $page, ['path' => Paginator::resolveCurrentPath()]);
345
    }
346
347
    /**
348
     * Add a basic where clause to the query.
349
     *
350
     * @param  string $column
351
     * @param  string $operator
352
     * @param  mixed  $value
353
     * @param  string $boolean
354
     * @return $this
355
     */
356
    public function where($column, $operator = null, $value = null, $boolean = 'and')
0 ignored issues
show
The parameter $operator is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
The parameter $value is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
357
    {
358
        if ($column instanceof Closure) {
359
            $query = $this->newQueryWithoutScopes();
360
361
            call_user_func($column, $query);
362
363
            $this->query->addNestedWhereQuery($query->getQuery(), $boolean);
364
        } else {
365
            call_user_func_array([$this->query, 'where'], func_get_args());
366
        }
367
368
        return $this;
369
    }
370
371
    /**
372
     * Add an "or where" clause to the query.
373
     *
374
     * @param  string $column
375
     * @param  string $operator
376
     * @param  mixed  $value
377
     * @return \Analogue\ORM\System\Query
378
     */
379
    public function orWhere($column, $operator = null, $value = null)
380
    {
381
        return $this->where($column, $operator, $value, 'or');
382
    }
383
384
    /**
385
     * Add a relationship count condition to the query.
386
     *
387
     * @param  string   $relation
388
     * @param  string   $operator
389
     * @param  int      $count
390
     * @param  string   $boolean
391
     * @param  \Closure $callback
392
     * @return \Analogue\ORM\System\Query
393
     */
394
    public function has($relation, $operator = '>=', $count = 1, $boolean = 'and', $callback = null)
395
    {
396
        $entity = $this->mapper->newInstance();
397
398
        $relation = $this->getHasRelationQuery($relation, $entity);
399
400
        $query = $relation->getRelationCountQuery($relation->getRelatedMapper()->getQuery(), $this);
0 ignored issues
show
Documentation Bug introduced by
The method getRelatedMapper does not exist on object<Analogue\ORM\System\Query>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
Documentation Bug introduced by
The method getRelationCountQuery does not exist on object<Analogue\ORM\System\Query>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
401
402
        if ($callback) {
403
            call_user_func($callback, $query);
404
        }
405
406
        return $this->addHasWhere($query, $relation, $operator, $count, $boolean);
0 ignored issues
show
$relation is of type object<Analogue\ORM\System\Query>, but the function expects a object<Analogue\ORM\Relationships\Relationship>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
407
    }
408
409
    /**
410
     * Add a relationship count condition to the query with where clauses.
411
     *
412
     * @param  string   $relation
413
     * @param  \Closure $callback
414
     * @param  string   $operator
415
     * @param  int      $count
416
     * @return \Analogue\ORM\System\Query
417
     */
418
    public function whereHas($relation, Closure $callback, $operator = '>=', $count = 1)
419
    {
420
        return $this->has($relation, $operator, $count, 'and', $callback);
421
    }
422
423
    /**
424
     * Add a relationship count condition to the query with an "or".
425
     *
426
     * @param  string $relation
427
     * @param  string $operator
428
     * @param  int    $count
429
     * @return \Analogue\ORM\System\Query
430
     */
431
    public function orHas($relation, $operator = '>=', $count = 1)
432
    {
433
        return $this->has($relation, $operator, $count, 'or');
434
    }
435
436
    /**
437
     * Add a relationship count condition to the query with where clauses and an "or".
438
     *
439
     * @param  string   $relation
440
     * @param  \Closure $callback
441
     * @param  string   $operator
442
     * @param  int      $count
443
     * @return \Analogue\ORM\System\Query
444
     */
445
    public function orWhereHas($relation, Closure $callback, $operator = '>=', $count = 1)
446
    {
447
        return $this->has($relation, $operator, $count, 'or', $callback);
448
    }
449
450
    /**
451
     * Add the "has" condition where clause to the query.
452
     *
453
     * @param  \Analogue\ORM\System\Query               $hasQuery
454
     * @param  \Analogue\ORM\Relationships\Relationship $relation
455
     * @param  string                                   $operator
456
     * @param  int                                      $count
457
     * @param  string                                   $boolean
458
     * @return \Analogue\ORM\System\Query
459
     */
460
    protected function addHasWhere(Query $hasQuery, Relationship $relation, $operator, $count, $boolean)
461
    {
462
        $this->mergeWheresToHas($hasQuery, $relation);
463
464
        if (is_numeric($count)) {
465
            $count = new Expression($count);
466
        }
467
468
        return $this->where(new Expression('(' . $hasQuery->toSql() . ')'), $operator, $count, $boolean);
0 ignored issues
show
Documentation Bug introduced by
The method toSql does not exist on object<Analogue\ORM\System\Query>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
469
    }
470
471
    /**
472
     * Merge the "wheres" from a relation query to a has query.
473
     *
474
     * @param  \Analogue\ORM\System\Query               $hasQuery
475
     * @param  \Analogue\ORM\Relationships\Relationship $relation
476
     * @return void
477
     */
478
    protected function mergeWheresToHas(Query $hasQuery, Relationship $relation)
479
    {
480
        // Here we have the "has" query and the original relation. We need to copy over any
481
        // where clauses the developer may have put in the relationship function over to
482
        // the has query, and then copy the bindings from the "has" query to the main.
483
        $relationQuery = $relation->getBaseQuery();
484
485
        $hasQuery->mergeWheres(
0 ignored issues
show
The method mergeWheres() does not exist on Analogue\ORM\System\Query. Did you maybe mean mergeWheresToHas()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
486
            $relationQuery->wheres, $relationQuery->getBindings()
487
        );
488
489
        $this->query->mergeBindings($hasQuery->getQuery());
490
    }
491
492
    /**
493
     * Get the "has relation" base query instance.
494
     *
495
     * @param  string $relation
496
     * @param         $entity
497
     * @return \Analogue\ORM\System\Query
498
     */
499
    protected function getHasRelationQuery($relation, $entity)
500
    {
501
        return Relationship::noConstraints(function () use ($relation, $entity) {
502
            return $this->entityMap->$relation($entity);
503
        });
504
    }
505
506
    /**
507
     * Get the table for the current query object
508
     *
509
     * @return string
510
     */
511
    public function getTable()
512
    {
513
        return $this->entityMap->getTable();
514
    }
515
516
    /**
517
     * Set the relationships that should be eager loaded.
518
     *
519
     * @param  mixed $relations
520
     * @return $this
521
     */
522
    public function with($relations)
523
    {
524
        if (is_string($relations)) {
525
            $relations = func_get_args();
526
        }
527
528
        $eagers = $this->parseRelations($relations);
529
530
        $this->eagerLoad = array_merge($this->eagerLoad, $eagers);
531
532
        return $this;
533
    }
534
535
    /**
536
     * Parse a list of relations into individuals.
537
     *
538
     * @param  array $relations
539
     * @return array
540
     */
541
    protected function parseRelations(array $relations)
542
    {
543
        $results = [];
544
545
        foreach ($relations as $name => $constraints) {
546
            // If the "relation" value is actually a numeric key, we can assume that no
547
            // constraints have been specified for the eager load and we'll just put
548
            // an empty Closure with the loader so that we can treat all the same.
549
            if (is_numeric($name)) {
550
                $f = function () {};
551
552
                list($name, $constraints) = [$constraints, $f];
553
            }
554
555
            // We need to separate out any nested includes. Which allows the developers
556
            // to load deep relationships using "dots" without stating each level of
557
            // the relationship with its own key in the array of eager load names.
558
            $results = $this->parseNested($name, $results);
559
560
            $results[$name] = $constraints;
561
        }
562
563
        return $results;
564
    }
565
566
567
    /**
568
     * Parse the nested relationships in a relation.
569
     *
570
     * @param  string $name
571
     * @param  array  $results
572
     * @return array
573
     */
574
    protected function parseNested($name, $results)
575
    {
576
        $progress = [];
577
578
        // If the relation has already been set on the result array, we will not set it
579
        // again, since that would override any constraints that were already placed
580
        // on the relationships. We will only set the ones that are not specified.
581
        foreach (explode('.', $name) as $segment) {
582
            $progress[] = $segment;
583
584
            if (!isset($results[$last = implode('.', $progress)])) {
585
                $results[$last] = function () {};
586
            }
587
        }
588
589
        return $results;
590
    }
591
592
    /**
593
     * Get the relationships being eagerly loaded.
594
     *
595
     * @return array
596
     */
597
    public function getEagerLoads()
598
    {
599
        return $this->eagerLoad;
600
    }
601
602
    /**
603
     * Set the relationships being eagerly loaded.
604
     *
605
     * @param  array $eagerLoad
606
     * @return void
607
     */
608
    public function setEagerLoads(array $eagerLoad)
609
    {
610
        $this->eagerLoad = $eagerLoad;
611
    }
612
613
    /**
614
     * Eager load the relationships for the entities.
615
     *
616
     * @param  array $entities
617
     * @return array
618
     */
619
    public function eagerLoadRelations($entities)
620
    {
621
        foreach ($this->eagerLoad as $name => $constraints) {
622
            // For nested eager loads we'll skip loading them here and they will be set as an
623
            // eager load on the query to retrieve the relation so that they will be eager
624
            // loaded on that query, because that is where they get hydrated as models.
625
            if (strpos($name, '.') === false) {
626
                $entities = $this->loadRelation($entities, $name, $constraints);
627
            }
628
        }
629
630
        return $entities;
631
    }
632
633
    /**
634
     * Eagerly load the relationship on a set of entities.
635
     *
636
     * @param  array    $entities
637
     * @param  string   $name
638
     * @param  \Closure $constraints
639
     * @return array
640
     */
641
    protected function loadRelation(array $entities, $name, Closure $constraints)
642
    {
643
        // First we will "back up" the existing where conditions on the query so we can
644
        // add our eager constraints. Then we will merge the wheres that were on the
645
        // query back to it in order that any where conditions might be specified.
646
        $relation = $this->getRelation($name);
647
648
        $relation->addEagerConstraints($entities);
649
650
        call_user_func($constraints, $relation);
651
652
        $entities = $relation->initRelation($entities, $name);
653
654
        // Once we have the results, we just match those back up to their parent models
655
        // using the relationship instance. Then we just return the finished arrays
656
        // of models which have been eagerly hydrated and are readied for return.
657
658
        $results = $relation->getEager();
659
660
        return $relation->match($entities, $results, $name);
661
    }
662
663
    /**
664
     * Get the relation instance for the given relation name.
665
     *
666
     * @param  string $relation
667
     * @return \Analogue\ORM\Relationships\Relationship
668
     */
669
    public function getRelation($relation)
670
    {
671
        // We want to run a relationship query without any constrains so that we will
672
        // not have to remove these where clauses manually which gets really hacky
673
        // and is error prone while we remove the developer's own where clauses.
674
        $query = Relationship::noConstraints(function () use ($relation) {
675
            return $this->entityMap->$relation($this->getEntityInstance());
676
        });
677
678
        $nested = $this->nestedRelations($relation);
679
680
        // If there are nested relationships set on the query, we will put those onto
681
        // the query instances so that they can be handled after this relationship
682
        // is loaded. In this way they will all trickle down as they are loaded.
683
        if (count($nested) > 0) {
684
            $query->getQuery()->with($nested);
685
        }
686
687
        return $query;
688
    }
689
690
    /**
691
     * Get the deeply nested relations for a given top-level relation.
692
     *
693
     * @param  string $relation
694
     * @return array
695
     */
696
    protected function nestedRelations($relation)
697
    {
698
        $nested = [];
699
700
        // We are basically looking for any relationships that are nested deeper than
701
        // the given top-level relationship. We will just check for any relations
702
        // that start with the given top relations and adds them to our arrays.
703
        foreach ($this->eagerLoad as $name => $constraints) {
704
            if ($this->isNested($name, $relation)) {
705
                $nested[substr($name, strlen($relation . '.'))] = $constraints;
706
            }
707
        }
708
709
        return $nested;
710
    }
711
712
    /**
713
     * Determine if the relationship is nested.
714
     *
715
     * @param  string $name
716
     * @param  string $relation
717
     * @return bool
718
     */
719
    protected function isNested($name, $relation)
720
    {
721
        $dots = str_contains($name, '.');
722
723
        return $dots && starts_with($name, $relation . '.');
724
    }
725
726
    /**
727
     * Add the Entity primary key if not in requested columns
728
     *
729
     * @param  array $columns
730
     * @return array
731
     */
732
    protected function enforceIdColumn($columns)
733
    {
734
        if (!in_array($this->entityMap->getKeyName(), $columns)) {
735
            $columns[] = $this->entityMap->getKeyName();
736
        }
737
        return $columns;
738
    }
739
740
    /**
741
     * Get the hydrated models without eager loading.
742
     *
743
     * @param  array  $columns
744
     * @return \Analogue\ORM\EntityCollection
745
     */
746
    public function getEntities($columns = ['*'])
747
    {
748
        // As we need the primary key to feed the
749
        // entity cache, we need it loaded on each
750
        // request
751
        $columns = $this->enforceIdColumn($columns);
752
753
        // Run the query
754
        $results = $this->query->get($columns);
755
756
        $builder = new EntityBuilder($this->mapper, array_keys($this->getEagerLoads()));
757
758
        return $builder->build($results);
759
    }
760
761
    /**
762
     * Get a new instance for the entity
763
     *
764
     * @param  array  $attributes
765
     * @return \Analogue\ORM\Entity
766
     */
767
    public function getEntityInstance(array $attributes = [])
768
    {
769
        return $this->mapper->newInstance($attributes);
770
    }
771
772
    /**
773
     * Extend the builder with a given callback.
774
     *
775
     * @param  string   $name
776
     * @param  \Closure $callback
777
     * @return void
778
     */
779
    public function macro($name, Closure $callback)
780
    {
781
        $this->macros[$name] = $callback;
782
    }
783
784
    /**
785
     * Get the given macro by name.
786
     *
787
     * @param  string $name
788
     * @return \Closure
789
     */
790
    public function getMacro($name)
791
    {
792
        return array_get($this->macros, $name);
793
    }
794
795
    /**
796
     * Get a new query builder for the model's table.
797
     *
798
     * @return \Analogue\ORM\System\Query
799
     */
800
    public function newQuery()
801
    {
802
        $builder = new Query($this->mapper, $this->adapter);
803
804
        return $this->applyGlobalScopes($builder);
0 ignored issues
show
Documentation Bug introduced by
The method applyGlobalScopes does not exist on object<Analogue\ORM\System\Query>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
805
    }
806
807
    /**
808
     * Get a new query builder without any scope applied.
809
     *
810
     * @return \Analogue\ORM\System\Query
811
     */
812
    public function newQueryWithoutScopes()
813
    {
814
        return new Query($this->mapper, $this->adapter);
815
    }
816
817
    /**
818
     * Get the Mapper instance for this Query Builder
819
     *
820
     * @return \Analogue\ORM\System\Mapper
821
     */
822
    public function getMapper()
823
    {
824
        return $this->mapper;
825
    }
826
827
    /**
828
     * Get the underlying query adapter
829
     *
830
     * (REFACTOR: this method should move out, we need to provide the client classes
831
     * with the adapter instead.)
832
     *
833
     * @return \Analogue\ORM\Drivers\QueryAdapter|\Analogue\ORM\Drivers\IlluminateQueryAdapter
834
     */
835
    public function getQuery()
836
    {
837
        return $this->query;
838
    }
839
840
    /**
841
     * Dynamically handle calls into the query instance.
842
     *
843
     * @param  string $method
844
     * @param  array  $parameters
845
     * @throws Exception
846
     * @return mixed
847
     */
848
    public function __call($method, $parameters)
849
    {
850
        if (isset($this->macros[$method])) {
851
            array_unshift($parameters, $this);
852
853
            return call_user_func_array($this->macros[$method], $parameters);
854
        }
855
        
856
        if (in_array($method, $this->blacklist)) {
857
            throw new Exception("Method $method doesn't exist");
858
        }
859
860
        $result = call_user_func_array([$this->query, $method], $parameters);
861
862
        return in_array($method, $this->passthru) ? $result : $this;
863
    }
864
}
865