Test Failed
Pull Request — master (#36)
by Alex
07:31
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
            $sourceEntityInstance = $this->getSourceEntityInstance($resourceSet);
0 ignored issues
show
Bug introduced by
It seems like $resourceSet defined by parameter $resourceSet on line 157 can be null; however, AlgoWeb\PODataLaravel\Qu...tSourceEntityInstance() does not accept null, maybe add an additional type check?

It seems like you allow that null is being passed for a parameter, however the function which is called does not seem to accept null.

We recommend to add an additional type check (or disallow null for the parameter):

function notNullable(stdClass $x) { }

// Unsafe
function withoutCheck(stdClass $x = null) {
    notNullable($x);
}

// Safe - Alternative 1: Adding Additional Type-Check
function withCheck(stdClass $x = null) {
    if ($x instanceof stdClass) {
        notNullable($x);
    }
}

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