Passed
Pull Request — master (#182)
by Alex
07:12
created

LaravelQuery::getMetadataProvider()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 1
c 0
b 0
f 0
dl 0
loc 3
ccs 0
cts 2
cp 0
rs 10
cc 1
nc 1
nop 0
crap 2
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\Relation;
12
use Illuminate\Support\Facades\App;
13
use Illuminate\Support\Facades\DB;
14
use POData\Common\InvalidOperationException;
15
use POData\Common\ODataException;
16
use POData\Providers\Metadata\ResourceProperty;
17
use POData\Providers\Metadata\ResourceSet;
18
use POData\Providers\Query\IQueryProvider;
19
use POData\Providers\Query\QueryResult;
20
use POData\Providers\Query\QueryType;
21
use POData\UriProcessor\QueryProcessor\ExpressionParser\FilterInfo;
22
use POData\UriProcessor\QueryProcessor\OrderByParser\InternalOrderByInfo;
23
use POData\UriProcessor\QueryProcessor\SkipTokenParser\SkipTokenInfo;
24
use POData\UriProcessor\ResourcePathProcessor\SegmentParser\KeyDescriptor;
25
use Symfony\Component\Process\Exception\InvalidArgumentException;
26 5
27
class LaravelQuery extends LaravelBaseQuery implements IQueryProvider
28
{
29 5
    protected $expression;
30 5
    protected $reader;
31 5
    protected $modelHook;
32 5
    protected $bulk;
33
    public $queryProviderClassName;
34
    private $verbMap = [];
35
    protected $controllerContainer;
36
    private static $touchList = [];
37
    private static $inBatch;
38
39
    public function __construct(AuthInterface $auth = null)
40
    {
41
        parent::__construct($auth);
42
        /* MySQLExpressionProvider();*/
43
        $this->expression = new LaravelExpressionProvider(); //PHPExpressionProvider('expression');
44
        $this->queryProviderClassName = get_class($this);
45
        $this->reader = new LaravelReadQuery($this->getAuth());
46
        $this->modelHook = new LaravelHookQuery($this->getAuth());
47
        $this->bulk = new LaravelBulkQuery($this, $this->getAuth());
48
49
        $this->controllerContainer = App::make('metadataControllers');
50
        self::$touchList = [];
51
        self::$inBatch = false;
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
     * Gets the LaravelHookQuery instance used to handle hook/unhook queries (repetitious, nyet?).
88 3
     *
89
     * @return LaravelHookQuery
90 3
     */
91
    public function getModelHook()
92
    {
93
        return $this->modelHook;
94
    }
95
96
    /**
97 1
     * Gets the LaravelBulkQuery instance used to handle bulk queries (repetitious, nyet?).
98
     *
99
     * @return LaravelBulkQuery
100 3
     */
101 1
    public function getBulk()
102 1
    {
103 3
        return $this->bulk;
104 1
    }
105 1
106
    /**
107 3
     * Dig out local copy of controller metadata mapping.
108
     *
109 3
     * @return MetadataControllerContainer
110
     * @throws InvalidOperationException
111
     */
112
    public function getControllerContainer()
113
    {
114
        if (null === $this->controllerContainer) {
115
            throw new InvalidOperationException('Controller container must not be null');
116
        }
117 3
        return $this->controllerContainer;
118 1
    }
119 1
120 1
    public function getVerbMap()
121 1
    {
122 1
        if (0 == count($this->verbMap)) {
123 3
            $this->verbMap['create'] = ActionVerb::CREATE();
124 3
            $this->verbMap['update'] = ActionVerb::UPDATE();
125
            $this->verbMap['delete'] = ActionVerb::DELETE();
126
        }
127 3
        return $this->verbMap;
128 3
    }
129 3
130
    /**
131
     * Gets collection of entities belongs to an entity set
132
     * IE: http://host/EntitySet
133
     *  http://host/EntitySet?$skip=10&$top=5&filter=Prop gt Value.
134
     *
135
     * @param QueryType                $queryType            Is this is a query for a count, entities,
136
     *                                                       or entities-with-count?
137
     * @param ResourceSet              $resourceSet          The entity set containing the entities to fetch
138
     * @param FilterInfo|null          $filterInfo           The $filter parameter of the OData query.  NULL if absent
139
     * @param null|InternalOrderByInfo $orderBy              sorted order if we want to get the data in some
140
     *                                                       specific order
141
     * @param int|null                 $top                  number of records which need to be retrieved
142
     * @param int|null                 $skip                 number of records which need to be skipped
143
     * @param SkipTokenInfo|null       $skipToken            value indicating what records to skip
144
     * @param string[]|null            $eagerLoad            array of relations to eager load
145
     * @param Model|Relation|null      $sourceEntityInstance Starting point of query
146
     *
147
     * @return QueryResult
148
     * @throws InvalidOperationException
149
     * @throws ODataException
150
     * @throws \ReflectionException
151
     */
152
    public function getResourceSet(
153
        QueryType $queryType,
154
        ResourceSet $resourceSet,
155
        $filterInfo = null,
156
        $orderBy = null,
157
        $top = null,
158
        $skip = null,
159
        $skipToken = null,
160
        array $eagerLoad = null,
161
        $sourceEntityInstance = null
162
    ) {
163
        /** @var Model|Relation|null $source */
164
        $source = $this->unpackSourceEntity($sourceEntityInstance);
165
        return $this->getReader()->getResourceSet(
166
            $queryType,
167
            $resourceSet,
168
            $filterInfo,
169
            $orderBy,
170
            $top,
171
            $skip,
172
            $skipToken,
173
            $eagerLoad,
174
            $source
175
        );
176
    }
177
    /**
178
     * Gets an entity instance from an entity set identified by a key
179
     * IE: http://host/EntitySet(1L)
180
     * http://host/EntitySet(KeyA=2L,KeyB='someValue').
181
     *
182
     * @param ResourceSet        $resourceSet   The entity set containing the entity to fetch
183
     * @param KeyDescriptor|null $keyDescriptor The key identifying the entity to fetch
184
     * @param string[]|null      $eagerLoad     array of relations to eager load
185
     *
186
     * @return Model|null Returns entity instance if found else null
187
     * @throws \Exception
188
     */
189
    public function getResourceFromResourceSet(
190
        ResourceSet $resourceSet,
191
        KeyDescriptor $keyDescriptor = null,
192
        array $eagerLoad = null
193
    ) {
194
        return $this->getReader()->getResourceFromResourceSet($resourceSet, $keyDescriptor, $eagerLoad);
195
    }
196
197
    /**
198
     * Get related resource set for a resource
199 3
     * IE: http://host/EntitySet(1L)/NavigationPropertyToCollection
200
     * http://host/EntitySet?$expand=NavigationPropertyToCollection.
201
     *
202
     * @param QueryType          $queryType            Is this is a query for a count, entities, or entities-with-count
203
     * @param ResourceSet        $sourceResourceSet    The entity set containing the source entity
204
     * @param object             $sourceEntityInstance The source entity instance
205
     * @param ResourceSet        $targetResourceSet    The resource set pointed to by the navigation property
206
     * @param ResourceProperty   $targetProperty       The navigation property to retrieve
207
     * @param FilterInfo|null    $filter               The $filter parameter of the OData query.  NULL if none specified
208
     * @param mixed|null         $orderBy              sorted order if we want to get the data in some specific order
209
     * @param int|null           $top                  number of records which need to be retrieved
210 3
     * @param int|null           $skip                 number of records which need to be skipped
211 3
     * @param SkipTokenInfo|null $skipToken            value indicating what records to skip
212
     *
213 3
     * @return QueryResult
214 3
     * @throws \Exception
215 3
     */
216 3
    public function getRelatedResourceSet(
217 3
        QueryType $queryType,
218 3
        ResourceSet $sourceResourceSet,
219 3
        $sourceEntityInstance,
220
        ResourceSet $targetResourceSet,
221 3
        ResourceProperty $targetProperty,
222
        FilterInfo $filter = null,
223
        $orderBy = null,
224
        $top = null,
225
        $skip = null,
226
        $skipToken = null
227
    ) {
228
        $source = $this->unpackSourceEntity($sourceEntityInstance);
229
        return $this->getReader()->getRelatedResourceSet(
230
            $queryType,
231
            $sourceResourceSet,
232
            $source,
233
            $targetResourceSet,
234
            $targetProperty,
235
            $filter,
236
            $orderBy,
237
            $top,
238
            $skip,
239
            $skipToken
240
        );
241
    }
242
243
    /**
244
     * Gets a related entity instance from an entity set identified by a key
245
     * IE: http://host/EntitySet(1L)/NavigationPropertyToCollection(33).
246
     *
247
     * @param ResourceSet      $sourceResourceSet    The entity set containing the source entity
248
     * @param object           $sourceEntityInstance the source entity instance
249
     * @param ResourceSet      $targetResourceSet    The entity set containing the entity to fetch
250
     * @param ResourceProperty $targetProperty       the metadata of the target property
251
     * @param KeyDescriptor    $keyDescriptor        The key identifying the entity to fetch
252
     *
253
     * @return Model|null Returns entity instance if found else null
254
     * @throws \Exception
255
     */
256
    public function getResourceFromRelatedResourceSet(
257
        ResourceSet $sourceResourceSet,
258
        $sourceEntityInstance,
259
        ResourceSet $targetResourceSet,
260
        ResourceProperty $targetProperty,
261
        KeyDescriptor $keyDescriptor
262
    ) {
263
        $source = $this->unpackSourceEntity($sourceEntityInstance);
264
        return $this->getReader()->getResourceFromRelatedResourceSet(
265
            $sourceResourceSet,
266
            $source,
267
            $targetResourceSet,
268
            $targetProperty,
269
            $keyDescriptor
270
        );
271
    }
272
273
    /**
274
     * Get related resource for a resource
275
     * IE: http://host/EntitySet(1L)/NavigationPropertyToSingleEntity
276
     * http://host/EntitySet?$expand=NavigationPropertyToSingleEntity.
277
     *
278
     * @param ResourceSet      $sourceResourceSet    The entity set containing the source entity
279
     * @param object           $sourceEntityInstance the source entity instance
280
     * @param ResourceSet      $targetResourceSet    The entity set containing the entity pointed to by the nav property
281
     * @param ResourceProperty $targetProperty       The navigation property to fetch
282
     *
283
     * @return Model|null The related resource if found else null
284
     * @throws \Exception
285
     */
286
    public function getRelatedResourceReference(
287
        ResourceSet $sourceResourceSet,
288
        $sourceEntityInstance,
289
        ResourceSet $targetResourceSet,
290
        ResourceProperty $targetProperty
291
    ) {
292 1
        $source = $this->unpackSourceEntity($sourceEntityInstance);
293
294
        $result = $this->getReader()->getRelatedResourceReference(
295
            $sourceResourceSet,
296
            $source,
297
            $targetResourceSet,
298
            $targetProperty
299 1
        );
300 1
        return $result;
301
    }
302 1
303
    /**
304 1
     * Updates a resource.
305
     *
306 1
     * @param ResourceSet       $sourceResourceSet    The entity set containing the source entity
307
     * @param Model|Relation    $sourceEntityInstance The source entity instance
308
     * @param KeyDescriptor     $keyDescriptor        The key identifying the entity to fetch
309 1
     * @param object            $data                 the New data for the entity instance
310
     * @param bool              $shouldUpdate         Should undefined values be updated or reset to default
311
     *
312
     * @return Model|null the new resource value if it is assignable or throw exception for null
313
     * @throws \Exception
314
     */
315
    public function updateResource(
316
        ResourceSet $sourceResourceSet,
317
        $sourceEntityInstance,
318 1
        KeyDescriptor $keyDescriptor,
319
        $data,
320
        $shouldUpdate = false
321
    ) {
322 1
        $verb = 'update';
323 1
        return $this->createUpdateMainWrapper($sourceResourceSet, $sourceEntityInstance, $data, $verb);
324 1
    }
325 1
    /**
326 1
     * Delete resource from a resource set.
327
     *
328 1
     * @param ResourceSet $sourceResourceSet
329
     * @param object      $sourceEntityInstance
330 1
     *
331 1
     * @return bool true if resources sucessfully deteled, otherwise false
332
     * @throws \Exception
333
     */
334 1
    public function deleteResource(
335
        ResourceSet $sourceResourceSet,
336
        $sourceEntityInstance
337
    ) {
338
        $source = $this->unpackSourceEntity($sourceEntityInstance);
339
340
        $verb = 'delete';
341
        if (!($source instanceof Model)) {
342
            throw new InvalidArgumentException('Source entity must be an Eloquent model.');
343 1
        }
344
345
        $class = $sourceResourceSet->getResourceType()->getInstanceType()->getName();
346
        $id = $source->getKey();
347
        $name = $source->getKeyName();
348 1
        $data = [$name => $id];
349 1
350
        $data = $this->createUpdateDeleteCore($source, $data, $class, $verb);
351 1
352
        $success = isset($data['id']);
353 1
        if ($success) {
354
            return true;
355 1
        }
356
        throw new ODataException('Target model not successfully deleted', 422);
357
    }
358 1
    /**
359
     * @param ResourceSet     $resourceSet          The entity set containing the entity to fetch
360
     * @param Model|Relation  $sourceEntityInstance The source entity instance
361
     * @param object          $data                 the New data for the entity instance
362
     *
363
     * @return Model|null                           returns the newly created model if successful,
364
     *                                              or null if model creation failed.
365
     * @throws \Exception
366
     */
367
    public function createResourceforResourceSet(
368
        ResourceSet $resourceSet,
369
        $sourceEntityInstance,
370 3
        $data
371
    ) {
372 3
        $verb = 'create';
373 3
        return $this->createUpdateMainWrapper($resourceSet, $sourceEntityInstance, $data, $verb);
374
    }
375 3
376
    /**
377
     * @param $sourceEntityInstance
378 3
     * @param $data
379 3
     * @param $class
380
     * @param string $verb
381
     *
382
     * @throws ODataException
383
     * @throws InvalidOperationException
384
     * @return array|mixed
385 3
     */
386
    private function createUpdateDeleteCore($sourceEntityInstance, $data, $class, $verb)
387
    {
388 3
        $raw = App::make('metadataControllers');
389 2
        $map = $raw->getMetadata();
390 2
391 3
        if (!array_key_exists($class, $map)) {
392
            throw new InvalidOperationException('Controller mapping missing for class ' . $class . '.');
393
        }
394
        $goal = $raw->getMapping($class, $verb);
395
        if (null == $goal) {
396
            throw new InvalidOperationException(
397 3
                'Controller mapping missing for ' . $verb . ' verb on class ' . $class . '.'
398 3
            );
399 3
        }
400 3
401 3
        if (null === $data) {
402
            $msg = 'Data must not be null';
403 3
            throw new InvalidOperationException($msg);
404 3
        }
405 3
        if (is_object($data)) {
406 3
            $arrayData = (array) $data;
407 2
        } else {
408 2
            $arrayData = $data;
409 2
        }
410 2
        if (!is_array($arrayData)) {
411 2
            throw ODataException::createPreConditionFailedError(
412
                'Data not resolvable to key-value array.'
413
            );
414
        }
415 2
416
        $controlClass = $goal['controller'];
417
        $method = $goal['method'];
418 3
        $paramList = $goal['parameters'];
419 3
        $controller = App::make($controlClass);
420
        $parms = $this->createUpdateDeleteProcessInput($arrayData, $paramList, $sourceEntityInstance);
421 3
        unset($data);
422
423 3
        $result = call_user_func_array(array($controller, $method), $parms);
424
425
        return $this->createUpdateDeleteProcessOutput($result);
426 3
    }
427 3
428 3
    /**
429 3
     * Puts an entity instance to entity set identified by a key.
430
     *
431 3
     * @param ResourceSet   $resourceSet   The entity set containing the entity to update
432
     * @param KeyDescriptor $keyDescriptor The key identifying the entity to update
433
     * @param $data
434 3
     *
435
     * @return bool|null Returns result of executing query
436
     */
437
    public function putResource(
438
        ResourceSet $resourceSet,
439 3
        KeyDescriptor $keyDescriptor,
440
        $data
441
    ) {
442
        // TODO: Implement putResource() method.
443
        return true;
444
    }
445
446
    /**
447
     * @param ResourceSet $sourceResourceSet
448
     * @param $data
449
     * @param                            $verb
450
     * @param  Model|null                $source
451
     * @throws InvalidOperationException
452
     * @throws ODataException
453
     * @throws \Exception
454
     * @return Model|null
455
     */
456
    protected function createUpdateCoreWrapper(ResourceSet $sourceResourceSet, $data, $verb, Model $source = null)
457
    {
458
        $lastWord = 'update' == $verb ? 'updated' : 'created';
459
        $class = $sourceResourceSet->getResourceType()->getInstanceType()->getName();
460
        if (!$this->getAuth()->canAuth($this->getVerbMap()[$verb], $class, $source)) {
461
            throw new ODataException('Access denied', 403);
462
        }
463
464
        $payload = $this->createUpdateDeleteCore($source, $data, $class, $verb);
465
466
        $success = isset($payload['id']);
467
468
        if ($success) {
469
            try {
470
                return $class::findOrFail($payload['id']);
471
            } catch (\Exception $e) {
472
                throw new ODataException($e->getMessage(), 500);
473
            }
474
        }
475
        throw new ODataException('Target model not successfully ' . $lastWord, 422);
476
    }
477
478
    /**
479
     * @param $data
480
     * @param $paramList
481
     * @param Model|null $sourceEntityInstance
482
     * @return array
483
     */
484
    protected function createUpdateDeleteProcessInput($data, $paramList, Model $sourceEntityInstance)
485
    {
486
        $parms = [];
487
488
        foreach ($paramList as $spec) {
489
            $varType = isset($spec['type']) ? $spec['type'] : null;
490
            $varName = $spec['name'];
491
            if (null == $varType) {
492
                $parms[] = ('id' == $varName) ? $sourceEntityInstance->getKey() : $sourceEntityInstance->$varName;
493
                continue;
494
            }
495
            // TODO: Give this smarts and actively pick up instantiation details
496
            $var = new $varType();
497
            if ($spec['isRequest']) {
498
                $var->setMethod('POST');
499
                $var->request = new \Symfony\Component\HttpFoundation\ParameterBag($data);
500
            }
501
            $parms[] = $var;
502
        }
503
        return $parms;
504
    }
505
506
    /**
507
     * @param $result
508
     * @throws ODataException
509
     * @return array|mixed
510
     */
511
    private function createUpdateDeleteProcessOutput($result)
512
    {
513
        if (!($result instanceof \Illuminate\Http\JsonResponse)) {
514
            throw ODataException::createInternalServerError('Controller response not well-formed json.');
515
        }
516
        $outData = $result->getData();
517
        if (is_object($outData)) {
518
            $outData = (array) $outData;
519
        }
520
521
        if (!is_array($outData)) {
522
            throw ODataException::createInternalServerError('Controller response does not have an array.');
523
        }
524
        if (!(key_exists('id', $outData) && key_exists('status', $outData) && key_exists('errors', $outData))) {
525
            throw ODataException::createInternalServerError(
526
                'Controller response array missing at least one of id, status and/or errors fields.'
527
            );
528
        }
529
        return $outData;
530
    }
531
532
    /**
533
     * @param $sourceEntityInstance
534
     * @return mixed|null|\object[]
535
     */
536
    private function unpackSourceEntity($sourceEntityInstance)
537
    {
538
        if ($sourceEntityInstance instanceof QueryResult) {
539
            $source = $sourceEntityInstance->results;
540
            $source = (is_array($source)) ? $source[0] : $source;
541
            return $source;
542
        }
543
        return $sourceEntityInstance;
544
    }
545
546
    /**
547
     * Create multiple new resources in a resource set.
548
     *
549
     * @param ResourceSet $sourceResourceSet The entity set containing the entity to fetch
550
     * @param object[]    $data              The new data for the entity instance
551
     *
552
     * @return object[] returns the newly created model if successful, or throws an exception if model creation failed
553
     * @throws InvalidOperationException
554
     * @throws \ReflectionException
555
     * @throw  \Exception
556
     */
557
    public function createBulkResourceforResourceSet(
558
        ResourceSet $sourceResourceSet,
559
        array $data
560
    ) {
561
        return $this->getBulk()->createBulkResourceForResourceSet($sourceResourceSet, $data);
562
    }
563
564
    /**
565
     * Updates a group of resources in a resource set.
566
     *
567
     * @param ResourceSet     $sourceResourceSet    The entity set containing the source entity
568
     * @param Model|Relation  $sourceEntityInstance The source entity instance
569
     * @param KeyDescriptor[] $keyDescriptor        The key identifying the entity to fetch
570
     * @param object[]        $data                 The new data for the entity instances
571
     * @param bool            $shouldUpdate         Should undefined values be updated or reset to default
572
     *
573
     * @return object[] the new resource value if it is assignable, or throw exception for null
574
     * @throw  \Exception
575
     * @throws InvalidOperationException
576
     */
577
    public function updateBulkResource(
578
        ResourceSet $sourceResourceSet,
579
        $sourceEntityInstance,
580
        array $keyDescriptor,
581
        array $data,
582
        $shouldUpdate = false
583
    ) {
584
        return $this->getBulk()
585
            ->updateBulkResource(
586
                $sourceResourceSet,
587
                $sourceEntityInstance,
588
                $keyDescriptor,
589
                $data,
590
                $shouldUpdate
591
            );
592
    }
593
594
    /**
595
     * Attaches child model to parent model.
596
     *
597
     * @param ResourceSet $sourceResourceSet
598
     * @param Model       $sourceEntityInstance
599
     * @param ResourceSet $targetResourceSet
600
     * @param Model       $targetEntityInstance
601
     * @param $navPropName
602
     *
603
     * @return bool
604
     * @throws InvalidOperationException
605
     */
606
    public function hookSingleModel(
607
        ResourceSet $sourceResourceSet,
608
        $sourceEntityInstance,
609
        ResourceSet $targetResourceSet,
610
        $targetEntityInstance,
611
        $navPropName
612
    ) {
613
        return $this->getModelHook()->hookSingleModel(
614
            $sourceResourceSet,
615
            $sourceEntityInstance,
616
            $targetResourceSet,
617
            $targetEntityInstance,
618
            $navPropName
619
        );
620
    }
621
622
    /**
623
     * Removes child model from parent model.
624
     *
625
     * @param ResourceSet $sourceResourceSet
626
     * @param Model       $sourceEntityInstance
627
     * @param ResourceSet $targetResourceSet
628
     * @param Model       $targetEntityInstance
629
     * @param $navPropName
630
     *
631
     * @return bool
632
     * @throws InvalidOperationException
633
     */
634
    public function unhookSingleModel(
635
        ResourceSet $sourceResourceSet,
636
        $sourceEntityInstance,
637
        ResourceSet $targetResourceSet,
638
        $targetEntityInstance,
639
        $navPropName
640
    ) {
641
        return $this->getModelHook()->unhookSingleModel(
642
            $sourceResourceSet,
643
            $sourceEntityInstance,
644
            $targetResourceSet,
645
            $targetEntityInstance,
646
            $navPropName
647
        );
648
    }
649
650
    /**
651
     * Start database transaction.
652
     * @param bool $isBulk
653
     */
654
    public function startTransaction($isBulk = false)
655
    {
656
        self::$touchList = [];
657
        self::$inBatch = true === $isBulk;
658
        DB::beginTransaction();
659
    }
660
661
    /**
662
     * Commit database transaction.
663
     */
664
    public function commitTransaction()
665
    {
666
        // fire model save again, to give Laravel app final chance to finalise anything that needs finalising after
667
        // batch processing
668
        foreach (self::$touchList as $model) {
669
            $model->save();
670
        }
671
672
        DB::commit();
673
        self::$touchList = [];
674
        self::$inBatch = false;
675
    }
676
677
    /**
678
     * Abort database transaction.
679
     */
680
    public function rollBackTransaction()
681
    {
682
        DB::rollBack();
683
        self::$touchList = [];
684
        self::$inBatch = false;
685
    }
686
687
    public static function queueModel(Model &$model)
688
    {
689
        // if we're not processing a batch, don't queue anything
690
        if (!self::$inBatch) {
691
            return;
692
        }
693
        // if we are in a batch, add to queue to process on transaction commit
694
        self::$touchList[] = $model;
695
    }
696
697
    /**
698
     * @param ResourceSet $resourceSet
699
     * @param Model|Relation|null $sourceEntityInstance
700
     * @param mixed $data
701
     * @param mixed $verb
702
     * @return Model|null
703
     * @throws InvalidOperationException
704
     * @throws ODataException
705
     */
706
    protected function createUpdateMainWrapper(ResourceSet $resourceSet, $sourceEntityInstance, $data, $verb)
707
    {
708
        /** @var Model|null $source */
709
        $source = $this->unpackSourceEntity($sourceEntityInstance);
710
711
        $result = $this->createUpdateCoreWrapper($resourceSet, $data, $verb, $source);
712
        if (null !== $result) {
713
            self::queueModel($result);
714
        }
715
        return $result;
716
    }
717
}
718