Completed
Pull Request — master (#126)
by Alex
01:49
created

LaravelReadQuery   F

Complexity

Total Complexity 69

Size/Duplication

Total Lines 463
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 18

Importance

Changes 0
Metric Value
wmc 69
lcom 1
cbo 18
dl 0
loc 463
rs 2.8301
c 0
b 0
f 0

13 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 2
F getResourceSet() 0 172 35
B getRelatedResourceSet() 0 29 1
A getResourceFromResourceSet() 0 7 1
B getResource() 0 38 6
A getRelatedResourceReference() 0 17 2
A getResourceFromRelatedResourceSet() 0 22 3
A getSourceEntityInstance() 0 5 1
A checkSourceInstance() 0 7 4
A getAuth() 0 4 1
B checkAuth() 0 11 6
A processKeyDescriptor() 0 10 4
A processEagerLoadList() 0 11 3

How to fix   Complexity   

Complex Class

Complex classes like LaravelReadQuery 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 LaravelReadQuery, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace AlgoWeb\PODataLaravel\Query;
4
5
use AlgoWeb\PODataLaravel\Auth\NullAuthProvider;
6
use AlgoWeb\PODataLaravel\Enums\ActionVerb;
7
use AlgoWeb\PODataLaravel\Interfaces\AuthInterface;
8
use Illuminate\Database\Eloquent\Model;
9
use Illuminate\Database\Eloquent\Relations\Relation;
10
use Illuminate\Support\Facades\App;
11
use POData\Common\InvalidOperationException;
12
use POData\Common\ODataException;
13
use POData\Providers\Metadata\ResourceProperty;
14
use POData\Providers\Metadata\ResourceSet;
15
use POData\Providers\Query\QueryResult;
16
use POData\Providers\Query\QueryType;
17
use POData\UriProcessor\QueryProcessor\ExpressionParser\FilterInfo;
18
use POData\UriProcessor\QueryProcessor\OrderByParser\InternalOrderByInfo;
19
use POData\UriProcessor\QueryProcessor\SkipTokenParser\SkipTokenInfo;
20
use POData\UriProcessor\ResourcePathProcessor\SegmentParser\KeyDescriptor;
21
use Symfony\Component\Process\Exception\InvalidArgumentException;
22
23
class LaravelReadQuery
24
{
25
    const PK = 'PrimaryKey';
26
27
    protected $auth;
28
29
    public function __construct(AuthInterface $auth = null)
30
    {
31
        $this->auth = isset($auth) ? $auth : new NullAuthProvider();
32
    }
33
34
    /**
35
     * Gets collection of entities belongs to an entity set
36
     * IE: http://host/EntitySet
37
     *  http://host/EntitySet?$skip=10&$top=5&filter=Prop gt Value.
38
     *
39
     * @param QueryType                $queryType            Is this is a query for a count, entities,
40
     *                                                       or entities-with-count?
41
     * @param ResourceSet              $resourceSet          The entity set containing the entities to fetch
42
     * @param FilterInfo|null          $filterInfo           The $filter parameter of the OData query.  NULL if absent
43
     * @param null|InternalOrderByInfo $orderBy              sorted order if we want to get the data in some
44
     *                                                       specific order
45
     * @param int|null                 $top                  number of records which need to be retrieved
46
     * @param int|null                 $skip                 number of records which need to be skipped
47
     * @param SkipTokenInfo|null       $skipToken            value indicating what records to skip
48
     * @param string[]|null            $eagerLoad            array of relations to eager load
49
     * @param Model|Relation|null      $sourceEntityInstance Starting point of query
50
     *
51
     * @return QueryResult
52
     */
53
    public function getResourceSet(
54
        QueryType $queryType,
55
        ResourceSet $resourceSet,
56
        $filterInfo = null,
57
        $orderBy = null,
58
        $top = null,
59
        $skip = null,
60
        $skipToken = null,
61
        array $eagerLoad = null,
62
        $sourceEntityInstance = null
63
    ) {
64
        if (null != $filterInfo && !($filterInfo instanceof FilterInfo)) {
65
            $msg = 'Filter info must be either null or instance of FilterInfo.';
66
            throw new InvalidArgumentException($msg);
67
        }
68
        if (null != $skipToken && !($skipToken instanceof SkipTokenInfo)) {
69
            $msg = 'Skip token must be either null or instance of SkipTokenInfo.';
70
            throw new InvalidArgumentException($msg);
71
        }
72
73
        $rawLoad = $this->processEagerLoadList($eagerLoad);
74
        $modelLoad = [];
75
76
        $this->checkSourceInstance($sourceEntityInstance);
77
        if (null == $sourceEntityInstance) {
78
            $sourceEntityInstance = $this->getSourceEntityInstance($resourceSet);
79
        }
80
81
        $keyName = null;
82
        if ($sourceEntityInstance instanceof Model) {
83
            $modelLoad = $sourceEntityInstance->getEagerLoad();
84
            $keyName = $sourceEntityInstance->getKeyName();
85
        } elseif ($sourceEntityInstance instanceof Relation) {
86
            $modelLoad = $sourceEntityInstance->getRelated()->getEagerLoad();
87
            $keyName = $sourceEntityInstance->getRelated()->getKeyName();
88
        }
89
        assert(isset($keyName));
90
        $rawLoad = array_values(array_unique(array_merge($rawLoad, $modelLoad)));
91
92
        $checkInstance = $sourceEntityInstance instanceof Model ? $sourceEntityInstance : null;
93
        $this->checkAuth($sourceEntityInstance, $checkInstance);
94
95
        $result          = new QueryResult();
96
        $result->results = null;
97
        $result->count   = null;
98
99
        if (null != $orderBy) {
100
            foreach ($orderBy->getOrderByInfo()->getOrderByPathSegments() as $order) {
101
                foreach ($order->getSubPathSegments() as $subOrder) {
102
                    $subName = $subOrder->getName();
103
                    $subName = (self::PK == $subName) ? $keyName : $subName;
104
                    $sourceEntityInstance = $sourceEntityInstance->orderBy(
105
                        $subName,
106
                        $order->isAscending() ? 'asc' : 'desc'
107
                    );
108
                }
109
            }
110
        }
111
112
        // throttle up for trench run
113
        if (null != $skipToken) {
114
            $parameters = [];
115
            $processed = [];
116
            $segments = $skipToken->getOrderByInfo()->getOrderByPathSegments();
117
            $values = $skipToken->getOrderByKeysInToken();
118
            $numValues = count($values);
119
            assert($numValues == count($segments));
120
121
            for ($i = 0; $i < $numValues; $i++) {
122
                $relation = $segments[$i]->isAscending() ? '>' : '<';
123
                $name = $segments[$i]->getSubPathSegments()[0]->getName();
124
                $name = (self::PK == $name) ? $keyName : $name;
125
                $parameters[$name] = ['direction' => $relation, 'value' => trim($values[$i][0], '\'')];
126
            }
127
128
            foreach ($parameters as $name => $line) {
129
                $processed[$name] = ['direction' => $line['direction'], 'value' => $line['value']];
130
                $sourceEntityInstance = $sourceEntityInstance
131
                    ->orWhere(
132
                        function ($query) use ($processed) {
133
                            foreach ($processed as $key => $proc) {
134
                                $query->where($key, $proc['direction'], $proc['value']);
135
                            }
136
                        }
137
                    );
138
                // now we've handled the later-in-order segment for this key, drop it back to equality in prep
139
                // for next key - same-in-order for processed keys and later-in-order for next
140
                $processed[$name]['direction'] = '=';
141
            }
142
        }
143
144
        if (!isset($skip)) {
145
            $skip = 0;
146
        }
147
        if (!isset($top)) {
148
            $top = PHP_INT_MAX;
149
        }
150
151
        $nullFilter = true;
152
        $isvalid = null;
153
        if (isset($filterInfo)) {
154
            $method = 'return ' . $filterInfo->getExpressionAsString() . ';';
155
            $clln = '$' . $resourceSet->getResourceType()->getName();
156
            $isvalid = create_function($clln, $method);
0 ignored issues
show
Security Best Practice introduced by
The use of create_function is highly discouraged, better use a closure.

create_function can pose a great security vulnerability as it is similar to eval, and could be used for arbitrary code execution. We highly recommend to use a closure instead.

// Instead of
$function = create_function('$a, $b', 'return $a + $b');

// Better use
$function = function($a, $b) { return $a + $b; }
Loading history...
157
            $nullFilter = false;
158
        }
159
160
        $bulkSetCount = $sourceEntityInstance->count();
161
        $bigSet = 20000 < $bulkSetCount;
162
163
        if ($nullFilter) {
164
            // default no-filter case, palm processing off to database engine - is a lot faster
165
            $resultSet = $sourceEntityInstance->skip($skip)->take($top)->with($rawLoad)->get();
166
            $resultCount = $bulkSetCount;
167
        } elseif ($bigSet) {
168
            assert(isset($isvalid), 'Filter closure not set');
169
            $resultSet = collect([]);
170
            $rawCount = 0;
171
            $rawTop = null === $top ? $bulkSetCount : $top;
172
173
            // loop thru, chunk by chunk, to reduce chances of exhausting memory
174
            $sourceEntityInstance->chunk(
175
                5000,
176
                function ($results) use ($isvalid, &$skip, &$resultSet, &$rawCount, $rawTop) {
177
                    // apply filter
178
                    $results = $results->filter($isvalid);
179
                    // need to iterate through full result set to find count of items matching filter,
180
                    // so we can't bail out early
181
                    $rawCount += $results->count();
182
                    // now bolt on filtrate to accumulating result set if we haven't accumulated enough bitz
183
                    if ($rawTop > $resultSet->count()+$skip) {
184
                        $resultSet = collect(array_merge($resultSet->all(), $results->all()));
185
                        $sliceAmount = min($skip, $resultSet->count());
186
                        $resultSet = $resultSet->slice($sliceAmount);
187
                        $skip -= $sliceAmount;
188
                    }
189
                }
190
            );
191
192
            // clean up residual to-be-skipped records
193
            $resultSet = $resultSet->slice($skip);
194
            $resultCount = $rawCount;
195
        } else {
196
            if ($sourceEntityInstance instanceof Model) {
197
                $sourceEntityInstance = $sourceEntityInstance->getQuery();
198
            }
199
            $resultSet = $sourceEntityInstance->with($rawLoad)->get();
200
            $resultSet = $resultSet->filter($isvalid);
201
            $resultCount = $resultSet->count();
202
203
            if (isset($skip)) {
204
                $resultSet = $resultSet->slice($skip);
205
            }
206
        }
207
208
        if (isset($top)) {
209
            $resultSet = $resultSet->take($top);
210
        }
211
212
        if (QueryType::ENTITIES() == $queryType || QueryType::ENTITIES_WITH_COUNT() == $queryType) {
213
            $result->results = [];
214
            foreach ($resultSet as $res) {
215
                $result->results[] = $res;
216
            }
217
        }
218
        if (QueryType::COUNT() == $queryType || QueryType::ENTITIES_WITH_COUNT() == $queryType) {
219
            $result->count = $resultCount;
220
        }
221
        $hazMore = $bulkSetCount > $skip+count($resultSet);
222
        $result->hasMore = $hazMore;
223
        return $result;
224
    }
225
226
    /**
227
     * Get related resource set for a resource
228
     * IE: http://host/EntitySet(1L)/NavigationPropertyToCollection
229
     * http://host/EntitySet?$expand=NavigationPropertyToCollection.
230
     *
231
     * @param QueryType          $queryType            Is this is a query for a count, entities, or entities-with-count
232
     * @param ResourceSet        $sourceResourceSet    The entity set containing the source entity
233
     * @param Model              $sourceEntityInstance The source entity instance
234
     * @param ResourceSet        $targetResourceSet    The resource set pointed to by the navigation property
235
     * @param ResourceProperty   $targetProperty       The navigation property to retrieve
236
     * @param FilterInfo|null    $filter               The $filter parameter of the OData query.  NULL if none specified
237
     * @param mixed|null         $orderBy              sorted order if we want to get the data in some specific order
238
     * @param int|null           $top                  number of records which need to be retrieved
239
     * @param int|null           $skip                 number of records which need to be skipped
240
     * @param SkipTokenInfo|null $skipToken            value indicating what records to skip
241
     *
242
     * @return QueryResult
243
     */
244
    public function getRelatedResourceSet(
245
        QueryType $queryType,
246
        ResourceSet $sourceResourceSet,
247
        Model $sourceEntityInstance,
248
        ResourceSet $targetResourceSet,
0 ignored issues
show
Unused Code introduced by
The parameter $targetResourceSet 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...
249
        ResourceProperty $targetProperty,
250
        FilterInfo $filter = null,
251
        $orderBy = null,
252
        $top = null,
253
        $skip = null,
254
        SkipTokenInfo $skipToken = null
255
    ) {
256
        $this->checkAuth($sourceEntityInstance);
257
258
        $propertyName = $targetProperty->getName();
259
        $results = $sourceEntityInstance->$propertyName();
260
261
        return $this->getResourceSet(
262
            $queryType,
263
            $sourceResourceSet,
264
            $filter,
265
            $orderBy,
266
            $top,
267
            $skip,
268
            $skipToken,
269
            null,
270
            $results
271
        );
272
    }
273
274
    /**
275
     * Gets an entity instance from an entity set identified by a key
276
     * IE: http://host/EntitySet(1L)
277
     * http://host/EntitySet(KeyA=2L,KeyB='someValue').
278
     *
279
     * @param ResourceSet        $resourceSet   The entity set containing the entity to fetch
280
     * @param KeyDescriptor|null $keyDescriptor The key identifying the entity to fetch
281
     * @param string[]|null      $eagerLoad     array of relations to eager load
282
     *
283
     * @return Model|null Returns entity instance if found else null
284
     */
285
    public function getResourceFromResourceSet(
286
        ResourceSet $resourceSet,
287
        KeyDescriptor $keyDescriptor = null,
288
        array $eagerLoad = null
289
    ) {
290
        return $this->getResource($resourceSet, $keyDescriptor, [], $eagerLoad);
291
    }
292
293
294
    /**
295
     * Common method for getResourceFromRelatedResourceSet() and getResourceFromResourceSet().
296
     *
297
     * @param ResourceSet|null    $resourceSet
298
     * @param KeyDescriptor|null  $keyDescriptor
299
     * @param Model|Relation|null $sourceEntityInstance Starting point of query
300
     * $param array               $whereCondition
301
     * @param string[]|null       $eagerLoad            array of relations to eager load
302
     *
303
     * @return Model|null
304
     */
305
    public function getResource(
306
        ResourceSet $resourceSet = null,
307
        KeyDescriptor $keyDescriptor = null,
308
        array $whereCondition = [],
309
        array $eagerLoad = null,
310
        $sourceEntityInstance = null
311
    ) {
312
        if (null == $resourceSet && null == $sourceEntityInstance) {
313
            $msg = 'Must supply at least one of a resource set and source entity.';
314
            throw new \Exception($msg);
315
        }
316
317
        $this->checkSourceInstance($sourceEntityInstance);
318
        $rawLoad = $this->processEagerLoadList($eagerLoad);
319
320
        if (null == $sourceEntityInstance) {
321
            assert(null != $resourceSet);
322
            $sourceEntityInstance = $this->getSourceEntityInstance($resourceSet);
323
        }
324
325
        $this->checkAuth($sourceEntityInstance);
326
327
        $this->processKeyDescriptor($sourceEntityInstance, $keyDescriptor);
328
        foreach ($whereCondition as $fieldName => $fieldValue) {
329
            $sourceEntityInstance = $sourceEntityInstance->where($fieldName, $fieldValue);
330
        }
331
        $modelLoad = $sourceEntityInstance->getEagerLoad();
332
        $rawLoad = array_values(array_unique(array_merge($rawLoad, $modelLoad)));
0 ignored issues
show
Unused Code introduced by
$rawLoad is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
333
        $sourceEntityInstance = $sourceEntityInstance->get();
334
        $sourceCount = $sourceEntityInstance->count();
335
        if (0 == $sourceCount) {
336
            return null;
337
        }
338
        $result = $sourceEntityInstance->first();
339
        $result->PrimaryKey = $result->getKey();
340
341
        return $result;
342
    }
343
344
    /**
345
     * Get related resource for a resource
346
     * IE: http://host/EntitySet(1L)/NavigationPropertyToSingleEntity
347
     * http://host/EntitySet?$expand=NavigationPropertyToSingleEntity.
348
     *
349
     * @param ResourceSet      $sourceResourceSet    The entity set containing the source entity
350
     * @param Model            $sourceEntityInstance the source entity instance
351
     * @param ResourceSet      $targetResourceSet    The entity set containing the entity pointed to by the nav property
352
     * @param ResourceProperty $targetProperty       The navigation property to fetch
353
     *
354
     * @return object|null The related resource if found else null
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use null|Model.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
355
     */
356
    public function getRelatedResourceReference(
357
        ResourceSet $sourceResourceSet,
0 ignored issues
show
Unused Code introduced by
The parameter $sourceResourceSet 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
        Model $sourceEntityInstance,
359
        ResourceSet $targetResourceSet,
360
        ResourceProperty $targetProperty
361
    ) {
362
        $this->checkAuth($sourceEntityInstance);
363
364
        $propertyName = $targetProperty->getName();
365
        $result = $sourceEntityInstance->$propertyName;
366
        if (null === $result) {
367
            return null;
368
        }
369
        assert($result instanceof Model, get_class($result));
370
        $result->PrimaryKey = $result->getKey();
0 ignored issues
show
Bug introduced by
The property PrimaryKey does not seem to exist. Did you mean primaryKey?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
371
        return $result;
372
    }
373
374
    /**
375
     * Gets a related entity instance from an entity set identified by a key
376
     * IE: http://host/EntitySet(1L)/NavigationPropertyToCollection(33).
377
     *
378
     * @param ResourceSet      $sourceResourceSet    The entity set containing the source entity
379
     * @param Model            $sourceEntityInstance the source entity instance
380
     * @param ResourceSet      $targetResourceSet    The entity set containing the entity to fetch
381
     * @param ResourceProperty $targetProperty       the metadata of the target property
382
     * @param KeyDescriptor    $keyDescriptor        The key identifying the entity to fetch
383
     *
384
     * @return Model|null Returns entity instance if found else null
385
     */
386
    public function getResourceFromRelatedResourceSet(
387
        ResourceSet $sourceResourceSet,
0 ignored issues
show
Unused Code introduced by
The parameter $sourceResourceSet 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...
388
        Model $sourceEntityInstance,
389
        ResourceSet $targetResourceSet,
390
        ResourceProperty $targetProperty,
391
        KeyDescriptor $keyDescriptor
392
    ) {
393
        $propertyName = $targetProperty->getName();
394
        if (!method_exists($sourceEntityInstance, $propertyName)) {
395
            $msg = 'Relation method, ' . $propertyName . ', does not exist on supplied entity.';
396
            throw new InvalidArgumentException($msg);
397
        }
398
        // take key descriptor and turn it into where clause here, rather than in getResource call
399
        $sourceEntityInstance = $sourceEntityInstance->$propertyName();
400
        $this->processKeyDescriptor($sourceEntityInstance, $keyDescriptor);
401
        $result = $this->getResource(null, null, [], [], $sourceEntityInstance);
402
        assert(
403
            $result instanceof Model || null == $result,
404
            'GetResourceFromRelatedResourceSet must return an entity or null'
405
        );
406
        return $result;
407
    }
408
409
410
    /**
411
     * @param  ResourceSet $resourceSet
412
     * @return mixed
413
     */
414
    protected function getSourceEntityInstance(ResourceSet $resourceSet)
415
    {
416
        $entityClassName = $resourceSet->getResourceType()->getInstanceType()->name;
417
        return App::make($entityClassName);
418
    }
419
420
    /**
421
     * @param Model|Relation|null $source
422
     */
423
    protected function checkSourceInstance($source)
424
    {
425
        if (!(null == $source || $source instanceof Model || $source instanceof Relation)) {
426
            $msg = 'Source entity instance must be null, a model, or a relation.';
427
            throw new InvalidArgumentException($msg);
428
        }
429
    }
430
431
    protected function getAuth()
432
    {
433
        return $this->auth;
434
    }
435
436
    /**
437
     * @param $sourceEntityInstance
438
     * @param null|mixed $checkInstance
439
     *
440
     * @throws ODataException
441
     */
442
    private function checkAuth($sourceEntityInstance, $checkInstance = null)
443
    {
444
        $check = $checkInstance instanceof Model ? $checkInstance
445
            : $checkInstance instanceof Relation ? $checkInstance
446
                : $sourceEntityInstance instanceof Model ? $sourceEntityInstance
447
                    : $sourceEntityInstance instanceof Relation ? $sourceEntityInstance
448
                        : null;
449
        if (!$this->getAuth()->canAuth(ActionVerb::READ(), get_class($sourceEntityInstance), $check)) {
450
            throw new ODataException('Access denied', 403);
451
        }
452
    }
453
454
    /**
455
     * @param $sourceEntityInstance
456
     * @param  KeyDescriptor|null        $keyDescriptor
457
     * @throws InvalidOperationException
458
     */
459
    private function processKeyDescriptor(&$sourceEntityInstance, KeyDescriptor $keyDescriptor = null)
460
    {
461
        if ($keyDescriptor) {
462
            foreach ($keyDescriptor->getValidatedNamedValues() as $key => $value) {
463
                $key = (self::PK == $key) ? $sourceEntityInstance->getKeyName() : $key;
464
                $trimValue = trim($value[0], '\'');
465
                $sourceEntityInstance = $sourceEntityInstance->where($key, $trimValue);
466
            }
467
        }
468
    }
469
470
    /**
471
     * @param array $eagerLoad
0 ignored issues
show
Documentation introduced by
Should the type for parameter $eagerLoad not be null|array? Also, consider making the array more specific, something like array<String>, or String[].

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive. In addition it looks for parameters that have the generic type array and suggests a stricter type like array<String>.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
472
     * @return array
473
     */
474
    private function processEagerLoadList(array $eagerLoad = null)
475
    {
476
        $load = (null === $eagerLoad) ? [] : $eagerLoad;
477
        $rawLoad = [];
478
        foreach ($load as $line) {
479
            assert(is_string($line), 'Eager-load elements must be non-empty strings');
480
            $remixLine = str_replace('/', '.', $line);
481
            $rawLoad[] = $remixLine;
482
        }
483
        return $rawLoad;
484
    }
485
}
486