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

LaravelQuery::createUpdateCoreWrapper()   B

Complexity

Conditions 6
Paths 8

Size

Total Lines 22
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 6.105

Importance

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