custom/static-plugins/CioFormBuilder/src/Subscriber/ProductDetailSubscriber.php line 58

Open in your IDE?
  1. <?php
  2. namespace CioFormBuilder\Subscriber;
  3. use CioFormBuilder\Definition\CioForm\CioFormEntity;
  4. use CioFormBuilder\Definition\CioFormField\CioFormFieldEntity;
  5. use CioFormBuilder\Model\CioFormBuilder;
  6. use CioFormBuilder\Model\Field\AbstractField;
  7. use CioFormBuilder\Model\Field\FileField;
  8. use CioFormBuilder\Model\Field\SelectField;
  9. use CioFormBuilder\Model\Field\TextField;
  10. use CioPodProducts\CioPodProducts;
  11. use Shopware\Core\Checkout\Cart\CartPersister;
  12. use Shopware\Core\Checkout\Cart\Event\AfterLineItemAddedEvent;
  13. use Shopware\Core\Checkout\Cart\Event\AfterLineItemQuantityChangedEvent;
  14. use Shopware\Core\Checkout\Cart\Event\BeforeLineItemAddedEvent;
  15. use Shopware\Core\Checkout\Cart\LineItem\LineItem;
  16. use Shopware\Core\Checkout\Customer\CustomerEntity;
  17. use Shopware\Core\Content\Media\File\FileSaver;
  18. use Shopware\Core\Content\Media\MediaService;
  19. use Shopware\Core\Framework\DataAbstractionLayer\EntityRepository;
  20. use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
  21. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
  22. use Shopware\Core\Framework\Uuid\Uuid;
  23. use Shopware\Storefront\Event\StorefrontRenderEvent;
  24. use Shopware\Storefront\Page\Product\ProductPage;
  25. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  26. use Symfony\Component\HttpFoundation\File\UploadedFile;
  27. use Symfony\Component\HttpFoundation\RequestStack;
  28. class ProductDetailSubscriber implements EventSubscriberInterface
  29. {
  30.     protected RequestStack $requestStack;
  31.     protected MediaService $mediaService;
  32.     protected CartPersister $cartPersister;
  33.     protected EntityRepository $cioFormRepository;
  34.     public function __construct(RequestStack $requestStackMediaService $mediaServiceCartPersister $cartPersisterEntityRepository $cioFormRepository)
  35.     {
  36.         $this->requestStack $requestStack;
  37.         $this->mediaService $mediaService;
  38.         $this->cartPersister $cartPersister;
  39.         $this->cioFormRepository $cioFormRepository;
  40.     }
  41.     public static function getSubscribedEvents()
  42.     {
  43.         return [
  44.             StorefrontRenderEvent::class => 'addProductDetailFormData',
  45.             AfterLineItemAddedEvent::class => 'onAfterLineItemAddedEvent',
  46.             AfterLineItemQuantityChangedEvent::class => 'onAfterLineItemQuantityChangedEvent'
  47.         ];
  48.     }
  49.     public function addProductDetailFormData(StorefrontRenderEvent $event)
  50.     {
  51.         $customer $event->getSalesChannelContext()->getCustomer();
  52.         if ($customer instanceof CustomerEntity) {
  53.             if (is_array($event->getParameters()) && key_exists('page'$event->getParameters())) {
  54.                 $page $event->getParameters()['page'];
  55.                 if ($page instanceof ProductPage) {
  56.                     $request $event->getRequest();
  57.                     $lineItem null;
  58.                     if ($request->query->has('podLineItemId') && $event->getSalesChannelContext()->getToken()) {
  59.                         $cart $this->cartPersister->load($event->getSalesChannelContext()->getToken(), $event->getSalesChannelContext());
  60.                         $lineItem $cart->getLineItems()->get($request->query->get('podLineItemId'));
  61.                     }
  62.                     $form null;
  63.                     $extensionFormData $page->getProduct()->getExtension('formData');
  64.                     if ($extensionFormData instanceof CioFormEntity) {
  65.                         $criteria = new Criteria();
  66.                         $criteria->addFilter(new EqualsFilter('id'$extensionFormData->getId()));
  67.                         $criteria->addAssociation('fields');
  68.                         /** @var CioFormEntity $formEntity */
  69.                         $formEntity $this->cioFormRepository->search($criteria$event->getContext())->first();
  70.                         $form = new CioFormBuilder($formEntity);
  71.                         $form->formName 'productDetailPageBuyProductForm';
  72.                         if ($lineItem instanceof LineItem && is_array($lineItem->getPayload())) {
  73.                             $payload $lineItem->getPayload();
  74.                             if (array_key_exists('customFields'$payload) && is_array($payload['customFields'])) {
  75.                                 $cf $payload['customFields'];
  76.                                 // Dynamische Zeilenanzahl beim Bearbeiten übernehmen
  77.                                 if (!empty($cf['cioDynamicRowMode'])) {
  78.                                     if (isset($cf['cioDynamicRowCount'])) {
  79.                                         $form->setOverrideRowCount((int) $cf['cioDynamicRowCount']);
  80.                                     } elseif (isset($cf['cioTableFormData']) && is_array($cf['cioTableFormData'])) {
  81.                                         $form->setOverrideRowCount((int) count($cf['cioTableFormData']));
  82.                                     }
  83.                                 } elseif (isset($cf['cioTableFormData']) && is_array($cf['cioTableFormData'])) {
  84.                                     // Legacy-Fall: Anzahl aus vorhandenen Tabellenzeilen ableiten
  85.                                     $form->setOverrideRowCount((int) count($cf['cioTableFormData']));
  86.                                 }
  87.                                 self::mapPayloadDataToFieldDefaults($form$payload['customFields']);
  88.                             }
  89.                         }
  90.                     }
  91.                     $event->setParameter('cioFormOnProductDetailPage'$form);
  92.                 }
  93.             }
  94.         }
  95.     }
  96.     public function onAfterLineItemAddedEvent(AfterLineItemAddedEvent $event)
  97.     {
  98.         $request $this->requestStack->getCurrentRequest();
  99.         if (count($event->getLineItems()) === 1) {
  100.             if ($request->request->has('cioProductFormId')) {
  101.                 $criteria = new Criteria();
  102.                 $criteria->addFilter(new EqualsFilter('id'$request->request->get('cioProductFormId')));
  103.                 $criteria->addAssociation('fields');
  104.                 $formEntity $this->cioFormRepository->search($criteria$event->getContext())->first();
  105.                 if ($formEntity instanceof CioFormEntity) {
  106.                     $form = new CioFormBuilder($formEntity);
  107.                     $formData = [];
  108.                     $tableFormData = [];
  109.                     if ($form->isValid($request)) {
  110.                         /** @var AbstractField $field */
  111.                         foreach ($form->getFormFields() as $field) {
  112.                             if ($field instanceof FileField) {
  113.                                 if (
  114.                                     $request->files->has($field->getEntity()->getId()) &&
  115.                                     !is_null($request->files->get($field->getEntity()->getId()))
  116.                                 ) {
  117.                                     $uploadedFile $request->files->get($field->getEntity()->getId());
  118.                                     if ($uploadedFile instanceof UploadedFile) {
  119.                                         $blob file_get_contents($uploadedFile->getPathname());
  120.                                         $extension $uploadedFile->getClientOriginalExtension();
  121.                                         $contentType $uploadedFile->getClientMimeType();
  122.                                         $filename $uploadedFile->getClientOriginalName();
  123.                                         $mediaId $this->mediaService->saveFile(
  124.                                             $blob,
  125.                                             $extension,
  126.                                             $contentType,
  127.                                             // remove file extension from filename
  128.                                             $this->cleanFilename($filename) . '_' Uuid::randomHex(),
  129.                                             $event->getContext(),
  130.                                             CioPodProducts::MEDIA_UPLOAD_FOLDER_ENTITY,
  131.                                             null,
  132.                                             false
  133.                                         );
  134.                                         $formData[] = [
  135.                                             'id' => $field->getEntity()->getId(),
  136.                                             'technicalName' => $field->getEntity()->getTechnicalName(),
  137.                                             'label' => $field->getEntity()->getLabel(),
  138.                                             'type' => $field->getEntity()->getType(),
  139.                                             'value' => $mediaId
  140.                                         ];
  141.                                     }
  142.                                 } else {
  143.                                     if ($request->request->has($field->getEntity()->getId() . '_media_id')) {
  144.                                         $formData[] = [
  145.                                             'id' => $field->getEntity()->getId(),
  146.                                             'technicalName' => $field->getEntity()->getTechnicalName(),
  147.                                             'label' => $field->getEntity()->getLabel(),
  148.                                             'type' => $field->getEntity()->getType(),
  149.                                             'value' => $request->request->get($field->getEntity()->getId() . '_media_id')
  150.                                         ];
  151.                                     }
  152.                                 }
  153.                             } else {
  154.                                 $formData[] = [
  155.                                     'id' => $field->getEntity()->getId(),
  156.                                     'technicalName' => $field->getEntity()->getTechnicalName(),
  157.                                     'label' => $field->getEntity()->getLabel(),
  158.                                     'type' => $field->getEntity()->getType(),
  159.                                     'value' => $field->getData($request)
  160.                                 ];
  161.                             }
  162.                         }
  163.                         // Build table form data
  164.                         if ($form->hasDynamicRowMode()) {
  165.                             $rowCount $this->computeDynamicRowCount($form$request);
  166.                             $tableFormData $this->buildTableFormDataDynamic($formEntity$request$rowCount);
  167.                         } else {
  168.                             if (is_array($form->getTableRepresentationFormFields()) && count($form->getTableRepresentationFormFields()) > && is_array($form->getTableRepresentationFormFields()[0])) {
  169.                                 /** @var array $row */
  170.                                 foreach ($form->getTableRepresentationFormFields() as $row) {
  171.                                     $rowData = [];
  172.                                     /** @var AbstractField $field */
  173.                                     foreach ($row as $field) {
  174.                                         if ($field instanceof TextField || $field instanceof SelectField) {
  175.                                             $rowData[] = [
  176.                                                 'id' => $field->getEntity()->getId(),
  177.                                                 'technicalName' => $field->getEntity()->getTechnicalName(),
  178.                                                 'label' => $field->getEntity()->getLabel(),
  179.                                                 'type' => $field->getEntity()->getType(),
  180.                                                 'value' => $field->getData($request)
  181.                                             ];
  182.                                         }
  183.                                     }
  184.                                     $tableFormData[] = $rowData;
  185.                                 }
  186.                             }
  187.                         }
  188.                         /** @var LineItem $newLineItem */
  189.                         $newLineItem $event->getLineItems()[0];
  190.                         $payload $newLineItem->getPayload();
  191.                         if (!isset($payload['customFields']) || !is_array($payload['customFields'])) {
  192.                             $payload['customFields'] = [];
  193.                         }
  194.                         $payload['customFields']['cioFormData'] = $formData;
  195.                         $payload['customFields']['cioTableFormData'] = $tableFormData;
  196.                         $payload['customFields']['cioTableFormPrefix'] = $form->getEntity()->getTableRowsPrefix();
  197.                         // Mark dynamic mode and count if applicable
  198.                         $cartLineItem $event->getCart()->getLineItems()->get($newLineItem->getId());
  199.                         if ($form->hasDynamicRowMode()) {
  200.                             $rowCount $rowCount ?? $this->computeDynamicRowCount($form$request);
  201.                             $payload['customFields']['cioDynamicRowMode'] = true;
  202.                             $payload['customFields']['cioDynamicRowCount'] = $rowCount;
  203.                         }
  204.                         // Always write payload first so user input is not lost even if quantity change fails
  205.                         $cartLineItem->setPayload($payload);
  206.                         // Set quantity to rowCount when possible (only for stackable items) – this was the working behavior for empty carts
  207.                         if ($form->hasDynamicRowMode()) {
  208.                             try {
  209.                                 $isStackable method_exists($cartLineItem'isStackable')
  210.                                     ? $cartLineItem->isStackable()
  211.                                     : (method_exists($cartLineItem'getStackable') ? $cartLineItem->getStackable() : true);
  212.                                 if ($isStackable) {
  213.                                     $cartLineItem->setQuantity(max(1, (int) $rowCount));
  214.                                 }
  215.                             } catch (\Throwable $e) {
  216.                                 // non-stackable or guarded – keep payload and continue without changing quantity
  217.                             }
  218.                         }
  219.                         $this->cartPersister->save($event->getCart(), $event->getSalesChannelContext());
  220.                         // TODO: check if save line item to database is needed?
  221.                     }
  222.                 }
  223.             }
  224.         }
  225.     }
  226.     protected function cleanFilename(string $filename): string
  227.     {
  228.         $filenameWithoutExtension substr($filename0strrpos($filename'.'));
  229.         $cleanFilename preg_replace('/[^A-Za-z0-9\-_]/''-'$filenameWithoutExtension);
  230.         $cleanFilename strtolower(trim($cleanFilename'-'));
  231.         return $cleanFilename;
  232.     }
  233.     protected static function mapPayloadDataToFieldDefaults(CioFormBuilder $formBuilder, array $payloadCustomFields): void
  234.     {
  235.         $simpleData = [];
  236.         if (array_key_exists('cioFormData'$payloadCustomFields) && is_array($payloadCustomFields['cioFormData'])) {
  237.             foreach ($payloadCustomFields['cioFormData'] as $fieldData) {
  238.                 if (array_key_exists('id'$fieldData)) {
  239.                     $simpleData[$fieldData['id']] = $fieldData['value'];
  240.                 }
  241.             }
  242.         }
  243.         $tableData = [];
  244.         if (array_key_exists('cioTableFormData'$payloadCustomFields) && is_array($payloadCustomFields['cioTableFormData'])) {
  245.             foreach ($payloadCustomFields['cioTableFormData'] as $row) {
  246.                 if (is_array($row)) {
  247.                     foreach ($row as $fieldData) {
  248.                         if (array_key_exists('id'$fieldData)) {
  249.                             $tableData[$fieldData['id']][] = $fieldData['value'];
  250.                         }
  251.                     }
  252.                 }
  253.             }
  254.         }
  255.         /** @var AbstractField $fieldInstance */
  256.         foreach ($formBuilder->getFormFields() as $fieldInstance) {
  257.             if (array_key_exists($fieldInstance->getEntity()->getId(), $simpleData)) {
  258.                 $fieldInstance->getEntity()->setDefaultValue($simpleData[$fieldInstance->getEntity()->getId()]);
  259.             }
  260.         }
  261.         foreach ($formBuilder->getTableRepresentationFormFields()[0] as $field) {
  262.             if (array_key_exists($field->getEntity()->getId(), $tableData)) {
  263.                 $field->getEntity()->setDefaultValue(json_encode($tableData[$field->getEntity()->getId()]));
  264.             }
  265.         }
  266.     }
  267.         /**
  268.          * Compute the dynamic row count from request and clamp to [min,max].
  269.          */
  270.         protected function computeDynamicRowCount(CioFormBuilder $form\Symfony\Component\HttpFoundation\Request $request): int
  271.         {
  272.             $min $form->getEntity()->getMinTableRowsNumber() ?? 0;
  273.             $max $form->getEntity()->getMaxTableRowsNumber() ?? 0;
  274.             // Prefer hidden field if present
  275.             $count 0;
  276.             if ($request->request->has('cioDynamicRowCount')) {
  277.                 $val = (int) $request->request->get('cioDynamicRowCount');
  278.                 if ($val 0) {
  279.                     $count $val;
  280.                 }
  281.             }
  282.             // Fallback: derive from posted table field names by finding max index+1
  283.             if ($count === 0) {
  284.                 $maxIndex = -1;
  285.                 // Build a list of field IDs that are shown as table fields (text/select)
  286.                 $tableFieldIds = [];
  287.                 foreach ($form->getEntity()->getFields() as $fieldEntity) {
  288.                     if ($fieldEntity instanceof \CioFormBuilder\Definition\CioFormField\CioFormFieldEntity) {
  289.                         if ($fieldEntity->getShowAsTable() === true) {
  290.                             $type $fieldEntity->getType();
  291.                             if (in_array($type, [CioFormBuilder::FIELD_TYPE_TEXTCioFormBuilder::FIELD_TYPE_SELECT], true)) {
  292.                                 $tableFieldIds[] = $fieldEntity->getId();
  293.                             }
  294.                         }
  295.                     }
  296.                 }
  297.                 foreach ($request->request->keys() as $name) {
  298.                     foreach ($tableFieldIds as $id) {
  299.                         // match id_N at the end
  300.                         if (preg_match('/^' preg_quote($id'/') . '_(\d+)$/'$name$m)) {
  301.                             $idx = (int) $m[1];
  302.                             if ($idx $maxIndex) {
  303.                                 $maxIndex $idx;
  304.                             }
  305.                         }
  306.                     }
  307.                 }
  308.                 if ($maxIndex >= 0) {
  309.                     $count $maxIndex 1;
  310.                 }
  311.             }
  312.             if ($min && $max >= $min) {
  313.                 if ($count === 0) {
  314.                     $count $min;
  315.                 }
  316.                 if ($count $min) {
  317.                     $count $min;
  318.                 }
  319.                 if ($count $max) {
  320.                     $count $max;
  321.                 }
  322.             }
  323.             return max(1, (int) $count);
  324.         }
  325.         /**
  326.          * Build table form data for dynamic mode up to given row count.
  327.          * Cuts off rows beyond the clamped max.
  328.          *
  329.          * @return array<array<array{id:string,technicalName:string,label:string,type:string,value:mixed}>>
  330.          */
  331.         protected function buildTableFormDataDynamic(CioFormEntity $formEntity\Symfony\Component\HttpFoundation\Request $requestint $rowCount): array
  332.         {
  333.             $result = [];
  334.             // Collect table field entities (only text/select supported for table)
  335.             $tableFields = [];
  336.             foreach ($formEntity->getFields() as $fieldEntity) {
  337.                 if ($fieldEntity instanceof \CioFormBuilder\Definition\CioFormField\CioFormFieldEntity) {
  338.                     if ($fieldEntity->getShowAsTable() === true) {
  339.                         $type $fieldEntity->getType();
  340.                         if (in_array($type, [CioFormBuilder::FIELD_TYPE_TEXTCioFormBuilder::FIELD_TYPE_SELECT], true)) {
  341.                             $tableFields[] = $fieldEntity;
  342.                         }
  343.                     }
  344.                 }
  345.             }
  346.             for ($i 0$i $rowCount$i++) {
  347.                 $rowData = [];
  348.                 foreach ($tableFields as $fieldEntity) {
  349.                     $name $fieldEntity->getId() . '_' $i;
  350.                     $value '';
  351.                     if ($request->request->has($name)) {
  352.                         $val $request->request->get($name);
  353.                         if (is_string($val)) {
  354.                             $value $val;
  355.                         }
  356.                     }
  357.                     $rowData[] = [
  358.                         'id' => $fieldEntity->getId(),
  359.                         'technicalName' => $fieldEntity->getTechnicalName(),
  360.                         'label' => $fieldEntity->getLabel(),
  361.                         'type' => $fieldEntity->getType(),
  362.                         'value' => $value
  363.                     ];
  364.                 }
  365.                 $result[] = $rowData;
  366.             }
  367.             return $result;
  368.         }
  369.         /**
  370.          * Enforce readonly quantity for dynamic-mode line items by resetting quantity to stored row count.
  371.          */
  372.         public function onAfterLineItemQuantityChangedEvent(AfterLineItemQuantityChangedEvent $event): void
  373.         {
  374.             // Shopware 6.4: Event-API variiert, daher ohne getLineItem()/getLineItemId arbeiten.
  375.             // Wir laufen über alle Positionen und erzwingen nur für Dynamic-Mode-Positionen die Menge.
  376.             $cart $event->getCart();
  377.             foreach ($cart->getLineItems() as $lineItem) {
  378.                 if (!$lineItem instanceof LineItem) {
  379.                     continue;
  380.                 }
  381.                 $payload $lineItem->getPayload();
  382.                 if (!is_array($payload) || !isset($payload['customFields']) || !is_array($payload['customFields'])) {
  383.                     continue;
  384.                 }
  385.                 $cf $payload['customFields'];
  386.                 if (empty($cf['cioDynamicRowMode']) || !isset($cf['cioDynamicRowCount'])) {
  387.                     continue;
  388.                 }
  389.                 $rowCount = (int) $cf['cioDynamicRowCount'];
  390.                 try {
  391.                     $isStackable method_exists($lineItem'isStackable')
  392.                         ? $lineItem->isStackable()
  393.                         : (method_exists($lineItem'getStackable') ? $lineItem->getStackable() : true);
  394.                     if ($isStackable) {
  395.                         $lineItem->setQuantity(max(1$rowCount));
  396.                     }
  397.                 } catch (\Throwable $e) {
  398.                     // ignore for non-stackable or guarded carts
  399.                 }
  400.             }
  401.             $this->cartPersister->save($cart$event->getSalesChannelContext());
  402.         }
  403.     }