Passed
Pull Request — master (#182)
by Alex
05:31
created

LaravelQuery::createUpdateDeleteCore()   B

Complexity

Conditions 6
Paths 7

Size

Total Lines 40
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 23
CRAP Score 6.6189

Importance

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