Passed
Pull Request — master (#153)
by Alex
07:18
created

LaravelQuery::deleteResource()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 23
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 3.0123

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 23
ccs 8
cts 9
cp 0.8889
rs 9.0856
cc 3
eloc 13
nc 3
nop 2
crap 3.0123
1
<?php
2
3
namespace AlgoWeb\PODataLaravel\Query;
4
5
use AlgoWeb\PODataLaravel\Auth\NullAuthProvider;
6
use AlgoWeb\PODataLaravel\Controllers\MetadataControllerContainer;
7
use AlgoWeb\PODataLaravel\Enums\ActionVerb;
8
use AlgoWeb\PODataLaravel\Interfaces\AuthInterface;
9
use AlgoWeb\PODataLaravel\Providers\MetadataProvider;
10
use Illuminate\Database\Eloquent\Model;
11
use Illuminate\Database\Eloquent\Relations\BelongsTo;
12
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
13
use Illuminate\Database\Eloquent\Relations\HasOneOrMany;
14
use Illuminate\Database\Eloquent\Relations\Relation;
15
use Illuminate\Support\Facades\App;
16
use Illuminate\Support\Facades\DB;
17
use POData\Common\InvalidOperationException;
18
use POData\Common\ODataException;
19
use POData\Providers\Metadata\ResourceProperty;
20
use POData\Providers\Metadata\ResourceSet;
21
use POData\Providers\Query\IQueryProvider;
22
use POData\Providers\Query\QueryResult;
23
use POData\Providers\Query\QueryType;
24
use POData\UriProcessor\QueryProcessor\ExpressionParser\FilterInfo;
25
use POData\UriProcessor\QueryProcessor\OrderByParser\InternalOrderByInfo;
26 5
use POData\UriProcessor\QueryProcessor\SkipTokenParser\SkipTokenInfo;
27
use POData\UriProcessor\ResourcePathProcessor\SegmentParser\KeyDescriptor;
28
use Symfony\Component\Process\Exception\InvalidArgumentException;
29 5
30 5
class LaravelQuery implements IQueryProvider
31 5
{
32 5
    protected $expression;
33
    protected $auth;
34
    protected $reader;
35
    public $queryProviderClassName;
36
    private $verbMap = [];
37
    protected $metadataProvider;
38
    protected $controllerContainer;
39
40
    public function __construct(AuthInterface $auth = null)
41
    {
42
        /* MySQLExpressionProvider();*/
43
        $this->expression = new LaravelExpressionProvider(); //PHPExpressionProvider('expression');
44
        $this->queryProviderClassName = get_class($this);
45
        $this->auth = isset($auth) ? $auth : new NullAuthProvider();
46
        $this->reader = new LaravelReadQuery($this->auth);
47
        $this->verbMap['create'] = ActionVerb::CREATE();
0 ignored issues
show
Bug introduced by
The method CREATE() does not exist on AlgoWeb\PODataLaravel\Enums\ActionVerb. Since you implemented __callStatic, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

47
        /** @scrutinizer ignore-call */ 
48
        $this->verbMap['create'] = ActionVerb::CREATE();
Loading history...
48
        $this->verbMap['update'] = ActionVerb::UPDATE();
0 ignored issues
show
Bug introduced by
The method UPDATE() does not exist on AlgoWeb\PODataLaravel\Enums\ActionVerb. Since you implemented __callStatic, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

48
        /** @scrutinizer ignore-call */ 
49
        $this->verbMap['update'] = ActionVerb::UPDATE();
Loading history...
49
        $this->verbMap['delete'] = ActionVerb::DELETE();
0 ignored issues
show
Bug introduced by
The method DELETE() does not exist on AlgoWeb\PODataLaravel\Enums\ActionVerb. Since you implemented __callStatic, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

49
        /** @scrutinizer ignore-call */ 
50
        $this->verbMap['delete'] = ActionVerb::DELETE();
Loading history...
50
        $this->metadataProvider = new MetadataProvider(App::make('app'));
51
        $this->controllerContainer = App::make('metadataControllers');
52
    }
53
54
    /**
55
     * Indicates if the QueryProvider can handle ordered paging, this means respecting order, skip, and top parameters
56
     * If the query provider can not handle ordered paging, it must return the entire result set and POData will
57
     * perform the ordering and paging.
58
     *
59
     * @return Boolean True if the query provider can handle ordered paging, false if POData should perform the paging
60
     */
61
    public function handlesOrderedPaging()
62
    {
63
        return true;
64
    }
65
66
    /**
67
     * Gets the expression provider used by to compile OData expressions into expression used by this query provider.
68
     *
69
     * @return \POData\Providers\Expression\IExpressionProvider
70 3
     */
71
    public function getExpressionProvider()
72
    {
73
        return $this->expression;
74
    }
75
76
    /**
77
     * Gets the LaravelReadQuery instance used to handle read queries (repetitious, nyet?).
78
     *
79 3
     * @return LaravelReadQuery
80
     */
81
    public function getReader()
82 3
    {
83 1
        return $this->reader;
84 1
    }
85
86 3
    /**
87 3
     * Dig out local copy of POData-Laravel metadata provider.
88 3
     *
89
     * @return MetadataProvider
90 3
     */
91
    public function getMetadataProvider()
92
    {
93
        return $this->metadataProvider;
94
    }
95
96
    /**
97 1
     * Dig out local copy of controller metadata mapping.
98
     *
99
     * @return MetadataControllerContainer
100 3
     */
101 1
    public function getControllerContainer()
102 1
    {
103 3
        assert(null !== $this->controllerContainer, get_class($this->controllerContainer));
104 1
        return $this->controllerContainer;
105 1
    }
106
107 3
    /**
108
     * Gets collection of entities belongs to an entity set
109 3
     * IE: http://host/EntitySet
110
     *  http://host/EntitySet?$skip=10&$top=5&filter=Prop gt Value.
111
     *
112
     * @param QueryType                $queryType            Is this is a query for a count, entities,
113
     *                                                       or entities-with-count?
114
     * @param ResourceSet              $resourceSet          The entity set containing the entities to fetch
115
     * @param FilterInfo|null          $filterInfo           The $filter parameter of the OData query.  NULL if absent
116
     * @param null|InternalOrderByInfo $orderBy              sorted order if we want to get the data in some
117 3
     *                                                       specific order
118 1
     * @param int|null                 $top                  number of records which need to be retrieved
119 1
     * @param int|null                 $skip                 number of records which need to be skipped
120 1
     * @param SkipTokenInfo|null       $skipToken            value indicating what records to skip
121 1
     * @param string[]|null            $eagerLoad            array of relations to eager load
122 1
     * @param Model|Relation|null      $sourceEntityInstance Starting point of query
123 3
     *
124 3
     * @return QueryResult
125
     */
126
    public function getResourceSet(
127 3
        QueryType $queryType,
128 3
        ResourceSet $resourceSet,
129 3
        $filterInfo = null,
130
        $orderBy = null,
131
        $top = null,
132
        $skip = null,
133
        $skipToken = null,
134
        array $eagerLoad = null,
135
        $sourceEntityInstance = null
136
    ) {
137
        $source = $this->unpackSourceEntity($sourceEntityInstance);
138
        return $this->getReader()->getResourceSet(
139
            $queryType,
140
            $resourceSet,
141
            $filterInfo,
142
            $orderBy,
143
            $top,
144
            $skip,
145
            $skipToken,
146
            $eagerLoad,
147
            $source
148
        );
149
    }
150
    /**
151
     * Gets an entity instance from an entity set identified by a key
152
     * IE: http://host/EntitySet(1L)
153
     * http://host/EntitySet(KeyA=2L,KeyB='someValue').
154
     *
155
     * @param ResourceSet        $resourceSet   The entity set containing the entity to fetch
156
     * @param KeyDescriptor|null $keyDescriptor The key identifying the entity to fetch
157
     * @param string[]|null      $eagerLoad     array of relations to eager load
158
     *
159
     * @return Model|null Returns entity instance if found else null
160
     */
161
    public function getResourceFromResourceSet(
162
        ResourceSet $resourceSet,
163
        KeyDescriptor $keyDescriptor = null,
164
        array $eagerLoad = null
165
    ) {
166
        return $this->getReader()->getResourceFromResourceSet($resourceSet, $keyDescriptor, $eagerLoad);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->getReader(...Descriptor, $eagerLoad) also could return the type Illuminate\Database\Eloquent\Model which is incompatible with the return type mandated by POData\Providers\Query\I...sourceFromResourceSet() of object|null.
Loading history...
167
    }
168
169
    /**
170
     * Get related resource set for a resource
171
     * IE: http://host/EntitySet(1L)/NavigationPropertyToCollection
172
     * http://host/EntitySet?$expand=NavigationPropertyToCollection.
173
     *
174
     * @param QueryType          $queryType            Is this is a query for a count, entities, or entities-with-count
175
     * @param ResourceSet        $sourceResourceSet    The entity set containing the source entity
176
     * @param object             $sourceEntityInstance The source entity instance
177
     * @param ResourceSet        $targetResourceSet    The resource set pointed to by the navigation property
178
     * @param ResourceProperty   $targetProperty       The navigation property to retrieve
179
     * @param FilterInfo|null    $filter               The $filter parameter of the OData query.  NULL if none specified
180
     * @param mixed|null         $orderBy              sorted order if we want to get the data in some specific order
181
     * @param int|null           $top                  number of records which need to be retrieved
182
     * @param int|null           $skip                 number of records which need to be skipped
183
     * @param SkipTokenInfo|null $skipToken            value indicating what records to skip
184
     *
185
     * @return QueryResult
186
     */
187
    public function getRelatedResourceSet(
188
        QueryType $queryType,
189
        ResourceSet $sourceResourceSet,
190
        $sourceEntityInstance,
191
        ResourceSet $targetResourceSet,
192
        ResourceProperty $targetProperty,
193
        FilterInfo $filter = null,
194
        $orderBy = null,
195
        $top = null,
196
        $skip = null,
197
        $skipToken = null
198
    ) {
199 3
        $source = $this->unpackSourceEntity($sourceEntityInstance);
200
        return $this->getReader()->getRelatedResourceSet(
201
            $queryType,
202
            $sourceResourceSet,
203
            $source,
204
            $targetResourceSet,
205
            $targetProperty,
206
            $filter,
207
            $orderBy,
208
            $top,
209
            $skip,
210 3
            $skipToken
211 3
        );
212
    }
213 3
214 3
    /**
215 3
     * Gets a related entity instance from an entity set identified by a key
216 3
     * IE: http://host/EntitySet(1L)/NavigationPropertyToCollection(33).
217 3
     *
218 3
     * @param ResourceSet      $sourceResourceSet    The entity set containing the source entity
219 3
     * @param object           $sourceEntityInstance the source entity instance
220
     * @param ResourceSet      $targetResourceSet    The entity set containing the entity to fetch
221 3
     * @param ResourceProperty $targetProperty       the metadata of the target property
222
     * @param KeyDescriptor    $keyDescriptor        The key identifying the entity to fetch
223
     *
224
     * @return Model|null Returns entity instance if found else null
225
     */
226
    public function getResourceFromRelatedResourceSet(
227
        ResourceSet $sourceResourceSet,
228
        $sourceEntityInstance,
229
        ResourceSet $targetResourceSet,
230
        ResourceProperty $targetProperty,
231
        KeyDescriptor $keyDescriptor
232
    ) {
233
        $source = $this->unpackSourceEntity($sourceEntityInstance);
234
        return $this->getReader()->getResourceFromRelatedResourceSet(
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->getReader(...operty, $keyDescriptor) also could return the type Illuminate\Database\Eloquent\Model which is incompatible with the return type mandated by POData\Providers\Query\I...romRelatedResourceSet() of object|null.
Loading history...
235
            $sourceResourceSet,
236
            $source,
237
            $targetResourceSet,
238
            $targetProperty,
239
            $keyDescriptor
240
        );
241
    }
242
243
    /**
244
     * Get related resource for a resource
245
     * IE: http://host/EntitySet(1L)/NavigationPropertyToSingleEntity
246
     * http://host/EntitySet?$expand=NavigationPropertyToSingleEntity.
247
     *
248
     * @param ResourceSet      $sourceResourceSet    The entity set containing the source entity
249
     * @param object           $sourceEntityInstance the source entity instance
250
     * @param ResourceSet      $targetResourceSet    The entity set containing the entity pointed to by the nav property
251
     * @param ResourceProperty $targetProperty       The navigation property to fetch
252
     *
253
     * @return object|null The related resource if found else null
254
     */
255
    public function getRelatedResourceReference(
256
        ResourceSet $sourceResourceSet,
257
        $sourceEntityInstance,
258
        ResourceSet $targetResourceSet,
259
        ResourceProperty $targetProperty
260
    ) {
261
        $source = $this->unpackSourceEntity($sourceEntityInstance);
262
263
        $result = $this->getReader()->getRelatedResourceReference(
264
            $sourceResourceSet,
265
            $source,
266
            $targetResourceSet,
267
            $targetProperty
268
        );
269
        return $result;
270
    }
271
272
    /**
273
     * Updates a resource.
274
     *
275
     * @param ResourceSet   $sourceResourceSet    The entity set containing the source entity
276
     * @param object        $sourceEntityInstance The source entity instance
277
     * @param KeyDescriptor $keyDescriptor        The key identifying the entity to fetch
278
     * @param object        $data                 the New data for the entity instance
279
     * @param bool          $shouldUpdate         Should undefined values be updated or reset to default
280
     *
281
     * @return object|null the new resource value if it is assignable or throw exception for null
282
     */
283
    public function updateResource(
284
        ResourceSet $sourceResourceSet,
285
        $sourceEntityInstance,
286
        KeyDescriptor $keyDescriptor,
287
        $data,
288
        $shouldUpdate = false
289
    ) {
290
        $source = $this->unpackSourceEntity($sourceEntityInstance);
291
292 1
        $verb = 'update';
293
        return $this->createUpdateCoreWrapper($sourceResourceSet, $data, $verb, $source);
294
    }
295
    /**
296
     * Delete resource from a resource set.
297
     *
298
     * @param ResourceSet $sourceResourceSet
299 1
     * @param object      $sourceEntityInstance
300 1
     *
301
     * return bool true if resources sucessfully deteled, otherwise false
302 1
     */
303
    public function deleteResource(
304 1
        ResourceSet $sourceResourceSet,
305
        $sourceEntityInstance
306 1
    ) {
307
        $source = $this->unpackSourceEntity($sourceEntityInstance);
308
309 1
        $verb = 'delete';
310
        if (!($source instanceof Model)) {
311
            throw new InvalidArgumentException('Source entity must be an Eloquent model.');
312
        }
313
314
        $class = $sourceResourceSet->getResourceType()->getInstanceType()->getName();
315
        $id = $source->getKey();
316
        $name = $source->getKeyName();
317
        $data = [$name => $id];
318 1
319
        $data = $this->createUpdateDeleteCore($source, $data, $class, $verb);
320
321
        $success = isset($data['id']);
322 1
        if ($success) {
323 1
            return true;
324 1
        }
325 1
        throw new ODataException('Target model not successfully deleted', 422);
326 1
    }
327
    /**
328 1
     * @param ResourceSet $resourceSet          The entity set containing the entity to fetch
329
     * @param object      $sourceEntityInstance The source entity instance
330 1
     * @param object      $data                 the New data for the entity instance
331 1
     *
332
     * @returns object|null                     returns the newly created model if successful,
333
     *                                          or null if model creation failed.
334 1
     */
335
    public function createResourceforResourceSet(
336
        ResourceSet $resourceSet,
337
        $sourceEntityInstance,
338
        $data
339
    ) {
340
        $source = $this->unpackSourceEntity($sourceEntityInstance);
341
342
        $verb = 'create';
343 1
        return $this->createUpdateCoreWrapper($resourceSet, $data, $verb, $source);
344
    }
345
346
    /**
347
     * @param $sourceEntityInstance
348 1
     * @param $data
349 1
     * @param $class
350
     * @param string $verb
351 1
     *
352
     * @throws ODataException
353 1
     * @throws InvalidOperationException
354
     * @return array|mixed
355 1
     */
356
    private function createUpdateDeleteCore($sourceEntityInstance, $data, $class, $verb)
357
    {
358 1
        $raw = App::make('metadataControllers');
359
        $map = $raw->getMetadata();
360
361
        if (!array_key_exists($class, $map)) {
362
            throw new \POData\Common\InvalidOperationException('Controller mapping missing for class ' . $class . '.');
363
        }
364
        $goal = $raw->getMapping($class, $verb);
365
        if (null == $goal) {
366
            throw new \POData\Common\InvalidOperationException(
367
                'Controller mapping missing for ' . $verb . ' verb on class ' . $class . '.'
368
            );
369
        }
370 3
371
        assert(null !== $data, 'Data must not be null');
372 3
        if (is_object($data)) {
373 3
            $arrayData = (array) $data;
374
        } else {
375 3
            $arrayData = $data;
376
        }
377
        if (!is_array($arrayData)) {
378 3
            throw \POData\Common\ODataException::createPreConditionFailedError(
379 3
                'Data not resolvable to key-value array.'
380
            );
381
        }
382
383
        $controlClass = $goal['controller'];
384
        $method = $goal['method'];
385 3
        $paramList = $goal['parameters'];
386
        $controller = App::make($controlClass);
387
        $parms = $this->createUpdateDeleteProcessInput($arrayData, $paramList, $sourceEntityInstance);
388 3
        unset($data);
389 2
390 2
        $result = call_user_func_array(array($controller, $method), $parms);
391 3
392
        return $this->createUpdateDeleteProcessOutput($result);
393
    }
394
395
    /**
396
     * Puts an entity instance to entity set identified by a key.
397 3
     *
398 3
     * @param ResourceSet   $resourceSet   The entity set containing the entity to update
399 3
     * @param KeyDescriptor $keyDescriptor The key identifying the entity to update
400 3
     * @param $data
401 3
     *
402
     * @return bool|null Returns result of executing query
403 3
     */
404 3
    public function putResource(
405 3
        ResourceSet $resourceSet,
406 3
        KeyDescriptor $keyDescriptor,
407 2
        $data
408 2
    ) {
409 2
        // TODO: Implement putResource() method.
0 ignored issues
show
Coding Style Best Practice introduced by
Comments for TODO tasks are often forgotten in the code; it might be better to use a dedicated issue tracker.
Loading history...
410 2
        return true;
411 2
    }
412
413
    /**
414
     * @param ResourceSet $sourceResourceSet
415 2
     * @param $data
416
     * @param                            $verb
417
     * @param  Model|null                $source
418 3
     * @throws InvalidOperationException
419 3
     * @throws ODataException
420
     * @return mixed
421 3
     */
422
    private function createUpdateCoreWrapper(ResourceSet $sourceResourceSet, $data, $verb, Model $source = null)
423 3
    {
424
        $lastWord = 'update' == $verb ? 'updated' : 'created';
425
        $class = $sourceResourceSet->getResourceType()->getInstanceType()->getName();
426 3
        if (!$this->auth->canAuth($this->verbMap[$verb], $class, $source)) {
427 3
            throw new ODataException('Access denied', 403);
428 3
        }
429 3
430
        $payload = $this->createUpdateDeleteCore($source, $data, $class, $verb);
431 3
432
        $success = isset($payload['id']);
433
434 3
        if ($success) {
435
            try {
436
                return $class::findOrFail($payload['id']);
437
            } catch (\Exception $e) {
438
                throw new ODataException($e->getMessage(), 500);
439 3
            }
440
        }
441
        throw new ODataException('Target model not successfully ' . $lastWord, 422);
442
    }
443
444
    /**
445
     * @param $data
446
     * @param $paramList
447
     * @param  Model|null $sourceEntityInstance
448
     * @return array
449
     */
450
    private function createUpdateDeleteProcessInput($data, $paramList, Model $sourceEntityInstance = null)
451
    {
452
        $parms = [];
453
454
        foreach ($paramList as $spec) {
455
            $varType = isset($spec['type']) ? $spec['type'] : null;
456
            $varName = $spec['name'];
457
            if (null == $varType) {
458
                $parms[] = ('id' == $varName) ? $sourceEntityInstance->getKey() : $sourceEntityInstance->$varName;
0 ignored issues
show
Bug introduced by
The method getKey() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

458
                $parms[] = ('id' == $varName) ? $sourceEntityInstance->/** @scrutinizer ignore-call */ getKey() : $sourceEntityInstance->$varName;

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
459
                continue;
460
            }
461
            // TODO: Give this smarts and actively pick up instantiation details
0 ignored issues
show
Coding Style Best Practice introduced by
Comments for TODO tasks are often forgotten in the code; it might be better to use a dedicated issue tracker.
Loading history...
462
            $var = new $varType();
463
            if ($spec['isRequest']) {
464
                $var->setMethod('POST');
465
                $var->request = new \Symfony\Component\HttpFoundation\ParameterBag($data);
466
            }
467
            $parms[] = $var;
468
        }
469
        return $parms;
470
    }
471
472
    /**
473
     * @param $result
474
     * @throws ODataException
475
     * @return array|mixed
476
     */
477
    private function createUpdateDeleteProcessOutput($result)
478
    {
479
        if (!($result instanceof \Illuminate\Http\JsonResponse)) {
480
            throw ODataException::createInternalServerError('Controller response not well-formed json.');
481
        }
482
        $outData = $result->getData();
483
        if (is_object($outData)) {
484
            $outData = (array) $outData;
485
        }
486
487
        if (!is_array($outData)) {
488
            throw ODataException::createInternalServerError('Controller response does not have an array.');
489
        }
490
        if (!(key_exists('id', $outData) && key_exists('status', $outData) && key_exists('errors', $outData))) {
491
            throw ODataException::createInternalServerError(
492
                'Controller response array missing at least one of id, status and/or errors fields.'
493
            );
494
        }
495
        return $outData;
496
    }
497
498
    /**
499
     * @param $sourceEntityInstance
500
     * @return mixed|null|\object[]
501
     */
502
    private function unpackSourceEntity($sourceEntityInstance)
503
    {
504
        if ($sourceEntityInstance instanceof QueryResult) {
505
            $source = $sourceEntityInstance->results;
506
            $source = (is_array($source)) ? $source[0] : $source;
507
            return $source;
508
        }
509
        return $sourceEntityInstance;
510
    }
511
512
    /**
513
     * Create multiple new resources in a resource set.
514
     *
515
     * @param ResourceSet $sourceResourceSet The entity set containing the entity to fetch
516
     * @param object[]    $data              The new data for the entity instance
517
     *
518
     * @return object[] returns the newly created model if successful, or throws an exception if model creation failed
519
     * @throw  \Exception
520
     */
521
    public function createBulkResourceforResourceSet(
522
        ResourceSet $sourceResourceSet,
523
        array $data
524
    ) {
525
        $verbName = 'bulkCreate';
526
        $mapping = $this->getOptionalVerbMapping($sourceResourceSet, $verbName);
527
528
        $result = [];
529
        try {
530
            DB::beginTransaction();
531
            if (null === $mapping) {
532
                foreach ($data as $newItem) {
533
                    $raw = $this->createResourceforResourceSet($sourceResourceSet, null, $newItem);
534
                    if (null === $raw) {
535
                        throw new \Exception('Bulk model creation failed');
536
                    }
537
                    $result[] = $raw;
538
                }
539
            } else {
540
                $keyDescriptor = null;
541
                $pastVerb = 'created';
542
                $result = $this->processBulkCustom($sourceResourceSet, $data, $mapping, $pastVerb, $keyDescriptor);
543
            }
544
            DB::commit();
545
        } catch (\Exception $e) {
546
            DB::rollBack();
547
            throw $e;
548
        }
549
        return $result;
550
    }
551
552
    /**
553
     * Updates a group of resources in a resource set.
554
     *
555
     * @param ResourceSet     $sourceResourceSet    The entity set containing the source entity
556
     * @param object          $sourceEntityInstance The source entity instance
557
     * @param KeyDescriptor[] $keyDescriptor        The key identifying the entity to fetch
558
     * @param object[]        $data                 The new data for the entity instances
559
     * @param bool            $shouldUpdate         Should undefined values be updated or reset to default
560
     *
561
     * @return object[] the new resource value if it is assignable, or throw exception for null
562
     * @throw  \Exception
563
     */
564
    public function updateBulkResource(
565
        ResourceSet $sourceResourceSet,
566
        $sourceEntityInstance,
567
        array $keyDescriptor,
568
        array $data,
569
        $shouldUpdate = false
570
    ) {
571
        $numKeys = count($keyDescriptor);
572
        if ($numKeys !== count($data)) {
573
            $msg = 'Key descriptor array and data array must be same length';
574
            throw new \InvalidArgumentException($msg);
575
        }
576
        $result = [];
577
578
        $verbName = 'bulkUpdate';
579
        $mapping = $this->getOptionalVerbMapping($sourceResourceSet, $verbName);
580
581
        try {
582
            DB::beginTransaction();
583
            if (null === $mapping) {
584
                for ($i = 0; $i < $numKeys; $i++) {
585
                    $newItem = $data[$i];
586
                    $newKey = $keyDescriptor[$i];
587
                    $raw = $this->updateResource($sourceResourceSet, $sourceEntityInstance, $newKey, $newItem);
588
                    if (null === $raw) {
589
                        throw new \Exception('Bulk model update failed');
590
                    }
591
                    $result[] = $raw;
592
                }
593
            } else {
594
                $pastVerb = 'updated';
595
                $result = $this->processBulkCustom($sourceResourceSet, $data, $mapping, $pastVerb, $keyDescriptor);
596
            }
597
            DB::commit();
598
        } catch (\Exception $e) {
599
            DB::rollBack();
600
            throw $e;
601
        }
602
        return $result;
603
    }
604
605
    /**
606
     * Attaches child model to parent model.
607
     *
608
     * @param ResourceSet $sourceResourceSet
609
     * @param object      $sourceEntityInstance
610
     * @param ResourceSet $targetResourceSet
611
     * @param object      $targetEntityInstance
612
     * @param $navPropName
613
     *
614
     * @return bool
615
     */
616
    public function hookSingleModel(
617
        ResourceSet $sourceResourceSet,
618
        $sourceEntityInstance,
619
        ResourceSet $targetResourceSet,
620
        $targetEntityInstance,
621
        $navPropName
622
    ) {
623
        $relation = $this->isModelHookInputsOk($sourceEntityInstance, $targetEntityInstance, $navPropName);
624
        assert(
625
            $sourceEntityInstance instanceof Model && $targetEntityInstance instanceof Model,
626
            'Both input entities must be Eloquent models'
627
        );
628
        // in case the fake 'PrimaryKey' attribute got set inbound for a polymorphic-affected model, flatten it now
629
        unset($targetEntityInstance->PrimaryKey);
630
631
        if ($relation instanceof BelongsTo) {
632
            $relation->associate($targetEntityInstance);
633
        } elseif ($relation instanceof BelongsToMany) {
634
            $relation->attach($targetEntityInstance);
635
        } elseif ($relation instanceof HasOneOrMany) {
636
            $relation->save($targetEntityInstance);
637
        }
638
        return true;
639
    }
640
641
    /**
642
     * Removes child model from parent model.
643
     *
644
     * @param ResourceSet $sourceResourceSet
645
     * @param object      $sourceEntityInstance
646
     * @param ResourceSet $targetResourceSet
647
     * @param object      $targetEntityInstance
648
     * @param $navPropName
649
     *
650
     * @return bool
651
     */
652
    public function unhookSingleModel(
653
        ResourceSet $sourceResourceSet,
654
        $sourceEntityInstance,
655
        ResourceSet $targetResourceSet,
656
        $targetEntityInstance,
657
        $navPropName
658
    ) {
659
        $relation = $this->isModelHookInputsOk($sourceEntityInstance, $targetEntityInstance, $navPropName);
660
        assert(
661
            $sourceEntityInstance instanceof Model && $targetEntityInstance instanceof Model,
662
            'Both input entities must be Eloquent models'
663
        );
664
        // in case the fake 'PrimaryKey' attribute got set inbound for a polymorphic-affected model, flatten it now
665
        unset($targetEntityInstance->PrimaryKey);
666
667
        if ($relation instanceof BelongsTo) {
668
            $relation->dissociate();
669
        } elseif ($relation instanceof BelongsToMany) {
670
            $relation->detach($targetEntityInstance);
671
        } elseif ($relation instanceof HasOneOrMany) {
672
            // dig up inverse property name, so we can pass it to unhookSingleModel with source and target elements
673
            // swapped
674
            $otherPropName = $this->getMetadataProvider()
675
                ->resolveReverseProperty($sourceEntityInstance, $navPropName);
676
            if (null === $otherPropName) {
677
                $srcClass = get_class($sourceEntityInstance);
678
                $msg = 'Bad navigation property, ' . $navPropName . ', on source model ' . $srcClass;
679
                throw new \InvalidArgumentException($msg);
680
            }
681
            $this->unhookSingleModel(
682
                $targetResourceSet,
683
                $targetEntityInstance,
684
                $sourceResourceSet,
685
                $sourceEntityInstance,
686
                $otherPropName
687
            );
688
        }
689
        return true;
690
    }
691
692
    /**
693
     * @param $sourceEntityInstance
694
     * @param $targetEntityInstance
695
     * @param $navPropName
696
     * @throws \InvalidArgumentException
697
     * @return Relation
698
     */
699
    protected function isModelHookInputsOk($sourceEntityInstance, $targetEntityInstance, $navPropName)
700
    {
701
        if (!$sourceEntityInstance instanceof Model || !$targetEntityInstance instanceof Model) {
702
            $msg = 'Both source and target must be Eloquent models';
703
            throw new \InvalidArgumentException($msg);
704
        }
705
        $relation = $sourceEntityInstance->$navPropName();
706
        if (!$relation instanceof Relation) {
707
            $msg = 'Navigation property must be an Eloquent relation';
708
            throw new \InvalidArgumentException($msg);
709
        }
710
        $targType = $relation->getRelated();
711
        if (!$targetEntityInstance instanceof $targType) {
712
            $msg = 'Target instance must be of type compatible with relation declared in method ' . $navPropName;
713
            throw new \InvalidArgumentException($msg);
714
        }
715
        return $relation;
716
    }
717
718
    /**
719
     * @param ResourceSet $sourceResourceSet
720
     * @param $verbName
721
     * @return array|null
722
     */
723
    protected function getOptionalVerbMapping(ResourceSet $sourceResourceSet, $verbName)
724
    {
725
        // dig up target class name
726
        $type = $sourceResourceSet->getResourceType()->getInstanceType();
727
        assert($type instanceof \ReflectionClass, get_class($type));
728
        $modelName = $type->getName();
729
        return $this->getControllerContainer()->getMapping($modelName, $verbName);
730
    }
731
732
    /**
733
     * Prepare bulk request from supplied data.  If $keyDescriptors is not null, its elements are assumed to
734
     * correspond 1-1 to those in $data.
735
     *
736
     * @param $paramList
737
     * @param array                $data
738
     * @param KeyDescriptor[]|null $keyDescriptors
739
     */
740
    protected function prepareBulkRequestInput($paramList, array $data, array $keyDescriptors = null)
741
    {
742
        $parms = [];
743
        $isCreate = null === $keyDescriptors;
744
745
        // for moment, we're only processing parameters of type Request
746
        foreach ($paramList as $spec) {
747
            $varType = isset($spec['type']) ? $spec['type'] : null;
748
            if (null !== $varType) {
749
                $var = new $varType();
750
                if ($spec['isRequest']) {
751
                    $var->setMethod($isCreate ? 'POST' : 'PUT');
752
                    $bulkData = ['data' => $data];
753
                    if (null !== $keyDescriptors) {
754
                        $keys = [];
755
                        foreach ($keyDescriptors as $desc) {
756
                            assert($desc instanceof KeyDescriptor, get_class($desc));
757
                            $rawPayload = $desc->getNamedValues();
758
                            $keyPayload = [];
759
                            foreach ($rawPayload as $keyName => $keyVal) {
760
                                $keyPayload[$keyName] = $keyVal[0];
761
                            }
762
                            $keys[] = $keyPayload;
763
                        }
764
                        $bulkData['keys'] = $keys;
765
                    }
766
                    $var->request = new \Symfony\Component\HttpFoundation\ParameterBag($bulkData);
767
                }
768
                $parms[] = $var;
769
            }
770
        }
771
        return $parms;
772
    }
773
774
    /**
775
     * @param ResourceSet $sourceResourceSet
776
     * @param array       $data
777
     * @param $mapping
778
     * @param $pastVerb
779
     * @param  KeyDescriptor[]|null $keyDescriptor
780
     * @throws ODataException
781
     * @return array
782
     */
783
    protected function processBulkCustom(
784
        ResourceSet $sourceResourceSet,
785
        array $data,
786
        $mapping,
787
        $pastVerb,
788
        array $keyDescriptor = null
789
    ) {
790
        $class = $sourceResourceSet->getResourceType()->getInstanceType()->getName();
791
        $controlClass = $mapping['controller'];
792
        $method = $mapping['method'];
793
        $paramList = $mapping['parameters'];
794
        $controller = App::make($controlClass);
795
        $parms = $this->prepareBulkRequestInput($paramList, $data, $keyDescriptor);
796
797
        $callResult = call_user_func_array(array($controller, $method), $parms);
798
        $payload = $this->createUpdateDeleteProcessOutput($callResult);
799
        $success = isset($payload['id']) && is_array($payload['id']);
800
801
        if ($success) {
802
            try {
803
                // return array of Model objects underlying collection returned by findMany
804
                $result = $class::findMany($payload['id'])->flatten()->all();
805
                return $result;
806
            } catch (\Exception $e) {
807
                throw new ODataException($e->getMessage(), 500);
808
            }
809
        } else {
810
            $msg = 'Target models not successfully ' . $pastVerb;
811
            throw new ODataException($msg, 422);
812
        }
813
    }
814
815
    /**
816
     * Start database transaction.
817
     */
818
    public function startTransaction()
819
    {
820
        DB::beginTransaction();
821
    }
822
823
    /**
824
     * Commit database transaction.
825
     */
826
    public function commitTransaction()
827
    {
828
        DB::commit();
829
    }
830
831
    /**
832
     * Abort database transaction.
833
     */
834
    public function rollBackTransaction()
835
    {
836
        DB::rollBack();
837
    }
838
}
839