Completed
Push — master ( 817152...5c67c4 )
by Alex
15s queued 11s
created

LaravelReadQuery::getResourceSet()   B

Complexity

Conditions 8
Paths 128

Size

Total Lines 68
Code Lines 37

Duplication

Lines 0
Ratio 0 %

Importance

Changes 13
Bugs 0 Features 0
Metric Value
eloc 37
c 13
b 0
f 0
dl 0
loc 68
rs 7.8968
cc 8
nc 128
nop 9

How to fix   Long Method    Many Parameters   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
<?php
2
3
namespace AlgoWeb\PODataLaravel\Query;
4
5
use AlgoWeb\PODataLaravel\Enums\ActionVerb;
6
use AlgoWeb\PODataLaravel\Models\MetadataTrait;
7
use Illuminate\Database\Eloquent\Builder;
8
use Illuminate\Database\Eloquent\Collection;
9
use Illuminate\Database\Eloquent\Model;
10
use Illuminate\Database\Eloquent\Relations\Relation;
11
use Illuminate\Support\Facades\App;
12
use POData\Common\InvalidOperationException;
13
use POData\Common\ODataException;
14
use POData\Providers\Metadata\ResourceProperty;
15
use POData\Providers\Metadata\ResourceSet;
16
use POData\Providers\Query\QueryResult;
17
use POData\Providers\Query\QueryType;
18
use POData\UriProcessor\QueryProcessor\ExpressionParser\FilterInfo;
19
use POData\UriProcessor\QueryProcessor\OrderByParser\InternalOrderByInfo;
20
use POData\UriProcessor\QueryProcessor\SkipTokenParser\SkipTokenInfo;
21
use POData\UriProcessor\ResourcePathProcessor\SegmentParser\KeyDescriptor;
22
use Symfony\Component\Process\Exception\InvalidArgumentException;
23
24
class LaravelReadQuery extends LaravelBaseQuery
25
{
26
    use LaravelReadQueryUtilityTrait;
27
28
    /**
29
     * Gets collection of entities belongs to an entity set
30
     * IE: http://host/EntitySet
31
     *  http://host/EntitySet?$skip=10&$top=5&filter=Prop gt Value.
32
     *
33
     * @param QueryType                $queryType            Is this is a query for a count, entities,
34
     *                                                       or entities-with-count?
35
     * @param ResourceSet              $resourceSet          The entity set containing the entities to fetch
36
     * @param FilterInfo|null          $filterInfo           The $filter parameter of the OData query.  NULL if absent
37
     * @param null|InternalOrderByInfo $orderBy              sorted order if we want to get the data in some
38
     *                                                       specific order
39
     * @param int|null                 $top                  number of records which need to be retrieved
40
     * @param int|null                 $skip                 number of records which need to be skipped
41
     * @param SkipTokenInfo|null       $skipToken            value indicating what records to skip
42
     * @param string[]|null            $eagerLoad            array of relations to eager load
43
     * @param Model|Relation|null      $sourceEntityInstance Starting point of query
44
     *
45
     * @return QueryResult
46
     * @throws InvalidArgumentException
47
     * @throws InvalidOperationException
48
     * @throws \ReflectionException
49
     * @throws ODataException
50
     */
51
    public function getResourceSet(
52
        QueryType $queryType,
53
        ResourceSet $resourceSet,
54
        FilterInfo $filterInfo = null,
55
        $orderBy = null,
56
        $top = null,
57
        $skip = null,
58
        SkipTokenInfo $skipToken = null,
59
        array $eagerLoad = null,
60
        $sourceEntityInstance = null
61
    ) {
62
        $rawLoad = $this->processEagerLoadList($eagerLoad);
63
64
        $sourceEntityInstance = $this->checkSourceInstance($sourceEntityInstance, $resourceSet);
65
66
        /** @var MetadataTrait $model */
67
        $model = $sourceEntityInstance instanceof Model ? $sourceEntityInstance : $sourceEntityInstance->getRelated();
68
        $modelLoad = $model->getEagerLoad();
69
        $tableName = $model->getTable();
70
        $rawLoad = array_values(array_unique(array_merge($rawLoad, $modelLoad)));
71
72
        $checkInstance = $sourceEntityInstance instanceof Model ? $sourceEntityInstance : null;
73
        $this->checkAuth($sourceEntityInstance, $checkInstance);
74
75
        $result          = new QueryResult();
76
        $result->results = null;
77
        $result->count   = null;
78
79
        $sourceEntityInstance = $this->buildOrderBy($orderBy, $sourceEntityInstance, $tableName);
80
81
        // throttle up for trench run
82
        if (null != $skipToken) {
83
            $sourceEntityInstance = $this->processSkipToken($skipToken, $sourceEntityInstance);
84
        }
85
86
        if (!isset($skip)) {
87
            $skip = 0;
88
        }
89
        if (!isset($top)) {
90
            $top = PHP_INT_MAX;
91
        }
92
93
        $nullFilter = !isset($filterInfo);
94
        $isvalid = null;
95
        if (isset($filterInfo)) {
96
            $method = 'return ' . $filterInfo->getExpressionAsString() . ';';
97
            $clln = $resourceSet->getResourceType()->getName();
98
            $isvalid = function ($inputD) use ($clln, $method) {
99
                $$clln = $inputD;
100
                return eval($method);
0 ignored issues
show
introduced by
The use of eval() is discouraged.
Loading history...
101
            };
102
        }
103
104
        list($bulkSetCount, $resultSet, $resultCount, $skip) = $this->applyFiltering(
105
            $top,
106
            $skip,
107
            $sourceEntityInstance,
108
            $nullFilter,
109
            $rawLoad,
110
            $isvalid
111
        );
112
113
        if (isset($top)) {
114
            $resultSet = $resultSet->take($top);
115
        }
116
117
        $this->packageResourceSetResults($queryType, $skip, $result, $resultSet, $resultCount, $bulkSetCount);
118
        return $result;
119
    }
120
121
    /**
122
     * Get related resource set for a resource
123
     * IE: http://host/EntitySet(1L)/NavigationPropertyToCollection
124
     * http://host/EntitySet?$expand=NavigationPropertyToCollection.
125
     *
126
     * @param QueryType          $queryType            Is this is a query for a count, entities, or entities-with-count
127
     * @param ResourceSet        $sourceResourceSet    The entity set containing the source entity
128
     * @param Model              $sourceEntityInstance The source entity instance
129
     * @param ResourceSet        $targetResourceSet    The resource set pointed to by the navigation property
130
     * @param ResourceProperty   $targetProperty       The navigation property to retrieve
131
     * @param FilterInfo|null    $filter               The $filter parameter of the OData query.  NULL if none specified
132
     * @param mixed|null         $orderBy              sorted order if we want to get the data in some specific order
133
     * @param int|null           $top                  number of records which need to be retrieved
134
     * @param int|null           $skip                 number of records which need to be skipped
135
     * @param SkipTokenInfo|null $skipToken            value indicating what records to skip
136
     *
137
     * @return QueryResult
138
     * @throws InvalidOperationException
139
     * @throws ODataException
140
     * @throws \ReflectionException
141
     */
142
    public function getRelatedResourceSet(
143
        QueryType $queryType,
144
        ResourceSet $sourceResourceSet,
145
        Model $sourceEntityInstance,
146
        /** @noinspection PhpUnusedParameterInspection */
147
        ResourceSet $targetResourceSet,
148
        ResourceProperty $targetProperty,
149
        FilterInfo $filter = null,
150
        $orderBy = null,
151
        $top = null,
152
        $skip = null,
153
        SkipTokenInfo $skipToken = null
154
    ) {
155
        $this->checkAuth($sourceEntityInstance);
156
157
        $propertyName = $targetProperty->getName();
158
        /** @var Relation $results */
159
        $results = $sourceEntityInstance->$propertyName();
160
161
        return $this->getResourceSet(
162
            $queryType,
163
            $sourceResourceSet,
164
            $filter,
165
            $orderBy,
166
            $top,
167
            $skip,
168
            $skipToken,
169
            null,
170
            $results
171
        );
172
    }
173
174
    /**
175
     * Gets an entity instance from an entity set identified by a key
176
     * IE: http://host/EntitySet(1L)
177
     * http://host/EntitySet(KeyA=2L,KeyB='someValue').
178
     *
179
     * @param ResourceSet        $resourceSet   The entity set containing the entity to fetch
180
     * @param KeyDescriptor|null $keyDescriptor The key identifying the entity to fetch
181
     * @param string[]|null      $eagerLoad     array of relations to eager load
182
     *
183
     * @return Model|null Returns entity instance if found else null
184
     * @throws \Exception;
185
     */
186
    public function getResourceFromResourceSet(
187
        ResourceSet $resourceSet,
188
        KeyDescriptor $keyDescriptor = null,
189
        array $eagerLoad = null
190
    ) {
191
        return $this->getResource($resourceSet, $keyDescriptor, [], $eagerLoad);
192
    }
193
194
195
    /**
196
     * Common method for getResourceFromRelatedResourceSet() and getResourceFromResourceSet().
197
     *
198
     * @param ResourceSet|null    $resourceSet
199
     * @param KeyDescriptor|null  $keyDescriptor
200
     * @param Model|Relation|null $sourceEntityInstance Starting point of query
201
     * @param array               $whereCondition
202
     * @param string[]|null       $eagerLoad            array of relations to eager load
203
     *
204
     * @return Model|null
205
     * @throws \Exception
206
     */
207
    public function getResource(
208
        ResourceSet $resourceSet = null,
209
        KeyDescriptor $keyDescriptor = null,
210
        array $whereCondition = [],
211
        array $eagerLoad = null,
212
        $sourceEntityInstance = null
213
    ) {
214
        if (null == $resourceSet && null == $sourceEntityInstance) {
215
            $msg = 'Must supply at least one of a resource set and source entity.';
216
            throw new \Exception($msg);
217
        }
218
219
        $sourceEntityInstance = $this->checkSourceInstance($sourceEntityInstance, $resourceSet);
220
221
        $this->checkAuth($sourceEntityInstance);
222
        $modelLoad = null;
223
        if ($sourceEntityInstance instanceof Model) {
224
            $modelLoad = $sourceEntityInstance->getEagerLoad();
225
        } elseif ($sourceEntityInstance instanceof Relation) {
226
            /** @var MetadataTrait $model */
227
            $model = $sourceEntityInstance->getRelated();
228
            $modelLoad = $model->getEagerLoad();
229
        }
230
        if (!(isset($modelLoad))) {
231
            throw new InvalidOperationException('');
232
        }
233
234
        $this->processKeyDescriptor($sourceEntityInstance, $keyDescriptor);
235
        foreach ($whereCondition as $fieldName => $fieldValue) {
236
            $sourceEntityInstance = $sourceEntityInstance->where($fieldName, $fieldValue);
237
        }
238
239
        $sourceEntityInstance = $sourceEntityInstance->get();
240
        $sourceCount = $sourceEntityInstance->count();
241
        if (0 == $sourceCount) {
242
            return null;
243
        }
244
        $result = $sourceEntityInstance->first();
245
246
        return $result;
247
    }
248
249
    /**
250
     * Get related resource for a resource
251
     * IE: http://host/EntitySet(1L)/NavigationPropertyToSingleEntity
252
     * http://host/EntitySet?$expand=NavigationPropertyToSingleEntity.
253
     *
254
     * @param ResourceSet      $sourceResourceSet    The entity set containing the source entity
255
     * @param Model            $sourceEntityInstance the source entity instance
256
     * @param ResourceSet      $targetResourceSet    The entity set containing the entity pointed to by the nav property
257
     * @param ResourceProperty $targetProperty       The navigation property to fetch
258
     *
259
     * @return Model|null The related resource if found else null
260
     * @throws ODataException
261
     * @throws InvalidOperationException
262
     * @throws \ReflectionException
263
     */
264
    public function getRelatedResourceReference(
265
        /** @noinspection PhpUnusedParameterInspection */
266
        ResourceSet $sourceResourceSet,
267
        Model $sourceEntityInstance,
268
        ResourceSet $targetResourceSet,
269
        ResourceProperty $targetProperty
270
    ) {
271
        $this->checkAuth($sourceEntityInstance);
272
273
        $propertyName = $targetProperty->getName();
274
        $propertyName = $this->getLaravelRelationName($propertyName);
275
        /** @var Model|null $result */
276
        $result = $sourceEntityInstance->$propertyName()->first();
277
        if (null === $result) {
278
            return null;
279
        }
280
        if ($targetProperty->getResourceType()->getInstanceType()->getName() != get_class($result)) {
281
            return null;
282
        }
283
        return $result;
284
    }
285
286
    /**
287
     * Gets a related entity instance from an entity set identified by a key
288
     * IE: http://host/EntitySet(1L)/NavigationPropertyToCollection(33).
289
     *
290
     * @param ResourceSet      $sourceResourceSet    The entity set containing the source entity
291
     * @param Model            $sourceEntityInstance the source entity instance
292
     * @param ResourceSet      $targetResourceSet    The entity set containing the entity to fetch
293
     * @param ResourceProperty $targetProperty       the metadata of the target property
294
     * @param KeyDescriptor    $keyDescriptor        The key identifying the entity to fetch
295
     *
296
     * @return Model|null Returns entity instance if found else null
297
     * @throws InvalidOperationException
298
     * @throws \Exception
299
     */
300
    public function getResourceFromRelatedResourceSet(
301
        /** @noinspection PhpUnusedParameterInspection */
302
        ResourceSet $sourceResourceSet,
303
        Model $sourceEntityInstance,
304
        ResourceSet $targetResourceSet,
305
        ResourceProperty $targetProperty,
306
        KeyDescriptor $keyDescriptor
307
    ) {
308
        $propertyName = $targetProperty->getName();
309
        if (!method_exists($sourceEntityInstance, $propertyName)) {
310
            $msg = 'Relation method, ' . $propertyName . ', does not exist on supplied entity.';
311
            throw new InvalidArgumentException($msg);
312
        }
313
        // take key descriptor and turn it into where clause here, rather than in getResource call
314
        /** @var Relation $sourceEntityInstance */
315
        $sourceEntityInstance = $sourceEntityInstance->$propertyName();
316
        $this->processKeyDescriptor($sourceEntityInstance, $keyDescriptor);
317
        $result = $this->getResource(null, null, [], [], $sourceEntityInstance);
318
        if (!(null == $result || $result instanceof Model)) {
0 ignored issues
show
introduced by
$result is always a sub-type of Illuminate\Database\Eloquent\Model.
Loading history...
319
            $msg = 'GetResourceFromRelatedResourceSet must return an entity or null';
320
            throw new InvalidOperationException($msg);
321
        }
322
        return $result;
323
    }
324
325
    /**
326
     * @param Model|Relation|null $sourceEntityInstance
327
     * @param null|mixed $checkInstance
328
     *
329
     * @throws ODataException
330
     */
331
    private function checkAuth($sourceEntityInstance, $checkInstance = null)
332
    {
333
        $check = array_reduce([$sourceEntityInstance, $checkInstance], function ($carry, $item) {
334
            if ($item instanceof Model || $item instanceof Relation) {
335
                return $item;
336
            }
337
        }, null);
338
        $sourceName = null !== $check ? get_class($check) : null;
339
        if (!$this->getAuth()->canAuth(ActionVerb::READ(), $sourceName, $check)) {
340
            throw new ODataException('Access denied', 403);
341
        }
342
    }
343
344
    /**
345
     * @param $top
346
     * @param $skip
347
     * @param Model|Builder $sourceEntityInstance
348
     * @param $nullFilter
349
     * @param $rawLoad
350
     * @param callable|null $isvalid
351
     * @return array
352
     * @throws InvalidOperationException
353
     */
354
    protected function applyFiltering(
355
        $top,
356
        $skip,
357
        $sourceEntityInstance,
358
        $nullFilter,
359
        $rawLoad,
360
        callable $isvalid = null
361
    ) {
362
        $bulkSetCount = $sourceEntityInstance->count();
363
        $bigSet = 20000 < $bulkSetCount;
364
365
        if ($nullFilter) {
366
            // default no-filter case, palm processing off to database engine - is a lot faster
367
            $resultSet = $sourceEntityInstance->skip($skip)->take($top)->with($rawLoad)
368
                ->get();
369
            $resultCount = $bulkSetCount;
370
        } elseif ($bigSet) {
371
            if (!(isset($isvalid))) {
372
                $msg = 'Filter closure not set';
373
                throw new InvalidOperationException($msg);
374
            }
375
            $resultSet = new Collection([]);
376
            $rawCount = 0;
377
            $rawTop = null === $top ? $bulkSetCount : $top;
378
379
            // loop thru, chunk by chunk, to reduce chances of exhausting memory
380
            $sourceEntityInstance->chunk(
381
                5000,
382
                function (Collection $results) use ($isvalid, &$skip, &$resultSet, &$rawCount, $rawTop) {
383
                    // apply filter
384
                    $results = $results->filter($isvalid);
385
                    // need to iterate through full result set to find count of items matching filter,
386
                    // so we can't bail out early
387
                    $rawCount += $results->count();
388
                    // now bolt on filtrate to accumulating result set if we haven't accumulated enough bitz
389
                    if ($rawTop > $resultSet->count() + $skip) {
390
                        $resultSet = collect(array_merge($resultSet->all(), $results->all()));
391
                        $sliceAmount = min($skip, $resultSet->count());
392
                        $resultSet = $resultSet->slice($sliceAmount);
393
                        $skip -= $sliceAmount;
394
                    }
395
                }
396
            );
397
398
            // clean up residual to-be-skipped records
399
            $resultSet = $resultSet->slice($skip);
400
            $resultCount = $rawCount;
401
        } else {
402
            /** @var Collection $resultSet */
403
            $resultSet = $sourceEntityInstance->with($rawLoad)->get();
404
            $resultSet = $resultSet->filter($isvalid);
405
            $resultCount = $resultSet->count();
406
407
            if (isset($skip)) {
408
                $resultSet = $resultSet->slice($skip);
409
            }
410
        }
411
        return [$bulkSetCount, $resultSet, $resultCount, $skip];
412
    }
413
414
    /**
415
     * @param $orderBy
416
     * @param $sourceEntityInstance
417
     * @param $tableName
418
     * @return mixed
419
     */
420
    protected function buildOrderBy($orderBy, $sourceEntityInstance, $tableName)
421
    {
422
        if (null != $orderBy) {
423
            foreach ($orderBy->getOrderByInfo()->getOrderByPathSegments() as $order) {
424
                foreach ($order->getSubPathSegments() as $subOrder) {
425
                    $subName = $subOrder->getName();
426
                    $subName = $tableName . '.' . $subName;
427
                    $sourceEntityInstance = $sourceEntityInstance->orderBy(
428
                        $subName,
429
                        $order->isAscending() ? 'asc' : 'desc'
430
                    );
431
                }
432
            }
433
        }
434
        return $sourceEntityInstance;
435
    }
436
437
    /**
438
     * @param QueryType $queryType
439
     * @param $skip
440
     * @param QueryResult $result
441
     * @param $resultSet
442
     * @param $resultCount
443
     * @param $bulkSetCount
444
     */
445
    protected function packageResourceSetResults(
446
        QueryType $queryType,
447
        $skip,
448
        QueryResult $result,
449
        $resultSet,
450
        $resultCount,
451
        $bulkSetCount
452
    ) {
453
        $qVal = $queryType;
454
        if (QueryType::ENTITIES() == $qVal || QueryType::ENTITIES_WITH_COUNT() == $qVal) {
455
            $result->results = [];
456
            foreach ($resultSet as $res) {
457
                $result->results[] = $res;
458
            }
459
        }
460
        if (QueryType::COUNT() == $qVal || QueryType::ENTITIES_WITH_COUNT() == $qVal) {
461
            $result->count = $resultCount;
462
        }
463
        $hazMore = $bulkSetCount > $skip + count($resultSet);
464
        $result->hasMore = $hazMore;
465
    }
466
}
467