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