Passed
Pull Request — master (#202)
by Alex
06:30
created

LaravelReadQuery::getResourceSet()   F

Complexity

Conditions 17
Paths 1024

Size

Total Lines 90
Code Lines 52

Duplication

Lines 0
Ratio 0 %

Importance

Changes 15
Bugs 1 Features 0
Metric Value
eloc 52
c 15
b 1
f 0
dl 0
loc 90
rs 1.0499
cc 17
nc 1024
nop 9

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