Test Failed
Push — master ( 5e1fd5...e1913c )
by Alex
01:08
created

LaravelReadQuery::getRelatedResourceSet()   B

Complexity

Conditions 2
Paths 2

Size

Total Lines 34
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 34
rs 8.8571
c 0
b 0
f 0
cc 2
eloc 26
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
            $segments = $skipToken->getOrderByInfo()->getOrderByPathSegments();
103
            $values = $skipToken->getOrderByKeysInToken();
104
            $numValues = count($values);
105
            assert($numValues == count($segments));
106
107
            for ($i = 0; $i < $numValues; $i++) {
108
                $relation = $segments[$i]->isAscending() ? '>=' : '<=';
109
                $name = $segments[$i]->getSubPathSegments()[0]->getName();
110
                $line = ['direction' => $relation, 'value' => $values[$i][0]];
111
                $parameters[$name] = $line;
112
            }
113
114
            foreach ($parameters as $name => $line) {
115
                $direction = $line['direction'];
116
                if ($keyName == $name) {
117
                    $direction = '!=';
118
                }
119
                $value = trim($line['value'], "\'");
120
                $sourceEntityInstance = $sourceEntityInstance->where($name, $direction, $value);
121
            }
122
        }
123
124
        if (!isset($skip)) {
125
            $skip = 0;
126
        }
127
        if (!isset($top)) {
128
            $top = PHP_INT_MAX;
129
        }
130
131
        $nullFilter = true;
132
        $isvalid = null;
133
        if (isset($filterInfo)) {
134
            $method = "return ".$filterInfo->getExpressionAsString().";";
135
            $clln = "$".$resourceSet->getResourceType()->getName();
136
            $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...
137
            $nullFilter = false;
138
        }
139
140
        $bulkSetCount = $sourceEntityInstance->count();
141
        $bigSet = 20000 < $bulkSetCount;
142
143
        if ($nullFilter) {
144
            // default no-filter case, palm processing off to database engine - is a lot faster
145
            $resultSet = $sourceEntityInstance->skip($skip)->take($top)->with($eagerLoad)->get();
146
            $resultCount = $bulkSetCount;
147
        } elseif ($bigSet) {
148
            assert(isset($isvalid), "Filter closure not set");
149
            $resultSet = collect([]);
150
            $rawCount = 0;
151
            $rawTop = null === $top ? $bulkSetCount : $top;
152
153
            // loop thru, chunk by chunk, to reduce chances of exhausting memory
154
            $sourceEntityInstance->chunk(
155
                5000,
156
                function ($results) use ($isvalid, &$skip, &$resultSet, &$rawCount, $rawTop) {
157
                    // apply filter
158
                    $results = $results->filter($isvalid);
159
                    // need to iterate through full result set to find count of items matching filter,
160
                    // so we can't bail out early
161
                    $rawCount += $results->count();
162
                    // now bolt on filtrate to accumulating result set if we haven't accumulated enough bitz
163
                    if ($rawTop > $resultSet->count() + $skip) {
164
                        $resultSet = collect(array_merge($resultSet->all(), $results->all()));
165
                        $sliceAmount = min($skip, $resultSet->count());
166
                        $resultSet = $resultSet->slice($sliceAmount);
167
                        $skip -= $sliceAmount;
168
                    }
169
                }
170
            );
171
172
            // clean up residual to-be-skipped records
173
            $resultSet = $resultSet->slice($skip);
174
            $resultCount = $rawCount;
175
        } else {
176
            if ($sourceEntityInstance instanceof Model) {
177
                $sourceEntityInstance = $sourceEntityInstance->getQuery();
178
            }
179
            $resultSet = $sourceEntityInstance->with($eagerLoad)->get();
180
            $resultSet = $resultSet->filter($isvalid);
181
            $resultCount = $resultSet->count();
182
183
            if (isset($skip)) {
184
                $resultSet = $resultSet->slice($skip);
185
            }
186
        }
187
188
        if (isset($top)) {
189
            $resultSet = $resultSet->take($top);
190
        }
191
192
193
        if (QueryType::ENTITIES() == $queryType || QueryType::ENTITIES_WITH_COUNT() == $queryType) {
194
            $result->results = array();
195
            foreach ($resultSet as $res) {
196
                $result->results[] = $res;
197
            }
198
        }
199
        if (QueryType::COUNT() == $queryType || QueryType::ENTITIES_WITH_COUNT() == $queryType) {
200
            $result->count = $resultCount;
201
        }
202
        return $result;
203
    }
204
205
    /**
206
     * Get related resource set for a resource
207
     * IE: http://host/EntitySet(1L)/NavigationPropertyToCollection
208
     * http://host/EntitySet?$expand=NavigationPropertyToCollection
209
     *
210
     * @param QueryType $queryType indicates if this is a query for a count, entities, or entities with a count
211
     * @param ResourceSet $sourceResourceSet The entity set containing the source entity
212
     * @param object $sourceEntityInstance The source entity instance.
213
     * @param ResourceSet $targetResourceSet The resource set of containing the target of the navigation property
214
     * @param ResourceProperty $targetProperty The navigation property to retrieve
215
     * @param FilterInfo $filter represents the $filter parameter of the OData query.  NULL if no $filter specified
216
     * @param mixed $orderBy sorted order if we want to get the data in some specific order
217
     * @param int $top number of records which  need to be skip
218
     * @param String $skip value indicating what records to skip
219
     *
220
     * @return QueryResult
221
     *
222
     */
223
    public function getRelatedResourceSet(
224
        QueryType $queryType,
225
        ResourceSet $sourceResourceSet,
226
        $sourceEntityInstance,
227
        ResourceSet $targetResourceSet,
228
        ResourceProperty $targetProperty,
229
        $filter = null,
230
        $orderBy = null,
231
        $top = null,
232
        $skip = null,
233
        $skipToken = null
0 ignored issues
show
Unused Code introduced by
The parameter $skipToken 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...
234
    ) {
235
        if (!($sourceEntityInstance instanceof Model)) {
236
            throw new InvalidArgumentException('Source entity must be an Eloquent model.');
237
        }
238
239
        assert(null != $sourceEntityInstance, "Source instance must not be null");
240
        $this->checkSourceInstance($sourceEntityInstance);
241
242
        $this->checkAuth($sourceEntityInstance);
243
244
        $propertyName = $targetProperty->getName();
245
        $results = $sourceEntityInstance->$propertyName();
246
247
        return $this->getResourceSet(
248
            $queryType,
249
            $sourceResourceSet,
250
            $filter,
251
            $orderBy,
252
            $top,
253
            $skip,
254
            $results
255
        );
256
    }
257
258
    /**
259
     * Gets an entity instance from an entity set identified by a key
260
     * IE: http://host/EntitySet(1L)
261
     * http://host/EntitySet(KeyA=2L,KeyB='someValue')
262
     *
263
     * @param ResourceSet $resourceSet The entity set containing the entity to fetch
264
     * @param KeyDescriptor $keyDescriptor The key identifying the entity to fetch
265
     *
266
     * @return object|null Returns entity instance if found else null
267
     */
268
    public function getResourceFromResourceSet(
269
        ResourceSet $resourceSet,
270
        KeyDescriptor $keyDescriptor = null
271
    ) {
272
        return $this->getResource($resourceSet, $keyDescriptor);
273
    }
274
275
276
    /**
277
     * Common method for getResourceFromRelatedResourceSet() and getResourceFromResourceSet()
278
     * @param ResourceSet|null $resourceSet
279
     * @param KeyDescriptor|null $keyDescriptor
280
     * @param Model|Relation|null $sourceEntityInstance Starting point of query
281
     */
282
    public function getResource(
283
        ResourceSet $resourceSet = null,
284
        KeyDescriptor $keyDescriptor = null,
285
        array $whereCondition = [],
286
        $sourceEntityInstance = null
287
    ) {
288
        if (null == $resourceSet && null == $sourceEntityInstance) {
289
            throw new \Exception('Must supply at least one of a resource set and source entity.');
290
        }
291
292
        $this->checkSourceInstance($sourceEntityInstance);
293
294
        if (null == $sourceEntityInstance) {
295
            assert(null != $resourceSet);
296
            $sourceEntityInstance = $this->getSourceEntityInstance($resourceSet);
297
        }
298
299
        $this->checkAuth($sourceEntityInstance);
300
301
        if ($keyDescriptor) {
302
            foreach ($keyDescriptor->getValidatedNamedValues() as $key => $value) {
303
                $trimValue = trim($value[0], "\"'");
304
                $sourceEntityInstance = $sourceEntityInstance->where($key, $trimValue);
305
            }
306
        }
307
        foreach ($whereCondition as $fieldName => $fieldValue) {
308
            $sourceEntityInstance = $sourceEntityInstance->where($fieldName, $fieldValue);
309
        }
310
        $sourceEntityInstance = $sourceEntityInstance->get();
311
        return (0 == $sourceEntityInstance->count()) ? null : $sourceEntityInstance->first();
312
    }
313
314
    /**
315
     * Get related resource for a resource
316
     * IE: http://host/EntitySet(1L)/NavigationPropertyToSingleEntity
317
     * http://host/EntitySet?$expand=NavigationPropertyToSingleEntity
318
     *
319
     * @param ResourceSet $sourceResourceSet The entity set containing the source entity
320
     * @param object $sourceEntityInstance The source entity instance.
321
     * @param ResourceSet $targetResourceSet The entity set containing the entity pointed to by the navigation property
322
     * @param ResourceProperty $targetProperty The navigation property to fetch
323
     *
324
     * @return object|null The related resource if found else null
325
     */
326 View Code Duplication
    public function getRelatedResourceReference(
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
327
        ResourceSet $sourceResourceSet,
328
        $sourceEntityInstance,
329
        ResourceSet $targetResourceSet,
330
        ResourceProperty $targetProperty
331
    ) {
332
        if (!($sourceEntityInstance instanceof Model)) {
333
            throw new InvalidArgumentException('Source entity must be an Eloquent model.');
334
        }
335
        $this->checkSourceInstance($sourceEntityInstance);
336
337
        $this->checkAuth($sourceEntityInstance);
338
339
        $propertyName = $targetProperty->getName();
340
        return $sourceEntityInstance->$propertyName;
341
    }
342
343
    /**
344
     * Gets a related entity instance from an entity set identified by a key
345
     * IE: http://host/EntitySet(1L)/NavigationPropertyToCollection(33)
346
     *
347
     * @param ResourceSet $sourceResourceSet The entity set containing the source entity
348
     * @param object $sourceEntityInstance The source entity instance.
349
     * @param ResourceSet $targetResourceSet The entity set containing the entity to fetch
350
     * @param ResourceProperty $targetProperty The metadata of the target property.
351
     * @param KeyDescriptor $keyDescriptor The key identifying the entity to fetch
352
     *
353
     * @return object|null Returns entity instance if found else null
354
     */
355 View Code Duplication
    public function getResourceFromRelatedResourceSet(
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
356
        ResourceSet $sourceResourceSet,
357
        $sourceEntityInstance,
358
        ResourceSet $targetResourceSet,
359
        ResourceProperty $targetProperty,
360
        KeyDescriptor $keyDescriptor
361
    ) {
362
        if (!($sourceEntityInstance instanceof Model)) {
363
            throw new InvalidArgumentException('Source entity must be an Eloquent model.');
364
        }
365
        $propertyName = $targetProperty->getName();
366
        return $this->getResource(null, $keyDescriptor, [], $sourceEntityInstance->$propertyName);
367
    }
368
369
370
    /**
371
     * @param ResourceSet $resourceSet
372
     * @return mixed
373
     */
374
    protected function getSourceEntityInstance(ResourceSet $resourceSet)
375
    {
376
        $entityClassName = $resourceSet->getResourceType()->getInstanceType()->name;
377
        return App::make($entityClassName);
378
    }
379
380
    /**
381
     * @param Model|Relation|null $source
382
     */
383
    protected function checkSourceInstance($source)
384
    {
385
        if (!(null == $source || $source instanceof Model || $source instanceof Relation)) {
386
            throw new InvalidArgumentException('Source entity instance must be null, a model, or a relation.');
387
        }
388
    }
389
390
    protected function getAuth()
391
    {
392
        return $this->auth;
393
    }
394
395
    /**
396
     * @param $sourceEntityInstance
397
     * @throws ODataException
398
     */
399
    private function checkAuth($sourceEntityInstance, $checkInstance = null)
400
    {
401
        $check = $checkInstance instanceof Model ? $checkInstance
402
            : $checkInstance instanceof Relation ? $checkInstance
403
                : $sourceEntityInstance instanceof Model ? $sourceEntityInstance
404
                    : $sourceEntityInstance instanceof Relation ? $sourceEntityInstance
405
                        : null;
406
        if (!$this->getAuth()->canAuth(ActionVerb::READ(), get_class($sourceEntityInstance), $check)) {
407
            throw new ODataException("Access denied", 403);
408
        }
409
    }
410
}
411