Completed
Push — labs ( 04d0b8...211687 )
by Christian
14:25
created

ApiController::searchAction()   B

Complexity

Conditions 2
Paths 2

Size

Total Lines 25
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 25
c 0
b 0
f 0
rs 8.8571
ccs 13
cts 13
cp 1
cc 2
eloc 14
nc 2
nop 3
crap 2
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()),
0 ignored issues
show
Bug introduced by
It seems like $definition can also be of type object<Shopware\Api\Entity\EntityDefinition>; however, Shopware\Rest\Controller...createListingCriteria() does only seem to accept string, maybe add an additional type check?

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:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
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());
0 ignored issues
show
Bug introduced by
It seems like $definition can also be of type object<Shopware\Api\Entity\EntityDefinition>; however, Shopware\Rest\Controller...createListingCriteria() does only seem to accept string, maybe add an additional type check?

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:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
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,
0 ignored issues
show
Bug introduced by
It seems like $definition can also be of type object<Shopware\Api\Entity\EntityDefinition>; however, Shopware\Api\Entity\Sear...uilder::handleRequest() does only seem to accept string, maybe add an additional type check?

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:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
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);
0 ignored issues
show
Bug introduced by
It seems like $reference can also be of type object<Shopware\Api\Entity\EntityDefinition>; however, Shopware\Rest\Controller...executeWriteOperation() does only seem to accept string, maybe add an additional type check?

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:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
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);
0 ignored issues
show
Bug introduced by
It seems like $reference can also be of type object<Shopware\Api\Entity\EntityDefinition>; however, Shopware\Rest\Response\R...reateRedirectResponse() does only seem to accept string, maybe add an additional type check?

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:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
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:
0 ignored issues
show
Coding Style introduced by
The case body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a case statement must start on the line immediately following the case statement.

switch ($expr) {
case "A":
    doSomething(); //right
    break;
case "B":

    doSomethingElse(); //wrong
    break;

}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
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'));
0 ignored issues
show
Bug introduced by
It seems like $definition can also be of type object<Shopware\Api\Entity\EntityDefinition>; however, Shopware\Rest\Controller...roller::parseSortings() does only seem to accept string, maybe add an additional type check?

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:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
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');
0 ignored issues
show
Bug introduced by
It seems like $request->getContent() targeting Symfony\Component\HttpFo...n\Request::getContent() can also be of type resource; however, Symfony\Component\Serializer\Serializer::decode() does only seem to accept string, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
696
                case 'application/json':
697
                    return $this->serializer->decode($request->getContent(), 'json');
0 ignored issues
show
Bug introduced by
It seems like $request->getContent() targeting Symfony\Component\HttpFo...n\Request::getContent() can also be of type resource; however, Symfony\Component\Serializer\Serializer::decode() does only seem to accept string, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
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