Completed
Pull Request — master (#12)
by Alex
03:28
created

LaravelQuery::createResourceforResourceSet()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 17
Code Lines 11

Duplication

Lines 17
Ratio 100 %

Code Coverage

Tests 7
CRAP Score 2.0078

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 17
loc 17
ccs 7
cts 8
cp 0.875
rs 9.4285
cc 2
eloc 11
nc 2
nop 3
crap 2.0078
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
19
20
use Illuminate\Support\Facades\DB;
21
use Illuminate\Support\Facades\App;
22
23
class LaravelQuery implements IQueryProvider
24
{
25
    protected $expression;
26
    protected $auth;
27
    public $queryProviderClassName;
28
29 5
    public function __construct(AuthInterface $auth = null)
30
    {
31
        /* MySQLExpressionProvider();*/
32 5
        $this->expression = new LaravelExpressionProvider(); //PHPExpressionProvider('expression');
33 5
        $this->queryProviderClassName = get_class($this);
34 5
        $this->auth = isset($auth) ? $auth : new NullAuthProvider();
35 5
    }
36
37
    /**
38
     * Indicates if the QueryProvider can handle ordered paging, this means respecting order, skip, and top parameters
39
     * If the query provider can not handle ordered paging, it must return the entire result set and POData will
40
     * perform the ordering and paging
41
     *
42
     * @return Boolean True if the query provider can handle ordered paging, false if POData should perform the paging
43
     */
44
    public function handlesOrderedPaging()
45
    {
46
        return true;
47
    }
48
49
    /**
50
     * Gets the expression provider used by to compile OData expressions into expression used by this query provider.
51
     *
52
     * @return \POData\Providers\Expression\IExpressionProvider
53
     */
54
    public function getExpressionProvider()
55
    {
56
        return $this->expression;
57
    }
58
59
    /**
60
     * Gets collection of entities belongs to an entity set
61
     * IE: http://host/EntitySet
62
     *  http://host/EntitySet?$skip=10&$top=5&filter=Prop gt Value
63
     *
64
     * @param QueryType $queryType indicates if this is a query for a count, entities, or entities with a count
65
     * @param ResourceSet $resourceSet The entity set containing the entities to fetch
66
     * @param FilterInfo $filterInfo represents the $filter parameter of the OData query.  NULL if no $filter specified
67
     * @param mixed $orderBy sorted order if we want to get the data in some specific order
68
     * @param int $top number of records which  need to be skip
69
     * @param String $skipToken value indicating what records to skip
70
     *
71
     * @return QueryResult
72
     */
73 3
    public function getResourceSet(
74
        QueryType $queryType,
75
        ResourceSet $resourceSet,
76
        $filterInfo = null,
77
        $orderBy = null,
78
        $top = null,
79
        $skipToken = null,
80
        $sourceEntityInstance = null
81
    ) {
82 3
        if ($resourceSet == null && $sourceEntityInstance == null) {
83
            throw new \Exception('Must supply at least one of a resource set and source entity');
84
        }
85 3
        if ($sourceEntityInstance == null) {
86 1
            $sourceEntityInstance = $this->getSourceEntityInstance($resourceSet);
87 1
        }
88
89 3
        $result          = new QueryResult();
90 3
        $result->results = null;
91 3
        $result->count   = null;
92
93 3
        if (isset($orderBy) && 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
                }
101
            }
102
        }
103 3
        if (isset($skipToken)) {
104 1
            $sourceEntityInstance = $sourceEntityInstance->skip($skipToken);
105 1
        }
106 3
        if (isset($top)) {
107 1
            $sourceEntityInstance = $sourceEntityInstance->take($top);
108 1
        }
109
110 3
        $resultSet = $sourceEntityInstance->get();
111
112 3
        if (isset($filterInfo)) {
113
            $method = "return ".$filterInfo->getExpressionAsString().";";
114
            $clln = "$".$resourceSet->getResourceType()->getName();
115
            $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...
116
            $resultSet = $resultSet->filter($isvalid);
117
        }
118
119
120 3
        if (QueryType::ENTITIES() == $queryType || QueryType::ENTITIES_WITH_COUNT() == $queryType) {
121 1
            $result->results = array();
122 1
            foreach ($resultSet as $res) {
123 1
                $result->results[] = $res;
124 1
            }
125 1
        }
126 3
        if (QueryType::COUNT() == $queryType || QueryType::ENTITIES_WITH_COUNT() == $queryType) {
127 3
            if (is_array($resultSet)) {
128
                $resultSet = collect($resultSet);
129
            }
130 3
            $result->count = $resultSet->count();
131 3
        }
132 3
        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,
158
        $keyDescriptor,
159
        array $whereCondition = [],
160
        $sourceEntityInstance = null
161
    ) {
162
        if ($resourceSet == null && $sourceEntityInstance == null) {
163
            throw new \Exception('Must supply at least one of a resource set and source entity');
164
        }
165
        if ($sourceEntityInstance == null) {
166
            $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...
167
            $sourceEntityInstance = new $entityClassName();
168
        }
169
        if ($keyDescriptor) {
170
            foreach ($keyDescriptor->getValidatedNamedValues() as $key => $value) {
171
                $sourceEntityInstance = $sourceEntityInstance->where($key, $value[0]);
172
            }
173
        }
174
        foreach ($whereCondition as $fieldName => $fieldValue) {
175
            $sourceEntityInstance = $sourceEntityInstance->where($fieldName, $fieldValue);
176
        }
177
        $sourceEntityInstance = $sourceEntityInstance->get();
178
        if (0 == $sourceEntityInstance->count()) {
179
            return null;
180
        }
181
        return $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
     * @return QueryResult
200
     *
201
     */
202 3
    public function getRelatedResourceSet(
203
        QueryType $queryType,
204
        ResourceSet $sourceResourceSet,
205
        $sourceEntityInstance,
206
        ResourceSet $targetResourceSet,
207
        ResourceProperty $targetProperty,
208
        $filter = null,
209
        $orderBy = null,
210
        $top = null,
211
        $skip = null
212
    ) {
213 3
        $propertyName = $targetProperty->getName();
214 3
        $results = $sourceEntityInstance->$propertyName();
215
216 3
        return $this->getResourceSet(
217 3
            $queryType,
218 3
            $sourceResourceSet,
219 3
            $filter,
220 3
            $orderBy,
221 3
            $top,
222 3
            $skip,
223
            $results
224 3
        );
225
226
    }
227
228
    /**
229
     * Gets a related entity instance from an entity set identified by a key
230
     * IE: http://host/EntitySet(1L)/NavigationPropertyToCollection(33)
231
     *
232
     * @param ResourceSet $sourceResourceSet The entity set containing the source entity
233
     * @param object $sourceEntityInstance The source entity instance.
234
     * @param ResourceSet $targetResourceSet The entity set containing the entity to fetch
235
     * @param ResourceProperty $targetProperty The metadata of the target property.
236
     * @param KeyDescriptor $keyDescriptor The key identifying the entity to fetch
237
     *
238
     * @return object|null Returns entity instance if found else null
239
     */
240
    public function getResourceFromRelatedResourceSet(
241
        ResourceSet $sourceResourceSet,
242
        $sourceEntityInstance,
243
        ResourceSet $targetResourceSet,
244
        ResourceProperty $targetProperty,
245
        KeyDescriptor $keyDescriptor
246
    ) {
247
        $propertyName = $targetProperty->getName();
248
        return $this->getResource(null, $keyDescriptor, [], $sourceEntityInstance->$propertyName);
249
    }
250
251
    /**
252
     * Get related resource for a resource
253
     * IE: http://host/EntitySet(1L)/NavigationPropertyToSingleEntity
254
     * http://host/EntitySet?$expand=NavigationPropertyToSingleEntity
255
     *
256
     * @param ResourceSet $sourceResourceSet The entity set containing the source entity
257
     * @param object $sourceEntityInstance The source entity instance.
258
     * @param ResourceSet $targetResourceSet The entity set containing the entity pointed to by the navigation property
259
     * @param ResourceProperty $targetProperty The navigation property to fetch
260
     *
261
     * @return object|null The related resource if found else null
262
     */
263
    public function getRelatedResourceReference(
264
        ResourceSet $sourceResourceSet,
265
        $sourceEntityInstance,
266
        ResourceSet $targetResourceSet,
267
        ResourceProperty $targetProperty
268
    ) {
269
        $propertyName = $targetProperty->getName();
270
        return $sourceEntityInstance->$propertyName;
271
    }
272
273
    /**
274
     * @param ResourceSet $resourceSet
275
     * @return mixed
276
     */
277
    protected function getSourceEntityInstance(ResourceSet $resourceSet)
278
    {
279
        $entityClassName = $resourceSet->getResourceType()->getInstanceType()->name;
280
        $sourceEntityInstance = new $entityClassName();
281
        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...
282
    }
283
284
    /**
285
     * Updates a resource
286
     *
287
     * @param ResourceSet      $sourceResourceSet    The entity set containing the source entity
288
     * @param object           $sourceEntityInstance The source entity instance
289
     * @param KeyDescriptor    $keyDescriptor        The key identifying the entity to fetch
290
     * @param object           $data                 The New data for the entity instance.
291
     * @param bool             $shouldUpdate        Should undefined values be updated or reset to default
292
     *
293
     * @return object|null The new resource value if it is assignable or throw exception for null.
294
     */
295 1 View Code Duplication
    public function updateResource(
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
296
        ResourceSet $sourceResourceSet,
297
        $sourceEntityInstance,
298
        KeyDescriptor $keyDescriptor,
299
        $data,
300
        $shouldUpdate = false
301
    ) {
302 1
        $verb = 'update';
303 1
        $class = $sourceResourceSet->getResourceType()->getInstanceType()->name;
304
305 1
        $data = $this->createUpdateDeleteCore($sourceEntityInstance, $data, $class, $verb);
306
307 1
        $success = isset($data['id']);
308
309 1
        if ($success) {
310
            return $class::findOrFail($data['id']);
311
        }
312 1
        throw new ODataException('Target model not successfully updated', 422);
313
    }
314
    /**
315
     * Delete resource from a resource set.
316
     * @param ResourceSet|null $resourceSet
0 ignored issues
show
Documentation introduced by
There is no parameter named $resourceSet. Did you maybe mean $sourceResourceSet?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function. It has, however, found a similar but not annotated parameter which might be a good fit.

Consider the following example. The parameter $ireland is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $ireland
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was changed, but the annotation was not.

Loading history...
317
     * @param object           $sourceEntityInstance
318
     *
319
     * return bool true if resources sucessfully deteled, otherwise false.
320
     */
321 1 View Code Duplication
    public function deleteResource(
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
322
        ResourceSet $sourceResourceSet,
323
        $sourceEntityInstance
324
    ) {
325 1
        $verb = 'delete';
326 1
        $class = $sourceResourceSet->getResourceType()->getInstanceType()->name;
327
328 1
        $data = $this->createUpdateDeleteCore($sourceEntityInstance, null, $class, $verb);
329
330 1
        $success = isset($data['id']);
331 1
        if ($success) {
332
            return $class::findOrFail($data['id']);
333
        }
334 1
        throw new ODataException('Target model not successfully deleted', 422);
335
    }
336
    /**
337
     * @param ResourceSet      $resourceSet   The entity set containing the entity to fetch
338
     * @param object           $sourceEntityInstance The source entity instance
339
     * @param object           $data                 The New data for the entity instance.
340
     *
341
     * returns object|null returns the newly created model if sucessful or null if model creation failed.
342
     */
343 1 View Code Duplication
    public function createResourceforResourceSet(
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
344
        ResourceSet $resourceSet,
345
        $sourceEntityInstance,
346
        $data
347
    ) {
348 1
        $verb = 'create';
349 1
        $class = $resourceSet->getResourceType()->getInstanceType()->name;
350
351 1
        $data = $this->createUpdateDeleteCore($sourceEntityInstance, $data, $class, $verb);
352
353 1
        $success = isset($data['id']);
354
355 1
        if ($success) {
356
            return $class::findOrFail($data['id']);
357
        }
358 1
        throw new ODataException('Target model not successfully created', 422);
359
    }
360
361
    /**
362
     * @param $sourceEntityInstance
363
     * @param $data
364
     * @param $class
365
     * @param $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 1
            $data = [];
387 1
        }
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 = new $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