vendor/shopware/core/Framework/Api/Controller/ApiController.php line 424

Open in your IDE?
  1. <?php declare(strict_types=1);
  2. namespace Shopware\Core\Framework\Api\Controller;
  3. use OpenApi\Annotations as OA;
  4. use Shopware\Core\Defaults;
  5. use Shopware\Core\Framework\Api\Acl\AclCriteriaValidator;
  6. use Shopware\Core\Framework\Api\Acl\Role\AclRoleDefinition;
  7. use Shopware\Core\Framework\Api\Converter\ApiVersionConverter;
  8. use Shopware\Core\Framework\Api\Converter\Exceptions\ApiConversionException;
  9. use Shopware\Core\Framework\Api\Exception\InvalidVersionNameException;
  10. use Shopware\Core\Framework\Api\Exception\LiveVersionDeleteException;
  11. use Shopware\Core\Framework\Api\Exception\MissingPrivilegeException;
  12. use Shopware\Core\Framework\Api\Exception\NoEntityClonedException;
  13. use Shopware\Core\Framework\Api\Exception\ResourceNotFoundException;
  14. use Shopware\Core\Framework\Api\Response\ResponseFactoryInterface;
  15. use Shopware\Core\Framework\Context;
  16. use Shopware\Core\Framework\DataAbstractionLayer\DefinitionInstanceRegistry;
  17. use Shopware\Core\Framework\DataAbstractionLayer\Entity;
  18. use Shopware\Core\Framework\DataAbstractionLayer\EntityCollection;
  19. use Shopware\Core\Framework\DataAbstractionLayer\EntityDefinition;
  20. use Shopware\Core\Framework\DataAbstractionLayer\EntityProtection\EntityProtectionValidator;
  21. use Shopware\Core\Framework\DataAbstractionLayer\EntityProtection\ReadProtection;
  22. use Shopware\Core\Framework\DataAbstractionLayer\EntityProtection\WriteProtection;
  23. use Shopware\Core\Framework\DataAbstractionLayer\EntityRepository;
  24. use Shopware\Core\Framework\DataAbstractionLayer\EntityTranslationDefinition;
  25. use Shopware\Core\Framework\DataAbstractionLayer\Event\EntityWrittenContainerEvent;
  26. use Shopware\Core\Framework\DataAbstractionLayer\Exception\DefinitionNotFoundException;
  27. use Shopware\Core\Framework\DataAbstractionLayer\Exception\MissingReverseAssociation;
  28. use Shopware\Core\Framework\DataAbstractionLayer\Field\AssociationField;
  29. use Shopware\Core\Framework\DataAbstractionLayer\Field\Field;
  30. use Shopware\Core\Framework\DataAbstractionLayer\Field\ManyToManyAssociationField;
  31. use Shopware\Core\Framework\DataAbstractionLayer\Field\ManyToOneAssociationField;
  32. use Shopware\Core\Framework\DataAbstractionLayer\Field\OneToManyAssociationField;
  33. use Shopware\Core\Framework\DataAbstractionLayer\Field\OneToOneAssociationField;
  34. use Shopware\Core\Framework\DataAbstractionLayer\Field\TranslationsAssociationField;
  35. use Shopware\Core\Framework\DataAbstractionLayer\FieldCollection;
  36. use Shopware\Core\Framework\DataAbstractionLayer\Search\CompositeEntitySearcher;
  37. use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
  38. use Shopware\Core\Framework\DataAbstractionLayer\Search\EntitySearchResult;
  39. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
  40. use Shopware\Core\Framework\DataAbstractionLayer\Search\IdSearchResult;
  41. use Shopware\Core\Framework\DataAbstractionLayer\Search\RequestCriteriaBuilder;
  42. use Shopware\Core\Framework\DataAbstractionLayer\Write\CloneBehavior;
  43. use Shopware\Core\Framework\Routing\Annotation\RouteScope;
  44. use Shopware\Core\Framework\Routing\Annotation\Since;
  45. use Shopware\Core\Framework\Uuid\Exception\InvalidUuidException;
  46. use Shopware\Core\Framework\Uuid\Uuid;
  47. use Shopware\Core\PlatformRequest;
  48. use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
  49. use Symfony\Component\HttpFoundation\JsonResponse;
  50. use Symfony\Component\HttpFoundation\Request;
  51. use Symfony\Component\HttpFoundation\Response;
  52. use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
  53. use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
  54. use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
  55. use Symfony\Component\HttpKernel\Exception\UnsupportedMediaTypeHttpException;
  56. use Symfony\Component\Routing\Annotation\Route;
  57. use Symfony\Component\Serializer\Exception\InvalidArgumentException;
  58. use Symfony\Component\Serializer\Exception\UnexpectedValueException;
  59. use Symfony\Component\Serializer\Serializer;
  60. /**
  61.  * @RouteScope(scopes={"api"})
  62.  */
  63. class ApiController extends AbstractController
  64. {
  65.     public const WRITE_UPDATE 'update';
  66.     public const WRITE_CREATE 'create';
  67.     public const WRITE_DELETE 'delete';
  68.     /**
  69.      * @var DefinitionInstanceRegistry
  70.      */
  71.     private $definitionRegistry;
  72.     /**
  73.      * @var Serializer
  74.      */
  75.     private $serializer;
  76.     /**
  77.      * @var RequestCriteriaBuilder
  78.      */
  79.     private $searchCriteriaBuilder;
  80.     /**
  81.      * @var CompositeEntitySearcher
  82.      */
  83.     private $compositeEntitySearcher;
  84.     /**
  85.      * @var ApiVersionConverter
  86.      */
  87.     private $apiVersionConverter;
  88.     /**
  89.      * @var EntityProtectionValidator
  90.      */
  91.     private $entityProtectionValidator;
  92.     /**
  93.      * @var AclCriteriaValidator
  94.      */
  95.     private $criteriaValidator;
  96.     public function __construct(
  97.         DefinitionInstanceRegistry $definitionRegistry,
  98.         Serializer $serializer,
  99.         RequestCriteriaBuilder $searchCriteriaBuilder,
  100.         CompositeEntitySearcher $compositeEntitySearcher,
  101.         ApiVersionConverter $apiVersionConverter,
  102.         EntityProtectionValidator $entityProtectionValidator,
  103.         AclCriteriaValidator $criteriaValidator
  104.     ) {
  105.         $this->definitionRegistry $definitionRegistry;
  106.         $this->serializer $serializer;
  107.         $this->searchCriteriaBuilder $searchCriteriaBuilder;
  108.         $this->compositeEntitySearcher $compositeEntitySearcher;
  109.         $this->apiVersionConverter $apiVersionConverter;
  110.         $this->entityProtectionValidator $entityProtectionValidator;
  111.         $this->criteriaValidator $criteriaValidator;
  112.     }
  113.     /**
  114.      * @Since("6.0.0.0")
  115.      * @OA\Get(
  116.      *      path="/_search",
  117.      *      summary="Search for multiple entites by a given term",
  118.      *      operationId="compositeSearch",
  119.      *      tags={"Admin Api"},
  120.      *      @OA\Parameter(
  121.      *          name="limit",
  122.      *          in="query",
  123.      *          description="Max amount of resources per entity",
  124.      *          @OA\Schema(type="integer"),
  125.      *      ),
  126.      *      @OA\Parameter(
  127.      *          name="term",
  128.      *          in="query",
  129.      *          description="The term to search for",
  130.      *          required=true,
  131.      *          @OA\Schema(type="string")
  132.      *      ),
  133.      *      @OA\Response(
  134.      *          response="200",
  135.      *          description="The list of found entities",
  136.      *          @OA\JsonContent(
  137.      *              type="array",
  138.      *              @OA\Items(
  139.      *                  type="object",
  140.      *                  @OA\Property(
  141.      *                      property="entity",
  142.      *                      type="string",
  143.      *                      description="The name of the entity",
  144.      *                  ),
  145.      *                  @OA\Property(
  146.      *                      property="total",
  147.      *                      type="integer",
  148.      *                      description="The total amount of search results for this entity",
  149.      *                  ),
  150.      *                  @OA\Property(
  151.      *                      property="entities",
  152.      *                      type="array",
  153.      *                      description="The found entities",
  154.      *                      @OA\Items(type="object", additionalProperties=true),
  155.      *                  ),
  156.      *              ),
  157.      *          ),
  158.      *      ),
  159.      *      @OA\Response(
  160.      *          response="400",
  161.      *          ref="#/components/responses/400"
  162.      *      ),
  163.      *     @OA\Response(
  164.      *          response="401",
  165.      *          ref="#/components/responses/401"
  166.      *      )
  167.      * )
  168.      * @Route("/api/v{version}/_search", name="api.composite.search", methods={"GET","POST"}, requirements={"version"="\d+"})
  169.      */
  170.     public function compositeSearch(Request $requestContext $contextint $version): JsonResponse
  171.     {
  172.         $term $request->query->get('term');
  173.         $limit $request->query->getInt('limit'5);
  174.         $results $this->compositeEntitySearcher->search($term$limit$context$version);
  175.         foreach ($results as &$result) {
  176.             $definition $this->definitionRegistry->getByEntityName($result['entity']);
  177.             /** @var EntityCollection $entityCollection */
  178.             $entityCollection $result['entities'];
  179.             $entities = [];
  180.             foreach ($entityCollection->getElements() as $key => $entity) {
  181.                 $entities[$key] = $this->apiVersionConverter->convertEntity($definition$entity$version);
  182.             }
  183.             $result['entities'] = $entities;
  184.         }
  185.         return new JsonResponse(['data' => $results]);
  186.     }
  187.     /**
  188.      * @Since("6.0.0.0")
  189.      * @Route("/api/v{version}/_action/clone/{entity}/{id}", name="api.clone", methods={"POST"}, requirements={
  190.      *     "version"="\d+", "entity"="[a-zA-Z-]+", "id"="[0-9a-f]{32}"
  191.      * })
  192.      *
  193.      * @throws DefinitionNotFoundException
  194.      */
  195.     public function clone(Context $contextstring $entitystring $idint $versionRequest $request): JsonResponse
  196.     {
  197.         $behavior = new CloneBehavior(
  198.             $request->request->get('overwrites', []),
  199.             $request->request->get('cloneChildren'true)
  200.         );
  201.         $entity $this->urlToSnakeCase($entity);
  202.         $this->checkIfRouteAvailableInApiVersion($entity$version);
  203.         $definition $this->definitionRegistry->getByEntityName($entity);
  204.         $missing $this->validateAclPermissions($context$definitionAclRoleDefinition::PRIVILEGE_CREATE);
  205.         if ($missing) {
  206.             throw new MissingPrivilegeException([$missing]);
  207.         }
  208.         $eventContainer $context->scope(Context::CRUD_API_SCOPE, function (Context $context) use ($definition$id$behavior): EntityWrittenContainerEvent {
  209.             /** @var EntityRepository $entityRepo */
  210.             $entityRepo $this->definitionRegistry->getRepository($definition->getEntityName());
  211.             return $entityRepo->clone($id$contextnull$behavior);
  212.         });
  213.         $event $eventContainer->getEventByEntityName($definition->getEntityName());
  214.         if (!$event) {
  215.             throw new NoEntityClonedException($entity$id);
  216.         }
  217.         $ids $event->getIds();
  218.         $newId array_shift($ids);
  219.         return new JsonResponse(['id' => $newId]);
  220.     }
  221.     /**
  222.      * @Since("6.0.0.0")
  223.      * @Route("/api/v{version}/_action/version/{entity}/{id}", name="api.createVersion", methods={"POST"},
  224.      *     requirements={"version"="\d+", "entity"="[a-zA-Z-]+", "id"="[0-9a-f]{32}"
  225.      * })
  226.      *
  227.      * @throws InvalidUuidException
  228.      * @throws InvalidVersionNameException
  229.      */
  230.     public function createVersion(Request $requestContext $contextstring $entitystring $idint $version): Response
  231.     {
  232.         $entity $this->urlToSnakeCase($entity);
  233.         $this->checkIfRouteAvailableInApiVersion($entity$version);
  234.         $versionId $request->request->get('versionId');
  235.         $versionName $request->request->get('versionName');
  236.         if ($versionId !== null && !Uuid::isValid($versionId)) {
  237.             throw new InvalidUuidException($versionId);
  238.         }
  239.         if ($versionName !== null && !ctype_alnum($versionName)) {
  240.             throw new InvalidVersionNameException();
  241.         }
  242.         try {
  243.             $entityDefinition $this->definitionRegistry->getByEntityName($entity);
  244.         } catch (DefinitionNotFoundException $e) {
  245.             throw new NotFoundHttpException($e->getMessage(), $e);
  246.         }
  247.         $versionId $context->scope(Context::CRUD_API_SCOPE, function (Context $context) use ($entityDefinition$id$versionName$versionId): string {
  248.             return $this->definitionRegistry->getRepository($entityDefinition->getEntityName())->createVersion($id$context$versionName$versionId);
  249.         });
  250.         return new JsonResponse([
  251.             'versionId' => $versionId,
  252.             'versionName' => $versionName,
  253.             'id' => $id,
  254.             'entity' => $entity,
  255.         ]);
  256.     }
  257.     /**
  258.      * @Since("6.0.0.0")
  259.      * @Route("/api/v{version}/_action/version/merge/{entity}/{versionId}", name="api.mergeVersion", methods={"POST"},
  260.      *     requirements={"version"="\d+", "entity"="[a-zA-Z-]+", "versionId"="[0-9a-f]{32}"
  261.      * })
  262.      *
  263.      * @throws InvalidUuidException
  264.      */
  265.     public function mergeVersion(Context $contextstring $entitystring $versionIdint $version): JsonResponse
  266.     {
  267.         $entity $this->urlToSnakeCase($entity);
  268.         $this->checkIfRouteAvailableInApiVersion($entity$version);
  269.         if (!Uuid::isValid($versionId)) {
  270.             throw new InvalidUuidException($versionId);
  271.         }
  272.         $entityDefinition $this->getEntityDefinition($entity);
  273.         $repository $this->definitionRegistry->getRepository($entityDefinition->getEntityName());
  274.         // change scope to be able to update write protected fields
  275.         $context->scope(Context::SYSTEM_SCOPE, function (Context $context) use ($repository$versionId): void {
  276.             $repository->merge($versionId$context);
  277.         });
  278.         return new JsonResponse(nullResponse::HTTP_NO_CONTENT);
  279.     }
  280.     /**
  281.      * @Since("6.0.0.0")
  282.      * @Route("/api/v{version}/_action/version/{versionId}/{entity}/{entityId}", name="api.deleteVersion", methods={"POST"},
  283.      *     requirements={"version"="\d+", "entity"="[a-zA-Z-]+", "id"="[0-9a-f]{32}"
  284.      * })
  285.      *
  286.      * @throws InvalidUuidException
  287.      * @throws InvalidVersionNameException
  288.      * @throws LiveVersionDeleteException
  289.      */
  290.     public function deleteVersion(Context $contextstring $entitystring $entityIdstring $versionId): JsonResponse
  291.     {
  292.         if ($versionId !== null && !Uuid::isValid($versionId)) {
  293.             throw new InvalidUuidException($versionId);
  294.         }
  295.         if ($versionId === Defaults::LIVE_VERSION) {
  296.             throw new LiveVersionDeleteException();
  297.         }
  298.         if ($entityId !== null && !Uuid::isValid($entityId)) {
  299.             throw new InvalidUuidException($entityId);
  300.         }
  301.         try {
  302.             $entityDefinition $this->definitionRegistry->getByEntityName($this->urlToSnakeCase($entity));
  303.         } catch (DefinitionNotFoundException $e) {
  304.             throw new NotFoundHttpException($e->getMessage(), $e);
  305.         }
  306.         $versionContext $context->createWithVersionId($versionId);
  307.         $entityRepository $this->definitionRegistry->getRepository($entityDefinition->getEntityName());
  308.         $versionContext->scope(Context::CRUD_API_SCOPE, function (Context $versionContext) use ($entityId$entityRepository): void {
  309.             $entityRepository->delete([['id' => $entityId]], $versionContext);
  310.         });
  311.         $versionRepository $this->definitionRegistry->getRepository('version');
  312.         $versionRepository->delete([['id' => $versionId]], $context);
  313.         return new JsonResponse();
  314.     }
  315.     public function detail(Request $requestContext $contextResponseFactoryInterface $responseFactorystring $entityNamestring $path): Response
  316.     {
  317.         $pathSegments $this->buildEntityPath($entityName$path$request->attributes->getInt('version'), $context);
  318.         $permissions $this->validatePathSegments($context$pathSegmentsAclRoleDefinition::PRIVILEGE_READ);
  319.         $root $pathSegments[0]['entity'];
  320.         $id $pathSegments[\count($pathSegments) - 1]['value'];
  321.         $definition $this->definitionRegistry->getByEntityName($root);
  322.         $associations array_column($pathSegments'entity');
  323.         array_shift($associations);
  324.         if (empty($associations)) {
  325.             $repository $this->definitionRegistry->getRepository($definition->getEntityName());
  326.         } else {
  327.             $field $this->getAssociation($definition->getFields(), $associations);
  328.             $definition $field->getReferenceDefinition();
  329.             if ($field instanceof ManyToManyAssociationField) {
  330.                 $definition $field->getToManyReferenceDefinition();
  331.             }
  332.             $repository $this->definitionRegistry->getRepository($definition->getEntityName());
  333.         }
  334.         $criteria = new Criteria();
  335.         $criteria $this->searchCriteriaBuilder->handleRequest($request$criteria$definition$context);
  336.         $criteria->setIds([$id]);
  337.         // trigger acl validation
  338.         $missing $this->criteriaValidator->validate($definition->getEntityName(), $criteria$context);
  339.         $permissions array_unique(array_filter(array_merge($permissions$missing)));
  340.         if (!empty($permissions)) {
  341.             throw new MissingPrivilegeException($permissions);
  342.         }
  343.         $entity $context->scope(Context::CRUD_API_SCOPE, function (Context $context) use ($repository$criteria$id): ?Entity {
  344.             return $repository->search($criteria$context)->get($id);
  345.         });
  346.         if ($entity === null) {
  347.             throw new ResourceNotFoundException($definition->getEntityName(), ['id' => $id]);
  348.         }
  349.         return $responseFactory->createDetailResponse($criteria$entity$definition$request$context);
  350.     }
  351.     public function searchIds(Request $requestContext $contextResponseFactoryInterface $responseFactorystring $entityNamestring $path): Response
  352.     {
  353.         [$criteria$repository] = $this->resolveSearch($request$context$entityName$path);
  354.         $result $context->scope(Context::CRUD_API_SCOPE, function (Context $context) use ($repository$criteria): IdSearchResult {
  355.             return $repository->searchIds($criteria$context);
  356.         });
  357.         return new JsonResponse([
  358.             'total' => $result->getTotal(),
  359.             'data' => array_values($result->getIds()),
  360.         ]);
  361.     }
  362.     public function search(Request $requestContext $contextResponseFactoryInterface $responseFactorystring $entityNamestring $path): Response
  363.     {
  364.         [$criteria$repository] = $this->resolveSearch($request$context$entityName$path);
  365.         $result $context->scope(Context::CRUD_API_SCOPE, function (Context $context) use ($repository$criteria): EntitySearchResult {
  366.             return $repository->search($criteria$context);
  367.         });
  368.         $definition $this->getDefinitionOfPath($entityName$path$request$context);
  369.         return $responseFactory->createListingResponse($criteria$result$definition$request$context);
  370.     }
  371.     public function list(Request $requestContext $contextResponseFactoryInterface $responseFactorystring $entityNamestring $path): Response
  372.     {
  373.         [$criteria$repository] = $this->resolveSearch($request$context$entityName$path);
  374.         $result $context->scope(Context::CRUD_API_SCOPE, function (Context $context) use ($repository$criteria): EntitySearchResult {
  375.             return $repository->search($criteria$context);
  376.         });
  377.         $definition $this->getDefinitionOfPath($entityName$path$request$context);
  378.         return $responseFactory->createListingResponse($criteria$result$definition$request$context);
  379.     }
  380.     public function create(Request $requestContext $contextResponseFactoryInterface $responseFactorystring $entityNamestring $path): Response
  381.     {
  382.         return $this->write($request$context$responseFactory$entityName$pathself::WRITE_CREATE);
  383.     }
  384.     public function update(Request $requestContext $contextResponseFactoryInterface $responseFactorystring $entityNamestring $path): Response
  385.     {
  386.         return $this->write($request$context$responseFactory$entityName$pathself::WRITE_UPDATE);
  387.     }
  388.     public function delete(Request $requestContext $contextResponseFactoryInterface $responseFactorystring $entityNamestring $path): Response
  389.     {
  390.         $pathSegments $this->buildEntityPath($entityName$path$request->attributes->getInt('version'), $context, [WriteProtection::class]);
  391.         $last $pathSegments[\count($pathSegments) - 1];
  392.         $id $last['value'];
  393.         $first array_shift($pathSegments);
  394.         /* @var EntityDefinition $definition */
  395.         if (\count($pathSegments) === 0) {
  396.             //first api level call /product/{id}
  397.             $definition $first['definition'];
  398.             $this->executeWriteOperation($definition, ['id' => $id], $contextself::WRITE_DELETE$request->attributes->getInt('version'));
  399.             return $responseFactory->createRedirectResponse($definition$id$request$context);
  400.         }
  401.         $child array_pop($pathSegments);
  402.         $parent $first;
  403.         if (!empty($pathSegments)) {
  404.             $parent array_pop($pathSegments);
  405.         }
  406.         $definition $child['definition'];
  407.         /** @var AssociationField $association */
  408.         $association $child['field'];
  409.         // DELETE api/product/{id}/manufacturer/{id}
  410.         if ($association instanceof ManyToOneAssociationField || $association instanceof OneToOneAssociationField) {
  411.             $this->executeWriteOperation($definition, ['id' => $id], $contextself::WRITE_DELETE$request->attributes->getInt('version'));
  412.             return $responseFactory->createRedirectResponse($definition$id$request$context);
  413.         }
  414.         // DELETE api/product/{id}/category/{id}
  415.         if ($association instanceof ManyToManyAssociationField) {
  416.             $local $definition->getFields()->getByStorageName(
  417.                 $association->getMappingLocalColumn()
  418.             );
  419.             $reference $definition->getFields()->getByStorageName(
  420.                 $association->getMappingReferenceColumn()
  421.             );
  422.             $mapping = [
  423.                 $local->getPropertyName() => $parent['value'],
  424.                 $reference->getPropertyName() => $id,
  425.             ];
  426.             /** @var EntityDefinition $parentDefinition */
  427.             $parentDefinition $parent['definition'];
  428.             if ($parentDefinition->isVersionAware()) {
  429.                 $versionField $parentDefinition->getEntityName() . 'VersionId';
  430.                 $mapping[$versionField] = $context->getVersionId();
  431.             }
  432.             if ($association->getToManyReferenceDefinition()->isVersionAware()) {
  433.                 $versionField $association->getToManyReferenceDefinition()->getEntityName() . 'VersionId';
  434.                 $mapping[$versionField] = Defaults::LIVE_VERSION;
  435.             }
  436.             $this->executeWriteOperation($definition$mapping$contextself::WRITE_DELETE$request->attributes->getInt('version'));
  437.             return $responseFactory->createRedirectResponse($definition$id$request$context);
  438.         }
  439.         if ($association instanceof TranslationsAssociationField) {
  440.             /** @var EntityTranslationDefinition $refClass */
  441.             $refClass $association->getReferenceDefinition();
  442.             $refPropName $refClass->getFields()->getByStorageName($association->getReferenceField())->getPropertyName();
  443.             $refLanguagePropName $refClass->getPrimaryKeys()->getByStorageName($association->getLanguageField())->getPropertyName();
  444.             $mapping = [
  445.                 $refPropName => $parent['value'],
  446.                 $refLanguagePropName => $id,
  447.             ];
  448.             $this->executeWriteOperation($definition$mapping$contextself::WRITE_DELETE$request->attributes->getInt('version'));
  449.             return $responseFactory->createRedirectResponse($definition$id$request$context);
  450.         }
  451.         if ($association instanceof OneToManyAssociationField) {
  452.             $this->executeWriteOperation($definition, ['id' => $id], $contextself::WRITE_DELETE$request->attributes->getInt('version'));
  453.             return $responseFactory->createRedirectResponse($definition$id$request$context);
  454.         }
  455.         throw new \RuntimeException(sprintf('Unsupported association for field %s'$association->getPropertyName()));
  456.     }
  457.     private function resolveSearch(Request $requestContext $contextstring $entityNamestring $path): array
  458.     {
  459.         $pathSegments $this->buildEntityPath($entityName$path$request->attributes->getInt('version'), $context);
  460.         $permissions $this->validatePathSegments($context$pathSegmentsAclRoleDefinition::PRIVILEGE_READ);
  461.         $first array_shift($pathSegments);
  462.         /** @var EntityDefinition|string $definition */
  463.         $definition $first['definition'];
  464.         if (!$definition) {
  465.             throw new NotFoundHttpException('The requested entity does not exist.');
  466.         }
  467.         $repository $this->definitionRegistry->getRepository($definition->getEntityName());
  468.         $criteria = new Criteria();
  469.         if (empty($pathSegments)) {
  470.             $criteria $this->searchCriteriaBuilder->handleRequest($request$criteria$definition$context);
  471.             // trigger acl validation
  472.             $nested $this->criteriaValidator->validate($definition->getEntityName(), $criteria$context);
  473.             $permissions array_unique(array_filter(array_merge($permissions$nested)));
  474.             if (!empty($permissions)) {
  475.                 throw new MissingPrivilegeException($permissions);
  476.             }
  477.             return [$criteria$repository];
  478.         }
  479.         $child array_pop($pathSegments);
  480.         $parent $first;
  481.         if (!empty($pathSegments)) {
  482.             $parent array_pop($pathSegments);
  483.         }
  484.         $association $child['field'];
  485.         $parentDefinition $parent['definition'];
  486.         $definition $child['definition'];
  487.         if ($association instanceof ManyToManyAssociationField) {
  488.             $definition $association->getToManyReferenceDefinition();
  489.         }
  490.         $criteria $this->searchCriteriaBuilder->handleRequest($request$criteria$definition$context);
  491.         if ($association instanceof ManyToManyAssociationField) {
  492.             //fetch inverse association definition for filter
  493.             $reverse $definition->getFields()->filter(
  494.                 function (Field $field) use ($association) {
  495.                     return $field instanceof ManyToManyAssociationField && $association->getMappingDefinition() === $field->getMappingDefinition();
  496.                 }
  497.             );
  498.             //contains now the inverse side association: category.products
  499.             $reverse $reverse->first();
  500.             if (!$reverse) {
  501.                 throw new MissingReverseAssociation($definition->getEntityName(), $parentDefinition);
  502.             }
  503.             /* @var ManyToManyAssociationField $reverse */
  504.             $criteria->addFilter(
  505.                 new EqualsFilter(
  506.                     sprintf('%s.%s.id'$definition->getEntityName(), $reverse->getPropertyName()),
  507.                     $parent['value']
  508.                 )
  509.             );
  510.             /** @var EntityDefinition $parentDefinition */
  511.             if ($parentDefinition->isVersionAware()) {
  512.                 $criteria->addFilter(
  513.                     new EqualsFilter(
  514.                         sprintf('%s.%s.versionId'$definition->getEntityName(), $reverse->getPropertyName()),
  515.                         $context->getVersionId()
  516.                     )
  517.                 );
  518.             }
  519.         } elseif ($association instanceof OneToManyAssociationField) {
  520.             /*
  521.              * Example
  522.              * Route:           /api/product/SW1/prices
  523.              * $definition:     \Shopware\Core\Content\Product\Definition\ProductPriceDefinition
  524.              */
  525.             //get foreign key definition of reference
  526.             $foreignKey $definition->getFields()->getByStorageName(
  527.                 $association->getReferenceField()
  528.             );
  529.             $criteria->addFilter(
  530.                 new EqualsFilter(
  531.                 //add filter to parent value: prices.productId = SW1
  532.                     $definition->getEntityName() . '.' $foreignKey->getPropertyName(),
  533.                     $parent['value']
  534.                 )
  535.             );
  536.         } elseif ($association instanceof ManyToOneAssociationField) {
  537.             /*
  538.              * Example
  539.              * Route:           /api/product/SW1/manufacturer
  540.              * $definition:     \Shopware\Core\Content\Product\Aggregate\ProductManufacturer\ProductManufacturerDefinition
  541.              */
  542.             //get inverse association to filter to parent value
  543.             $reverse $definition->getFields()->filter(
  544.                 function (Field $field) use ($parentDefinition) {
  545.                     return $field instanceof AssociationField && $parentDefinition === $field->getReferenceDefinition();
  546.                 }
  547.             );
  548.             $reverse $reverse->first();
  549.             if (!$reverse) {
  550.                 throw new MissingReverseAssociation($definition->getEntityName(), $parentDefinition);
  551.             }
  552.             /* @var OneToManyAssociationField $reverse */
  553.             $criteria->addFilter(
  554.                 new EqualsFilter(
  555.                 //filter inverse association to parent value:  manufacturer.products.id = SW1
  556.                     sprintf('%s.%s.id'$definition->getEntityName(), $reverse->getPropertyName()),
  557.                     $parent['value']
  558.                 )
  559.             );
  560.         } elseif ($association instanceof OneToOneAssociationField) {
  561.             /*
  562.              * Example
  563.              * Route:           /api/order/xxxx/orderCustomer
  564.              * $definition:     \Shopware\Core\Checkout\Order\Aggregate\OrderCustomer\OrderCustomerDefinition
  565.              */
  566.             //get inverse association to filter to parent value
  567.             $reverse $definition->getFields()->filter(
  568.                 function (Field $field) use ($parentDefinition) {
  569.                     return $field instanceof OneToOneAssociationField && $parentDefinition === $field->getReferenceDefinition();
  570.                 }
  571.             );
  572.             $reverse $reverse->first();
  573.             if (!$reverse) {
  574.                 throw new MissingReverseAssociation($definition->getEntityName(), $parentDefinition);
  575.             }
  576.             /* @var OneToManyAssociationField $reverse */
  577.             $criteria->addFilter(
  578.                 new EqualsFilter(
  579.                 //filter inverse association to parent value:  order_customer.order_id = xxxx
  580.                     sprintf('%s.%s.id'$definition->getEntityName(), $reverse->getPropertyName()),
  581.                     $parent['value']
  582.                 )
  583.             );
  584.         }
  585.         $repository $this->definitionRegistry->getRepository($definition->getEntityName());
  586.         $nested $this->criteriaValidator->validate($definition->getEntityName(), $criteria$context);
  587.         $permissions array_unique(array_filter(array_merge($permissions$nested)));
  588.         if (!empty($permissions)) {
  589.             throw new MissingPrivilegeException($permissions);
  590.         }
  591.         return [$criteria$repository];
  592.     }
  593.     private function getDefinitionOfPath(string $entityNamestring $pathRequest $requestContext $context): EntityDefinition
  594.     {
  595.         $pathSegments $this->buildEntityPath($entityName$path$request->attributes->getInt('version'), $context);
  596.         $first array_shift($pathSegments);
  597.         /** @var EntityDefinition|string $definition */
  598.         $definition $first['definition'];
  599.         if (empty($pathSegments)) {
  600.             return $definition;
  601.         }
  602.         $child array_pop($pathSegments);
  603.         $association $child['field'];
  604.         if ($association instanceof ManyToManyAssociationField) {
  605.             /*
  606.              * Example:
  607.              * route:           /api/product/SW1/categories
  608.              * $definition:     \Shopware\Core\Content\Category\CategoryDefinition
  609.              */
  610.             return $association->getToManyReferenceDefinition();
  611.         }
  612.         return $child['definition'];
  613.     }
  614.     private function write(Request $requestContext $contextResponseFactoryInterface $responseFactorystring $entityNamestring $pathstring $type): Response
  615.     {
  616.         $payload $this->getRequestBody($request);
  617.         $noContent = !$request->query->has('_response');
  618.         // safari bug prevents us from using the location header
  619.         $appendLocationHeader false;
  620.         if ($this->isCollection($payload)) {
  621.             throw new BadRequestHttpException('Only single write operations are supported. Please send the entities one by one or use the /sync api endpoint.');
  622.         }
  623.         $pathSegments $this->buildEntityPath($entityName$path$request->attributes->getInt('version'), $context, [WriteProtection::class]);
  624.         $last $pathSegments[\count($pathSegments) - 1];
  625.         if ($type === self::WRITE_CREATE && !empty($last['value'])) {
  626.             $methods = ['GET''PATCH''DELETE'];
  627.             throw new MethodNotAllowedHttpException($methodssprintf('No route found for "%s %s": Method Not Allowed (Allow: %s)'$request->getMethod(), $request->getPathInfo(), implode(', '$methods)));
  628.         }
  629.         if ($type === self::WRITE_UPDATE && isset($last['value'])) {
  630.             $payload['id'] = $last['value'];
  631.         }
  632.         $first array_shift($pathSegments);
  633.         if (\count($pathSegments) === 0) {
  634.             $definition $first['definition'];
  635.             $events $this->executeWriteOperation($definition$payload$context$type$request->attributes->getInt('version'));
  636.             $event $events->getEventByEntityName($definition->getEntityName());
  637.             $eventIds $event->getIds();
  638.             $entityId array_pop($eventIds);
  639.             if ($noContent) {
  640.                 return $responseFactory->createRedirectResponse($definition$entityId$request$context);
  641.             }
  642.             $repository $this->definitionRegistry->getRepository($definition->getEntityName());
  643.             $criteria = new Criteria($event->getIds());
  644.             $entities $repository->search($criteria$context);
  645.             return $responseFactory->createDetailResponse($criteria$entities->first(), $definition$request$context$appendLocationHeader);
  646.         }
  647.         $child array_pop($pathSegments);
  648.         $parent $first;
  649.         if (!empty($pathSegments)) {
  650.             $parent array_pop($pathSegments);
  651.         }
  652.         /** @var EntityDefinition $definition */
  653.         $definition $child['definition'];
  654.         $association $child['field'];
  655.         $parentDefinition $parent['definition'];
  656.         /* @var Entity $entity */
  657.         if ($association instanceof OneToManyAssociationField) {
  658.             $foreignKey $definition->getFields()
  659.                 ->getByStorageName($association->getReferenceField());
  660.             $payload[$foreignKey->getPropertyName()] = $parent['value'];
  661.             $events $this->executeWriteOperation($definition$payload$context$type$request->attributes->getInt('version'));
  662.             if ($noContent) {
  663.                 return $responseFactory->createRedirectResponse($definition$parent['value'], $request$context);
  664.             }
  665.             $event $events->getEventByEntityName($definition->getEntityName());
  666.             $repository $this->definitionRegistry->getRepository($definition->getEntityName());
  667.             $criteria = new Criteria($event->getIds());
  668.             $entities $repository->search($criteria$context);
  669.             return $responseFactory->createDetailResponse($criteria$entities->first(), $definition$request$context$appendLocationHeader);
  670.         }
  671.         if ($association instanceof ManyToOneAssociationField || $association instanceof OneToOneAssociationField) {
  672.             $events $this->executeWriteOperation($definition$payload$context$type$request->attributes->getInt('version'));
  673.             $event $events->getEventByEntityName($definition->getEntityName());
  674.             $entityIds $event->getIds();
  675.             $entityId array_pop($entityIds);
  676.             $foreignKey $parentDefinition->getFields()->getByStorageName($association->getStorageName());
  677.             $payload = [
  678.                 'id' => $parent['value'],
  679.                 $foreignKey->getPropertyName() => $entityId,
  680.             ];
  681.             $repository $this->definitionRegistry->getRepository($parentDefinition->getEntityName());
  682.             $repository->update([$payload], $context);
  683.             if ($noContent) {
  684.                 return $responseFactory->createRedirectResponse($definition$entityId$request$context);
  685.             }
  686.             $criteria = new Criteria($event->getIds());
  687.             $entities $repository->search($criteria$context);
  688.             return $responseFactory->createDetailResponse($criteria$entities->first(), $definition$request$context$appendLocationHeader);
  689.         }
  690.         /** @var ManyToManyAssociationField $manyToManyAssociation */
  691.         $manyToManyAssociation $association;
  692.         /** @var EntityDefinition|string $reference */
  693.         $reference $manyToManyAssociation->getToManyReferenceDefinition();
  694.         // check if we need to create the entity first
  695.         if (\count($payload) > || !\array_key_exists('id'$payload)) {
  696.             $events $this->executeWriteOperation($reference$payload$context$type$request->attributes->getInt('version'));
  697.             $event $events->getEventByEntityName($reference->getEntityName());
  698.             $ids $event->getIds();
  699.             $id array_shift($ids);
  700.         } else {
  701.             // only id provided - add assignment
  702.             $id $payload['id'];
  703.         }
  704.         $payload = [
  705.             'id' => $parent['value'],
  706.             $manyToManyAssociation->getPropertyName() => [
  707.                 ['id' => $id],
  708.             ],
  709.         ];
  710.         $repository $this->definitionRegistry->getRepository($parentDefinition->getEntityName());
  711.         $repository->update([$payload], $context);
  712.         $repository $this->definitionRegistry->getRepository($reference->getEntityName());
  713.         $criteria = new Criteria([$id]);
  714.         $entities $repository->search($criteria$context);
  715.         $entity $entities->first();
  716.         if ($noContent) {
  717.             return $responseFactory->createRedirectResponse($reference$entity->getId(), $request$context);
  718.         }
  719.         return $responseFactory->createDetailResponse($criteria$entity$definition$request$context$appendLocationHeader);
  720.     }
  721.     private function executeWriteOperation(
  722.         EntityDefinition $entity,
  723.         array $payload,
  724.         Context $context,
  725.         string $type,
  726.         int $apiVersion
  727.     ): EntityWrittenContainerEvent {
  728.         $repository $this->definitionRegistry->getRepository($entity->getEntityName());
  729.         $conversionException = new ApiConversionException();
  730.         $payload $this->apiVersionConverter->convertPayload($entity$payload$apiVersion$conversionException);
  731.         $conversionException->tryToThrow();
  732.         $event $context->scope(Context::CRUD_API_SCOPE, function (Context $context) use ($repository$payload$entity$type): ?EntityWrittenContainerEvent {
  733.             if ($type === self::WRITE_CREATE) {
  734.                 return $repository->create([$payload], $context);
  735.             }
  736.             if ($type === self::WRITE_UPDATE) {
  737.                 return $repository->update([$payload], $context);
  738.             }
  739.             if ($type === self::WRITE_DELETE) {
  740.                 $event $repository->delete([$payload], $context);
  741.                 if (!empty($event->getErrors())) {
  742.                     throw new ResourceNotFoundException($entity->getEntityName(), $payload);
  743.                 }
  744.                 return $event;
  745.             }
  746.             return null;
  747.         });
  748.         if (!$event) {
  749.             throw new \RuntimeException('Unsupported write operation.');
  750.         }
  751.         return $event;
  752.     }
  753.     private function getAssociation(FieldCollection $fields, array $keys): AssociationField
  754.     {
  755.         $key array_shift($keys);
  756.         /** @var AssociationField $field */
  757.         $field $fields->get($key);
  758.         if (empty($keys)) {
  759.             return $field;
  760.         }
  761.         $reference $field->getReferenceDefinition();
  762.         $nested $reference->getFields();
  763.         return $this->getAssociation($nested$keys);
  764.     }
  765.     private function buildEntityPath(
  766.         string $entityName,
  767.         string $pathInfo,
  768.         int $apiVersion,
  769.         Context $context,
  770.         array $protections = [ReadProtection::class]
  771.     ): array {
  772.         $pathInfo str_replace('/extensions/''/'$pathInfo);
  773.         $exploded explode('/'$entityName '/' ltrim($pathInfo'/'));
  774.         $parts = [];
  775.         foreach ($exploded as $index => $part) {
  776.             if ($index 2) {
  777.                 continue;
  778.             }
  779.             if (empty($part)) {
  780.                 continue;
  781.             }
  782.             $value $exploded[$index 1] ?? null;
  783.             if (empty($parts)) {
  784.                 $part $this->urlToSnakeCase($part);
  785.             } else {
  786.                 $part $this->urlToCamelCase($part);
  787.             }
  788.             $parts[] = [
  789.                 'entity' => $part,
  790.                 'value' => $value,
  791.             ];
  792.         }
  793.         $parts array_filter($parts);
  794.         $first array_shift($parts);
  795.         try {
  796.             $root $this->definitionRegistry->getByEntityName($first['entity']);
  797.         } catch (DefinitionNotFoundException $e) {
  798.             throw new NotFoundHttpException($e->getMessage(), $e);
  799.         }
  800.         $entities = [
  801.             [
  802.                 'entity' => $first['entity'],
  803.                 'value' => $first['value'],
  804.                 'definition' => $root,
  805.                 'field' => null,
  806.             ],
  807.         ];
  808.         foreach ($parts as $part) {
  809.             /** @var AssociationField|null $field */
  810.             $field $root->getFields()->get($part['entity']);
  811.             if (!$field) {
  812.                 $path implode('.'array_column($entities'entity')) . '.' $part['entity'];
  813.                 throw new NotFoundHttpException(sprintf('Resource at path "%s" is not an existing relation.'$path));
  814.             }
  815.             if ($field instanceof ManyToManyAssociationField) {
  816.                 $root $field->getToManyReferenceDefinition();
  817.             } else {
  818.                 $root $field->getReferenceDefinition();
  819.             }
  820.             $entities[] = [
  821.                 'entity' => $part['entity'],
  822.                 'value' => $part['value'],
  823.                 'definition' => $field->getReferenceDefinition(),
  824.                 'field' => $field,
  825.             ];
  826.         }
  827.         $context->scope(Context::CRUD_API_SCOPE, function (Context $context) use ($entities$protections): void {
  828.             $this->entityProtectionValidator->validateEntityPath($entities$protections$context);
  829.         });
  830.         $this->apiVersionConverter->validateEntityPath($entities$apiVersion);
  831.         return $entities;
  832.     }
  833.     private function urlToSnakeCase(string $name): string
  834.     {
  835.         return str_replace('-''_'$name);
  836.     }
  837.     private function urlToCamelCase(string $name): string
  838.     {
  839.         $parts explode('-'$name);
  840.         $parts array_map('ucfirst'$parts);
  841.         return lcfirst(implode(''$parts));
  842.     }
  843.     /**
  844.      * Return a nested array structure of based on the content-type
  845.      */
  846.     private function getRequestBody(Request $request): array
  847.     {
  848.         $contentType $request->headers->get('CONTENT_TYPE''');
  849.         $semicolonPosition mb_strpos($contentType';');
  850.         if ($semicolonPosition !== false) {
  851.             $contentType mb_substr($contentType0$semicolonPosition);
  852.         }
  853.         try {
  854.             switch ($contentType) {
  855.                 case 'application/vnd.api+json':
  856.                     return $this->serializer->decode($request->getContent(), 'jsonapi');
  857.                 case 'application/json':
  858.                     return $request->request->all();
  859.             }
  860.         } catch (InvalidArgumentException UnexpectedValueException $exception) {
  861.             throw new BadRequestHttpException($exception->getMessage());
  862.         }
  863.         throw new UnsupportedMediaTypeHttpException(sprintf('The Content-Type "%s" is unsupported.'$contentType));
  864.     }
  865.     private function isCollection(array $array): bool
  866.     {
  867.         return array_keys($array) === range(0, \count($array) - 1);
  868.     }
  869.     private function hasScope(Request $requeststring $scopeIdentifier): bool
  870.     {
  871.         $scopes array_flip($request->attributes->get(PlatformRequest::ATTRIBUTE_OAUTH_SCOPES));
  872.         return isset($scopes[$scopeIdentifier]);
  873.     }
  874.     private function getEntityDefinition(string $entityName): EntityDefinition
  875.     {
  876.         try {
  877.             $entityDefinition $this->definitionRegistry->getByEntityName($entityName);
  878.         } catch (DefinitionNotFoundException $e) {
  879.             throw new NotFoundHttpException($e->getMessage(), $e);
  880.         }
  881.         return $entityDefinition;
  882.     }
  883.     private function validateAclPermissions(Context $contextEntityDefinition $entitystring $privilege): ?string
  884.     {
  885.         $resource $entity->getEntityName();
  886.         if ($entity instanceof EntityTranslationDefinition) {
  887.             $resource $entity->getParentDefinition()->getEntityName();
  888.         }
  889.         if (!$context->isAllowed($resource ':' $privilege)) {
  890.             return $resource ':' $privilege;
  891.         }
  892.         return null;
  893.     }
  894.     private function validatePathSegments(Context $context, array $pathSegmentsstring $privilege): array
  895.     {
  896.         $child array_pop($pathSegments);
  897.         $missing = [];
  898.         foreach ($pathSegments as $segment) {
  899.             // you need detail privileges for every parent entity
  900.             $missing[] = $this->validateAclPermissions(
  901.                 $context,
  902.                 $this->getDefinitionForPathSegment($segment),
  903.                 AclRoleDefinition::PRIVILEGE_READ
  904.             );
  905.         }
  906.         $missing[] = $this->validateAclPermissions($context$this->getDefinitionForPathSegment($child), $privilege);
  907.         return array_unique(array_filter($missing));
  908.     }
  909.     private function getDefinitionForPathSegment(array $segment): EntityDefinition
  910.     {
  911.         $definition $segment['definition'];
  912.         if ($segment['field'] instanceof ManyToManyAssociationField) {
  913.             $definition $segment['field']->getToManyReferenceDefinition();
  914.         }
  915.         return $definition;
  916.     }
  917.     private function checkIfRouteAvailableInApiVersion(string $entityint $version): void
  918.     {
  919.         if (!$this->apiVersionConverter->isAllowed($entitynull$version)) {
  920.             throw new NotFoundHttpException();
  921.         }
  922.     }
  923. }