Test Failed
Pull Request — master (#73)
by Alex
02:54
created

LaravelQuery::getRelatedResourceReference()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 14
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

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