custom/plugins/CioPodProducts/src/Subscriber/PodCheckoutFormSubscriber.php line 149

Open in your IDE?
  1. <?php
  2. declare(strict_types=1);
  3. namespace CioPodProducts\Subscriber;
  4. use CioBudget\Definition\Budget\BudgetEntity;
  5. use CioBudget\Service\BudgetLoaderService;
  6. use CioBudget\Service\SessionService;
  7. use CioFormBuilder\Definition\CioForm\CioFormEntity;
  8. use CioFormBuilder\Definition\CioFormField\CioFormFieldEntity;
  9. use CioFormBuilder\Model\CioFormBuilder;
  10. use CioFormBuilder\Model\Field\AbstractField;
  11. use CioFormBuilder\Model\Field\FileField;
  12. use CioFormBuilder\Model\Field\SelectField;
  13. use CioFormBuilder\Model\Field\TextField;
  14. use CioPodProducts\CioPodProducts;
  15. use CioPodProducts\Error\PodInvalidFileTypeError;
  16. use CioPodProducts\Service\PodTypeConfigService;
  17. use Shopware\Core\Checkout\Cart\Event\CheckoutOrderPlacedEvent;
  18. use Shopware\Core\Checkout\Cart\LineItem\LineItem;
  19. use Shopware\Core\Checkout\Order\OrderEntity;
  20. use Shopware\Core\Content\Media\Exception\FileExtensionNotSupportedException;
  21. use Shopware\Core\Content\Media\MediaService;
  22. use Shopware\Core\Framework\Context;
  23. use Shopware\Core\Framework\DataAbstractionLayer\EntityRepository;
  24. use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
  25. use Shopware\Core\Framework\DataAbstractionLayer\Search\EntitySearchResult;
  26. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
  27. use Shopware\Core\Framework\Uuid\Uuid;
  28. use Shopware\Storefront\Event\StorefrontRenderEvent;
  29. use Shopware\Storefront\Page\Checkout\Confirm\CheckoutConfirmPage;
  30. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  31. use Symfony\Component\HttpFoundation\File\UploadedFile;
  32. use Symfony\Component\HttpFoundation\Request;
  33. use Symfony\Component\HttpFoundation\RequestStack;
  34. use Symfony\Contracts\Translation\TranslatorInterface;
  35. class PodCheckoutFormSubscriber implements EventSubscriberInterface
  36. {
  37.     private RequestStack $requestStack;
  38.     private EntityRepository $orderRepository;
  39.     private EntityRepository $cioFormRepository;
  40.     private PodTypeConfigService $podTypeConfigService;
  41.     private MediaService $mediaService;
  42.     private BudgetLoaderService $budgetLoaderService;
  43.     private SessionService $sessionService;
  44.     private TranslatorInterface $translator;
  45.     public function __construct(
  46.         RequestStack $requestStack,
  47.         EntityRepository $orderRepository,
  48.         EntityRepository $cioFormRepository,
  49.         PodTypeConfigService $podTypeConfigService,
  50.         MediaService $mediaService,
  51.         BudgetLoaderService $budgetLoaderService,
  52.         SessionService $sessionService,
  53.         TranslatorInterface $translator
  54.     ) {
  55.         $this->requestStack $requestStack;
  56.         $this->orderRepository $orderRepository;
  57.         $this->cioFormRepository $cioFormRepository;
  58.         $this->podTypeConfigService $podTypeConfigService;
  59.         $this->mediaService $mediaService;
  60.         $this->budgetLoaderService $budgetLoaderService;
  61.         $this->sessionService $sessionService;
  62.         $this->translator $translator;
  63.     }
  64.     /**
  65.      * @return array<string, string>
  66.      */
  67.     public static function getSubscribedEvents(): array
  68.     {
  69.         return [
  70.             StorefrontRenderEvent::class => 'onStorefrontRender',
  71.             CheckoutOrderPlacedEvent::class => 'onCheckoutOrderPlacedEvent',
  72.         ];
  73.     }
  74.     public function onStorefrontRender(StorefrontRenderEvent $event): void
  75.     {
  76.         $parameters $event->getParameters();
  77.         if (!\is_array($parameters) || !\array_key_exists('page'$parameters)) {
  78.             return;
  79.         }
  80.         $page $parameters['page'];
  81.         if (!$page instanceof CheckoutConfirmPage) {
  82.             return;
  83.         }
  84.         $cart $page->getCart();
  85.         $lineItems $cart->getLineItems();
  86.         $typeInfo $this->resolveSinglePodTypeForCart($lineItems$event->getSalesChannelContext()->getSalesChannelId());
  87.         if ($typeInfo === null) {
  88.             return;
  89.         }
  90.         $podCheckoutFormId $typeInfo['config']['podCheckoutFormId'];
  91.         if ($podCheckoutFormId === null) {
  92.             return;
  93.         }
  94.         $formEntity $this->getFormById($podCheckoutFormId$event->getContext());
  95.         if (!$formEntity instanceof CioFormEntity) {
  96.             return;
  97.         }
  98.         $formEntity->getFields()->forEach(function (CioFormFieldEntity $field) use ($event) {
  99.             if ($field->getType() === CioFormBuilder::FIELD_TYPE_BUDGET_SELECT) {
  100.                 $budgets $this->budgetLoaderService->getActiveBudgetsByCustomer($event->getSalesChannelContext()->getCustomer(), Context::createDefaultContext());
  101.                 $currentBudgetId $this->sessionService->getCurrentBudgetId();
  102.                 if ($budgets instanceof EntitySearchResult && $budgets->getTotal() > 0) {
  103.                     $budgets $budgets->getElements();
  104.                     if (is_string($currentBudgetId) && UUID::isValid($currentBudgetId)) {
  105.                         // sort so that current budget is first
  106.                         usort($budgets, function (BudgetEntity $aBudgetEntity $b) use ($currentBudgetId) {
  107.                             if ($a->getId() === $currentBudgetId) {
  108.                                 return -1;
  109.                             }
  110.                             if ($b->getId() === $currentBudgetId) {
  111.                                 return 1;
  112.                             }
  113.                             return 0;
  114.                         });
  115.                     }
  116.                     $selectionValues array_map(function (BudgetEntity $budget) {
  117.                         return $budget->getName() . ' (' $budget->getStore()->getExtid() . ')';
  118.                     }, $budgets);
  119.                     $field->setSelectionValues(implode(';'$selectionValues));
  120.                 } else {
  121.                     $field->setSelectionValues('');
  122.                 }
  123.             }
  124.         });
  125.         $formBuilder = new CioFormBuilder($formEntity);
  126.         $formBuilder->formName 'confirmOrderForm';
  127.         $event->setParameter('podCheckoutForm'$formBuilder);
  128.         $event->setParameter('podCheckoutFormId'$podCheckoutFormId);
  129.     }
  130.     public function onCheckoutOrderPlacedEvent(CheckoutOrderPlacedEvent $event): void
  131.     {
  132.         $request $this->requestStack->getCurrentRequest();
  133.         if (!$request instanceof Request) {
  134.             return;
  135.         }
  136.         $postedFormId $request->request->get('podCheckoutFormId');
  137.         if (!\is_string($postedFormId) || $postedFormId === '') {
  138.             return;
  139.         }
  140.         $order $event->getOrder();
  141.         $typeInfo $this->resolveSinglePodTypeForOrder($order$event->getSalesChannelId());
  142.         if ($typeInfo === null) {
  143.             return;
  144.         }
  145.         if ($typeInfo['config']['podCheckoutFormId'] !== $postedFormId) {
  146.             return;
  147.         }
  148.         $formEntity $this->getFormById($postedFormId$event->getContext());
  149.         if (!$formEntity instanceof CioFormEntity) {
  150.             return;
  151.         }
  152.         $formBuilder = new CioFormBuilder($formEntity);
  153.         $formBuilder->formName 'confirmOrderForm';
  154.         if ($formBuilder->isValid($request) === false) {
  155.             return;
  156.         }
  157.         [$formData$tableFormData$rowCount$tablePrefix$dynamicMode] = $this->collectFormData($formBuilder$formEntity$request$event->getContext());
  158.         $customFields $order->getCustomFields() ?? [];
  159.         $customFields['podCheckoutFormData'] = $formData;
  160.         $customFields['podCheckoutTableFormData'] = $tableFormData;
  161.         $customFields['podCheckoutTableFormPrefix'] = $tablePrefix;
  162.         $customFields['podCheckoutDynamicRowMode'] = $dynamicMode;
  163.         if ($dynamicMode) {
  164.             $customFields['podCheckoutDynamicRowCount'] = $rowCount;
  165.         }
  166.         $order->setCustomFields($customFields);
  167.         $this->orderRepository->update([
  168.             [
  169.                 'id' => $order->getId(),
  170.                 'customFields' => $customFields,
  171.             ],
  172.         ], $event->getContext());
  173.     }
  174.     private function resolveSinglePodTypeForCart(\Shopware\Core\Checkout\Cart\LineItem\LineItemCollection $lineItems, ?string $salesChannelId): ?array
  175.     {
  176.         $hasPod false;
  177.         $podTypes = [];
  178.         foreach ($lineItems as $lineItem) {
  179.             if (!$lineItem instanceof LineItem) {
  180.                 continue;
  181.             }
  182.             $payload $lineItem->getPayload();
  183.             if (!\is_array($payload) || !isset($payload['customFields']) || !\is_array($payload['customFields'])) {
  184.                 continue;
  185.             }
  186.             $customFields $payload['customFields'];
  187.             if (!empty($customFields['custom_pod_products_isPodProduct'])) {
  188.                 $hasPod true;
  189.             }
  190.             if (!empty($customFields['custom_pod_products_podProductType']) && \is_string($customFields['custom_pod_products_podProductType'])) {
  191.                 $podTypes[] = $customFields['custom_pod_products_podProductType'];
  192.             }
  193.         }
  194.         if (!$hasPod) {
  195.             return null;
  196.         }
  197.         $podTypes \array_values(\array_unique($podTypes));
  198.         if (\count($podTypes) !== 1) {
  199.             return null;
  200.         }
  201.         $typeConfig $this->podTypeConfigService->getTypeConfig($podTypes[0], $salesChannelId);
  202.         if (empty($typeConfig['podCheckoutFormId'])) {
  203.             return null;
  204.         }
  205.         if (!empty($typeConfig['podUniqueItem']) && $lineItems->count() > 1) {
  206.             return null;
  207.         }
  208.         return [
  209.             'typeKey' => $podTypes[0],
  210.             'config' => $typeConfig,
  211.         ];
  212.     }
  213.     private function resolveSinglePodTypeForOrder(OrderEntity $order, ?string $salesChannelId): ?array
  214.     {
  215.         $lineItems $order->getLineItems();
  216.         if ($lineItems === null) {
  217.             return null;
  218.         }
  219.         $hasPod false;
  220.         $podTypes = [];
  221.         foreach ($lineItems as $lineItem) {
  222.             $payload $lineItem->getPayload();
  223.             if (!\is_array($payload) || !isset($payload['customFields']) || !\is_array($payload['customFields'])) {
  224.                 continue;
  225.             }
  226.             $customFields $payload['customFields'];
  227.             if (!empty($customFields[CioPodProducts::POD_PRODUCTS_CUSTOM_FIELD_IS_POD])) {
  228.                 $hasPod true;
  229.             }
  230.             if (!empty($customFields['custom_pod_products_podProductType']) && \is_string($customFields['custom_pod_products_podProductType'])) {
  231.                 $podTypes[] = $customFields['custom_pod_products_podProductType'];
  232.             }
  233.         }
  234.         if (!$hasPod) {
  235.             return null;
  236.         }
  237.         $podTypes \array_values(\array_unique($podTypes));
  238.         if (\count($podTypes) !== 1) {
  239.             return null;
  240.         }
  241.         $typeConfig $this->podTypeConfigService->getTypeConfig($podTypes[0], $salesChannelId);
  242.         if (empty($typeConfig['podCheckoutFormId'])) {
  243.             return null;
  244.         }
  245.         if (!empty($typeConfig['podUniqueItem']) && $lineItems->count() > 1) {
  246.             return null;
  247.         }
  248.         return [
  249.             'typeKey' => $podTypes[0],
  250.             'config' => $typeConfig,
  251.         ];
  252.     }
  253.     private function getFormById(string $idContext $context): ?CioFormEntity
  254.     {
  255.         $criteria = (new Criteria())
  256.             ->addFilter(new EqualsFilter('id'$id))
  257.             ->addAssociation('fields');
  258.         $formEntity $this->cioFormRepository->search($criteria$context)->first();
  259.         return $formEntity instanceof CioFormEntity $formEntity null;
  260.     }
  261.     /**
  262.      * @return array{0: array<int, array{id:string,technicalName:string,label:string,type:string,value:mixed}>, 1: array<int, array<int, array{id:string,technicalName:string,label:string,type:string,value:mixed}>>, 2: int, 3: ?string, 4: bool}
  263.      */
  264.     private function collectFormData(CioFormBuilder $formBuilderCioFormEntity $formEntityRequest $requestContext $context): array
  265.     {
  266.         $formData = [];
  267.         $tableFormData = [];
  268.         /** @var AbstractField $field */
  269.         foreach ($formBuilder->getFormFields() as $field) {
  270.             if ($field instanceof FileField) {
  271.                 if ($request->files->has($field->getEntity()->getId()) && $request->files->get($field->getEntity()->getId()) instanceof UploadedFile) {
  272.                     /** @var UploadedFile $uploadedFile */
  273.                     $uploadedFile $request->files->get($field->getEntity()->getId());
  274.                     try {
  275.                         $mediaId $this->mediaService->saveFile(
  276.                             \file_get_contents($uploadedFile->getPathname()),
  277.                             $uploadedFile->getClientOriginalExtension(),
  278.                             $uploadedFile->getClientMimeType(),
  279.                             $this->cleanFilename($uploadedFile->getClientOriginalName()) . '_' Uuid::randomHex(),
  280.                             $context,
  281.                             CioPodProducts::MEDIA_UPLOAD_FOLDER_ENTITY,
  282.                             null,
  283.                             false
  284.                         );
  285.                     } catch (FileExtensionNotSupportedException $exception) {
  286.                         // Specific error for invalid file type in POD checkout uploads.
  287.                         $request $this->requestStack->getCurrentRequest();
  288.                         $session $request $request->getSession() : null;
  289.                         $flashBag $session $session->getFlashBag() : null;
  290.                         if ($flashBag !== null) {
  291.                             // remove add-to-cart success message
  292.                             $flashBag->get('success');
  293.                             $flashBag->add('danger'$this->translator->trans('error.pod-invalid-file-type-error'));
  294.                         }
  295.                         return [[], [], 0nullfalse];
  296.                     }
  297.                     $formData[] = [
  298.                         'id' => $field->getEntity()->getId(),
  299.                         'technicalName' => $field->getEntity()->getTechnicalName(),
  300.                         'label' => $field->getEntity()->getLabel(),
  301.                         'type' => $field->getEntity()->getType(),
  302.                         'value' => $mediaId,
  303.                     ];
  304.                 } elseif ($request->request->has($field->getEntity()->getId() . '_media_id')) {
  305.                     $formData[] = [
  306.                         'id' => $field->getEntity()->getId(),
  307.                         'technicalName' => $field->getEntity()->getTechnicalName(),
  308.                         'label' => $field->getEntity()->getLabel(),
  309.                         'type' => $field->getEntity()->getType(),
  310.                         'value' => $request->request->get($field->getEntity()->getId() . '_media_id'),
  311.                     ];
  312.                 }
  313.             } else {
  314.                 $formData[] = [
  315.                     'id' => $field->getEntity()->getId(),
  316.                     'technicalName' => $field->getEntity()->getTechnicalName(),
  317.                     'label' => $field->getEntity()->getLabel(),
  318.                     'type' => $field->getEntity()->getType(),
  319.                     'value' => $field->getData($request),
  320.                 ];
  321.             }
  322.         }
  323.         $rowCount 0;
  324.         $dynamicMode $formBuilder->hasDynamicRowMode();
  325.         if ($dynamicMode) {
  326.             $rowCount $this->computeDynamicRowCount($formBuilder$request);
  327.             $tableFormData $this->buildTableFormDataDynamic($formEntity$request$rowCount);
  328.         } else {
  329.             if (\is_array($formBuilder->getTableRepresentationFormFields())
  330.                 && \count($formBuilder->getTableRepresentationFormFields()) > 0
  331.                 && \is_array($formBuilder->getTableRepresentationFormFields()[0])) {
  332.                 foreach ($formBuilder->getTableRepresentationFormFields() as $row) {
  333.                     $rowData = [];
  334.                     /** @var AbstractField $field */
  335.                     foreach ($row as $field) {
  336.                         if ($field instanceof TextField || $field instanceof SelectField) {
  337.                             $rowData[] = [
  338.                                 'id' => $field->getEntity()->getId(),
  339.                                 'technicalName' => $field->getEntity()->getTechnicalName(),
  340.                                 'label' => $field->getEntity()->getLabel(),
  341.                                 'type' => $field->getEntity()->getType(),
  342.                                 'value' => $field->getData($request),
  343.                             ];
  344.                         }
  345.                     }
  346.                     $tableFormData[] = $rowData;
  347.                 }
  348.             }
  349.         }
  350.         $tablePrefix $formBuilder->getEntity()->getTableRowsPrefix();
  351.         return [$formData$tableFormData$rowCount$tablePrefix$dynamicMode];
  352.     }
  353.     private function cleanFilename(string $filename): string
  354.     {
  355.         $filenameWithoutExtension \substr($filename0\strrpos($filename'.'));
  356.         $cleanFilename \preg_replace('/[^A-Za-z0-9\-_]/''-'$filenameWithoutExtension);
  357.         return \strtolower(\trim((string) $cleanFilename'-'));
  358.     }
  359.     private function computeDynamicRowCount(CioFormBuilder $formRequest $request): int
  360.     {
  361.         $min $form->getEntity()->getMinTableRowsNumber() ?? 0;
  362.         $max $form->getEntity()->getMaxTableRowsNumber() ?? 0;
  363.         $count 0;
  364.         if ($request->request->has('podCheckoutDynamicRowCount')) {
  365.             $val = (int) $request->request->get('podCheckoutDynamicRowCount');
  366.             if ($val 0) {
  367.                 $count $val;
  368.             }
  369.         }
  370.         if ($count === 0) {
  371.             $maxIndex = -1;
  372.             $tableFieldIds = [];
  373.             foreach ($form->getEntity()->getFields() as $fieldEntity) {
  374.                 if ($fieldEntity instanceof \CioFormBuilder\Definition\CioFormField\CioFormFieldEntity && $fieldEntity->getShowAsTable() === true) {
  375.                     $type $fieldEntity->getType();
  376.                     if (\in_array($type, [CioFormBuilder::FIELD_TYPE_TEXTCioFormBuilder::FIELD_TYPE_SELECT], true)) {
  377.                         $tableFieldIds[] = $fieldEntity->getId();
  378.                     }
  379.                 }
  380.             }
  381.             foreach ($request->request->keys() as $name) {
  382.                 foreach ($tableFieldIds as $id) {
  383.                     if (\preg_match('/^' \preg_quote($id'/') . '_(\d+)$/'$name$m)) {
  384.                         $idx = (int) $m[1];
  385.                         if ($idx $maxIndex) {
  386.                             $maxIndex $idx;
  387.                         }
  388.                     }
  389.                 }
  390.             }
  391.             if ($maxIndex >= 0) {
  392.                 $count $maxIndex 1;
  393.             }
  394.         }
  395.         if ($min && $max >= $min) {
  396.             if ($count === 0) {
  397.                 $count $min;
  398.             }
  399.             if ($count $min) {
  400.                 $count $min;
  401.             }
  402.             if ($count $max) {
  403.                 $count $max;
  404.             }
  405.         }
  406.         return \max(1, (int) $count);
  407.     }
  408.     /**
  409.      * @return array<int, array<int, array{id:string,technicalName:string,label:string,type:string,value:mixed}>>
  410.      */
  411.     private function buildTableFormDataDynamic(CioFormEntity $formEntityRequest $requestint $rowCount): array
  412.     {
  413.         $result = [];
  414.         $tableFields = [];
  415.         foreach ($formEntity->getFields() as $fieldEntity) {
  416.             if ($fieldEntity instanceof \CioFormBuilder\Definition\CioFormField\CioFormFieldEntity && $fieldEntity->getShowAsTable() === true) {
  417.                 $type $fieldEntity->getType();
  418.                 if (\in_array($type, [CioFormBuilder::FIELD_TYPE_TEXTCioFormBuilder::FIELD_TYPE_SELECT], true)) {
  419.                     $tableFields[] = $fieldEntity;
  420.                 }
  421.             }
  422.         }
  423.         for ($i 0$i $rowCount$i++) {
  424.             $rowData = [];
  425.             foreach ($tableFields as $fieldEntity) {
  426.                 $name $fieldEntity->getId() . '_' $i;
  427.                 $value '';
  428.                 if ($request->request->has($name)) {
  429.                     $val $request->request->get($name);
  430.                     if (\is_string($val)) {
  431.                         $value $val;
  432.                     }
  433.                 }
  434.                 $rowData[] = [
  435.                     'id' => $fieldEntity->getId(),
  436.                     'technicalName' => $fieldEntity->getTechnicalName(),
  437.                     'label' => $fieldEntity->getLabel(),
  438.                     'type' => $fieldEntity->getType(),
  439.                     'value' => $value,
  440.                 ];
  441.             }
  442.             $result[] = $rowData;
  443.         }
  444.         return $result;
  445.     }
  446. }