Test Failed
Pull Request — master (#73)
by Alex
03:30
created

LaravelReadQuery   D

Complexity

Total Complexity 55

Size/Duplication

Total Lines 357
Duplicated Lines 8.12 %

Coupling/Cohesion

Components 1
Dependencies 16

Importance

Changes 0
Metric Value
wmc 55
lcom 1
cbo 16
dl 29
loc 357
rs 4.5205
c 0
b 0
f 0

11 Methods

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