Test Setup Failed
Pull Request — master (#36)
by Alex
02:55
created

LaravelQuery::getRelatedResourceSet()   B

Complexity

Conditions 2
Paths 2

Size

Total Lines 30
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 2

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 30
ccs 10
cts 10
cp 1
rs 8.8571
cc 2
eloc 24
nc 2
nop 9
crap 2

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 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