1
|
|
|
<?php declare(strict_types=1); |
2
|
|
|
|
3
|
|
|
namespace Shopware\Rest\Controller; |
4
|
|
|
|
5
|
|
|
use Shopware\Api\Entity\DefinitionRegistry; |
6
|
|
|
use Shopware\Api\Entity\Entity; |
7
|
|
|
use Shopware\Api\Entity\EntityDefinition; |
8
|
|
|
use Shopware\Api\Entity\Field\AssociationInterface; |
9
|
|
|
use Shopware\Api\Entity\Field\Field; |
10
|
|
|
use Shopware\Api\Entity\Field\ManyToManyAssociationField; |
11
|
|
|
use Shopware\Api\Entity\Field\ManyToOneAssociationField; |
12
|
|
|
use Shopware\Api\Entity\Field\OneToManyAssociationField; |
13
|
|
|
use Shopware\Api\Entity\Field\ReferenceVersionField; |
14
|
|
|
use Shopware\Api\Entity\Field\VersionField; |
15
|
|
|
use Shopware\Api\Entity\FieldCollection; |
16
|
|
|
use Shopware\Api\Entity\RepositoryInterface; |
17
|
|
|
use Shopware\Api\Entity\Search\Criteria; |
18
|
|
|
use Shopware\Api\Entity\Search\Parser\QueryStringParser; |
19
|
|
|
use Shopware\Api\Entity\Search\Query\TermQuery; |
20
|
|
|
use Shopware\Api\Entity\Search\SearchCriteriaBuilder; |
21
|
|
|
use Shopware\Api\Entity\Search\Sorting\FieldSorting; |
22
|
|
|
use Shopware\Api\Entity\Search\Term\EntityScoreQueryBuilder; |
23
|
|
|
use Shopware\Api\Entity\Search\Term\SearchTermInterpreter; |
24
|
|
|
use Shopware\Api\Entity\Write\EntityWriterInterface; |
25
|
|
|
use Shopware\Api\Entity\Write\FieldException\WriteStackException; |
26
|
|
|
use Shopware\Api\Entity\Write\GenericWrittenEvent; |
27
|
|
|
use Shopware\Api\Entity\Write\WriteContext; |
28
|
|
|
use Shopware\Context\Struct\ShopContext; |
29
|
|
|
use Shopware\Rest\Exception\ResourceNotFoundException; |
30
|
|
|
use Shopware\Rest\Exception\WriteStackHttpException; |
31
|
|
|
use Shopware\Rest\Response\ResponseFactory; |
32
|
|
|
use Shopware\Rest\RestContext; |
33
|
|
|
use Symfony\Bundle\FrameworkBundle\Controller\Controller; |
34
|
|
|
use Symfony\Component\HttpFoundation\Request; |
35
|
|
|
use Symfony\Component\HttpFoundation\Response; |
36
|
|
|
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; |
37
|
|
|
use Symfony\Component\HttpKernel\Exception\HttpException; |
38
|
|
|
use Symfony\Component\HttpKernel\Exception\UnsupportedMediaTypeHttpException; |
39
|
|
|
use Symfony\Component\Routing\Annotation\Route; |
40
|
|
|
use Symfony\Component\Serializer\Exception\InvalidArgumentException; |
41
|
|
|
use Symfony\Component\Serializer\Exception\UnexpectedValueException; |
42
|
|
|
use Symfony\Component\Serializer\Serializer; |
43
|
|
|
|
44
|
|
|
class ApiController extends Controller |
45
|
|
|
{ |
46
|
|
|
public const WRITE_UPDATE = 'update'; |
47
|
|
|
public const WRITE_CREATE = 'create'; |
48
|
|
|
|
49
|
|
|
public const RESPONSE_BASIC = 'basic'; |
50
|
|
|
public const RESPONSE_DETAIL = 'detail'; |
51
|
|
|
|
52
|
|
|
/** |
53
|
|
|
* @var DefinitionRegistry |
54
|
|
|
*/ |
55
|
|
|
private $definitionRegistry; |
56
|
|
|
|
57
|
|
|
/** |
58
|
|
|
* @var Serializer |
59
|
|
|
*/ |
60
|
|
|
private $serializer; |
61
|
|
|
|
62
|
|
|
/** |
63
|
|
|
* @var ResponseFactory |
64
|
|
|
*/ |
65
|
|
|
private $responseFactory; |
66
|
|
|
|
67
|
|
|
/** |
68
|
|
|
* @var EntityWriterInterface |
69
|
|
|
*/ |
70
|
14 |
|
private $entityWriter; |
71
|
|
|
/** |
72
|
14 |
|
* @var SearchCriteriaBuilder |
73
|
14 |
|
*/ |
74
|
14 |
|
private $searchCriteriaBuilder; |
75
|
14 |
|
|
76
|
14 |
|
public function __construct( |
77
|
|
|
DefinitionRegistry $definitionRegistry, |
78
|
6 |
|
Serializer $serializer, |
79
|
|
|
ResponseFactory $responseFactory, |
80
|
6 |
|
EntityWriterInterface $entityWriter, |
81
|
|
|
SearchCriteriaBuilder $searchCriteriaBuilder |
82
|
6 |
|
) { |
83
|
6 |
|
$this->definitionRegistry = $definitionRegistry; |
84
|
|
|
$this->serializer = $serializer; |
85
|
6 |
|
$this->responseFactory = $responseFactory; |
86
|
|
|
$this->entityWriter = $entityWriter; |
87
|
6 |
|
$this->searchCriteriaBuilder = $searchCriteriaBuilder; |
88
|
6 |
|
} |
89
|
|
|
|
90
|
6 |
|
public function detailAction(Request $request, RestContext $context): Response |
91
|
6 |
|
{ |
92
|
|
|
$path = $this->buildEntityPath($request->getPathInfo()); |
93
|
|
|
|
94
|
|
|
$root = $path[0]['entity']; |
95
|
|
|
$id = $path[\count($path) - 1]['value']; |
96
|
|
|
|
97
|
|
|
$definition = $this->definitionRegistry->get($root); |
98
|
|
|
|
99
|
|
|
$associations = array_column($path, 'entity'); |
100
|
|
|
array_shift($associations); |
101
|
|
|
|
102
|
|
|
if (empty($associations)) { |
103
|
|
|
$repository = $this->get($definition::getRepositoryClass()); |
104
|
|
|
} else { |
105
|
6 |
|
/** @var EntityDefinition $definition */ |
106
|
|
|
$field = $this->getAssociation($definition::getFields(), $associations); |
107
|
6 |
|
|
108
|
|
|
$definition = $field->getReferenceClass(); |
109
|
6 |
|
if ($field instanceof ManyToManyAssociationField) { |
110
|
|
|
$definition = $field->getReferenceDefinition(); |
111
|
|
|
} |
112
|
|
|
|
113
|
6 |
|
$repository = $this->get($definition::getRepositoryClass()); |
114
|
|
|
} |
115
|
|
|
|
116
|
5 |
|
/** @var RepositoryInterface $repository */ |
117
|
|
|
$entities = $repository->readDetail([$id], $context->getShopContext()); |
118
|
5 |
|
|
119
|
|
|
$entity = $entities->get($id); |
120
|
5 |
|
|
121
|
|
|
if ($entity === null) { |
122
|
|
|
throw new ResourceNotFoundException($definition::getEntityName(), $id); |
123
|
5 |
|
} |
124
|
|
|
|
125
|
|
|
return $this->responseFactory->createDetailResponse($entity, (string) $definition, $context); |
126
|
5 |
|
} |
127
|
|
|
|
128
|
5 |
|
public function listAction(Request $request, RestContext $context): Response |
129
|
1 |
|
{ |
130
|
1 |
|
$path = $this->buildEntityPath($request->getPathInfo()); |
131
|
1 |
|
|
132
|
|
|
$first = array_shift($path); |
133
|
|
|
|
134
|
1 |
|
/** @var EntityDefinition|string $definition */ |
135
|
|
|
$definition = $first['definition']; |
136
|
|
|
|
137
|
4 |
|
if (!$definition) { |
138
|
4 |
|
throw new BadRequestHttpException('Unsupported API request'); |
139
|
|
|
} |
140
|
4 |
|
|
141
|
|
|
/** @var RepositoryInterface $repository */ |
142
|
|
|
$repository = $this->get($definition::getRepositoryClass()); |
143
|
|
|
|
144
|
4 |
|
if (empty($path)) { |
145
|
|
|
$data = $repository->search( |
146
|
4 |
|
$this->createListingCriteria($definition, $request, $context->getShopContext()), |
|
|
|
|
147
|
|
|
$context->getShopContext() |
148
|
4 |
|
); |
149
|
|
|
|
150
|
4 |
|
return $this->responseFactory->createListingResponse($data, (string) $definition, $context); |
151
|
|
|
} |
152
|
4 |
|
|
153
|
|
|
$child = array_pop($path); |
154
|
|
|
$parent = $first; |
155
|
|
|
|
156
|
|
|
if (!empty($path)) { |
157
|
|
|
$parent = array_pop($path); |
158
|
2 |
|
} |
159
|
|
|
|
160
|
|
|
$criteria = $this->createListingCriteria($definition, $request, $context->getShopContext()); |
|
|
|
|
161
|
2 |
|
|
162
|
2 |
|
$association = $child['field']; |
163
|
2 |
|
|
164
|
2 |
|
$parentDefinition = $parent['definition']; |
165
|
|
|
|
166
|
|
|
$definition = $child['definition']; |
167
|
|
|
|
168
|
2 |
|
if ($association instanceof ManyToManyAssociationField) { |
169
|
|
|
/* |
170
|
|
|
* Example: |
171
|
2 |
|
* route: /api/product/SW1/categories |
172
|
2 |
|
* $definition: \Shopware\Category\Definition\CategoryDefinition |
173
|
2 |
|
*/ |
174
|
2 |
|
$definition = $association->getReferenceDefinition(); |
175
|
|
|
|
176
|
|
|
//fetch inverse association definition for filter |
177
|
2 |
|
$reverse = $definition::getFields()->filter( |
178
|
|
|
function (Field $field) use ($association) { |
179
|
|
|
return $field instanceof ManyToManyAssociationField && $association->getMappingDefinition() === $field->getMappingDefinition(); |
180
|
|
|
} |
181
|
|
|
); |
182
|
|
|
|
183
|
|
|
//contains now the inverse side association: category.products |
184
|
|
|
$reverse = $reverse->first(); |
185
|
1 |
|
|
186
|
1 |
|
/* @var ManyToManyAssociationField $reverse */ |
187
|
|
|
$criteria->addFilter( |
188
|
|
|
new TermQuery( |
189
|
1 |
|
sprintf('%s.%s.id', $definition::getEntityName(), $reverse->getPropertyName()), |
190
|
1 |
|
$parent['value'] |
191
|
|
|
) |
192
|
1 |
|
); |
193
|
1 |
|
} elseif ($association instanceof OneToManyAssociationField) { |
194
|
|
|
/* |
195
|
|
|
* Example |
196
|
1 |
|
* Route: /api/product/SW1/prices |
197
|
|
|
* $definition: \Shopware\Product\Definition\ProductPriceDefinition |
198
|
|
|
*/ |
199
|
|
|
|
200
|
|
|
//get foreign key definition of reference |
201
|
|
|
$foreignKey = $definition::getFields()->getByStorageName( |
202
|
|
|
$association->getReferenceField() |
203
|
|
|
); |
204
|
1 |
|
|
205
|
1 |
|
$criteria->addFilter( |
206
|
1 |
|
new TermQuery( |
207
|
1 |
|
//add filter to parent value: prices.productId = SW1 |
208
|
|
|
$definition::getEntityName() . '.' . $foreignKey->getPropertyName(), |
209
|
1 |
|
$parent['value'] |
210
|
|
|
) |
211
|
|
|
); |
212
|
1 |
|
} elseif ($association instanceof ManyToOneAssociationField) { |
213
|
1 |
|
/* |
214
|
|
|
* Example |
215
|
1 |
|
* Route: /api/product/SW1/manufacturer |
216
|
1 |
|
* $definition: \Shopware\Product\Definition\ProductManufacturerDefinition |
217
|
|
|
*/ |
218
|
|
|
|
219
|
|
|
//get inverse association to filter to parent value |
220
|
|
|
$reverse = $definition::getFields()->filter( |
221
|
|
|
function (Field $field) use ($parentDefinition) { |
222
|
4 |
|
return $field instanceof OneToManyAssociationField && $parentDefinition === $field->getReferenceClass(); |
223
|
|
|
} |
224
|
4 |
|
); |
225
|
|
|
$reverse = $reverse->first(); |
226
|
4 |
|
|
227
|
|
|
/* @var OneToManyAssociationField $reverse */ |
228
|
|
|
$criteria->addFilter( |
229
|
9 |
|
new TermQuery( |
230
|
|
|
//filter inverse association to parent value: manufacturer.products.id = SW1 |
231
|
9 |
|
sprintf('%s.%s.id', $definition::getEntityName(), $reverse->getPropertyName()), |
232
|
|
|
$parent['value'] |
233
|
|
|
) |
234
|
1 |
|
); |
235
|
|
|
} |
236
|
1 |
|
|
237
|
|
|
/** @var RepositoryInterface $repository */ |
238
|
|
|
$repository = $this->get($definition::getRepositoryClass()); |
239
|
7 |
|
|
240
|
|
|
$result = $repository->search($criteria, $context->getShopContext()); |
241
|
7 |
|
|
242
|
|
|
return $this->responseFactory->createListingResponse($result, (string) $definition, $context); |
243
|
7 |
|
} |
244
|
|
|
|
245
|
7 |
|
public function createAction(Request $request, RestContext $context): Response |
246
|
|
|
{ |
247
|
7 |
|
return $this->write($request, $context, self::WRITE_CREATE); |
248
|
|
|
} |
249
|
|
|
|
250
|
7 |
|
public function updateAction(Request $request, RestContext $context) |
251
|
|
|
{ |
252
|
4 |
|
return $this->write($request, $context, self::WRITE_UPDATE); |
253
|
|
|
} |
254
|
4 |
|
|
255
|
|
|
public function deleteAction(Request $request, RestContext $context): Response |
256
|
4 |
|
{ |
257
|
|
|
$path = $this->buildEntityPath($request->getPathInfo()); |
258
|
|
|
|
259
|
3 |
|
$last = $path[\count($path) - 1]; |
260
|
3 |
|
|
261
|
3 |
|
$id = $last['value']; |
262
|
|
|
|
263
|
|
|
$first = array_shift($path); |
264
|
|
|
|
265
|
3 |
|
/* @var string|EntityDefinition $definition */ |
266
|
|
|
if (\count($path) === 0) { |
267
|
|
|
//first api level call /product/{id} |
268
|
3 |
|
$definition = $first['definition']; |
269
|
|
|
|
270
|
3 |
|
$this->doDelete($context, $definition, $id); |
271
|
1 |
|
|
272
|
|
|
return $this->responseFactory->createRedirectResponse($definition, $id, $context); |
273
|
1 |
|
} |
274
|
|
|
|
275
|
|
|
$child = array_pop($path); |
276
|
|
|
$parent = $first; |
277
|
2 |
|
if (!empty($path)) { |
278
|
1 |
|
$parent = array_pop($path); |
279
|
|
|
} |
280
|
1 |
|
|
281
|
|
|
$definition = $child['definition']; |
282
|
|
|
|
283
|
|
|
/** @var AssociationInterface $association */ |
284
|
1 |
|
$association = $child['field']; |
285
|
1 |
|
|
286
|
1 |
|
if ($association instanceof OneToManyAssociationField) { |
287
|
|
|
$this->doDelete($context, $definition, $id); |
288
|
|
|
|
289
|
1 |
|
return $this->responseFactory->createRedirectResponse($definition, $id, $context); |
290
|
1 |
|
} |
291
|
|
|
|
292
|
|
|
// DELETE api/product/{id}/manufacturer/{id} |
293
|
|
|
if ($association instanceof ManyToOneAssociationField) { |
294
|
1 |
|
$this->doDelete($context, $definition, $id); |
295
|
1 |
|
|
296
|
|
|
return $this->responseFactory->createRedirectResponse($definition, $id, $context); |
297
|
|
|
} |
298
|
1 |
|
|
299
|
1 |
|
// DELETE api/product/{id}/category/{id} |
300
|
1 |
|
if ($association instanceof ManyToManyAssociationField) { |
301
|
1 |
|
$local = $definition::getFields()->getByStorageName( |
302
|
|
|
$association->getMappingLocalColumn() |
303
|
|
|
); |
304
|
1 |
|
|
305
|
|
|
$reference = $definition::getFields()->getByStorageName( |
306
|
|
|
$association->getMappingReferenceColumn() |
307
|
|
|
); |
308
|
|
|
|
309
|
|
|
$mapping = [ |
310
|
9 |
|
$local->getPropertyName() => $parent['value'], |
311
|
|
|
$reference->getPropertyName() => $id, |
312
|
9 |
|
]; |
313
|
9 |
|
|
314
|
9 |
|
$this->entityWriter->delete( |
315
|
|
|
$definition, |
316
|
9 |
|
[$mapping], |
317
|
|
|
WriteContext::createFromShopContext($context->getShopContext()) |
318
|
|
|
); |
319
|
|
|
|
320
|
9 |
|
return $this->responseFactory->createRedirectResponse($definition, $id, $context); |
321
|
|
|
} |
322
|
9 |
|
|
323
|
|
|
throw new \RuntimeException(sprintf('Unsupported association for field %s', $association->getPropertyName())); |
324
|
9 |
|
} |
325
|
1 |
|
|
326
|
|
|
public function searchAction(Request $request, RestContext $context, string $path): Response |
327
|
|
|
{ |
328
|
9 |
|
$path = $this->buildEntityPath($path); |
329
|
|
|
$first = array_shift($path); |
330
|
|
|
|
331
|
9 |
|
/** @var EntityDefinition|string $definition */ |
332
|
9 |
|
$definition = $first['definition']; |
333
|
|
|
|
334
|
|
|
/** @var RepositoryInterface $repository */ |
335
|
9 |
|
$repository = $this->get($definition::getRepositoryClass()); |
336
|
|
|
|
337
|
9 |
|
if (empty($path)) { |
338
|
9 |
|
$data = $repository->search( |
339
|
9 |
|
$this->searchCriteriaBuilder->handleRequest( |
340
|
9 |
|
$request, |
341
|
|
|
$definition, |
|
|
|
|
342
|
9 |
|
$context->getShopContext() |
343
|
9 |
|
), |
344
|
|
|
$context->getShopContext() |
345
|
|
|
); |
346
|
1 |
|
|
347
|
1 |
|
return $this->responseFactory->createListingResponse($data, (string) $definition, $context); |
348
|
|
|
} |
349
|
1 |
|
throw new \RuntimeException('Only entities are supported'); |
350
|
|
|
} |
351
|
|
|
|
352
|
1 |
|
private function write(Request $request, RestContext $context, string $type): Response |
353
|
|
|
{ |
354
|
|
|
$payload = $this->getRequestBody($request); |
355
|
3 |
|
$responseDataType = $this->getResponseDataType($request); |
356
|
|
|
$appendLocationHeader = $type === self::WRITE_CREATE; |
357
|
3 |
|
|
358
|
3 |
|
if ($this->isCollection($payload)) { |
359
|
|
|
throw new BadRequestHttpException('Only single write operations are supported. Please send the entities one by one or use the /sync api endpoint.'); |
360
|
|
|
} |
361
|
|
|
|
362
|
3 |
|
$path = $this->buildEntityPath($request->getPathInfo()); |
363
|
|
|
|
364
|
3 |
|
$last = $path[\count($path) - 1]; |
365
|
|
|
|
366
|
|
|
if ($type === self::WRITE_UPDATE && isset($last['value'])) { |
367
|
3 |
|
$payload['id'] = $last['value']; |
368
|
|
|
} |
369
|
|
|
|
370
|
|
|
$first = array_shift($path); |
371
|
|
|
|
372
|
3 |
|
/* @var string|EntityDefinition $definition */ |
373
|
1 |
|
if (\count($path) === 0) { |
374
|
1 |
|
$definition = $first['definition']; |
375
|
|
|
|
376
|
1 |
|
/** @var RepositoryInterface $repository */ |
377
|
|
|
$repository = $this->get($definition::getRepositoryClass()); |
378
|
1 |
|
|
379
|
|
|
$events = $this->executeWriteOperation($definition, $payload, $context, $type); |
380
|
1 |
|
$event = $events->getEventByDefinition($definition); |
381
|
1 |
|
$eventIds = $event->getIds(); |
382
|
|
|
$entityId = array_shift($eventIds); |
383
|
|
|
|
384
|
|
|
if (!$responseDataType) { |
385
|
|
|
return $this->responseFactory->createRedirectResponse($definition, $entityId, $context); |
386
|
|
|
} |
387
|
|
|
|
388
|
|
|
if ($responseDataType === self::RESPONSE_DETAIL) { |
389
|
|
|
$entities = $repository->readDetail($event->getIds(), $context->getShopContext()); |
390
|
|
|
} else { |
391
|
|
|
$entities = $repository->readBasic($event->getIds(), $context->getShopContext()); |
392
|
|
|
} |
393
|
|
|
|
394
|
|
|
return $this->responseFactory->createDetailResponse($entities->first(), $definition, $context, $appendLocationHeader); |
395
|
|
|
} |
396
|
|
|
|
397
|
2 |
|
$child = array_pop($path); |
398
|
1 |
|
|
399
|
1 |
|
$parent = $first; |
400
|
|
|
if (!empty($path)) { |
401
|
1 |
|
$parent = array_pop($path); |
402
|
1 |
|
} |
403
|
|
|
|
404
|
1 |
|
$definition = $child['definition']; |
405
|
|
|
|
406
|
|
|
$association = $child['field']; |
407
|
1 |
|
|
408
|
1 |
|
/** @var EntityDefinition $parentDefinition */ |
409
|
|
|
$parentDefinition = $parent['definition']; |
410
|
|
|
|
411
|
1 |
|
/* @var RepositoryInterface $repository */ |
412
|
1 |
|
|
413
|
|
|
/* @var Entity $entity */ |
414
|
1 |
|
if ($association instanceof OneToManyAssociationField) { |
415
|
1 |
|
$foreignKey = $definition::getFields() |
416
|
|
|
->getByStorageName($association->getReferenceField()); |
417
|
|
|
|
418
|
|
|
$payload[$foreignKey->getPropertyName()] = $parent['value']; |
419
|
|
|
|
420
|
|
|
$events = $this->executeWriteOperation($definition, $payload, $context, $type); |
421
|
|
|
|
422
|
|
|
if (!$responseDataType) { |
423
|
|
|
return $this->responseFactory->createRedirectResponse($definition, $parent['value'], $context); |
424
|
|
|
} |
425
|
|
|
|
426
|
|
|
$event = $events->getEventByDefinition($definition); |
427
|
|
|
|
428
|
|
|
$repository = $this->get($definition::getRepositoryClass()); |
429
|
|
|
|
430
|
1 |
|
if ($responseDataType === self::RESPONSE_DETAIL) { |
431
|
|
|
$entities = $repository->readDetail($event->getIds(), $context->getShopContext()); |
432
|
1 |
|
} else { |
433
|
1 |
|
$entities = $repository->readBasic($event->getIds(), $context->getShopContext()); |
434
|
|
|
} |
435
|
1 |
|
|
436
|
|
|
return $this->responseFactory->createDetailResponse($entities->first(), $definition, $context, $appendLocationHeader); |
437
|
1 |
|
} |
438
|
|
|
|
439
|
|
|
if ($association instanceof ManyToOneAssociationField) { |
440
|
1 |
|
$events = $this->executeWriteOperation($definition, $payload, $context, $type); |
441
|
|
|
$event = $events->getEventByDefinition($definition); |
442
|
|
|
|
443
|
1 |
|
$entityIds = $event->getIds(); |
444
|
|
|
$entityId = array_pop($entityIds); |
445
|
1 |
|
|
446
|
|
|
$foreignKey = $parentDefinition::getFields()->getByStorageName($association->getStorageName()); |
447
|
|
|
|
448
|
1 |
|
$payload = [ |
449
|
1 |
|
'id' => $parent['value'], |
450
|
1 |
|
$foreignKey->getPropertyName() => $entityId, |
451
|
|
|
]; |
452
|
|
|
|
453
|
|
|
$repository = $this->get($parentDefinition::getRepositoryClass()); |
454
|
1 |
|
$repository->update([$payload], $context->getShopContext()); |
455
|
|
|
|
456
|
1 |
|
if (!$responseDataType) { |
457
|
1 |
|
return $this->responseFactory->createRedirectResponse($definition, $entityId, $context); |
458
|
|
|
} |
459
|
|
|
|
460
|
|
|
if ($responseDataType === self::RESPONSE_DETAIL) { |
461
|
|
|
$entities = $repository->readDetail($event->getIds(), $context->getShopContext()); |
462
|
|
|
} else { |
463
|
9 |
|
$entities = $repository->readBasic($event->getIds(), $context->getShopContext()); |
464
|
|
|
} |
465
|
|
|
|
466
|
9 |
|
return $this->responseFactory->createDetailResponse($entities->first(), $definition, $context, $appendLocationHeader); |
467
|
|
|
} |
468
|
|
|
|
469
|
|
|
/** @var ManyToManyAssociationField $association */ |
470
|
|
|
|
471
|
9 |
|
/** @var EntityDefinition|string $reference */ |
472
|
|
|
$reference = $association->getReferenceDefinition(); |
473
|
9 |
|
|
474
|
|
|
$events = $this->executeWriteOperation($reference, $payload, $context, $type); |
|
|
|
|
475
|
1 |
|
$event = $events->getEventByDefinition($reference); |
476
|
|
|
|
477
|
1 |
|
$repository = $this->get($reference::getRepositoryClass()); |
478
|
|
|
|
479
|
|
|
if ($responseDataType === self::RESPONSE_DETAIL) { |
480
|
|
|
$entities = $repository->readDetail($event->getIds(), $context->getShopContext()); |
481
|
|
|
} else { |
482
|
|
|
$entities = $repository->readBasic($event->getIds(), $context->getShopContext()); |
483
|
|
|
} |
484
|
|
|
|
485
|
|
|
$entity = $entities->first(); |
486
|
|
|
|
487
|
|
|
$repository = $this->get($parentDefinition::getRepositoryClass()); |
488
|
|
|
|
489
|
|
|
$payload = [ |
490
|
|
|
'id' => $parent['value'], |
491
|
|
|
$association->getPropertyName() => [ |
492
|
|
|
['id' => $entity->getId()], |
493
|
|
|
], |
494
|
|
|
]; |
495
|
|
|
|
496
|
|
|
$repository->update([$payload], $context->getShopContext()); |
497
|
|
|
|
498
|
|
|
if (!$responseDataType) { |
499
|
|
|
return $this->responseFactory->createRedirectResponse($reference, $entity->getId(), $context); |
|
|
|
|
500
|
|
|
} |
501
|
|
|
|
502
|
|
|
return $this->responseFactory->createDetailResponse($entity, $definition, $context, $appendLocationHeader); |
503
|
14 |
|
} |
504
|
|
|
|
505
|
14 |
|
private function executeWriteOperation(string $definition, array $payload, RestContext $context, string $type): GenericWrittenEvent |
506
|
14 |
|
{ |
507
|
|
|
/** @var EntityDefinition $definition */ |
508
|
14 |
|
$repository = $this->get($definition::getRepositoryClass()); |
509
|
14 |
|
|
510
|
14 |
|
try { |
511
|
13 |
|
/* @var RepositoryInterface $repository */ |
512
|
|
|
switch ($type) { |
513
|
14 |
|
case self::WRITE_CREATE: |
|
|
|
|
514
|
|
|
|
515
|
|
|
return $repository->create([$payload], $context->getShopContext()); |
516
|
14 |
|
|
517
|
14 |
|
case self::WRITE_UPDATE: |
518
|
13 |
|
default: |
519
|
|
|
return $repository->update([$payload], $context->getShopContext()); |
520
|
|
|
} |
521
|
14 |
|
} catch (WriteStackException $exceptionStack) { |
522
|
14 |
|
throw new WriteStackHttpException($exceptionStack); |
523
|
|
|
} |
524
|
7 |
|
} |
525
|
|
|
|
526
|
|
|
private function getAssociation(FieldCollection $fields, array $keys): AssociationInterface |
527
|
14 |
|
{ |
528
|
14 |
|
$key = array_shift($keys); |
529
|
14 |
|
|
530
|
|
|
/** @var AssociationInterface $field */ |
531
|
|
|
$field = $fields->get($key); |
532
|
|
|
|
533
|
14 |
|
if (empty($keys)) { |
534
|
14 |
|
return $field; |
535
|
|
|
} |
536
|
14 |
|
|
537
|
|
|
/** @var string|EntityDefinition $reference */ |
538
|
|
|
$reference = $field->getReferenceClass(); |
539
|
|
|
|
540
|
14 |
|
$nested = $reference::getFields(); |
541
|
14 |
|
|
542
|
14 |
|
return $this->getAssociation($nested, $keys); |
543
|
|
|
} |
544
|
|
|
|
545
|
|
|
private function buildEntityPath(string $pathInfo): array |
546
|
|
|
{ |
547
|
14 |
|
$exploded = str_replace('/api/', '', $pathInfo); |
548
|
7 |
|
$exploded = explode('/', $exploded); |
549
|
|
|
|
550
|
|
|
$parts = []; |
551
|
7 |
|
foreach ($exploded as $index => $part) { |
552
|
7 |
|
if ($index % 2) { |
553
|
|
|
continue; |
554
|
|
|
} |
555
|
|
|
if (empty($part)) { |
556
|
|
|
continue; |
557
|
7 |
|
} |
558
|
7 |
|
$value = null; |
559
|
7 |
|
if (isset($exploded[$index + 1])) { |
560
|
7 |
|
$value = $exploded[$index + 1]; |
561
|
7 |
|
} |
562
|
|
|
|
563
|
|
|
if (empty($parts)) { |
564
|
|
|
$part = $this->urlToSnakeCase($part); |
565
|
14 |
|
} else { |
566
|
|
|
$part = $this->urlToCamelCase($part); |
567
|
|
|
} |
568
|
14 |
|
|
569
|
|
|
$parts[] = [ |
570
|
14 |
|
'entity' => $part, |
571
|
|
|
'value' => $value, |
572
|
|
|
]; |
573
|
7 |
|
} |
574
|
|
|
|
575
|
7 |
|
$parts = array_filter($parts); |
576
|
7 |
|
$first = array_shift($parts); |
577
|
|
|
|
578
|
7 |
|
$root = $this->definitionRegistry->get($first['entity']); |
579
|
|
|
|
580
|
|
|
$entities = [ |
581
|
5 |
|
[ |
582
|
|
|
'entity' => $first['entity'], |
583
|
5 |
|
'value' => $first['value'], |
584
|
5 |
|
'definition' => $root, |
585
|
5 |
|
'field' => null, |
586
|
|
|
], |
587
|
5 |
|
]; |
588
|
|
|
|
589
|
|
|
foreach ($parts as $part) { |
590
|
|
|
$fields = $root::getFields(); |
591
|
5 |
|
|
592
|
|
|
/** @var AssociationInterface $field */ |
593
|
|
|
$field = $fields->get($part['entity']); |
594
|
|
|
if (!$field) { |
595
|
5 |
|
throw new \InvalidArgumentException( |
596
|
|
|
sprintf('') |
597
|
|
|
); |
598
|
|
|
} |
599
|
|
|
$entities[] = [ |
600
|
5 |
|
'entity' => $part['entity'], |
601
|
|
|
'value' => $part['value'], |
602
|
|
|
'definition' => $field->getReferenceClass(), |
603
|
|
|
'field' => $field, |
604
|
|
|
]; |
605
|
|
|
} |
606
|
|
|
|
607
|
|
|
return $entities; |
608
|
|
|
} |
609
|
|
|
|
610
|
|
|
private function urlToSnakeCase(string $name): string |
611
|
|
|
{ |
612
|
|
|
return str_replace('-', '_', $name); |
613
|
|
|
} |
614
|
|
|
|
615
|
|
|
private function urlToCamelCase(string $name): string |
616
|
5 |
|
{ |
617
|
|
|
$parts = explode('-', $name); |
618
|
|
|
$parts = array_map('ucfirst', $parts); |
619
|
|
|
|
620
|
|
|
return lcfirst(implode('', $parts)); |
621
|
5 |
|
} |
622
|
|
|
|
623
|
5 |
|
private function createListingCriteria(string $definition, Request $request, ShopContext $context): Criteria |
624
|
|
|
{ |
625
|
|
|
$criteria = new Criteria(); |
626
|
|
|
$criteria->setFetchCount(Criteria::FETCH_COUNT_TOTAL); |
627
|
5 |
|
$criteria->setLimit(10); |
628
|
|
|
|
629
|
|
|
if ($request->query->has('offset')) { |
630
|
|
|
$criteria->setOffset((int) $request->query->get('offset')); |
631
|
5 |
|
} |
632
|
|
|
|
633
|
|
|
if ($request->query->has('limit')) { |
634
|
|
|
$criteria->setLimit((int) $request->query->get('limit')); |
635
|
|
|
} |
636
|
|
|
|
637
|
|
|
if ($request->query->has('query')) { |
638
|
|
|
$criteria->addFilter( |
639
|
|
|
QueryStringParser::fromUrl($request->query->get('query')) |
640
|
|
|
); |
641
|
12 |
|
} |
642
|
|
|
if ($request->query->has('term')) { |
643
|
12 |
|
$pattern = $this->get(SearchTermInterpreter::class)->interpret( |
644
|
12 |
|
(string) $request->query->get('term'), |
645
|
|
|
$context |
646
|
12 |
|
); |
647
|
|
|
|
648
|
|
|
/** @var EntityDefinition|string $definition */ |
649
|
|
|
$queries = $this->get(EntityScoreQueryBuilder::class)->buildScoreQueries( |
650
|
|
|
$pattern, |
651
|
|
|
$definition, |
652
|
12 |
|
$definition::getEntityName() |
653
|
|
|
); |
654
|
12 |
|
|
655
|
12 |
|
$criteria->addQueries($queries); |
656
|
|
|
} |
657
|
6 |
|
|
658
|
6 |
|
if ($request->query->has('sort')) { |
659
|
|
|
$sortings = $this->parseSortings($definition, $request->query->get('sort')); |
|
|
|
|
660
|
|
|
$criteria->addSortings($sortings); |
661
|
|
|
} |
662
|
|
|
|
663
|
|
|
$pageQuery = $request->query->get('page', []); |
664
|
|
|
|
665
|
|
|
if (array_key_exists('offset', $pageQuery)) { |
666
|
9 |
|
$criteria->setOffset((int) $pageQuery['offset']); |
667
|
|
|
} |
668
|
9 |
|
|
669
|
|
|
if (array_key_exists('limit', $pageQuery)) { |
670
|
|
|
$criteria->setLimit((int) $pageQuery['limit']); |
671
|
|
|
} |
672
|
|
|
|
673
|
|
|
return $criteria; |
674
|
|
|
} |
675
|
|
|
|
676
|
|
|
/** |
677
|
|
|
* Return a nested array structure of based on the content-type |
678
|
6 |
|
* |
679
|
|
|
* @param Request $request |
680
|
|
|
* |
681
|
6 |
|
* @return array |
682
|
|
|
*/ |
683
|
6 |
|
private function getRequestBody(Request $request): array |
684
|
6 |
|
{ |
685
|
6 |
|
$contentType = $request->headers->get('CONTENT_TYPE'); |
686
|
6 |
|
$semicolonPosition = strpos($contentType, ';'); |
687
|
|
|
|
688
|
|
|
if ($semicolonPosition !== false) { |
689
|
6 |
|
$contentType = substr($contentType, 0, $semicolonPosition); |
690
|
6 |
|
} |
691
|
|
|
|
692
|
|
|
try { |
693
|
|
|
switch ($contentType) { |
694
|
6 |
|
case 'application/vnd.api+json': |
695
|
|
|
return $this->serializer->decode($request->getContent(), 'jsonapi'); |
|
|
|
|
696
|
|
|
case 'application/json': |
697
|
|
|
return $this->serializer->decode($request->getContent(), 'json'); |
|
|
|
|
698
|
|
|
} |
699
|
|
|
} catch (InvalidArgumentException | UnexpectedValueException $exception) { |
700
|
6 |
|
throw new BadRequestHttpException($exception->getMessage()); |
701
|
|
|
} catch (\Exception $exception) { |
702
|
|
|
throw new HttpException(500, $exception->getMessage()); |
703
|
6 |
|
} |
704
|
|
|
|
705
|
6 |
|
throw new UnsupportedMediaTypeHttpException(sprintf('The Content-Type "%s" is unsupported.', $contentType)); |
706
|
|
|
} |
707
|
|
|
|
708
|
6 |
|
private function isCollection(array $array): bool |
709
|
6 |
|
{ |
710
|
|
|
return array_keys($array) === range(0, \count($array) - 1); |
711
|
9 |
|
} |
712
|
|
|
|
713
|
9 |
|
/** |
714
|
9 |
|
* @param RestContext $context |
715
|
|
|
* @param string|EntityDefinition $definition |
716
|
|
|
* @param string $id |
717
|
1 |
|
* |
718
|
1 |
|
* @throws \RuntimeException |
719
|
|
|
*/ |
720
|
1 |
|
private function doDelete(RestContext $context, $definition, $id): void |
721
|
1 |
|
{ |
722
|
|
|
/** @var RepositoryInterface $repository */ |
723
|
|
|
$repository = $this->get($definition::getRepositoryClass()); |
724
|
1 |
|
|
725
|
|
|
$payload = []; |
726
|
|
|
$fields = $definition::getPrimaryKeys()->filter(function (Field $field) { |
727
|
|
|
return !$field instanceof VersionField && !$field instanceof ReferenceVersionField; |
728
|
|
|
}); |
729
|
|
|
|
730
|
|
|
try { |
731
|
|
|
$payload = $this->getRequestBody($context->getRequest()); |
732
|
|
|
} catch (\Exception $exception) { |
733
|
|
|
// empty payload is allowed for DELETE requests |
734
|
|
|
} |
735
|
|
|
|
736
|
|
|
if ($fields->count() > 1 && empty($payload)) { |
737
|
|
|
throw new \RuntimeException( |
738
|
|
|
sprintf('Entity primary key is defined by multiple columns. Please provide primary key in payload.') |
739
|
|
|
); |
740
|
|
|
} |
741
|
|
|
|
742
|
|
|
if ($fields->count() > 1) { |
743
|
|
|
$mapping = $payload; |
744
|
|
|
} else { |
745
|
|
|
$pk = $fields->first(); |
746
|
|
|
/** @var Field $pk */ |
747
|
|
|
$mapping = [$pk->getPropertyName() => $id]; |
748
|
|
|
} |
749
|
|
|
|
750
|
|
|
$repository->delete([$mapping], $context->getShopContext()); |
751
|
|
|
} |
752
|
|
|
|
753
|
|
|
private function getResponseDataType(Request $request): ?string |
754
|
|
|
{ |
755
|
|
|
if ($request->query->has('_response') === false) { |
756
|
|
|
return null; |
757
|
|
|
} |
758
|
|
|
|
759
|
|
|
$responses = [self::RESPONSE_BASIC, self::RESPONSE_DETAIL]; |
760
|
|
|
$response = $request->query->get('_response'); |
761
|
|
|
|
762
|
|
|
if (!\in_array($response, $responses, true)) { |
763
|
|
|
throw new BadRequestHttpException(sprintf('The response type "%s" is not supported. Available types are: %s', $response, implode(', ', $responses))); |
764
|
|
|
} |
765
|
|
|
|
766
|
|
|
return $response; |
767
|
|
|
} |
768
|
|
|
|
769
|
|
|
private function parseSortings(string $definition, string $query): array |
770
|
|
|
{ |
771
|
|
|
$parts = array_filter(explode(',', $query)); |
772
|
|
|
|
773
|
|
|
$sortings = []; |
774
|
|
|
foreach ($parts as $part) { |
775
|
|
|
$first = substr($part, 0, 1); |
776
|
|
|
|
777
|
|
|
$direction = $first === '-' ? FieldSorting::DESCENDING : FieldSorting::ASCENDING; |
778
|
|
|
|
779
|
|
|
if ($direction === FieldSorting::DESCENDING) { |
780
|
|
|
$part = substr($part, 1); |
781
|
|
|
} |
782
|
|
|
|
783
|
|
|
$subParts = explode('.', $part); |
784
|
|
|
|
785
|
|
|
/** @var string|EntityDefinition $definition */ |
786
|
|
|
$root = $definition::getEntityName(); |
787
|
|
|
|
788
|
|
|
if ($subParts[0] !== $root) { |
789
|
|
|
$part = $definition::getEntityName() . '.' . $part; |
790
|
|
|
} |
791
|
|
|
|
792
|
|
|
$sortings[] = new FieldSorting($part, $direction); |
793
|
|
|
} |
794
|
|
|
|
795
|
|
|
return $sortings; |
796
|
|
|
} |
797
|
|
|
} |
798
|
|
|
|
If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:
If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.