Test Failed
Pull Request — master (#36)
by Alex
25:34
created

LaravelQuery   F

Complexity

Total Complexity 65

Size/Duplication

Total Lines 485
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 15

Test Coverage

Coverage 60.92%

Importance

Changes 0
Metric Value
wmc 65
c 0
b 0
f 0
lcom 1
cbo 15
dl 0
loc 485
ccs 106
cts 174
cp 0.6092
rs 3.3333

18 Methods

Rating   Name   Duplication   Size   Complexity  
A getResourceFromResourceSet() 0 6 1
A __construct() 0 7 2
A handlesOrderedPaging() 0 4 1
A getExpressionProvider() 0 4 1
C getResourceSet() 0 62 16
C getResource() 0 27 8
B getRelatedResourceSet() 0 30 2
A getResourceFromRelatedResourceSet() 0 13 2
A getRelatedResourceReference() 0 12 2
A getSourceEntityInstance() 0 6 1
A updateResource() 0 10 1
A deleteResource() 0 22 3
A createResourceforResourceSet() 0 8 1
B createUpdateDeleteCore() 0 37 6
A putResource() 0 7 1
B createUpdateCoreWrapper() 0 18 5
B createUpdateDeleteProcessInput() 0 21 5
B createUpdateDeleteProcessOutput() 0 20 7

How to fix   Complexity   

Complex Class

Complex classes like LaravelQuery 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 LaravelQuery, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace AlgoWeb\PODataLaravel\Query;
4
5
use POData\Providers\Metadata\ResourceProperty;
6
use POData\Providers\Metadata\ResourceSet;
7
use POData\UriProcessor\QueryProcessor\Expression\Parser\IExpressionProvider;
8
use POData\UriProcessor\QueryProcessor\ExpressionParser\FilterInfo;
9
use POData\UriProcessor\ResourcePathProcessor\SegmentParser\KeyDescriptor;
10
use POData\Providers\Query\IQueryProvider;
11
use POData\Providers\Expression\MySQLExpressionProvider;
12
use POData\Providers\Query\QueryType;
13
use POData\Providers\Query\QueryResult;
14
use POData\Providers\Expression\PHPExpressionProvider;
15
use \POData\Common\ODataException;
16
use AlgoWeb\PODataLaravel\Interfaces\AuthInterface;
17
use AlgoWeb\PODataLaravel\Auth\NullAuthProvider;
18
use Illuminate\Support\Facades\App;
19
use Illuminate\Database\Eloquent\Model;
20
use Symfony\Component\Process\Exception\InvalidArgumentException;
21
22
class LaravelQuery implements IQueryProvider
23
{
24
    protected $expression;
25
    protected $auth;
26 5
    public $queryProviderClassName;
27
28
    public function __construct(AuthInterface $auth = null)
29 5
    {
30 5
        /* MySQLExpressionProvider();*/
31 5
        $this->expression = new LaravelExpressionProvider(); //PHPExpressionProvider('expression');
32 5
        $this->queryProviderClassName = get_class($this);
33
        $this->auth = isset($auth) ? $auth : new NullAuthProvider();
34
    }
35
36
    /**
37
     * Indicates if the QueryProvider can handle ordered paging, this means respecting order, skip, and top parameters
38
     * If the query provider can not handle ordered paging, it must return the entire result set and POData will
39
     * perform the ordering and paging
40
     *
41
     * @return Boolean True if the query provider can handle ordered paging, false if POData should perform the paging
42
     */
43
    public function handlesOrderedPaging()
44
    {
45
        return true;
46
    }
47
48
    /**
49
     * Gets the expression provider used by to compile OData expressions into expression used by this query provider.
50
     *
51
     * @return \POData\Providers\Expression\IExpressionProvider
52
     */
53
    public function getExpressionProvider()
54
    {
55
        return $this->expression;
56
    }
57
58
    /**
59
     * Gets collection of entities belongs to an entity set
60
     * IE: http://host/EntitySet
61
     *  http://host/EntitySet?$skip=10&$top=5&filter=Prop gt Value
62
     *
63
     * @param QueryType $queryType indicates if this is a query for a count, entities, or entities with a count
64
     * @param ResourceSet $resourceSet The entity set containing the entities to fetch
65
     * @param FilterInfo $filterInfo represents the $filter parameter of the OData query.  NULL if no $filter specified
66
     * @param mixed $orderBy sorted order if we want to get the data in some specific order
67
     * @param int $top number of records which  need to be skip
68
     * @param String $skipToken value indicating what records to skip
69
     *
70 3
     * @return QueryResult
71
     */
72
    public function getResourceSet(
73
        QueryType $queryType,
74
        ResourceSet $resourceSet,
75
        $filterInfo = null,
76
        $orderBy = null,
77
        $top = null,
78
        $skipToken = null,
79 3
        Model $sourceEntityInstance = null
80
    ) {
81
        if (null != $filterInfo && !($filterInfo instanceof FilterInfo)) {
82 3
            throw new InvalidArgumentException('Filter info must be either null or instance of FilterInfo.');
83 1
        }
84 1
85
        if (null == $sourceEntityInstance) {
86 3
            $sourceEntityInstance = $this->getSourceEntityInstance($resourceSet);
87 3
        }
88 3
89
        $result          = new QueryResult();
90 3
        $result->results = null;
91
        $result->count   = null;
92
93
        if (null != $orderBy) {
94
            foreach ($orderBy->getOrderByInfo()->getOrderByPathSegments() as $order) {
95
                foreach ($order->getSubPathSegments() as $subOrder) {
96
                    $sourceEntityInstance = $sourceEntityInstance->orderBy(
97 1
                        $subOrder->getName(),
98
                        $order->isAscending() ? 'asc' : 'desc'
99
                    );
100 3
                }
101 1
            }
102 1
        }
103 3
104 1
        $resultSet = $sourceEntityInstance->get();
105 1
106
        if (isset($filterInfo)) {
107 3
            $method = "return ".$filterInfo->getExpressionAsString().";";
108
            $clln = "$".$resourceSet->getResourceType()->getName();
109 3
            $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...
110
            $resultSet = $resultSet->filter($isvalid);
111
        }
112
113
        $resultCount = $resultSet->count();
114
115
        if (isset($skipToken)) {
116
            $resultSet = $resultSet->slice($skipToken);
117 3
        }
118 1
        if (isset($top)) {
119 1
            $resultSet = $resultSet->take($top);
120 1
        }
121 1
122 1
123 3
        if (QueryType::ENTITIES() == $queryType || QueryType::ENTITIES_WITH_COUNT() == $queryType) {
124 3
            $result->results = array();
125
            foreach ($resultSet as $res) {
126
                $result->results[] = $res;
127 3
            }
128 3
        }
129 3
        if (QueryType::COUNT() == $queryType || QueryType::ENTITIES_WITH_COUNT() == $queryType) {
130
            $result->count = $resultCount;
131
        }
132
        return $result;
133
    }
134
    /**
135
     * Gets an entity instance from an entity set identified by a key
136
     * IE: http://host/EntitySet(1L)
137
     * http://host/EntitySet(KeyA=2L,KeyB='someValue')
138
     *
139
     * @param ResourceSet $resourceSet The entity set containing the entity to fetch
140
     * @param KeyDescriptor $keyDescriptor The key identifying the entity to fetch
141
     *
142
     * @return object|null Returns entity instance if found else null
143
     */
144
    public function getResourceFromResourceSet(
145
        ResourceSet $resourceSet,
146
        KeyDescriptor $keyDescriptor = null
147
    ) {
148
        return $this->getResource($resourceSet, $keyDescriptor);
149
    }
150
151
    /**
152
     * Common method for getResourceFromRelatedResourceSet() and getResourceFromResourceSet()
153
     * @param ResourceSet|null $resourceSet
154
     * @param null|KeyDescriptor $keyDescriptor
155
     */
156
    protected function getResource(
157
        ResourceSet $resourceSet = null,
158
        KeyDescriptor $keyDescriptor = null,
159
        array $whereCondition = [],
160
        Model $sourceEntityInstance = null
161
    ) {
162
        if (null == $resourceSet && null == $sourceEntityInstance) {
163
            throw new \Exception('Must supply at least one of a resource set and source entity.');
164
        }
165
166
        if (null == $sourceEntityInstance) {
167
            $entityClassName = $resourceSet->getResourceType()->getInstanceType()->name;
0 ignored issues
show
Bug introduced by
It seems like $resourceSet is not always an object, but can also be of type null. Maybe add an additional type check?

If a variable is not always an object, we recommend to add an additional type check to ensure your method call is safe:

function someFunction(A $objectMaybe = null)
{
    if ($objectMaybe instanceof A) {
        $objectMaybe->doSomething();
    }
}
Loading history...
168
            $sourceEntityInstance = App::make($entityClassName);
169
        }
170
171
        if ($keyDescriptor) {
172
            foreach ($keyDescriptor->getValidatedNamedValues() as $key => $value) {
173
                $trimValue = trim($value[0], "\"'");
174
                $sourceEntityInstance = $sourceEntityInstance->where($key, $trimValue);
175
            }
176
        }
177
        foreach ($whereCondition as $fieldName => $fieldValue) {
178
            $sourceEntityInstance = $sourceEntityInstance->where($fieldName, $fieldValue);
179
        }
180
        $sourceEntityInstance = $sourceEntityInstance->get();
181
        return (0 == $sourceEntityInstance->count()) ? null : $sourceEntityInstance->first();
182
    }
183
184
    /**
185
     * Get related resource set for a resource
186
     * IE: http://host/EntitySet(1L)/NavigationPropertyToCollection
187
     * http://host/EntitySet?$expand=NavigationPropertyToCollection
188
     *
189
     * @param QueryType $queryType indicates if this is a query for a count, entities, or entities with a count
190
     * @param ResourceSet $sourceResourceSet The entity set containing the source entity
191
     * @param object $sourceEntityInstance The source entity instance.
192
     * @param ResourceSet $targetResourceSet The resource set of containing the target of the navigation property
193
     * @param ResourceProperty $targetProperty The navigation property to retrieve
194
     * @param FilterInfo $filter represents the $filter parameter of the OData query.  NULL if no $filter specified
195
     * @param mixed $orderBy sorted order if we want to get the data in some specific order
196
     * @param int $top number of records which  need to be skip
197
     * @param String $skip value indicating what records to skip
198
     *
199 3
     * @return QueryResult
200
     *
201
     */
202
    public function getRelatedResourceSet(
203
        QueryType $queryType,
204
        ResourceSet $sourceResourceSet,
205
        $sourceEntityInstance,
206
        ResourceSet $targetResourceSet,
207
        ResourceProperty $targetProperty,
208
        $filter = null,
209
        $orderBy = null,
210 3
        $top = null,
211 3
        $skip = null
212
    ) {
213 3
        if (!($sourceEntityInstance instanceof Model)) {
214 3
            throw new InvalidArgumentException('Source entity must be an Eloquent model.');
215 3
        }
216 3
217 3
        $propertyName = $targetProperty->getName();
218 3
        $results = $sourceEntityInstance->$propertyName();
219 3
        $relatedClass = $results->getRelated();
220
        $sourceEntityInstance = new $relatedClass();
221 3
222
        return $this->getResourceSet(
223
            $queryType,
224
            $sourceResourceSet,
225
            $filter,
226
            $orderBy,
227
            $top,
228
            $skip,
229
            $sourceEntityInstance
230
        );
231
    }
232
233
    /**
234
     * Gets a related entity instance from an entity set identified by a key
235
     * IE: http://host/EntitySet(1L)/NavigationPropertyToCollection(33)
236
     *
237
     * @param ResourceSet $sourceResourceSet The entity set containing the source entity
238
     * @param object $sourceEntityInstance The source entity instance.
239
     * @param ResourceSet $targetResourceSet The entity set containing the entity to fetch
240
     * @param ResourceProperty $targetProperty The metadata of the target property.
241
     * @param KeyDescriptor $keyDescriptor The key identifying the entity to fetch
242
     *
243
     * @return object|null Returns entity instance if found else null
244
     */
245
    public function getResourceFromRelatedResourceSet(
246
        ResourceSet $sourceResourceSet,
247
        $sourceEntityInstance,
248
        ResourceSet $targetResourceSet,
249
        ResourceProperty $targetProperty,
250
        KeyDescriptor $keyDescriptor
251
    ) {
252
        if (!($sourceEntityInstance instanceof Model)) {
253
            throw new InvalidArgumentException('Source entity must be an Eloquent model.');
254
        }
255
        $propertyName = $targetProperty->getName();
256
        return $this->getResource(null, $keyDescriptor, [], $sourceEntityInstance->$propertyName);
257
    }
258
259
    /**
260
     * Get related resource for a resource
261
     * IE: http://host/EntitySet(1L)/NavigationPropertyToSingleEntity
262
     * http://host/EntitySet?$expand=NavigationPropertyToSingleEntity
263
     *
264
     * @param ResourceSet $sourceResourceSet The entity set containing the source entity
265
     * @param object $sourceEntityInstance The source entity instance.
266
     * @param ResourceSet $targetResourceSet The entity set containing the entity pointed to by the navigation property
267
     * @param ResourceProperty $targetProperty The navigation property to fetch
268
     *
269
     * @return object|null The related resource if found else null
270
     */
271
    public function getRelatedResourceReference(
272
        ResourceSet $sourceResourceSet,
273
        $sourceEntityInstance,
274
        ResourceSet $targetResourceSet,
275
        ResourceProperty $targetProperty
276
    ) {
277
        if (!($sourceEntityInstance instanceof Model)) {
278
            throw new InvalidArgumentException('Source entity must be an Eloquent model.');
279
        }
280
        $propertyName = $targetProperty->getName();
281
        return $sourceEntityInstance->$propertyName;
282
    }
283
284
    /**
285
     * @param ResourceSet $resourceSet
286
     * @return mixed
287
     */
288
    protected function getSourceEntityInstance(ResourceSet $resourceSet)
289
    {
290
        $entityClassName = $resourceSet->getResourceType()->getInstanceType()->name;
291
        $sourceEntityInstance = App::make($entityClassName);
292 1
        return $sourceEntityInstance->newQuery();
293
    }
294
295
    /**
296
     * Updates a resource
297
     *
298
     * @param ResourceSet      $sourceResourceSet    The entity set containing the source entity
299 1
     * @param object           $sourceEntityInstance The source entity instance
300 1
     * @param KeyDescriptor    $keyDescriptor        The key identifying the entity to fetch
301
     * @param object           $data                 The New data for the entity instance.
302 1
     * @param bool             $shouldUpdate        Should undefined values be updated or reset to default
303
     *
304 1
     * @return object|null The new resource value if it is assignable or throw exception for null.
305
     */
306 1
    public function updateResource(
307
        ResourceSet $sourceResourceSet,
308
        $sourceEntityInstance,
309 1
        KeyDescriptor $keyDescriptor,
310
        $data,
311
        $shouldUpdate = false
312
    ) {
313
        $verb = 'update';
314
        return $this->createUpdateCoreWrapper($sourceResourceSet, $sourceEntityInstance, $data, $verb);
315
    }
316
    /**
317
     * Delete resource from a resource set.
318 1
     * @param ResourceSet|null $sourceResourceSet
319
     * @param object           $sourceEntityInstance
320
     *
321
     * return bool true if resources sucessfully deteled, otherwise false.
322 1
     */
323 1
    public function deleteResource(
324 1
        ResourceSet $sourceResourceSet,
325 1
        $sourceEntityInstance
326 1
    ) {
327
        $verb = 'delete';
328 1
        if (!($sourceEntityInstance instanceof Model)) {
329
            throw new InvalidArgumentException('Source entity must be an Eloquent model.');
330 1
        }
331 1
332
        $class = $sourceResourceSet->getResourceType()->getInstanceType()->name;
333
        $id = $sourceEntityInstance->getKey();
334 1
        $name = $sourceEntityInstance->getKeyName();
335
        $data = [$name => $id];
336
337
        $data = $this->createUpdateDeleteCore($sourceEntityInstance, $data, $class, $verb);
338
339
        $success = isset($data['id']);
340
        if ($success) {
341
            return true;
342
        }
343 1
        throw new ODataException('Target model not successfully deleted', 422);
344
    }
345
    /**
346
     * @param ResourceSet      $resourceSet   The entity set containing the entity to fetch
347
     * @param object           $sourceEntityInstance The source entity instance
348 1
     * @param object           $data                 The New data for the entity instance.
349 1
     *
350
     * returns object|null returns the newly created model if sucessful or null if model creation failed.
351 1
     */
352
    public function createResourceforResourceSet(
353 1
        ResourceSet $resourceSet,
354
        $sourceEntityInstance,
355 1
        $data
356
    ) {
357
        $verb = 'create';
358 1
        return $this->createUpdateCoreWrapper($resourceSet, $sourceEntityInstance, $data, $verb);
359
    }
360
361
    /**
362
     * @param $sourceEntityInstance
363
     * @param $data
364
     * @param $class
365
     * @param string $verb
366
     * @return array|mixed
367
     * @throws ODataException
368
     * @throws \POData\Common\InvalidOperationException
369
     */
370 3
    private function createUpdateDeleteCore($sourceEntityInstance, $data, $class, $verb)
371
    {
372 3
        $raw = App::make('metadataControllers');
373 3
        $map = $raw->getMetadata();
374
375 3
        if (!array_key_exists($class, $map)) {
376
            throw new \POData\Common\InvalidOperationException('Controller mapping missing for class '.$class.'.');
377
        }
378 3
        $goal = $raw->getMapping($class, $verb);
379 3
        if (null == $goal) {
380
            throw new \POData\Common\InvalidOperationException(
381
                'Controller mapping missing for '.$verb.' verb on class '.$class.'.'
382
            );
383
        }
384
385 3
        if (null == $data) {
386
            $data = [];
387
        } elseif (is_object($data)) {
388 3
            $data = (array) $data;
389 2
        }
390 2
        if (!is_array($data)) {
391 3
            throw \POData\Common\ODataException::createPreConditionFailedError(
392
                'Data not resolvable to key-value array.'
393
            );
394
        }
395
396
        $controlClass = $goal['controller'];
397 3
        $method = $goal['method'];
398 3
        $paramList = $goal['parameters'];
399 3
        $controller = App::make($controlClass);
400 3
        $parms = $this->createUpdateDeleteProcessInput($sourceEntityInstance, $data, $paramList);
401 3
        unset($data);
402
403 3
        $result = call_user_func_array(array($controller, $method), $parms);
404 3
405 3
        return $this->createUpdateDeleteProcessOutput($result);
406 3
    }
407 2
408 2
    /**
409 2
     * Puts an entity instance to entity set identified by a key.
410 2
     *
411 2
     * @param ResourceSet $resourceSet The entity set containing the entity to update
412
     * @param KeyDescriptor $keyDescriptor The key identifying the entity to update
413
     * @param $data
414
     *
415 2
     * @return bool|null Returns result of executiong query
416
     */
417
    public function putResource(
418 3
        ResourceSet $resourceSet,
419 3
        KeyDescriptor $keyDescriptor,
420
        $data
421 3
    ) {
422
        // TODO: Implement putResource() method.
423 3
    }
424
425
    /**
426 3
     * @param ResourceSet $sourceResourceSet
427 3
     * @param $sourceEntityInstance
428 3
     * @param $data
429 3
     * @param $verb
430
     * @return mixed
431 3
     * @throws ODataException
432
     * @throws \POData\Common\InvalidOperationException
433
     */
434 3
    private function createUpdateCoreWrapper(ResourceSet $sourceResourceSet, $sourceEntityInstance, $data, $verb)
435
    {
436
        $lastWord = 'update' == $verb ? 'updated' : 'created';
437
        if (!(null == $sourceEntityInstance || $sourceEntityInstance instanceof Model)) {
438
            throw new InvalidArgumentException('Source entity must either be null or an Eloquent model.');
439 3
        }
440
441
        $class = $sourceResourceSet->getResourceType()->getInstanceType()->name;
442
443
        $data = $this->createUpdateDeleteCore($sourceEntityInstance, $data, $class, $verb);
444
445
        $success = isset($data['id']);
446
447
        if ($success) {
448
            return $class::findOrFail($data['id']);
449
        }
450
        throw new ODataException('Target model not successfully '.$lastWord, 422);
451
    }
452
453
    /**
454
     * @param $sourceEntityInstance
455
     * @param $data
456
     * @param $paramList
457
     * @return array
458
     */
459
    private function createUpdateDeleteProcessInput($sourceEntityInstance, $data, $paramList)
460
    {
461
        $parms = [];
462
463
        foreach ($paramList as $spec) {
464
            $varType = isset($spec['type']) ? $spec['type'] : null;
465
            $varName = $spec['name'];
466
            if (null == $varType) {
467
                $parms[] = $sourceEntityInstance->$varName;
468
                continue;
469
            }
470
            // TODO: Give this smarts and actively pick up instantiation details
471
            $var = new $varType();
472
            if ($spec['isRequest']) {
473
                $var->setMethod('POST');
474
                $var->request = new \Symfony\Component\HttpFoundation\ParameterBag($data);
475
            }
476
            $parms[] = $var;
477
        }
478
        return $parms;
479
    }
480
481
    /**
482
     * @param $result
483
     * @return array|mixed
484
     * @throws ODataException
485
     */
486
    private function createUpdateDeleteProcessOutput($result)
487
    {
488
        if (!($result instanceof \Illuminate\Http\JsonResponse)) {
489
            throw ODataException::createInternalServerError('Controller response not well-formed json.');
490
        }
491
        $outData = $result->getData();
492
        if (is_object($outData)) {
493
            $outData = (array)$outData;
494
        }
495
496
        if (!is_array($outData)) {
497
            throw ODataException::createInternalServerError('Controller response does not have an array.');
498
        }
499
        if (!(key_exists('id', $outData) && key_exists('status', $outData) && key_exists('errors', $outData))) {
500
            throw ODataException::createInternalServerError(
501
                'Controller response array missing at least one of id, status and/or errors fields.'
502
            );
503
        }
504
        return $outData;
505
    }
506
}
507