LaravelBulkQuery   A
last analyzed

Complexity

Total Complexity 39

Size/Duplication

Total Lines 273
Duplicated Lines 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
wmc 39
eloc 116
c 2
b 0
f 0
dl 0
loc 273
rs 9.28

9 Methods

Rating   Name   Duplication   Size   Complexity  
A getQuery() 0 3 1
A __construct() 0 6 2
B prepareBulkRequestInput() 0 36 10
A createBulkResourceforResourceSet() 0 29 5
A getOptionalVerbMapping() 0 10 3
A getControllerContainer() 0 6 2
B updateBulkResource() 0 40 6
A createUpdateDeleteProcessOutput() 0 13 5
A processBulkCustom() 0 36 5
1
<?php
2
3
declare(strict_types=1);
4
5
namespace AlgoWeb\PODataLaravel\Query;
6
7
use AlgoWeb\PODataLaravel\Auth\NullAuthProvider;
8
use AlgoWeb\PODataLaravel\Controllers\MetadataControllerContainer;
9
use AlgoWeb\PODataLaravel\Interfaces\AuthInterface;
10
use AlgoWeb\PODataLaravel\Providers\MetadataProvider;
11
use Illuminate\Database\Eloquent\Model;
12
use Illuminate\Database\Eloquent\Relations\Relation;
13
use Illuminate\Http\JsonResponse;
14
use Illuminate\Http\Request;
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\ResourceSet;
20
use POData\UriProcessor\ResourcePathProcessor\SegmentParser\KeyDescriptor;
21
22
class LaravelBulkQuery
23
{
24
    /** @var AuthInterface */
25
    protected $auth;
26
    /** @var MetadataProvider */
27
    protected $metadataProvider;
28
    /** @var LaravelQuery */
29
    protected $query;
30
    /** @var MetadataControllerContainer */
31
    protected $controllerContainer;
32
33
    public function __construct(LaravelQuery &$query, AuthInterface $auth = null)
34
    {
35
        $this->auth                = isset($auth) ? $auth : new NullAuthProvider();
36
        $this->metadataProvider    = new MetadataProvider(App::make('app'));
37
        $this->query               = $query;
38
        $this->controllerContainer = App::make('metadataControllers');
39
    }
40
41
    /**
42
     * Create multiple new resources in a resource set.
43
     *
44
     * @param ResourceSet $sourceResourceSet The entity set containing the entity to fetch
45
     * @param object[]    $data              The new data for the entity instance
46
     *
47
     * @throws \Exception
48
     * @throws InvalidOperationException
49
     * @throws \ReflectionException
50
     * @return object[]                  returns the newly created model if successful,
51
     *                                   or throws an exception if model creation failed
52
     */
53
    public function createBulkResourceforResourceSet(
54
        ResourceSet $sourceResourceSet,
55
        array $data
56
    ) {
57
        $verbName = 'bulkCreate';
58
        $mapping  = $this->getOptionalVerbMapping($sourceResourceSet, $verbName);
59
60
        $result = [];
61
        try {
62
            DB::beginTransaction();
63
            if (null === $mapping) {
64
                foreach ($data as $newItem) {
65
                    $raw = $this->getQuery()->createResourceforResourceSet($sourceResourceSet, null, $newItem);
66
                    if (null === $raw) {
67
                        throw new \Exception('Bulk model creation failed');
68
                    }
69
                    $result[] = $raw;
70
                }
71
            } else {
72
                $keyDescriptor = [];
73
                $pastVerb      = 'created';
74
                $result        = $this->processBulkCustom($sourceResourceSet, $data, $mapping, $pastVerb, $keyDescriptor);
75
            }
76
            DB::commit();
77
        } catch (\Exception $e) {
78
            DB::rollBack();
79
            throw $e;
80
        }
81
        return $result;
82
    }
83
84
    /**
85
     * Updates a group of resources in a resource set.
86
     *
87
     * @param ResourceSet     $sourceResourceSet    The entity set containing the source entity
88
     * @param Model|Relation  $sourceEntityInstance The source entity instance
89
     * @param KeyDescriptor[] $keyDescriptor        The key identifying the entity to fetch
90
     * @param object[]        $data                 The new data for the entity instances
91
     * @param bool            $shouldUpdate         Should undefined values be updated or reset to default
92
     *
93
     * @throws \Exception
94
     * @throws InvalidOperationException
95
     * @return object[]                  the new resource value if it is assignable, or throw exception for null
96
     */
97
    public function updateBulkResource(
98
        ResourceSet $sourceResourceSet,
99
        $sourceEntityInstance,
100
        array $keyDescriptor,
101
        array $data,
102
        $shouldUpdate = false
103
    ) {
104
        $numKeys = count($keyDescriptor);
105
        if ($numKeys !== count($data)) {
106
            $msg = 'Key descriptor array and data array must be same length';
107
            throw new \InvalidArgumentException($msg);
108
        }
109
        $result = [];
110
111
        $verbName = 'bulkUpdate';
112
        $mapping  = $this->getOptionalVerbMapping($sourceResourceSet, $verbName);
113
114
        try {
115
            DB::beginTransaction();
116
            if (null === $mapping) {
117
                for ($i = 0; $i < $numKeys; $i++) {
118
                    $newItem = $data[$i];
119
                    $newKey  = $keyDescriptor[$i];
120
                    $raw     = $this->getQuery()->
121
                        updateResource($sourceResourceSet, $sourceEntityInstance, $newKey, $newItem);
122
                    if (null === $raw) {
123
                        throw new \Exception('Bulk model update failed');
124
                    }
125
                    $result[] = $raw;
126
                }
127
            } else {
128
                $pastVerb = 'updated';
129
                $result   = $this->processBulkCustom($sourceResourceSet, $data, $mapping, $pastVerb, $keyDescriptor);
130
            }
131
            DB::commit();
132
        } catch (\Exception $e) {
133
            DB::rollBack();
134
            throw $e;
135
        }
136
        return $result;
137
    }
138
139
    /**
140
     * Prepare bulk request from supplied data.  If $keyDescriptors is not null, its elements are assumed to
141
     * correspond 1-1 to those in $data.
142
     *
143
     * @param  mixed[]                   $paramList
144
     * @param  mixed[]                   $data
145
     * @param  KeyDescriptor[]           $keyDescriptors
146
     * @throws InvalidOperationException
147
     * @return Request[]
148
     */
149
    protected function prepareBulkRequestInput(array $paramList, array $data, array $keyDescriptors = [])
150
    {
151
        $parms    = [];
152
        $isCreate = empty($keyDescriptors);
153
154
        // for moment, we're only processing parameters of type Request
155
        foreach ($paramList as $spec) {
156
            $varType = isset($spec['type']) ? $spec['type'] : null;
157
            if (null !== $varType) {
158
                /** @var Request $var */
159
                $var = new $varType();
160
                if ($spec['isRequest']) {
161
                    $var->setMethod($isCreate ? 'POST' : 'PUT');
162
                    $bulkData = ['data' => $data];
163
                    if (!$isCreate) {
164
                        $keys = [];
165
                        foreach ($keyDescriptors as $desc) {
166
                            if (!($desc instanceof KeyDescriptor)) {
167
                                $msg = get_class($desc);
168
                                throw new InvalidOperationException($msg);
169
                            }
170
                            $rawPayload = $desc->getNamedValues();
171
                            $keyPayload = [];
172
                            foreach ($rawPayload as $keyName => $keyVal) {
173
                                $keyPayload[$keyName] = $keyVal[0];
174
                            }
175
                            $keys[] = $keyPayload;
176
                        }
177
                        $bulkData['keys'] = $keys;
178
                    }
179
                    $var->request = new \Symfony\Component\HttpFoundation\ParameterBag($bulkData);
180
                }
181
                $parms[] = $var;
182
            }
183
        }
184
        return $parms;
185
    }
186
187
    /**
188
     * @param  ResourceSet               $sourceResourceSet
189
     * @param  mixed[]                   $data
190
     * @param  array[]|string[]          $mapping
191
     * @param  string                    $pastVerb
192
     * @param  KeyDescriptor[]           $keyDescriptor
193
     * @throws ODataException
194
     * @throws \ReflectionException
195
     * @throws InvalidOperationException
196
     * @return mixed[]
197
     */
198
    protected function processBulkCustom(
199
        ResourceSet $sourceResourceSet,
200
        array $data,
201
        array $mapping,
202
        string $pastVerb,
203
        array $keyDescriptor = []
204
    ) {
205
        $class        = $sourceResourceSet->getResourceType()->getInstanceType()->getName();
206
        /** @var string $controlClass */
207
        $controlClass = $mapping['controller'];
208
        $method       = $mapping['method'];
209
        /** @var array[] $paramList */
210
        $paramList    = $mapping['parameters'];
211
        $controller   = App::make($controlClass);
212
        $parms        = $this->prepareBulkRequestInput($paramList, $data, $keyDescriptor);
213
214
        $callResult = call_user_func_array(array($controller, $method), $parms);
215
        $payload    = $this->createUpdateDeleteProcessOutput($callResult);
216
        $success    = isset($payload['id']) && is_array($payload['id']);
217
218
        if ($success) {
219
            try {
220
                // return array of Model objects underlying collection returned by findMany
221
                /** @var Model $actClass */
222
                $actClass = App::make($class);
223
                $result   = $actClass->findMany($payload['id'])->flatten()->all();
224
                foreach ($result as $model) {
225
                    LaravelQuery::queueModel($model);
226
                }
227
                return $result;
228
            } catch (\Exception $e) {
229
                throw new ODataException($e->getMessage(), 500);
230
            }
231
        }
232
        $msg = 'Target models not successfully ' . $pastVerb;
233
        throw new ODataException($msg, 422);
234
    }
235
236
    /**
237
     * @param  ResourceSet               $sourceResourceSet
238
     * @param  string                    $verbName
239
     * @throws InvalidOperationException
240
     * @throws \ReflectionException
241
     * @return mixed[]|null
242
     */
243
    protected function getOptionalVerbMapping(ResourceSet $sourceResourceSet, string $verbName)
244
    {
245
        // dig up target class name
246
        $type = $sourceResourceSet->getResourceType()->getInstanceType();
247
        if (!($type instanceof \ReflectionClass)) {
248
            $msg = null == $type ? 'Null' : get_class($type);
249
            throw new InvalidOperationException($msg);
250
        }
251
        $modelName = $type->getName();
252
        return $this->getControllerContainer()->getMapping($modelName, $verbName);
253
    }
254
255
    /**
256
     * @return LaravelQuery
257
     */
258
    public function getQuery()
259
    {
260
        return $this->query;
261
    }
262
263
    /**
264
     * Dig out local copy of controller metadata mapping.
265
     *
266
     * @throws InvalidOperationException
267
     * @return MetadataControllerContainer
268
     */
269
    public function getControllerContainer()
270
    {
271
        if (null === $this->controllerContainer) {
272
            throw new InvalidOperationException('Controller container must not be null');
273
        }
274
        return $this->controllerContainer;
275
    }
276
277
    /**
278
     * @param JsonResponse $result
279
     * @throws ODataException
280
     * @return array|mixed
281
     */
282
    protected function createUpdateDeleteProcessOutput(JsonResponse $result)
283
    {
284
        $outData = $result->getData(true);
285
286
        if (!is_array($outData)) {
287
            throw ODataException::createInternalServerError('Controller response does not have an array.');
288
        }
289
        if (!(key_exists('id', $outData) && key_exists('status', $outData) && key_exists('errors', $outData))) {
290
            throw ODataException::createInternalServerError(
291
                'Controller response array missing at least one of id, status and/or errors fields.'
292
            );
293
        }
294
        return $outData;
295
    }
296
}
297