Test Failed
Pull Request — master (#82)
by Alex
16:38
created

LaravelReadQuery::getRelatedResourceSet()   B

Complexity

Conditions 2
Paths 2

Size

Total Lines 35
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 35
rs 8.8571
c 0
b 0
f 0
cc 2
eloc 27
nc 2
nop 10

How to fix   Many Parameters   

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