Test Setup Failed
Pull Request — master (#53)
by Alex
12:38
created

LaravelQuery::createUpdateCoreWrapper()   C

Complexity

Conditions 7
Paths 10

Size

Total Lines 25
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 10.5454

Importance

Changes 0
Metric Value
dl 0
loc 25
ccs 7
cts 12
cp 0.5833
rs 6.7272
c 0
b 0
f 0
cc 7
eloc 15
nc 10
nop 4
crap 10.5454
1
<?php
2
3
namespace AlgoWeb\PODataLaravel\Query;
4
5
use AlgoWeb\PODataLaravel\Enums\ActionVerb;
6
use Illuminate\Database\Eloquent\Relations\Relation;
7
use POData\Providers\Metadata\ResourceProperty;
8
use POData\Providers\Metadata\ResourceSet;
9
use POData\UriProcessor\QueryProcessor\Expression\Parser\IExpressionProvider;
10
use POData\UriProcessor\QueryProcessor\ExpressionParser\FilterInfo;
11
use POData\UriProcessor\ResourcePathProcessor\SegmentParser\KeyDescriptor;
12
use POData\Providers\Query\IQueryProvider;
13
use POData\Providers\Expression\MySQLExpressionProvider;
14
use POData\Providers\Query\QueryType;
15
use POData\Providers\Query\QueryResult;
16
use POData\Providers\Expression\PHPExpressionProvider;
17
use \POData\Common\ODataException;
18
use AlgoWeb\PODataLaravel\Interfaces\AuthInterface;
19
use AlgoWeb\PODataLaravel\Auth\NullAuthProvider;
20
use Illuminate\Support\Facades\App;
21
use Illuminate\Database\Eloquent\Model;
22
use Symfony\Component\Process\Exception\InvalidArgumentException;
23
24
class LaravelQuery implements IQueryProvider
25
{
26 5
    protected $expression;
27
    protected $auth;
28
    protected $reader;
29 5
    public $queryProviderClassName;
30 5
    private $verbMap = [];
31 5
32 5
    public function __construct(AuthInterface $auth = null)
33
    {
34
        /* MySQLExpressionProvider();*/
35
        $this->expression = new LaravelExpressionProvider(); //PHPExpressionProvider('expression');
36
        $this->queryProviderClassName = get_class($this);
37
        $this->auth = isset($auth) ? $auth : new NullAuthProvider();
38
        $this->reader = new LaravelReadQuery($this->auth);
39
        $this->verbMap['create'] = ActionVerb::CREATE();
40
        $this->verbMap['update'] = ActionVerb::UPDATE();
41
        $this->verbMap['delete'] = ActionVerb::DELETE();
42
    }
43
44
    /**
45
     * Indicates if the QueryProvider can handle ordered paging, this means respecting order, skip, and top parameters
46
     * If the query provider can not handle ordered paging, it must return the entire result set and POData will
47
     * perform the ordering and paging
48
     *
49
     * @return Boolean True if the query provider can handle ordered paging, false if POData should perform the paging
50
     */
51
    public function handlesOrderedPaging()
52
    {
53
        return true;
54
    }
55
56
    /**
57
     * Gets the expression provider used by to compile OData expressions into expression used by this query provider.
58
     *
59
     * @return \POData\Providers\Expression\IExpressionProvider
60
     */
61
    public function getExpressionProvider()
62
    {
63
        return $this->expression;
64
    }
65
66
    /**
67
     * Gets the LaravelReadQuery instance used to handle read queries (repetitious, nyet?)
68
     *
69
     * @return LaravelReadQuery
70 3
     */
71
    public function getReader()
72
    {
73
        return $this->reader;
74
    }
75
76
    /**
77
     * Gets collection of entities belongs to an entity set
78
     * IE: http://host/EntitySet
79 3
     *  http://host/EntitySet?$skip=10&$top=5&filter=Prop gt Value
80
     *
81
     * @param QueryType $queryType indicates if this is a query for a count, entities, or entities with a count
82 3
     * @param ResourceSet $resourceSet The entity set containing the entities to fetch
83 1
     * @param FilterInfo $filterInfo represents the $filter parameter of the OData query.  NULL if no $filter specified
84 1
     * @param mixed $orderBy sorted order if we want to get the data in some specific order
85
     * @param int $top number of records which  need to be skip
86 3
     * @param String $skipToken value indicating what records to skip
87 3
     * @param Model|Relation|null $sourceEntityInstance Starting point of query
88 3
     *
89
     * @return QueryResult
90 3
     */
91
    public function getResourceSet(
92
        QueryType $queryType,
93
        ResourceSet $resourceSet,
94
        $filterInfo = null,
95
        $orderBy = null,
96
        $top = null,
97 1
        $skipToken = null,
98
        $sourceEntityInstance = null
99
    ) {
100 3
        return $this->getReader()->getResourceSet(
101 1
            $queryType,
102 1
            $resourceSet,
103 3
            $filterInfo,
104 1
            $orderBy,
105 1
            $top,
106
            $skipToken,
107 3
            $sourceEntityInstance
108
        );
109 3
    }
110
    /**
111
     * Gets an entity instance from an entity set identified by a key
112
     * IE: http://host/EntitySet(1L)
113
     * http://host/EntitySet(KeyA=2L,KeyB='someValue')
114
     *
115
     * @param ResourceSet $resourceSet The entity set containing the entity to fetch
116
     * @param KeyDescriptor $keyDescriptor The key identifying the entity to fetch
117 3
     *
118 1
     * @return object|null Returns entity instance if found else null
119 1
     */
120 1
    public function getResourceFromResourceSet(
121 1
        ResourceSet $resourceSet,
122 1
        KeyDescriptor $keyDescriptor = null
123 3
    ) {
124 3
        return $this->getReader()->getResourceFromResourceSet($resourceSet, $keyDescriptor);
125
    }
126
127 3
    /**
128 3
     * Get related resource set for a resource
129 3
     * IE: http://host/EntitySet(1L)/NavigationPropertyToCollection
130
     * http://host/EntitySet?$expand=NavigationPropertyToCollection
131
     *
132
     * @param QueryType $queryType indicates if this is a query for a count, entities, or entities with a count
133
     * @param ResourceSet $sourceResourceSet The entity set containing the source entity
134
     * @param object $sourceEntityInstance The source entity instance.
135
     * @param ResourceSet $targetResourceSet The resource set of containing the target of the navigation property
136
     * @param ResourceProperty $targetProperty The navigation property to retrieve
137
     * @param FilterInfo $filter represents the $filter parameter of the OData query.  NULL if no $filter specified
138
     * @param mixed $orderBy sorted order if we want to get the data in some specific order
139
     * @param int $top number of records which  need to be skip
140
     * @param String $skip value indicating what records to skip
141
     *
142
     * @return QueryResult
143
     *
144
     */
145
    public function getRelatedResourceSet(
146
        QueryType $queryType,
147
        ResourceSet $sourceResourceSet,
148
        $sourceEntityInstance,
149
        ResourceSet $targetResourceSet,
150
        ResourceProperty $targetProperty,
151
        $filter = null,
152
        $orderBy = null,
153
        $top = null,
154
        $skip = null
155
    ) {
156
        return $this->getReader()->getRelatedResourceSet(
157
            $queryType,
158
            $sourceResourceSet,
159
            $sourceEntityInstance,
160
            $targetResourceSet,
161
            $targetProperty,
162
            $filter,
163
            $orderBy,
164
            $top,
165
            $skip
166
        );
167
    }
168
169
    /**
170
     * Gets a related entity instance from an entity set identified by a key
171
     * IE: http://host/EntitySet(1L)/NavigationPropertyToCollection(33)
172
     *
173
     * @param ResourceSet $sourceResourceSet The entity set containing the source entity
174
     * @param object $sourceEntityInstance The source entity instance.
175
     * @param ResourceSet $targetResourceSet The entity set containing the entity to fetch
176
     * @param ResourceProperty $targetProperty The metadata of the target property.
177
     * @param KeyDescriptor $keyDescriptor The key identifying the entity to fetch
178
     *
179
     * @return object|null Returns entity instance if found else null
180
     */
181
    public function getResourceFromRelatedResourceSet(
182
        ResourceSet $sourceResourceSet,
183
        $sourceEntityInstance,
184
        ResourceSet $targetResourceSet,
185
        ResourceProperty $targetProperty,
186
        KeyDescriptor $keyDescriptor
187
    ) {
188
        return $this->getReader()->getResourceFromRelatedResourceSet(
189
            $sourceResourceSet,
190
            $sourceEntityInstance,
191
            $targetResourceSet,
192
            $targetProperty,
193
            $keyDescriptor
194
        );
195
    }
196
197
    /**
198
     * Get related resource for a resource
199 3
     * IE: http://host/EntitySet(1L)/NavigationPropertyToSingleEntity
200
     * http://host/EntitySet?$expand=NavigationPropertyToSingleEntity
201
     *
202
     * @param ResourceSet $sourceResourceSet The entity set containing the source entity
203
     * @param object $sourceEntityInstance The source entity instance.
204
     * @param ResourceSet $targetResourceSet The entity set containing the entity pointed to by the navigation property
205
     * @param ResourceProperty $targetProperty The navigation property to fetch
206
     *
207
     * @return object|null The related resource if found else null
208
     */
209
    public function getRelatedResourceReference(
210 3
        ResourceSet $sourceResourceSet,
211 3
        $sourceEntityInstance,
212
        ResourceSet $targetResourceSet,
213 3
        ResourceProperty $targetProperty
214 3
    ) {
215 3
        return $this->getReader()->getRelatedResourceReference(
216 3
            $sourceResourceSet,
217 3
            $sourceEntityInstance,
218 3
            $targetResourceSet,
219 3
            $targetProperty
220
        );
221 3
    }
222
223
    /**
224
     * Updates a resource
225
     *
226
     * @param ResourceSet      $sourceResourceSet    The entity set containing the source entity
227
     * @param object           $sourceEntityInstance The source entity instance
228
     * @param KeyDescriptor    $keyDescriptor        The key identifying the entity to fetch
229
     * @param object           $data                 The New data for the entity instance.
230
     * @param bool             $shouldUpdate        Should undefined values be updated or reset to default
231
     *
232
     * @return object|null The new resource value if it is assignable or throw exception for null.
233
     */
234
    public function updateResource(
235
        ResourceSet $sourceResourceSet,
236
        $sourceEntityInstance,
237
        KeyDescriptor $keyDescriptor,
238
        $data,
239
        $shouldUpdate = false
240
    ) {
241
        $verb = 'update';
242
        return $this->createUpdateCoreWrapper($sourceResourceSet, $sourceEntityInstance, $data, $verb);
243
    }
244
    /**
245
     * Delete resource from a resource set.
246
     * @param ResourceSet|null $sourceResourceSet
247
     * @param object           $sourceEntityInstance
248
     *
249
     * return bool true if resources sucessfully deteled, otherwise false.
250
     */
251
    public function deleteResource(
252
        ResourceSet $sourceResourceSet,
253
        $sourceEntityInstance
254
    ) {
255
        $verb = 'delete';
256
        if (!($sourceEntityInstance instanceof Model)) {
257
            throw new InvalidArgumentException('Source entity must be an Eloquent model.');
258
        }
259
260
        $class = $sourceResourceSet->getResourceType()->getInstanceType()->getName();
261
        $id = $sourceEntityInstance->getKey();
262
        $name = $sourceEntityInstance->getKeyName();
263
        $data = [$name => $id];
264
265
        $data = $this->createUpdateDeleteCore($sourceEntityInstance, $data, $class, $verb);
266
267
        $success = isset($data['id']);
268
        if ($success) {
269
            return true;
270
        }
271
        throw new ODataException('Target model not successfully deleted', 422);
272
    }
273
    /**
274
     * @param ResourceSet      $resourceSet   The entity set containing the entity to fetch
275
     * @param object           $sourceEntityInstance The source entity instance
276
     * @param object           $data                 The New data for the entity instance.
277
     *
278
     * returns object|null returns the newly created model if sucessful or null if model creation failed.
279
     */
280
    public function createResourceforResourceSet(
281
        ResourceSet $resourceSet,
282
        $sourceEntityInstance,
283
        $data
284
    ) {
285
        $verb = 'create';
286
        return $this->createUpdateCoreWrapper($resourceSet, $sourceEntityInstance, $data, $verb);
287
    }
288
289
    /**
290
     * @param $sourceEntityInstance
291
     * @param $data
292 1
     * @param $class
293
     * @param string $verb
294
     * @return array|mixed
295
     * @throws ODataException
296
     * @throws \POData\Common\InvalidOperationException
297
     */
298
    private function createUpdateDeleteCore($sourceEntityInstance, $data, $class, $verb)
299 1
    {
300 1
        $raw = App::make('metadataControllers');
301
        $map = $raw->getMetadata();
302 1
303
        if (!array_key_exists($class, $map)) {
304 1
            throw new \POData\Common\InvalidOperationException('Controller mapping missing for class '.$class.'.');
305
        }
306 1
        $goal = $raw->getMapping($class, $verb);
307
        if (null == $goal) {
308
            throw new \POData\Common\InvalidOperationException(
309 1
                'Controller mapping missing for '.$verb.' verb on class '.$class.'.'
310
            );
311
        }
312
313
        assert($data != null, "Data must not be null");
314
        if (is_object($data)) {
315
            $data = (array) $data;
316
        }
317
        if (!is_array($data)) {
318 1
            throw \POData\Common\ODataException::createPreConditionFailedError(
319
                'Data not resolvable to key-value array.'
320
            );
321
        }
322 1
323 1
        $controlClass = $goal['controller'];
324 1
        $method = $goal['method'];
325 1
        $paramList = $goal['parameters'];
326 1
        $controller = App::make($controlClass);
327
        $parms = $this->createUpdateDeleteProcessInput($sourceEntityInstance, $data, $paramList);
328 1
        unset($data);
329
330 1
        $result = call_user_func_array(array($controller, $method), $parms);
331 1
332
        return $this->createUpdateDeleteProcessOutput($result);
333
    }
334 1
335
    /**
336
     * Puts an entity instance to entity set identified by a key.
337
     *
338
     * @param ResourceSet $resourceSet The entity set containing the entity to update
339
     * @param KeyDescriptor $keyDescriptor The key identifying the entity to update
340
     * @param $data
341
     *
342
     * @return bool|null Returns result of executing query
343 1
     */
344
    public function putResource(
345
        ResourceSet $resourceSet,
346
        KeyDescriptor $keyDescriptor,
347
        $data
348 1
    ) {
349 1
        // TODO: Implement putResource() method.
350
        return true;
351 1
    }
352
353 1
    /**
354
     * @param ResourceSet $sourceResourceSet
355 1
     * @param $sourceEntityInstance
356
     * @param $data
357
     * @param $verb
358 1
     * @return mixed
359
     * @throws ODataException
360
     * @throws \POData\Common\InvalidOperationException
361
     */
362
    private function createUpdateCoreWrapper(ResourceSet $sourceResourceSet, $sourceEntityInstance, $data, $verb)
363
    {
364
        $lastWord = 'update' == $verb ? 'updated' : 'created';
365
        if (!(null == $sourceEntityInstance || $sourceEntityInstance instanceof Model)) {
366
            throw new InvalidArgumentException('Source entity must either be null or an Eloquent model.');
367
        }
368
369
        $class = $sourceResourceSet->getResourceType()->getInstanceType()->getName();
370 3
        if (!$this->auth->canAuth($this->verbMap[$verb], $class, $sourceEntityInstance)) {
371
            throw new ODataException("Access denied", 403);
372 3
        }
373 3
374
        $data = $this->createUpdateDeleteCore($sourceEntityInstance, $data, $class, $verb);
375 3
376
        $success = isset($data['id']);
377
378 3
        if ($success) {
379 3
            try {
380
                return $class::findOrFail($data['id']);
381
            } catch (\Exception $e) {
382
                throw new ODataException($e->getMessage(), 500);
383
            }
384
        }
385 3
        throw new ODataException('Target model not successfully '.$lastWord, 422);
386
    }
387
388 3
    /**
389 2
     * @param $sourceEntityInstance
390 2
     * @param $data
391 3
     * @param $paramList
392
     * @return array
393
     */
394
    private function createUpdateDeleteProcessInput($sourceEntityInstance, $data, $paramList)
395
    {
396
        $parms = [];
397 3
398 3
        foreach ($paramList as $spec) {
399 3
            $varType = isset($spec['type']) ? $spec['type'] : null;
400 3
            $varName = $spec['name'];
401 3
            if (null == $varType) {
402
                $parms[] = $sourceEntityInstance->$varName;
403 3
                continue;
404 3
            }
405 3
            // TODO: Give this smarts and actively pick up instantiation details
406 3
            $var = new $varType();
407 2
            if ($spec['isRequest']) {
408 2
                $var->setMethod('POST');
409 2
                $var->request = new \Symfony\Component\HttpFoundation\ParameterBag($data);
410 2
            }
411 2
            $parms[] = $var;
412
        }
413
        return $parms;
414
    }
415 2
416
    /**
417
     * @param $result
418 3
     * @return array|mixed
419 3
     * @throws ODataException
420
     */
421 3
    private function createUpdateDeleteProcessOutput($result)
422
    {
423 3
        if (!($result instanceof \Illuminate\Http\JsonResponse)) {
424
            throw ODataException::createInternalServerError('Controller response not well-formed json.');
425
        }
426 3
        $outData = $result->getData();
427 3
        if (is_object($outData)) {
428 3
            $outData = (array)$outData;
429 3
        }
430
431 3
        if (!is_array($outData)) {
432
            throw ODataException::createInternalServerError('Controller response does not have an array.');
433
        }
434 3
        if (!(key_exists('id', $outData) && key_exists('status', $outData) && key_exists('errors', $outData))) {
435
            throw ODataException::createInternalServerError(
436
                'Controller response array missing at least one of id, status and/or errors fields.'
437
            );
438
        }
439 3
        return $outData;
440
    }
441
}
442