vendor/thelia/core/lib/Thelia/Action/Product.php line 89

Open in your IDE?
  1. <?php
  2. /*
  3.  * This file is part of the Thelia package.
  4.  * http://www.thelia.net
  5.  *
  6.  * (c) OpenStudio <info@thelia.net>
  7.  *
  8.  * For the full copyright and license information, please view the LICENSE
  9.  * file that was distributed with this source code.
  10.  */
  11. namespace Thelia\Action;
  12. use Propel\Runtime\ActiveQuery\Criteria;
  13. use Propel\Runtime\Exception\PropelException;
  14. use Propel\Runtime\Propel;
  15. use Symfony\Component\EventDispatcher\EventDispatcherInterface;
  16. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  17. use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
  18. use Thelia\Core\Event\Feature\FeatureAvCreateEvent;
  19. use Thelia\Core\Event\Feature\FeatureAvDeleteEvent;
  20. use Thelia\Core\Event\FeatureProduct\FeatureProductDeleteEvent;
  21. use Thelia\Core\Event\FeatureProduct\FeatureProductUpdateEvent;
  22. use Thelia\Core\Event\File\FileDeleteEvent;
  23. use Thelia\Core\Event\Product\ProductAddAccessoryEvent;
  24. use Thelia\Core\Event\Product\ProductAddCategoryEvent;
  25. use Thelia\Core\Event\Product\ProductAddContentEvent;
  26. use Thelia\Core\Event\Product\ProductCloneEvent;
  27. use Thelia\Core\Event\Product\ProductCreateEvent;
  28. use Thelia\Core\Event\Product\ProductDeleteAccessoryEvent;
  29. use Thelia\Core\Event\Product\ProductDeleteCategoryEvent;
  30. use Thelia\Core\Event\Product\ProductDeleteContentEvent;
  31. use Thelia\Core\Event\Product\ProductDeleteEvent;
  32. use Thelia\Core\Event\Product\ProductSetTemplateEvent;
  33. use Thelia\Core\Event\Product\ProductToggleVisibilityEvent;
  34. use Thelia\Core\Event\Product\ProductUpdateEvent;
  35. use Thelia\Core\Event\ProductSaleElement\ProductSaleElementDeleteEvent;
  36. use Thelia\Core\Event\Template\TemplateDeleteAttributeEvent;
  37. use Thelia\Core\Event\Template\TemplateDeleteFeatureEvent;
  38. use Thelia\Core\Event\TheliaEvents;
  39. use Thelia\Core\Event\UpdatePositionEvent;
  40. use Thelia\Core\Event\UpdateSeoEvent;
  41. use Thelia\Core\Event\ViewCheckEvent;
  42. use Thelia\Model\Accessory;
  43. use Thelia\Model\AccessoryQuery;
  44. use Thelia\Model\AttributeTemplateQuery;
  45. use Thelia\Model\Currency as CurrencyModel;
  46. use Thelia\Model\FeatureAvI18n;
  47. use Thelia\Model\FeatureAvI18nQuery;
  48. use Thelia\Model\FeatureAvQuery;
  49. use Thelia\Model\FeatureProduct;
  50. use Thelia\Model\FeatureProductQuery;
  51. use Thelia\Model\FeatureTemplateQuery;
  52. use Thelia\Model\Map\AttributeTemplateTableMap;
  53. use Thelia\Model\Map\FeatureTemplateTableMap;
  54. use Thelia\Model\Map\ProductSaleElementsTableMap;
  55. use Thelia\Model\Map\ProductTableMap;
  56. use Thelia\Model\Product as ProductModel;
  57. use Thelia\Model\ProductAssociatedContent;
  58. use Thelia\Model\ProductAssociatedContentQuery;
  59. use Thelia\Model\ProductCategory;
  60. use Thelia\Model\ProductCategoryQuery;
  61. use Thelia\Model\ProductDocument;
  62. use Thelia\Model\ProductDocumentQuery;
  63. use Thelia\Model\ProductI18n;
  64. use Thelia\Model\ProductI18nQuery;
  65. use Thelia\Model\ProductImage;
  66. use Thelia\Model\ProductImageQuery;
  67. use Thelia\Model\ProductPrice;
  68. use Thelia\Model\ProductPriceQuery;
  69. use Thelia\Model\ProductQuery;
  70. use Thelia\Model\ProductSaleElementsQuery;
  71. use Thelia\Model\TaxRuleQuery;
  72. class Product extends BaseAction implements EventSubscriberInterface
  73. {
  74.     /** @var EventDispatcherInterface */
  75.     protected $eventDispatcher;
  76.     public function __construct(EventDispatcherInterface $eventDispatcher)
  77.     {
  78.         $this->eventDispatcher $eventDispatcher;
  79.     }
  80.     /**
  81.      * Create a new product entry.
  82.      */
  83.     public function create(ProductCreateEvent $event): void
  84.     {
  85.         $defaultTaxRuleId null;
  86.         if (null !== $defaultTaxRule TaxRuleQuery::create()->findOneByIsDefault(true)) {
  87.             $defaultTaxRuleId $defaultTaxRule->getId();
  88.         }
  89.         $product = new ProductModel();
  90.         $product
  91.             ->setRef($event->getRef())
  92.             ->setLocale($event->getLocale())
  93.             ->setTitle($event->getTitle())
  94.             ->setVisible($event->getVisible() ? 0)
  95.             ->setVirtual($event->getVirtual() ? 0)
  96.             ->setTemplateId($event->getTemplateId())
  97.             ->create(
  98.                 $event->getDefaultCategory(),
  99.                 $event->getBasePrice(),
  100.                 $event->getCurrencyId(),
  101.                 // Set the default tax rule if not defined
  102.                 $event->getTaxRuleId() ?: $defaultTaxRuleId,
  103.                 $event->getBaseWeight(),
  104.                 $event->getBaseQuantity()
  105.             )
  106.         ;
  107.         $event->setProduct($product);
  108.     }
  109.     /*******************
  110.      * CLONING PROCESS *
  111.      *******************/
  112.     /**
  113.      * @throws \Exception
  114.      */
  115.     public function cloneProduct(ProductCloneEvent $event): void
  116.     {
  117.         $con Propel::getWriteConnection(ProductTableMap::DATABASE_NAME);
  118.         $con->beginTransaction();
  119.         try {
  120.             // Get important datas
  121.             $lang $event->getLang();
  122.             $originalProduct $event->getOriginalProduct();
  123.             if (null === $originalProductDefaultI18n ProductI18nQuery::create()
  124.                 ->findPk([$originalProduct->getId(), $lang])) {
  125.                 // No i18n entry for the current language. Try to find one for creating the product.
  126.                 // It will be updated later by updateClone()
  127.                 $originalProductDefaultI18n ProductI18nQuery::create()
  128.                     ->findOneById($originalProduct->getId())
  129.                     ;
  130.             }
  131.             $originalProductDefaultPrice ProductPriceQuery::create()
  132.                 ->findOneByProductSaleElementsId($originalProduct->getDefaultSaleElements()->getId());
  133.             // Cloning process
  134.             $this->createClone($event$originalProductDefaultI18n$originalProductDefaultPrice);
  135.             $this->updateClone($event$originalProductDefaultPrice);
  136.             $this->cloneFeatureCombination($event);
  137.             $this->cloneAssociatedContent($event);
  138.             $this->cloneAccessories($event);
  139.             $this->cloneAdditionalCategories($event);
  140.             // Dispatch event for file cloning
  141.             $this->eventDispatcher->dispatch($eventTheliaEvents::FILE_CLONE);
  142.             // Dispatch event for PSE cloning
  143.             $this->eventDispatcher->dispatch($eventTheliaEvents::PSE_CLONE);
  144.             $con->commit();
  145.         } catch (\Exception $e) {
  146.             $con->rollBack();
  147.             throw $e;
  148.         }
  149.     }
  150.     public function createClone(ProductCloneEvent $eventProductI18n $originalProductDefaultI18nProductPrice $originalProductDefaultPrice): void
  151.     {
  152.         // Build event and dispatch creation of the clone product
  153.         $createCloneEvent = new ProductCreateEvent();
  154.         $createCloneEvent
  155.             ->setTitle($originalProductDefaultI18n->getTitle())
  156.             ->setRef($event->getRef())
  157.             ->setLocale($event->getLang())
  158.             ->setVisible(0)
  159.             ->setQuantity(0)
  160.             ->setVirtual($event->getOriginalProduct()->getVirtual())
  161.             ->setTaxRuleId($event->getOriginalProduct()->getTaxRuleId())
  162.             ->setDefaultCategory($event->getOriginalProduct()->getDefaultCategoryId())
  163.             ->setBasePrice($originalProductDefaultPrice->getPrice())
  164.             ->setCurrencyId($originalProductDefaultPrice->getCurrencyId())
  165.             ->setBaseWeight($event->getOriginalProduct()->getDefaultSaleElements()->getWeight());
  166.         $this->eventDispatcher->dispatch($createCloneEventTheliaEvents::PRODUCT_CREATE);
  167.         $event->setClonedProduct($createCloneEvent->getProduct());
  168.     }
  169.     public function updateClone(ProductCloneEvent $eventProductPrice $originalProductDefaultPrice): void
  170.     {
  171.         // Get original product's I18ns
  172.         $originalProductI18ns ProductI18nQuery::create()
  173.             ->findById($event->getOriginalProduct()->getId());
  174.         $clonedProductUpdateEvent = new ProductUpdateEvent($event->getClonedProduct()->getId());
  175.         /** @var ProductI18n $originalProductI18n */
  176.         foreach ($originalProductI18ns as $originalProductI18n) {
  177.             $clonedProductUpdateEvent
  178.                 ->setRef($event->getClonedProduct()->getRef())
  179.                 ->setVisible($event->getClonedProduct()->getVisible())
  180.                 ->setVirtual($event->getClonedProduct()->getVirtual())
  181.                 ->setLocale($originalProductI18n->getLocale())
  182.                 ->setTitle($originalProductI18n->getTitle())
  183.                 ->setChapo($originalProductI18n->getChapo())
  184.                 ->setDescription($originalProductI18n->getDescription())
  185.                 ->setPostscriptum($originalProductI18n->getPostscriptum())
  186.                 ->setBasePrice($originalProductDefaultPrice->getPrice())
  187.                 ->setCurrencyId($originalProductDefaultPrice->getCurrencyId())
  188.                 ->setBaseWeight($event->getOriginalProduct()->getDefaultSaleElements()->getWeight())
  189.                 ->setTaxRuleId($event->getOriginalProduct()->getTaxRuleId())
  190.                 ->setBrandId($event->getOriginalProduct()->getBrandId())
  191.                 ->setDefaultCategory($event->getOriginalProduct()->getDefaultCategoryId());
  192.             $this->eventDispatcher->dispatch($clonedProductUpdateEventTheliaEvents::PRODUCT_UPDATE);
  193.             // SEO info
  194.             $clonedProductUpdateSeoEvent = new UpdateSeoEvent($event->getClonedProduct()->getId());
  195.             $clonedProductUpdateSeoEvent
  196.                 ->setLocale($originalProductI18n->getLocale())
  197.                 ->setMetaTitle($originalProductI18n->getMetaTitle())
  198.                 ->setMetaDescription($originalProductI18n->getMetaDescription())
  199.                 ->setMetaKeywords($originalProductI18n->getMetaKeywords())
  200.                 ->setUrl(null);
  201.             $this->eventDispatcher->dispatch($clonedProductUpdateSeoEventTheliaEvents::PRODUCT_UPDATE_SEO);
  202.         }
  203.         $event->setClonedProduct($clonedProductUpdateEvent->getProduct());
  204.         // Set clone's template
  205.         $clonedProductUpdateTemplateEvent = new ProductSetTemplateEvent(
  206.             $event->getClonedProduct(),
  207.             $event->getOriginalProduct()->getTemplateId(),
  208.             $originalProductDefaultPrice->getCurrencyId()
  209.         );
  210.         $this->eventDispatcher->dispatch($clonedProductUpdateTemplateEventTheliaEvents::PRODUCT_SET_TEMPLATE);
  211.     }
  212.     public function cloneFeatureCombination(ProductCloneEvent $event): void
  213.     {
  214.         // Get original product FeatureProduct list
  215.         $originalProductFeatureList FeatureProductQuery::create()
  216.             ->findByProductId($event->getOriginalProduct()->getId());
  217.         // Set clone product FeatureProducts
  218.         /** @var FeatureProduct $originalProductFeature */
  219.         foreach ($originalProductFeatureList as $originalProductFeature) {
  220.             // Get original FeatureAvI18n list
  221.             $originalProductFeatureAvI18nList FeatureAvI18nQuery::create()
  222.                 ->findById($originalProductFeature->getFeatureAvId());
  223.             /** @var FeatureAvI18n $originalProductFeatureAvI18n */
  224.             foreach ($originalProductFeatureAvI18nList as $originalProductFeatureAvI18n) {
  225.                 // Create a FeatureProduct for each FeatureAv (not for each FeatureAvI18n)
  226.                 $clonedProductCreateFeatureEvent = new FeatureProductUpdateEvent(
  227.                     $event->getClonedProduct()->getId(),
  228.                     $originalProductFeature->getFeatureId(),
  229.                     $originalProductFeature->getFeatureAvId()
  230.                 );
  231.                 $clonedProductCreateFeatureEvent->setLocale($originalProductFeatureAvI18n->getLocale());
  232.                 // If it's a free text value, pass the FeatureAvI18n's title as featureValue to the event
  233.                 if ($originalProductFeature->getIsFreeText()) {
  234.                     $clonedProductCreateFeatureEvent->setFeatureValue($originalProductFeatureAvI18n->getTitle());
  235.                     $clonedProductCreateFeatureEvent->setIsTextValue(true);
  236.                 }
  237.                 $this->eventDispatcher->dispatch($clonedProductCreateFeatureEventTheliaEvents::PRODUCT_FEATURE_UPDATE_VALUE);
  238.             }
  239.         }
  240.     }
  241.     public function cloneAssociatedContent(ProductCloneEvent $event): void
  242.     {
  243.         // Get original product associated contents
  244.         $originalProductAssocConts ProductAssociatedContentQuery::create()
  245.             ->findByProductId($event->getOriginalProduct()->getId());
  246.         // Set clone product associated contents
  247.         /** @var ProductAssociatedContent $originalProductAssocCont */
  248.         foreach ($originalProductAssocConts as $originalProductAssocCont) {
  249.             $clonedProductCreatePAC = new ProductAddContentEvent($event->getClonedProduct(), $originalProductAssocCont->getContentId());
  250.             $this->eventDispatcher->dispatch($clonedProductCreatePACTheliaEvents::PRODUCT_ADD_CONTENT);
  251.         }
  252.     }
  253.     public function cloneAccessories(ProductCloneEvent $event): void
  254.     {
  255.         // Get original product accessories
  256.         $originalProductAccessoryList AccessoryQuery::create()
  257.             ->findByProductId($event->getOriginalProduct()->getId());
  258.         // Set clone product accessories
  259.         /** @var Accessory $originalProductAccessory */
  260.         foreach ($originalProductAccessoryList as $originalProductAccessory) {
  261.             $clonedProductAddAccessoryEvent = new ProductAddAccessoryEvent($event->getClonedProduct(), $originalProductAccessory->getAccessory());
  262.             $this->eventDispatcher->dispatch($clonedProductAddAccessoryEventTheliaEvents::PRODUCT_ADD_ACCESSORY);
  263.         }
  264.     }
  265.     public function cloneAdditionalCategories(ProductCloneEvent $event): void
  266.     {
  267.         // Get original product additional categories
  268.         $originalProductAdditionalCategoryList ProductCategoryQuery::create()
  269.             ->filterByProductId($event->getOriginalProduct()->getId())
  270.             ->filterByDefaultCategory(false)
  271.             ->find();
  272.         // Set clone product additional categories
  273.         /** @var ProductCategory $originalProductCategory */
  274.         foreach ($originalProductAdditionalCategoryList as $originalProductCategory) {
  275.             $clonedProductAddCategoryEvent = new ProductAddCategoryEvent($event->getClonedProduct(), $originalProductCategory->getCategoryId());
  276.             $this->eventDispatcher->dispatch($clonedProductAddCategoryEventTheliaEvents::PRODUCT_ADD_CATEGORY);
  277.         }
  278.     }
  279.     /***************
  280.      * END CLONING *
  281.      ***************/
  282.     /**
  283.      * Change a product.
  284.      *
  285.      * @throws PropelException
  286.      * @throws \Exception
  287.      */
  288.     public function update(ProductUpdateEvent $event): void
  289.     {
  290.         if (null !== $product ProductQuery::create()->findPk($event->getProductId())) {
  291.             $con Propel::getWriteConnection(ProductTableMap::DATABASE_NAME);
  292.             $con->beginTransaction();
  293.             try {
  294.                 $prevRef $product->getRef();
  295.                 $product
  296.                     ->setRef($event->getRef())
  297.                     ->setLocale($event->getLocale())
  298.                     ->setTitle($event->getTitle())
  299.                     ->setDescription($event->getDescription())
  300.                     ->setChapo($event->getChapo())
  301.                     ->setPostscriptum($event->getPostscriptum())
  302.                     ->setVisible($event->getVisible() ? 0)
  303.                     ->setVirtual($event->getVirtual() ? 0)
  304.                     ->setBrandId($event->getBrandId() <= null $event->getBrandId())
  305.                     ->save($con)
  306.                 ;
  307.                 // Update default PSE (if product has no attributes and the product's ref change)
  308.                 $defaultPseRefChange $prevRef !== $product->getRef()
  309.                     && === $product->getDefaultSaleElements()->countAttributeCombinations();
  310.                 if ($defaultPseRefChange) {
  311.                     $defaultPse $product->getDefaultSaleElements();
  312.                     $defaultPse->setRef($product->getRef())->save();
  313.                 }
  314.                 // Update default category (if required)
  315.                 $product->setDefaultCategory($event->getDefaultCategory());
  316.                 $event->setProduct($product);
  317.                 $con->commit();
  318.             } catch (PropelException $e) {
  319.                 $con->rollBack();
  320.                 throw $e;
  321.             }
  322.         }
  323.     }
  324.     /**
  325.      * @param $eventName
  326.      */
  327.     public function updateSeo(UpdateSeoEvent $event$eventNameEventDispatcherInterface $dispatcher)
  328.     {
  329.         return $this->genericUpdateSeo(ProductQuery::create(), $event$dispatcher);
  330.     }
  331.     /**
  332.      * Delete a product entry.
  333.      *
  334.      * @throws \Exception
  335.      */
  336.     public function delete(ProductDeleteEvent $event): void
  337.     {
  338.         if (null !== $product ProductQuery::create()->findPk($event->getProductId())) {
  339.             $con Propel::getWriteConnection(ProductTableMap::DATABASE_NAME);
  340.             $con->beginTransaction();
  341.             try {
  342.                 $fileList = ['images' => [], 'documentList' => []];
  343.                 // Get product's files to delete after product deletion
  344.                 $fileList['images']['list'] = ProductImageQuery::create()
  345.                     ->findByProductId($event->getProductId());
  346.                 $fileList['images']['type'] = TheliaEvents::IMAGE_DELETE;
  347.                 $fileList['documentList']['list'] = ProductDocumentQuery::create()
  348.                     ->findByProductId($event->getProductId());
  349.                 $fileList['documentList']['type'] = TheliaEvents::DOCUMENT_DELETE;
  350.                 // Delete product
  351.                 $product
  352.                     ->delete($con)
  353.                 ;
  354.                 $event->setProduct($product);
  355.                 // Dispatch delete product's files event
  356.                 foreach ($fileList as $fileTypeList) {
  357.                     foreach ($fileTypeList['list'] as $fileToDelete) {
  358.                         $fileDeleteEvent = new FileDeleteEvent($fileToDelete);
  359.                         $this->eventDispatcher->dispatch($fileDeleteEvent$fileTypeList['type']);
  360.                     }
  361.                 }
  362.                 $con->commit();
  363.             } catch (\Exception $e) {
  364.                 $con->rollBack();
  365.                 throw $e;
  366.             }
  367.         }
  368.     }
  369.     /**
  370.      * Toggle product visibility. No form used here.
  371.      */
  372.     public function toggleVisibility(ProductToggleVisibilityEvent $event): void
  373.     {
  374.         $product $event->getProduct();
  375.         $product
  376.             ->setVisible($product->getVisible() ? false true)
  377.             ->save()
  378.             ;
  379.         $event->setProduct($product);
  380.     }
  381.     /**
  382.      * Changes position, selecting absolute ou relative change.
  383.      *
  384.      * @param $eventName
  385.      */
  386.     public function updatePosition(UpdatePositionEvent $event$eventNameEventDispatcherInterface $dispatcher): void
  387.     {
  388.         $this->genericUpdateDelegatePosition(
  389.             ProductCategoryQuery::create()
  390.                 ->filterByProductId($event->getObjectId())
  391.                 ->filterByCategoryId($event->getReferrerId()),
  392.             $event,
  393.             $dispatcher
  394.         );
  395.     }
  396.     public function addContent(ProductAddContentEvent $event): void
  397.     {
  398.         if (ProductAssociatedContentQuery::create()
  399.             ->filterByContentId($event->getContentId())
  400.              ->filterByProduct($event->getProduct())->count() <= 0) {
  401.             $content = new ProductAssociatedContent();
  402.             $content
  403.                 ->setProduct($event->getProduct())
  404.                 ->setContentId($event->getContentId())
  405.                 ->save()
  406.             ;
  407.         }
  408.     }
  409.     public function removeContent(ProductDeleteContentEvent $event): void
  410.     {
  411.         $content ProductAssociatedContentQuery::create()
  412.             ->filterByContentId($event->getContentId())
  413.             ->filterByProduct($event->getProduct())->findOne()
  414.         ;
  415.         if ($content !== null) {
  416.             $content
  417.                 ->delete()
  418.             ;
  419.         }
  420.     }
  421.     public function addCategory(ProductAddCategoryEvent $event): void
  422.     {
  423.         if (ProductCategoryQuery::create()
  424.             ->filterByProduct($event->getProduct())
  425.             ->filterByCategoryId($event->getCategoryId())
  426.             ->count() <= 0) {
  427.             $productCategory = (new ProductCategory())
  428.                 ->setProduct($event->getProduct())
  429.                 ->setCategoryId($event->getCategoryId())
  430.                 ->setDefaultCategory(false);
  431.             $productCategory
  432.                 ->setPosition($productCategory->getNextPosition())
  433.                 ->save();
  434.         }
  435.     }
  436.     public function removeCategory(ProductDeleteCategoryEvent $event): void
  437.     {
  438.         $productCategory ProductCategoryQuery::create()
  439.             ->filterByProduct($event->getProduct())
  440.             ->filterByCategoryId($event->getCategoryId())
  441.             ->findOne();
  442.         if ($productCategory != null) {
  443.             $productCategory->delete();
  444.         }
  445.     }
  446.     public function addAccessory(ProductAddAccessoryEvent $event): void
  447.     {
  448.         if (AccessoryQuery::create()
  449.             ->filterByAccessory($event->getAccessoryId())
  450.             ->filterByProductId($event->getProduct()->getId())->count() <= 0) {
  451.             $accessory = new Accessory();
  452.             $accessory
  453.                 ->setProductId($event->getProduct()->getId())
  454.                 ->setAccessory($event->getAccessoryId())
  455.             ->save()
  456.             ;
  457.         }
  458.     }
  459.     public function removeAccessory(ProductDeleteAccessoryEvent $event): void
  460.     {
  461.         $accessory AccessoryQuery::create()
  462.             ->filterByAccessory($event->getAccessoryId())
  463.             ->filterByProductId($event->getProduct()->getId())->findOne()
  464.         ;
  465.         if ($accessory !== null) {
  466.             $accessory
  467.                 ->delete()
  468.             ;
  469.         }
  470.     }
  471.     public function setProductTemplate(ProductSetTemplateEvent $event): void
  472.     {
  473.         $con Propel::getWriteConnection(ProductTableMap::DATABASE_NAME);
  474.         $con->beginTransaction();
  475.         try {
  476.             $product $event->getProduct();
  477.             // Check differences between current coobination and the next one, and clear obsoletes values.
  478.             $nextTemplateId $event->getTemplateId();
  479.             $currentTemplateId $product->getTemplateId();
  480.             // 1. Process product features.
  481.             $currentFeatures FeatureTemplateQuery::create()
  482.                 ->filterByTemplateId($currentTemplateId)
  483.                 ->select([FeatureTemplateTableMap::COL_FEATURE_ID])
  484.                 ->find($con);
  485.             $nextFeatures FeatureTemplateQuery::create()
  486.                 ->filterByTemplateId($nextTemplateId)
  487.                 ->select([FeatureTemplateTableMap::COL_FEATURE_ID])
  488.                 ->find($con);
  489.             // Find features values we shoud delete. To do this, we have to
  490.             // find all features in $currentFeatures that are not present in $nextFeatures
  491.             $featuresToDelete array_diff($currentFeatures->getData(), $nextFeatures->getData());
  492.             // Delete obsolete features values
  493.             foreach ($featuresToDelete as $featureId) {
  494.                 $this->eventDispatcher->dispatch(
  495.                     new FeatureProductDeleteEvent($product->getId(), $featureId),
  496.                     TheliaEvents::PRODUCT_FEATURE_DELETE_VALUE
  497.                 );
  498.             }
  499.             // 2. Process product Attributes
  500.             $currentAttributes AttributeTemplateQuery::create()
  501.                 ->filterByTemplateId($currentTemplateId)
  502.                 ->select([AttributeTemplateTableMap::COL_ATTRIBUTE_ID])
  503.                 ->find($con);
  504.             $nextAttributes AttributeTemplateQuery::create()
  505.                 ->filterByTemplateId($nextTemplateId)
  506.                 ->select([AttributeTemplateTableMap::COL_ATTRIBUTE_ID])
  507.                 ->find($con);
  508.             // Find attributes values we shoud delete. To do this, we have to
  509.             // find all attributes in $currentAttributes that are not present in $nextAttributes
  510.             $attributesToDelete array_diff($currentAttributes->getData(), $nextAttributes->getData());
  511.             // Find PSE which includes $attributesToDelete for the current product/
  512.             $pseToDelete ProductSaleElementsQuery::create()
  513.                 ->filterByProductId($product->getId())
  514.                 ->useAttributeCombinationQuery()
  515.                     ->filterByAttributeId($attributesToDeleteCriteria::IN)
  516.                 ->endUse()
  517.                 ->select([ProductSaleElementsTableMap::COL_ID])
  518.                 ->find();
  519.             // Delete obsolete PSEs
  520.             foreach ($pseToDelete->getData() as $pseId) {
  521.                 $this->eventDispatcher->dispatch(
  522.                     new ProductSaleElementDeleteEvent(
  523.                         $pseId,
  524.                         CurrencyModel::getDefaultCurrency()->getId()
  525.                     ),
  526.                     TheliaEvents::PRODUCT_DELETE_PRODUCT_SALE_ELEMENT
  527.                 );
  528.             }
  529.             // Update the product template
  530.             $template_id $event->getTemplateId();
  531.             // Set it to null if it's zero.
  532.             if ($template_id <= 0) {
  533.                 $template_id null;
  534.             }
  535.             $product->setTemplateId($template_id)->save($con);
  536.             $product->clearProductSaleElementss();
  537.             $event->setProduct($product);
  538.             // Store all the stuff !
  539.             $con->commit();
  540.         } catch (\Exception $ex) {
  541.             $con->rollBack();
  542.             throw $ex;
  543.         }
  544.     }
  545.     /**
  546.      * Changes accessry position, selecting absolute ou relative change.
  547.      *
  548.      * @param $eventName
  549.      *
  550.      * @return object
  551.      */
  552.     public function updateAccessoryPosition(UpdatePositionEvent $event$eventNameEventDispatcherInterface $dispatcher)
  553.     {
  554.         return $this->genericUpdatePosition(AccessoryQuery::create(), $event$dispatcher);
  555.     }
  556.     /**
  557.      * Changes position, selecting absolute ou relative change.
  558.      *
  559.      * @param $eventName
  560.      *
  561.      * @return object
  562.      */
  563.     public function updateContentPosition(UpdatePositionEvent $event$eventNameEventDispatcherInterface $dispatcher)
  564.     {
  565.         return $this->genericUpdatePosition(ProductAssociatedContentQuery::create(), $event$dispatcher);
  566.     }
  567.     /**
  568.      * Update the value of a product feature.
  569.      */
  570.     public function updateFeatureProductValue(FeatureProductUpdateEvent $event): void
  571.     {
  572.         // Prepare the FeatureAv's ID
  573.         $featureAvId $event->getFeatureValue();
  574.         // Search for existing FeatureProduct
  575.         $featureProductQuery FeatureProductQuery::create()
  576.             ->filterByProductId($event->getProductId())
  577.             ->filterByFeatureId($event->getFeatureId())
  578.         ;
  579.         // If it's not a free text value, we can filter by the event's featureValue (which is an ID)
  580.         if ($event->getFeatureValue() !== null && $event->getIsTextValue() === false) {
  581.             $featureProductQuery->filterByFeatureAvId($featureAvId);
  582.         }
  583.         $featureProduct $featureProductQuery->findOne();
  584.         // If the FeatureProduct does not exist, create it
  585.         if ($featureProduct === null) {
  586.             $featureProduct = new FeatureProduct();
  587.             $featureProduct
  588.                 ->setProductId($event->getProductId())
  589.                 ->setFeatureId($event->getFeatureId())
  590.             ;
  591.             // If it's a free text value, create a FeatureAv to handle i18n
  592.             if ($event->getIsTextValue() === true) {
  593.                 $featureProduct->setIsFreeText(true);
  594.                 $createFeatureAvEvent = new FeatureAvCreateEvent();
  595.                 $createFeatureAvEvent
  596.                     ->setFeatureId($event->getFeatureId())
  597.                     ->setLocale($event->getLocale())
  598.                     ->setTitle($event->getFeatureValue());
  599.                 $this->eventDispatcher->dispatch($createFeatureAvEventTheliaEvents::FEATURE_AV_CREATE);
  600.                 $featureAvId $createFeatureAvEvent->getFeatureAv()->getId();
  601.             }
  602.         } // Else if the FeatureProduct exists and is a free text value
  603.         elseif ($featureProduct !== null && $event->getIsTextValue() === true) {
  604.             // Get the FeatureAv
  605.             $freeTextFeatureAv FeatureAvQuery::create()
  606.                 ->filterByFeatureProduct($featureProduct)
  607.                 ->findOneByFeatureId($event->getFeatureId());
  608.             // Get the FeatureAvI18n by locale
  609.             $freeTextFeatureAvI18n FeatureAvI18nQuery::create()
  610.                 ->filterById($freeTextFeatureAv->getId())
  611.                 ->findOneByLocale($event->getLocale());
  612.             // Nothing found for this lang and the new value is not empty : create FeatureAvI18n
  613.             if ($freeTextFeatureAvI18n === null && !empty($featureAvId)) {
  614.                 $featureAvI18n = new FeatureAvI18n();
  615.                 $featureAvI18n
  616.                     ->setId($freeTextFeatureAv->getId())
  617.                     ->setLocale($event->getLocale())
  618.                     ->setTitle($event->getFeatureValue())
  619.                     ->save();
  620.                 $featureAvId $featureAvI18n->getId();
  621.             } // Else if i18n exists but new value is empty : delete FeatureAvI18n
  622.             elseif ($freeTextFeatureAvI18n !== null && empty($featureAvId)) {
  623.                 $freeTextFeatureAvI18n->delete();
  624.                 // Check if there are still some FeatureAvI18n for this FeatureAv
  625.                 $freeTextFeatureAvI18ns FeatureAvI18nQuery::create()
  626.                     ->findById($freeTextFeatureAv->getId());
  627.                 // If there are no more FeatureAvI18ns for this FeatureAv, remove the corresponding FeatureProduct & FeatureAv
  628.                 if (\count($freeTextFeatureAvI18ns) == 0) {
  629.                     $deleteFeatureProductEvent = new FeatureProductDeleteEvent($event->getProductId(), $event->getFeatureId());
  630.                     $this->eventDispatcher->dispatch($deleteFeatureProductEventTheliaEvents::PRODUCT_FEATURE_DELETE_VALUE);
  631.                     $deleteFeatureAvEvent = new FeatureAvDeleteEvent($freeTextFeatureAv->getId());
  632.                     $this->eventDispatcher->dispatch($deleteFeatureAvEventTheliaEvents::FEATURE_AV_DELETE);
  633.                 }
  634.                 return;
  635.             } // Else if a FeatureAvI18n is found and the new value is not empty : update existing FeatureAvI18n
  636.             elseif ($freeTextFeatureAvI18n !== null && !empty($featureAvId)) {
  637.                 $freeTextFeatureAvI18n->setTitle($featureAvId);
  638.                 $freeTextFeatureAvI18n->save();
  639.                 $featureAvId $freeTextFeatureAvI18n->getId();
  640.             } //To prevent Integrity constraint violation
  641.             elseif (empty($featureAvId)) {
  642.                 return;
  643.             }
  644.         }
  645.         $featureProduct->setFeatureAvId($featureAvId);
  646.         $featureProduct->save();
  647.         $event->setFeatureProduct($featureProduct);
  648.     }
  649.     /**
  650.      * Delete a product feature value.
  651.      */
  652.     public function deleteFeatureProductValue(FeatureProductDeleteEvent $event): void
  653.     {
  654.         FeatureProductQuery::create()
  655.             ->filterByProductId($event->getProductId())
  656.             ->filterByFeatureId($event->getFeatureId())
  657.             ->delete()
  658.         ;
  659.     }
  660.     public function deleteImagePSEAssociations(FileDeleteEvent $event): void
  661.     {
  662.         $model $event->getFileToDelete();
  663.         if ($model instanceof ProductImage) {
  664.             $model->getProductSaleElementsProductImages()->delete();
  665.         }
  666.     }
  667.     public function deleteDocumentPSEAssociations(FileDeleteEvent $event): void
  668.     {
  669.         $model $event->getFileToDelete();
  670.         if ($model instanceof ProductDocument) {
  671.             $model->getProductSaleElementsProductDocuments()->delete();
  672.         }
  673.     }
  674.     /**
  675.      * When a feature is removed from a template, the products which are using this feature should be updated.
  676.      */
  677.     public function deleteTemplateFeature(TemplateDeleteFeatureEvent $eventstring $eventNameEventDispatcherInterface $dispatcher): void
  678.     {
  679.         // Detete the removed feature in all products which are using this template
  680.         $products ProductQuery::create()
  681.             ->filterByTemplateId($event->getTemplate()->getId())
  682.             ->find()
  683.         ;
  684.         foreach ($products as $product) {
  685.             $dispatcher->dispatch(
  686.                 new FeatureProductDeleteEvent($product->getId(), $event->getFeatureId()),
  687.                 TheliaEvents::PRODUCT_FEATURE_DELETE_VALUE
  688.             );
  689.         }
  690.     }
  691.     /**
  692.      * When an attribute is removed from a template, the conbinations and PSE of products which are using this template
  693.      * should be updated.
  694.      */
  695.     public function deleteTemplateAttribute(TemplateDeleteAttributeEvent $eventstring $eventNameEventDispatcherInterface $dispatcher): void
  696.     {
  697.         // Detete the removed attribute in all products which are using this template
  698.         $pseToDelete ProductSaleElementsQuery::create()
  699.             ->useProductQuery()
  700.                 ->filterByTemplateId($event->getTemplate()->getId())
  701.             ->endUse()
  702.             ->useAttributeCombinationQuery()
  703.                 ->filterByAttributeId($event->getAttributeId())
  704.             ->endUse()
  705.             ->select([ProductSaleElementsTableMap::COL_ID])
  706.             ->find();
  707.         $currencyId CurrencyModel::getDefaultCurrency()->getId();
  708.         foreach ($pseToDelete->getData() as $pseId) {
  709.             $dispatcher->dispatch(
  710.                 new ProductSaleElementDeleteEvent(
  711.                     $pseId,
  712.                     $currencyId
  713.                 ),
  714.                 TheliaEvents::PRODUCT_DELETE_PRODUCT_SALE_ELEMENT
  715.             );
  716.         }
  717.     }
  718.     /**
  719.      * Check if is a product view and if product_id is visible.
  720.      *
  721.      * @param string $eventName
  722.      */
  723.     public function viewCheck(ViewCheckEvent $event$eventNameEventDispatcherInterface $dispatcher): void
  724.     {
  725.         if ($event->getView() == 'product') {
  726.             $product ProductQuery::create()
  727.                 ->filterById($event->getViewId())
  728.                 ->filterByVisible(1)
  729.                 ->count();
  730.             if ($product == 0) {
  731.                 $dispatcher->dispatch($eventTheliaEvents::VIEW_PRODUCT_ID_NOT_VISIBLE);
  732.             }
  733.         }
  734.     }
  735.     /**
  736.      * @throws NotFoundHttpException
  737.      */
  738.     public function viewProductIdNotVisible(ViewCheckEvent $event): void
  739.     {
  740.         throw new NotFoundHttpException();
  741.     }
  742.     /**
  743.      * {@inheritDoc}
  744.      */
  745.     public static function getSubscribedEvents()
  746.     {
  747.         return [
  748.             TheliaEvents::PRODUCT_CREATE => ['create'128],
  749.             TheliaEvents::PRODUCT_CLONE => ['cloneProduct'128],
  750.             TheliaEvents::PRODUCT_UPDATE => ['update'128],
  751.             TheliaEvents::PRODUCT_DELETE => ['delete'128],
  752.             TheliaEvents::PRODUCT_TOGGLE_VISIBILITY => ['toggleVisibility'128],
  753.             TheliaEvents::PRODUCT_UPDATE_POSITION => ['updatePosition'128],
  754.             TheliaEvents::PRODUCT_UPDATE_SEO => ['updateSeo'128],
  755.             TheliaEvents::PRODUCT_ADD_CONTENT => ['addContent'128],
  756.             TheliaEvents::PRODUCT_REMOVE_CONTENT => ['removeContent'128],
  757.             TheliaEvents::PRODUCT_UPDATE_CONTENT_POSITION => ['updateContentPosition'128],
  758.             TheliaEvents::PRODUCT_ADD_ACCESSORY => ['addAccessory'128],
  759.             TheliaEvents::PRODUCT_REMOVE_ACCESSORY => ['removeAccessory'128],
  760.             TheliaEvents::PRODUCT_UPDATE_ACCESSORY_POSITION => ['updateAccessoryPosition'128],
  761.             TheliaEvents::PRODUCT_ADD_CATEGORY => ['addCategory'128],
  762.             TheliaEvents::PRODUCT_REMOVE_CATEGORY => ['removeCategory'128],
  763.             TheliaEvents::PRODUCT_SET_TEMPLATE => ['setProductTemplate'128],
  764.             TheliaEvents::PRODUCT_FEATURE_UPDATE_VALUE => ['updateFeatureProductValue'128],
  765.             TheliaEvents::PRODUCT_FEATURE_DELETE_VALUE => ['deleteFeatureProductValue'128],
  766.             TheliaEvents::TEMPLATE_DELETE_ATTRIBUTE => ['deleteTemplateAttribute'128],
  767.             TheliaEvents::TEMPLATE_DELETE_FEATURE => ['deleteTemplateFeature'128],
  768.             // Those two have to be executed before
  769.             TheliaEvents::IMAGE_DELETE => ['deleteImagePSEAssociations'192],
  770.             TheliaEvents::DOCUMENT_DELETE => ['deleteDocumentPSEAssociations'192],
  771.             TheliaEvents::VIEW_CHECK => ['viewCheck'128],
  772.             TheliaEvents::VIEW_PRODUCT_ID_NOT_VISIBLE => ['viewProductIdNotVisible'128],
  773.         ];
  774.     }
  775. }