Test Setup Failed
Pull Request — master (#68)
by Alex
02:38
created

LaravelReadQuery   C

Complexity

Total Complexity 55

Size/Duplication

Total Lines 354
Duplicated Lines 8.19 %

Coupling/Cohesion

Components 1
Dependencies 13

Importance

Changes 0
Metric Value
wmc 55
lcom 1
cbo 13
dl 29
loc 354
rs 6.8
c 0
b 0
f 0

11 Methods

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