Test Setup Failed
Pull Request — master (#36)
by Alex
08:28
created

LaravelQuery::__construct()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 7
ccs 4
cts 4
cp 1
rs 9.4285
cc 2
eloc 4
nc 2
nop 1
crap 2
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 = new $entityClassName();
292 1
        return $sourceEntityInstance = $sourceEntityInstance->newQuery();
0 ignored issues
show
Unused Code introduced by
$sourceEntityInstance is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
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
        }
388 3
        if (is_object($data)) {
389 2
            $data = (array) $data;
390 2
        }
391 3
        if (!is_array($data)) {
392
            throw \POData\Common\ODataException::createPreConditionFailedError(
393
                'Data not resolvable to key-value array.'
394
            );
395
        }
396
397 3
        $controlClass = $goal['controller'];
398 3
        $method = $goal['method'];
399 3
        $paramList = $goal['parameters'];
400 3
        $controller = App::make($controlClass);
401 3
        $parms = [];
402
403 3
        foreach ($paramList as $spec) {
404 3
            $varType = isset($spec['type']) ? $spec['type'] : null;
405 3
            $varName = $spec['name'];
406 3
            if ($spec['isRequest']) {
407 2
                $var = new $varType();
408 2
                $var->setMethod('POST');
409 2
                $var->request = new \Symfony\Component\HttpFoundation\ParameterBag($data);
410 2
            } else {
411 2
                if (null != $varType) {
412
                    // TODO: Give this smarts and actively pick up instantiation details
413
                    $var = new $varType();
414
                } else {
415 2
                    $var = $sourceEntityInstance->$varName;
416
                }
417
            }
418 3
            $parms[] = $var;
419 3
        }
420
421 3
        $result = call_user_func_array(array($controller, $method), $parms);
422
423 3
        if (!($result instanceof \Illuminate\Http\JsonResponse)) {
424
            throw ODataException::createInternalServerError('Controller response not well-formed json.');
425
        }
426 3
        $data = $result->getData();
427 3
        if (is_object($data)) {
428 3
            $data = (array) $data;
429 3
        }
430
431 3
        if (!is_array($data)) {
432
            throw ODataException::createInternalServerError('Controller response does not have an array.');
433
        }
434 3
        if (!(key_exists('id', $data) && key_exists('status', $data) && key_exists('errors', $data))) {
435
            throw ODataException::createInternalServerError(
436
                'Controller response array missing at least one of id, status and/or errors fields.'
437
            );
438
        }
439 3
        return $data;
440
    }
441
442
    /**
443
     * Puts an entity instance to entity set identified by a key.
444
     *
445
     * @param ResourceSet $resourceSet The entity set containing the entity to update
446
     * @param KeyDescriptor $keyDescriptor The key identifying the entity to update
447
     * @param $data
448
     *
449
     * @return bool|null Returns result of executiong query
450
     */
451
    public function putResource(
452
        ResourceSet $resourceSet,
453
        KeyDescriptor $keyDescriptor,
454
        $data
455
    ) {
456
        // TODO: Implement putResource() method.
457
    }
458
459
    /**
460
     * @param ResourceSet $sourceResourceSet
461
     * @param $sourceEntityInstance
462
     * @param $data
463
     * @param $verb
464
     * @return mixed
465
     * @throws ODataException
466
     * @throws \POData\Common\InvalidOperationException
467
     */
468
    private function createUpdateCoreWrapper(ResourceSet $sourceResourceSet, $sourceEntityInstance, $data, $verb)
469
    {
470
        $lastWord = 'update' == $verb ? 'updated' : 'created';
471
        if (!(null == $sourceEntityInstance || $sourceEntityInstance instanceof Model)) {
472
            throw new InvalidArgumentException('Source entity must either be null or an Eloquent model.');
473
        }
474
475
        $class = $sourceResourceSet->getResourceType()->getInstanceType()->name;
476
477
        $data = $this->createUpdateDeleteCore($sourceEntityInstance, $data, $class, $verb);
478
479
        $success = isset($data['id']);
480
481
        if ($success) {
482
            return $class::findOrFail($data['id']);
483
        }
484
        throw new ODataException('Target model not successfully '.$lastWord, 422);
485
    }
486
}
487