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

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