Test Failed
Pull Request — master (#74)
by Alex
02:59
created

LaravelReadQuery   F

Complexity

Total Complexity 62

Size/Duplication

Total Lines 388
Duplicated Lines 7.47 %

Coupling/Cohesion

Components 1
Dependencies 18

Importance

Changes 0
Metric Value
wmc 62
lcom 1
cbo 18
dl 29
loc 388
rs 2.9203
c 0
b 0
f 0

11 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 2
F getResourceSet() 0 156 33
B getRelatedResourceSet() 0 34 2
A getResourceFromResourceSet() 0 6 1
C getResource() 0 31 8
A getRelatedResourceReference() 16 16 2
A getResourceFromRelatedResourceSet() 13 13 2
A getSourceEntityInstance() 0 5 1
A checkSourceInstance() 0 6 4
A getAuth() 0 4 1
B checkAuth() 0 11 6

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like LaravelReadQuery often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use LaravelReadQuery, and based on these observations, apply Extract Interface, too.

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
            assert(count($values) == count($segments));
105
106
            for ($i = 0; $i < count($values); $i++) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
107
                $relation = $segments[$i]->isAscending() ? '>=' : '<=';
108
                $name = $segments[$i]->getSubPathSegments()[0]->getName();
109
                $line = ['direction' => $relation, 'value' => $values[$i][0]];
110
                $parameters[$name] = $line;
111
            }
112
113
            foreach ($parameters as $name => $line) {
114
                $direction = $line['direction'];
115
                if ($keyName == $name) {
116
                    $direction = '!=';
117
                }
118
                $value = trim($line['value'], "\'");
119
                $sourceEntityInstance = $sourceEntityInstance->where($name, $direction, $value);
120
            }
121
        }
122
123
        if (!isset($skip)) {
124
            $skip = 0;
125
        }
126
        if (!isset($top)) {
127
            $top = PHP_INT_MAX;
128
        }
129
130
        $nullFilter = true;
131
        $isvalid = null;
132
        if (isset($filterInfo)) {
133
            $method = "return ".$filterInfo->getExpressionAsString().";";
134
            $clln = "$".$resourceSet->getResourceType()->getName();
135
            $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...
136
            $nullFilter = false;
137
        }
138
139
        $bulkSetCount = $sourceEntityInstance->count();
140
        $bigSet = 20000 < $bulkSetCount;
141
142
        if ($nullFilter) {
143
            // default no-filter case, palm processing off to database engine - is a lot faster
144
            $resultSet = $sourceEntityInstance->skip($skip)->take($top)->with($eagerLoad)->get();
145
            $resultCount = $bulkSetCount;
146
        } elseif ($bigSet) {
147
            assert(isset($isvalid), "Filter closure not set");
148
            $resultSet = collect([]);
149
            $rawCount = 0;
150
            $rawTop = null === $top ? $bulkSetCount : $top;
151
152
            // loop thru, chunk by chunk, to reduce chances of exhausting memory
153
            $sourceEntityInstance->chunk(
154
                5000,
155
                function ($results) use ($isvalid, &$skip, &$resultSet, &$rawCount, $rawTop) {
156
                    // apply filter
157
                    $results = $results->filter($isvalid);
158
                    // need to iterate through full result set to find count of items matching filter,
159
                    // so we can't bail out early
160
                    $rawCount += $results->count();
161
                    // now bolt on filtrate to accumulating result set if we haven't accumulated enough bitz
162
                    if ($rawTop > $resultSet->count() + $skip) {
163
                        $resultSet = collect(array_merge($resultSet->all(), $results->all()));
164
                        $sliceAmount = min($skip, $resultSet->count());
165
                        $resultSet = $resultSet->slice($sliceAmount);
166
                        $skip -= $sliceAmount;
167
                    }
168
                }
169
            );
170
171
            // clean up residual to-be-skipped records
172
            $resultSet = $resultSet->slice($skip);
173
            $resultCount = $rawCount;
174
        } else {
175
            if ($sourceEntityInstance instanceof Model) {
176
                $sourceEntityInstance = $sourceEntityInstance->getQuery();
177
            }
178
            $resultSet = $sourceEntityInstance->with($eagerLoad)->get();
179
            $resultSet = $resultSet->filter($isvalid);
180
            $resultCount = $resultSet->count();
181
182
            if (isset($skip)) {
183
                $resultSet = $resultSet->slice($skip);
184
            }
185
        }
186
187
        if (isset($top)) {
188
            $resultSet = $resultSet->take($top);
189
        }
190
191
192
        if (QueryType::ENTITIES() == $queryType || QueryType::ENTITIES_WITH_COUNT() == $queryType) {
193
            $result->results = array();
194
            foreach ($resultSet as $res) {
195
                $result->results[] = $res;
196
            }
197
        }
198
        if (QueryType::COUNT() == $queryType || QueryType::ENTITIES_WITH_COUNT() == $queryType) {
199
            $result->count = $resultCount;
200
        }
201
        return $result;
202
    }
203
204
    /**
205
     * Get related resource set for a resource
206
     * IE: http://host/EntitySet(1L)/NavigationPropertyToCollection
207
     * http://host/EntitySet?$expand=NavigationPropertyToCollection
208
     *
209
     * @param QueryType $queryType indicates if this is a query for a count, entities, or entities with a count
210
     * @param ResourceSet $sourceResourceSet The entity set containing the source entity
211
     * @param object $sourceEntityInstance The source entity instance.
212
     * @param ResourceSet $targetResourceSet The resource set of containing the target of the navigation property
213
     * @param ResourceProperty $targetProperty The navigation property to retrieve
214
     * @param FilterInfo $filter represents the $filter parameter of the OData query.  NULL if no $filter specified
215
     * @param mixed $orderBy sorted order if we want to get the data in some specific order
216
     * @param int $top number of records which  need to be skip
217
     * @param String $skip value indicating what records to skip
218
     *
219
     * @return QueryResult
220
     *
221
     */
222
    public function getRelatedResourceSet(
223
        QueryType $queryType,
224
        ResourceSet $sourceResourceSet,
225
        $sourceEntityInstance,
226
        ResourceSet $targetResourceSet,
227
        ResourceProperty $targetProperty,
228
        $filter = null,
229
        $orderBy = null,
230
        $top = null,
231
        $skip = null,
232
        $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...
233
    ) {
234
        if (!($sourceEntityInstance instanceof Model)) {
235
            throw new InvalidArgumentException('Source entity must be an Eloquent model.');
236
        }
237
238
        assert(null != $sourceEntityInstance, "Source instance must not be null");
239
        $this->checkSourceInstance($sourceEntityInstance);
240
241
        $this->checkAuth($sourceEntityInstance);
242
243
        $propertyName = $targetProperty->getName();
244
        $results = $sourceEntityInstance->$propertyName();
245
246
        return $this->getResourceSet(
247
            $queryType,
248
            $sourceResourceSet,
249
            $filter,
250
            $orderBy,
251
            $top,
252
            $skip,
253
            $results
254
        );
255
    }
256
257
    /**
258
     * Gets an entity instance from an entity set identified by a key
259
     * IE: http://host/EntitySet(1L)
260
     * http://host/EntitySet(KeyA=2L,KeyB='someValue')
261
     *
262
     * @param ResourceSet $resourceSet The entity set containing the entity to fetch
263
     * @param KeyDescriptor $keyDescriptor The key identifying the entity to fetch
264
     *
265
     * @return object|null Returns entity instance if found else null
266
     */
267
    public function getResourceFromResourceSet(
268
        ResourceSet $resourceSet,
269
        KeyDescriptor $keyDescriptor = null
270
    ) {
271
        return $this->getResource($resourceSet, $keyDescriptor);
272
    }
273
274
275
    /**
276
     * Common method for getResourceFromRelatedResourceSet() and getResourceFromResourceSet()
277
     * @param ResourceSet|null $resourceSet
278
     * @param KeyDescriptor|null $keyDescriptor
279
     * @param Model|Relation|null $sourceEntityInstance Starting point of query
280
     */
281
    public function getResource(
282
        ResourceSet $resourceSet = null,
283
        KeyDescriptor $keyDescriptor = null,
284
        array $whereCondition = [],
285
        $sourceEntityInstance = null
286
    ) {
287
        if (null == $resourceSet && null == $sourceEntityInstance) {
288
            throw new \Exception('Must supply at least one of a resource set and source entity.');
289
        }
290
291
        $this->checkSourceInstance($sourceEntityInstance);
292
293
        if (null == $sourceEntityInstance) {
294
            assert(null != $resourceSet);
295
            $sourceEntityInstance = $this->getSourceEntityInstance($resourceSet);
296
        }
297
298
        $this->checkAuth($sourceEntityInstance);
299
300
        if ($keyDescriptor) {
301
            foreach ($keyDescriptor->getValidatedNamedValues() as $key => $value) {
302
                $trimValue = trim($value[0], "\"'");
303
                $sourceEntityInstance = $sourceEntityInstance->where($key, $trimValue);
304
            }
305
        }
306
        foreach ($whereCondition as $fieldName => $fieldValue) {
307
            $sourceEntityInstance = $sourceEntityInstance->where($fieldName, $fieldValue);
308
        }
309
        $sourceEntityInstance = $sourceEntityInstance->get();
310
        return (0 == $sourceEntityInstance->count()) ? null : $sourceEntityInstance->first();
311
    }
312
313
    /**
314
     * Get related resource for a resource
315
     * IE: http://host/EntitySet(1L)/NavigationPropertyToSingleEntity
316
     * http://host/EntitySet?$expand=NavigationPropertyToSingleEntity
317
     *
318
     * @param ResourceSet $sourceResourceSet The entity set containing the source entity
319
     * @param object $sourceEntityInstance The source entity instance.
320
     * @param ResourceSet $targetResourceSet The entity set containing the entity pointed to by the navigation property
321
     * @param ResourceProperty $targetProperty The navigation property to fetch
322
     *
323
     * @return object|null The related resource if found else null
324
     */
325 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...
326
        ResourceSet $sourceResourceSet,
327
        $sourceEntityInstance,
328
        ResourceSet $targetResourceSet,
329
        ResourceProperty $targetProperty
330
    ) {
331
        if (!($sourceEntityInstance instanceof Model)) {
332
            throw new InvalidArgumentException('Source entity must be an Eloquent model.');
333
        }
334
        $this->checkSourceInstance($sourceEntityInstance);
335
336
        $this->checkAuth($sourceEntityInstance);
337
338
        $propertyName = $targetProperty->getName();
339
        return $sourceEntityInstance->$propertyName;
340
    }
341
342
    /**
343
     * Gets a related entity instance from an entity set identified by a key
344
     * IE: http://host/EntitySet(1L)/NavigationPropertyToCollection(33)
345
     *
346
     * @param ResourceSet $sourceResourceSet The entity set containing the source entity
347
     * @param object $sourceEntityInstance The source entity instance.
348
     * @param ResourceSet $targetResourceSet The entity set containing the entity to fetch
349
     * @param ResourceProperty $targetProperty The metadata of the target property.
350
     * @param KeyDescriptor $keyDescriptor The key identifying the entity to fetch
351
     *
352
     * @return object|null Returns entity instance if found else null
353
     */
354 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...
355
        ResourceSet $sourceResourceSet,
356
        $sourceEntityInstance,
357
        ResourceSet $targetResourceSet,
358
        ResourceProperty $targetProperty,
359
        KeyDescriptor $keyDescriptor
360
    ) {
361
        if (!($sourceEntityInstance instanceof Model)) {
362
            throw new InvalidArgumentException('Source entity must be an Eloquent model.');
363
        }
364
        $propertyName = $targetProperty->getName();
365
        return $this->getResource(null, $keyDescriptor, [], $sourceEntityInstance->$propertyName);
366
    }
367
368
369
    /**
370
     * @param ResourceSet $resourceSet
371
     * @return mixed
372
     */
373
    protected function getSourceEntityInstance(ResourceSet $resourceSet)
374
    {
375
        $entityClassName = $resourceSet->getResourceType()->getInstanceType()->name;
376
        return App::make($entityClassName);
377
    }
378
379
    /**
380
     * @param Model|Relation|null $source
381
     */
382
    protected function checkSourceInstance($source)
383
    {
384
        if (!(null == $source || $source instanceof Model || $source instanceof Relation)) {
385
            throw new InvalidArgumentException('Source entity instance must be null, a model, or a relation.');
386
        }
387
    }
388
389
    protected function getAuth()
390
    {
391
        return $this->auth;
392
    }
393
394
    /**
395
     * @param $sourceEntityInstance
396
     * @throws ODataException
397
     */
398
    private function checkAuth($sourceEntityInstance, $checkInstance = null)
399
    {
400
        $check = $checkInstance instanceof Model ? $checkInstance
401
            : $checkInstance instanceof Relation ? $checkInstance
402
                : $sourceEntityInstance instanceof Model ? $sourceEntityInstance
403
                    : $sourceEntityInstance instanceof Relation ? $sourceEntityInstance
404
                        : null;
405
        if (!$this->getAuth()->canAuth(ActionVerb::READ(), get_class($sourceEntityInstance), $check)) {
406
            throw new ODataException("Access denied", 403);
407
        }
408
    }
409
}
410