Completed
Push — 5.1 ( af1a7b...f5b0b1 )
by Rémi
03:26
created

Query   C

Complexity

Total Complexity 71

Size/Duplication

Total Lines 845
Duplicated Lines 1.89 %

Coupling/Cohesion

Components 2
Dependencies 10

Importance

Changes 21
Bugs 8 Features 2
Metric Value
c 21
b 8
f 2
dl 16
loc 845
rs 5
wmc 71
lcom 2
cbo 10

44 Methods

Rating   Name   Duplication   Size   Complexity  
A newQueryWithoutScopes() 0 4 1
A enforceIdColumn() 0 7 2
A __construct() 0 13 1
A get() 0 14 2
A find() 0 10 2
A findMany() 0 10 2
A findOrFail() 8 8 2
A first() 0 4 1
A firstOrFail() 8 8 2
A pluck() 0 8 2
A chunk() 0 15 2
A lists() 0 4 1
A paginate() 0 13 2
A groupedPaginate() 0 6 1
A ungroupedPaginate() 0 13 1
A simplePaginate() 0 10 2
A where() 0 14 2
A orWhere() 0 4 1
A has() 0 14 2
A whereHas() 0 4 1
A orHas() 0 4 1
A orWhereHas() 0 4 1
A addHasWhere() 0 10 2
A mergeWheresToHas() 0 13 1
A getHasRelationQuery() 0 6 1
A getTable() 0 4 1
B parseRelations() 0 24 3
A parseNested() 0 17 3
A getEagerLoads() 0 4 1
A setEagerLoads() 0 4 1
A eagerLoadRelations() 0 13 3
A loadRelation() 0 21 1
A getRelation() 0 20 2
A nestedRelations() 0 15 3
A isNested() 0 6 2
A getEntityInstance() 0 4 1
A macro() 0 4 1
A getMacro() 0 4 1
A newQuery() 0 6 1
A getMapper() 0 4 1
A getQuery() 0 4 1
A __call() 0 16 4
A getEntities() 0 14 1
A with() 0 12 2

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like Query often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Query, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Analogue\ORM\System;
4
5
use Analogue\ORM\Drivers\QueryAdapter;
6
use Closure;
7
use Exception;
8
use Analogue\ORM\EntityCollection;
9
use Analogue\ORM\Relationships\Relationship;
10
use Analogue\ORM\Exceptions\EntityNotFoundException;
11
use Analogue\ORM\Drivers\DBAdapter;
12
use Illuminate\Pagination\Paginator;
13
use Illuminate\Pagination\LengthAwarePaginator;
14
use Illuminate\Database\Query\Expression;
15
16
/**
17
 * Analogue Query builder.
18
 *
19
 * @mixin QueryAdapter|\Analogue\ORM\Drivers\IlluminateQueryAdapter
20
 */
21
class Query
22
{
23
    /**
24
     * Mapper Instance
25
     *
26
     * @var \Analogue\ORM\System\Mapper
27
     */
28
    protected $mapper;
29
30
    /**
31
     * DB Adatper
32
     *
33
     * @var \Analogue\ORM\Drivers\DBAdapter
34
     */
35
    protected $adapter;
36
37
    /**
38
     * Query Builder Instance
39
     *
40
     * @var \Analogue\ORM\Drivers\QueryAdapter|\Analogue\ORM\Drivers\IlluminateQueryAdapter
41
     */
42
    protected $query;
43
44
    /**
45
     * Entity Map Instance
46
     *
47
     * @var \Analogue\ORM\EntityMap
48
     */
49
    protected $entityMap;
50
    
51
    /**
52
     * The relationships that should be eager loaded.
53
     *
54
     * @var array
55
     */
56
    protected $eagerLoad = [];
57
58
    /**
59
     * All of the registered builder macros.
60
     *
61
     * @var array
62
     */
63
    protected $macros = [];
64
65
    /**
66
     * The methods that should be returned from query builder.
67
     *
68
     * @var array
69
     */
70
    protected $passthru = [
71
        'toSql',
72
        'lists',
73
        'pluck',
74
        'count',
75
        'min',
76
        'max',
77
        'avg',
78
        'sum',
79
        'exists',
80
        'getBindings',
81
    ];
82
83
    /**
84
     * Query Builder Blacklist
85
     */
86
    protected $blacklist = [
87
        'insert',
88
        'insertGetId',
89
        'lock',
90
        'lockForUpdate',
91
        'sharedLock',
92
        'update',
93
        'increment',
94
        'decrement',
95
        'delete',
96
        'truncate',
97
        'raw',
98
    ];
99
100
    /**
101
     * Create a new Analogue Query Builder instance.
102
     *
103
     * @param  Mapper    $mapper
104
     * @param  DBAdapter $adapter
105
     */
106
    public function __construct(Mapper $mapper, DBAdapter $adapter)
107
    {
108
        $this->mapper = $mapper;
109
110
        $this->adapter = $adapter;
111
112
        $this->entityMap = $mapper->getEntityMap();
113
114
        // Specify the table to work on
115
        $this->query = $adapter->getQuery()->from($this->entityMap->getTable());
0 ignored issues
show
Bug introduced by
The method from() does not seem to exist on object<Analogue\ORM\Drivers\QueryAdapter>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
116
117
        $this->with($this->entityMap->getEagerloadedRelationships());
118
    }
119
120
    /**
121
     * Run the query and return the result
122
     *
123
     * @param  array $columns
124
     * @return \Analogue\ORM\EntityCollection
125
     */
126
    public function get($columns = ['*'])
127
    {
128
        $entities = $this->getEntities($columns);
129
130
        // If we actually found models we will also eager load any relationships that
131
        // have been specified as needing to be eager loaded, which will solve the
132
        // n+1 query issue for the developers to avoid running a lot of queries.
133
134
        if (count($entities) > 0) {
135
            $entities = $this->eagerLoadRelations($entities);
136
        }
137
138
        return $this->entityMap->newCollection($entities);
139
    }
140
141
    /**
142
     * Find an entity by its primary key
143
     *
144
     * @param  string|integer $id
145
     * @param  array          $columns
146
     * @return \Analogue\ORM\Mappable
147
     */
148
    public function find($id, $columns = ['*'])
149
    {
150
        if (is_array($id)) {
151
            return $this->findMany($id, $columns);
152
        }
153
154
        $this->query->where($this->entityMap->getQualifiedKeyName(), '=', $id);
0 ignored issues
show
Bug introduced by
The method where() does not seem to exist on object<Analogue\ORM\Drivers\QueryAdapter>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
155
156
        return $this->first($columns);
157
    }
158
159
    /**
160
     * Find many entities by their primary keys.
161
     *
162
     * @param  array $id
163
     * @param  array $columns
164
     * @return EntityCollection
165
     */
166
    public function findMany($id, $columns = ['*'])
167
    {
168
        if (empty($id)) {
169
            return new EntityCollection;
170
        }
171
172
        $this->query->whereIn($this->entityMap->getQualifiedKeyName(), $id);
0 ignored issues
show
Bug introduced by
The method whereIn() does not seem to exist on object<Analogue\ORM\Drivers\QueryAdapter>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
173
174
        return $this->get($columns);
175
    }
176
177
    /**
178
     * Find a model by its primary key or throw an exception.
179
     *
180
     * @param  mixed $id
181
     * @param  array $columns
182
     * @throws \Analogue\ORM\Exceptions\EntityNotFoundException
183
     * @return mixed|self
184
     */
185 View Code Duplication
    public function findOrFail($id, $columns = ['*'])
0 ignored issues
show
Duplication introduced by
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...
186
    {
187
        if (!is_null($entity = $this->find($id, $columns))) {
188
            return $entity;
189
        }
190
191
        throw (new EntityNotFoundException)->setEntity(get_class($this->entityMap));
192
    }
193
194
195
    /**
196
     * Execute the query and get the first result.
197
     *
198
     * @param  array $columns
199
     * @return \Analogue\ORM\Entity
200
     */
201
    public function first($columns = ['*'])
202
    {
203
        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...
204
    }
205
206
    /**
207
     * Execute the query and get the first result or throw an exception.
208
     *
209
     * @param  array $columns
210
     * @throws EntityNotFoundException
211
     * @return \Analogue\ORM\Entity
212
     */
213 View Code Duplication
    public function firstOrFail($columns = ['*'])
0 ignored issues
show
Duplication introduced by
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...
214
    {
215
        if (!is_null($entity = $this->first($columns))) {
216
            return $entity;
217
        }
218
219
        throw (new EntityNotFoundException)->setEntity(get_class($this->entityMap));
220
    }
221
222
    /**
223
     * Pluck a single column from the database.
224
     *
225
     * @param  string $column
226
     * @return mixed
227
     */
228
    public function pluck($column)
229
    {
230
        $result = $this->first([$column]);
231
232
        if ($result) {
233
            return $result->{$column};
234
        }
235
    }
236
237
    /**
238
     * Chunk the results of the query.
239
     *
240
     * @param  int      $count
241
     * @param  callable $callback
242
     * @return void
243
     */
244
    public function chunk($count, callable $callback)
245
    {
246
        $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...
247
248
        while (count($results) > 0) {
249
            // On each chunk result set, we will pass them to the callback and then let the
250
            // developer take care of everything within the callback, which allows us to
251
            // keep the memory low for spinning through large result sets for working.
252
            call_user_func($callback, $results);
253
254
            $page++;
255
256
            $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...
257
        }
258
    }
259
    
260
    /**
261
     * Get an array with the values of a given column.
262
     *
263
     * @param  string $column
264
     * @param  string $key
265
     * @return array
266
     */
267
    public function lists($column, $key = null)
268
    {
269
        return $this->query->lists($column, $key);
0 ignored issues
show
Bug introduced by
The method lists() does not seem to exist on object<Analogue\ORM\Drivers\QueryAdapter>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
270
    }
271
272
    /**
273
     * Get a paginator for the "select" statement.
274
     *
275
     * @param  int   $perPage
276
     * @param  array $columns
277
     * @return LengthAwarePaginator
278
     */
279
    public function paginate($perPage = null, $columns = ['*'])
280
    {
281
        $total = $this->query->getCountForPagination();
0 ignored issues
show
Bug introduced by
The method getCountForPagination() does not seem to exist on object<Analogue\ORM\Drivers\QueryAdapter>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
282
283
        $this->query->forPage(
0 ignored issues
show
Bug introduced by
The method forPage() does not seem to exist on object<Analogue\ORM\Drivers\QueryAdapter>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
284
            $page = Paginator::resolveCurrentPage(),
285
            $perPage = $perPage ?: $this->entityMap->getPerPage()
286
        );
287
288
        return new LengthAwarePaginator($this->get($columns)->all(), $total, $perPage, $page, [
289
            'path' => Paginator::resolveCurrentPath()
290
        ]);
291
    }
292
293
    /**
294
     * Get a paginator for a grouped statement.
295
     *
296
     * @param  \Illuminate\Pagination\Factory $paginator
297
     * @param  int                            $perPage
298
     * @param  array                          $columns
299
     * @return \Illuminate\Pagination\Paginator
300
     */
301
    protected function groupedPaginate($paginator, $perPage, $columns)
302
    {
303
        $results = $this->get($columns)->all();
304
305
        return $this->query->buildRawPaginator($paginator, $results, $perPage);
0 ignored issues
show
Bug introduced by
The method buildRawPaginator() does not seem to exist on object<Analogue\ORM\Drivers\QueryAdapter>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
306
    }
307
308
    /**
309
     * Get a paginator for an ungrouped statement.
310
     *
311
     * @param  \Illuminate\Pagination\Factory $paginator
312
     * @param  int                            $perPage
313
     * @param  array                          $columns
314
     * @return \Illuminate\Pagination\Paginator
315
     */
316
    protected function ungroupedPaginate($paginator, $perPage, $columns)
317
    {
318
        $total = $this->query->getPaginationCount();
0 ignored issues
show
Bug introduced by
The method getPaginationCount() does not seem to exist on object<Analogue\ORM\Drivers\QueryAdapter>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
319
320
        // Once we have the paginator we need to set the limit and offset values for
321
        // the query so we can get the properly paginated items. Once we have an
322
        // array of items we can create the paginator instances for the items.
323
        $page = $paginator->getCurrentPage($total);
324
325
        $this->query->forPage($page, $perPage);
0 ignored issues
show
Bug introduced by
The method forPage() does not seem to exist on object<Analogue\ORM\Drivers\QueryAdapter>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
326
327
        return $paginator->make($this->get($columns)->all(), $total, $perPage);
328
    }
329
330
    /**
331
     * Paginate the given query into a simple paginator.
332
     *
333
     * @param  int   $perPage
334
     * @param  array $columns
335
     * @return \Illuminate\Contracts\Pagination\Paginator
336
     */
337
    public function simplePaginate($perPage = null, $columns = ['*'])
338
    {
339
        $page = Paginator::resolveCurrentPage();
340
341
        $perPage = $perPage ?: $this->entityMap->getPerPage();
342
343
        $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...
344
345
        return new Paginator($this->get($columns)->all(), $perPage, $page, ['path' => Paginator::resolveCurrentPath()]);
346
    }
347
348
    /**
349
     * Add a basic where clause to the query.
350
     *
351
     * @param  string $column
352
     * @param  string $operator
353
     * @param  mixed  $value
354
     * @param  string $boolean
355
     * @return $this
356
     */
357
    public function where($column, $operator = null, $value = null, $boolean = 'and')
0 ignored issues
show
Unused Code introduced by
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...
Unused Code introduced by
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...
358
    {
359
        if ($column instanceof Closure) {
360
            $query = $this->newQueryWithoutScopes();
361
362
            call_user_func($column, $query);
363
364
            $this->query->addNestedWhereQuery($query->getQuery(), $boolean);
0 ignored issues
show
Bug introduced by
The method addNestedWhereQuery() does not seem to exist on object<Analogue\ORM\Drivers\QueryAdapter>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
365
        } else {
366
            call_user_func_array([$this->query, 'where'], func_get_args());
367
        }
368
369
        return $this;
370
    }
371
372
    /**
373
     * Add an "or where" clause to the query.
374
     *
375
     * @param  string $column
376
     * @param  string $operator
377
     * @param  mixed  $value
378
     * @return \Analogue\ORM\System\Query
379
     */
380
    public function orWhere($column, $operator = null, $value = null)
381
    {
382
        return $this->where($column, $operator, $value, 'or');
383
    }
384
385
    /**
386
     * Add a relationship count condition to the query.
387
     *
388
     * @param  string   $relation
389
     * @param  string   $operator
390
     * @param  int      $count
391
     * @param  string   $boolean
392
     * @param  \Closure $callback
393
     * @return \Analogue\ORM\System\Query
394
     */
395
    public function has($relation, $operator = '>=', $count = 1, $boolean = 'and', $callback = null)
396
    {
397
        $entity = $this->mapper->newInstance();
398
399
        $relation = $this->getHasRelationQuery($relation, $entity);
400
401
        $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...
402
403
        if ($callback) {
404
            call_user_func($callback, $query);
405
        }
406
407
        return $this->addHasWhere($query, $relation, $operator, $count, $boolean);
0 ignored issues
show
Documentation introduced by
$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...
408
    }
409
410
    /**
411
     * Add a relationship count condition to the query with where clauses.
412
     *
413
     * @param  string   $relation
414
     * @param  \Closure $callback
415
     * @param  string   $operator
416
     * @param  int      $count
417
     * @return \Analogue\ORM\System\Query
418
     */
419
    public function whereHas($relation, Closure $callback, $operator = '>=', $count = 1)
420
    {
421
        return $this->has($relation, $operator, $count, 'and', $callback);
422
    }
423
424
    /**
425
     * Add a relationship count condition to the query with an "or".
426
     *
427
     * @param  string $relation
428
     * @param  string $operator
429
     * @param  int    $count
430
     * @return \Analogue\ORM\System\Query
431
     */
432
    public function orHas($relation, $operator = '>=', $count = 1)
433
    {
434
        return $this->has($relation, $operator, $count, 'or');
435
    }
436
437
    /**
438
     * Add a relationship count condition to the query with where clauses and an "or".
439
     *
440
     * @param  string   $relation
441
     * @param  \Closure $callback
442
     * @param  string   $operator
443
     * @param  int      $count
444
     * @return \Analogue\ORM\System\Query
445
     */
446
    public function orWhereHas($relation, Closure $callback, $operator = '>=', $count = 1)
447
    {
448
        return $this->has($relation, $operator, $count, 'or', $callback);
449
    }
450
451
    /**
452
     * Add the "has" condition where clause to the query.
453
     *
454
     * @param  \Analogue\ORM\System\Query               $hasQuery
455
     * @param  \Analogue\ORM\Relationships\Relationship $relation
456
     * @param  string                                   $operator
457
     * @param  int                                      $count
458
     * @param  string                                   $boolean
459
     * @return \Analogue\ORM\System\Query
460
     */
461
    protected function addHasWhere(Query $hasQuery, Relationship $relation, $operator, $count, $boolean)
462
    {
463
        $this->mergeWheresToHas($hasQuery, $relation);
464
465
        if (is_numeric($count)) {
466
            $count = new Expression($count);
467
        }
468
469
        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...
470
    }
471
472
    /**
473
     * Merge the "wheres" from a relation query to a has query.
474
     *
475
     * @param  \Analogue\ORM\System\Query               $hasQuery
476
     * @param  \Analogue\ORM\Relationships\Relationship $relation
477
     * @return void
478
     */
479
    protected function mergeWheresToHas(Query $hasQuery, Relationship $relation)
480
    {
481
        // Here we have the "has" query and the original relation. We need to copy over any
482
        // where clauses the developer may have put in the relationship function over to
483
        // the has query, and then copy the bindings from the "has" query to the main.
484
        $relationQuery = $relation->getBaseQuery();
485
486
        $hasQuery->mergeWheres(
0 ignored issues
show
Bug introduced by
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...
487
            $relationQuery->wheres, $relationQuery->getBindings()
0 ignored issues
show
Bug introduced by
Accessing wheres on the interface Analogue\ORM\Drivers\QueryAdapter suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
Bug introduced by
The method getBindings() does not seem to exist on object<Analogue\ORM\Drivers\QueryAdapter>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
488
        );
489
490
        $this->query->mergeBindings($hasQuery->getQuery());
0 ignored issues
show
Bug introduced by
The method mergeBindings() does not seem to exist on object<Analogue\ORM\Drivers\QueryAdapter>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

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

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
756
757
        $builder = new EntityBuilder($this->mapper, array_keys($this->getEagerLoads()));
758
759
        return $builder->build($results);
760
    }
761
762
    /**
763
     * Get a new instance for the entity
764
     *
765
     * @param  array  $attributes
766
     * @return \Analogue\ORM\Entity
767
     */
768
    public function getEntityInstance(array $attributes = [])
769
    {
770
        return $this->mapper->newInstance($attributes);
771
    }
772
773
    /**
774
     * Extend the builder with a given callback.
775
     *
776
     * @param  string   $name
777
     * @param  \Closure $callback
778
     * @return void
779
     */
780
    public function macro($name, Closure $callback)
781
    {
782
        $this->macros[$name] = $callback;
783
    }
784
785
    /**
786
     * Get the given macro by name.
787
     *
788
     * @param  string $name
789
     * @return \Closure
790
     */
791
    public function getMacro($name)
792
    {
793
        return array_get($this->macros, $name);
794
    }
795
796
    /**
797
     * Get a new query builder for the model's table.
798
     *
799
     * @return \Analogue\ORM\System\Query
800
     */
801
    public function newQuery()
802
    {
803
        $builder = new Query($this->mapper, $this->adapter);
804
805
        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...
806
    }
807
808
    /**
809
     * Get a new query builder without any scope applied.
810
     *
811
     * @return \Analogue\ORM\System\Query
812
     */
813
    public function newQueryWithoutScopes()
814
    {
815
        return new Query($this->mapper, $this->adapter);
816
    }
817
818
    /**
819
     * Get the Mapper instance for this Query Builder
820
     *
821
     * @return \Analogue\ORM\System\Mapper
822
     */
823
    public function getMapper()
824
    {
825
        return $this->mapper;
826
    }
827
828
    /**
829
     * Get the underlying query adapter
830
     *
831
     * (REFACTOR: this method should move out, we need to provide the client classes
832
     * with the adapter instead.)
833
     *
834
     * @return \Analogue\ORM\Drivers\QueryAdapter|\Analogue\ORM\Drivers\IlluminateQueryAdapter
835
     */
836
    public function getQuery()
837
    {
838
        return $this->query;
839
    }
840
841
    /**
842
     * Dynamically handle calls into the query instance.
843
     *
844
     * @param  string $method
845
     * @param  array  $parameters
846
     * @throws Exception
847
     * @return mixed
848
     */
849
    public function __call($method, $parameters)
850
    {
851
        if (isset($this->macros[$method])) {
852
            array_unshift($parameters, $this);
853
854
            return call_user_func_array($this->macros[$method], $parameters);
855
        }
856
        
857
        if (in_array($method, $this->blacklist)) {
858
            throw new Exception("Method $method doesn't exist");
859
        }
860
861
        $result = call_user_func_array([$this->query, $method], $parameters);
862
863
        return in_array($method, $this->passthru) ? $result : $this;
864
    }
865
}
866