Test Failed
Pull Request — master (#90)
by Alex
03:42 queued 17s
created

LaravelReadQuery::getResourceSet()   F

Complexity

Conditions 34
Paths > 20000

Size

Total Lines 164
Code Lines 108

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 164
rs 2
cc 34
eloc 108
nc 21506
nop 8

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