core/lib/Thelia/Action/Product.php line 382

  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.     public function updateSeo(UpdateSeoEvent $event$eventNameEventDispatcherInterface $dispatcher)
  325.     {
  326.         return $this->genericUpdateSeo(ProductQuery::create(), $event$dispatcher);
  327.     }
  328.     /**
  329.      * Delete a product entry.
  330.      *
  331.      * @throws \Exception
  332.      */
  333.     public function delete(ProductDeleteEvent $event): void
  334.     {
  335.         if (null !== $product ProductQuery::create()->findPk($event->getProductId())) {
  336.             $con Propel::getWriteConnection(ProductTableMap::DATABASE_NAME);
  337.             $con->beginTransaction();
  338.             try {
  339.                 $fileList = ['images' => [], 'documentList' => []];
  340.                 // Get product's files to delete after product deletion
  341.                 $fileList['images']['list'] = ProductImageQuery::create()
  342.                     ->findByProductId($event->getProductId());
  343.                 $fileList['images']['type'] = TheliaEvents::IMAGE_DELETE;
  344.                 $fileList['documentList']['list'] = ProductDocumentQuery::create()
  345.                     ->findByProductId($event->getProductId());
  346.                 $fileList['documentList']['type'] = TheliaEvents::DOCUMENT_DELETE;
  347.                 // Delete product
  348.                 $product
  349.                     ->delete($con)
  350.                 ;
  351.                 $event->setProduct($product);
  352.                 // Dispatch delete product's files event
  353.                 foreach ($fileList as $fileTypeList) {
  354.                     foreach ($fileTypeList['list'] as $fileToDelete) {
  355.                         $fileDeleteEvent = new FileDeleteEvent($fileToDelete);
  356.                         $this->eventDispatcher->dispatch($fileDeleteEvent$fileTypeList['type']);
  357.                     }
  358.                 }
  359.                 $con->commit();
  360.             } catch (\Exception $e) {
  361.                 $con->rollBack();
  362.                 throw $e;
  363.             }
  364.         }
  365.     }
  366.     /**
  367.      * Toggle product visibility. No form used here.
  368.      */
  369.     public function toggleVisibility(ProductToggleVisibilityEvent $event): void
  370.     {
  371.         $product $event->getProduct();
  372.         $product
  373.             ->setVisible($product->getVisible() ? false true)
  374.             ->save()
  375.         ;
  376.         $event->setProduct($product);
  377.     }
  378.     /**
  379.      * Changes position, selecting absolute ou relative change.
  380.      */
  381.     public function updatePosition(UpdatePositionEvent $event$eventNameEventDispatcherInterface $dispatcher): void
  382.     {
  383.         $this->genericUpdateDelegatePosition(
  384.             ProductCategoryQuery::create()
  385.                 ->filterByProductId($event->getObjectId())
  386.                 ->filterByCategoryId($event->getReferrerId()),
  387.             $event,
  388.             $dispatcher
  389.         );
  390.     }
  391.     public function addContent(ProductAddContentEvent $event): void
  392.     {
  393.         if (ProductAssociatedContentQuery::create()
  394.             ->filterByContentId($event->getContentId())
  395.              ->filterByProduct($event->getProduct())->count() <= 0) {
  396.             $content = new ProductAssociatedContent();
  397.             $content
  398.                 ->setProduct($event->getProduct())
  399.                 ->setContentId($event->getContentId())
  400.                 ->save()
  401.             ;
  402.         }
  403.     }
  404.     public function removeContent(ProductDeleteContentEvent $event): void
  405.     {
  406.         $content ProductAssociatedContentQuery::create()
  407.             ->filterByContentId($event->getContentId())
  408.             ->filterByProduct($event->getProduct())->findOne()
  409.         ;
  410.         if ($content !== null) {
  411.             $content
  412.                 ->delete()
  413.             ;
  414.         }
  415.     }
  416.     public function addCategory(ProductAddCategoryEvent $event): void
  417.     {
  418.         if (ProductCategoryQuery::create()
  419.             ->filterByProduct($event->getProduct())
  420.             ->filterByCategoryId($event->getCategoryId())
  421.             ->count() <= 0) {
  422.             $productCategory = (new ProductCategory())
  423.                 ->setProduct($event->getProduct())
  424.                 ->setCategoryId($event->getCategoryId())
  425.                 ->setDefaultCategory(false);
  426.             $productCategory
  427.                 ->setPosition($productCategory->getNextPosition())
  428.                 ->save();
  429.         }
  430.     }
  431.     public function removeCategory(ProductDeleteCategoryEvent $event): void
  432.     {
  433.         $productCategory ProductCategoryQuery::create()
  434.             ->filterByProduct($event->getProduct())
  435.             ->filterByCategoryId($event->getCategoryId())
  436.             ->findOne();
  437.         if ($productCategory != null) {
  438.             $productCategory->delete();
  439.         }
  440.     }
  441.     public function addAccessory(ProductAddAccessoryEvent $event): void
  442.     {
  443.         if (AccessoryQuery::create()
  444.             ->filterByAccessory($event->getAccessoryId())
  445.             ->filterByProductId($event->getProduct()->getId())->count() <= 0) {
  446.             $accessory = new Accessory();
  447.             $accessory
  448.                 ->setProductId($event->getProduct()->getId())
  449.                 ->setAccessory($event->getAccessoryId())
  450.             ->save()
  451.             ;
  452.         }
  453.     }
  454.     public function removeAccessory(ProductDeleteAccessoryEvent $event): void
  455.     {
  456.         $accessory AccessoryQuery::create()
  457.             ->filterByAccessory($event->getAccessoryId())
  458.             ->filterByProductId($event->getProduct()->getId())->findOne()
  459.         ;
  460.         if ($accessory !== null) {
  461.             $accessory
  462.                 ->delete()
  463.             ;
  464.         }
  465.     }
  466.     public function setProductTemplate(ProductSetTemplateEvent $event): void
  467.     {
  468.         $con Propel::getWriteConnection(ProductTableMap::DATABASE_NAME);
  469.         $con->beginTransaction();
  470.         try {
  471.             $product $event->getProduct();
  472.             // Check differences between current coobination and the next one, and clear obsoletes values.
  473.             $nextTemplateId $event->getTemplateId();
  474.             $currentTemplateId $product->getTemplateId();
  475.             // 1. Process product features.
  476.             $currentFeatures FeatureTemplateQuery::create()
  477.                 ->filterByTemplateId($currentTemplateId)
  478.                 ->select([FeatureTemplateTableMap::COL_FEATURE_ID])
  479.                 ->find($con);
  480.             $nextFeatures FeatureTemplateQuery::create()
  481.                 ->filterByTemplateId($nextTemplateId)
  482.                 ->select([FeatureTemplateTableMap::COL_FEATURE_ID])
  483.                 ->find($con);
  484.             // Find features values we shoud delete. To do this, we have to
  485.             // find all features in $currentFeatures that are not present in $nextFeatures
  486.             $featuresToDelete array_diff($currentFeatures->getData(), $nextFeatures->getData());
  487.             // Delete obsolete features values
  488.             foreach ($featuresToDelete as $featureId) {
  489.                 $this->eventDispatcher->dispatch(
  490.                     new FeatureProductDeleteEvent($product->getId(), $featureId),
  491.                     TheliaEvents::PRODUCT_FEATURE_DELETE_VALUE
  492.                 );
  493.             }
  494.             // 2. Process product Attributes
  495.             $currentAttributes AttributeTemplateQuery::create()
  496.                 ->filterByTemplateId($currentTemplateId)
  497.                 ->select([AttributeTemplateTableMap::COL_ATTRIBUTE_ID])
  498.                 ->find($con);
  499.             $nextAttributes AttributeTemplateQuery::create()
  500.                 ->filterByTemplateId($nextTemplateId)
  501.                 ->select([AttributeTemplateTableMap::COL_ATTRIBUTE_ID])
  502.                 ->find($con);
  503.             // Find attributes values we shoud delete. To do this, we have to
  504.             // find all attributes in $currentAttributes that are not present in $nextAttributes
  505.             $attributesToDelete array_diff($currentAttributes->getData(), $nextAttributes->getData());
  506.             // Find PSE which includes $attributesToDelete for the current product/
  507.             $pseToDelete ProductSaleElementsQuery::create()
  508.                 ->filterByProductId($product->getId())
  509.                 ->useAttributeCombinationQuery()
  510.                     ->filterByAttributeId($attributesToDeleteCriteria::IN)
  511.                 ->endUse()
  512.                 ->select([ProductSaleElementsTableMap::COL_ID])
  513.                 ->find();
  514.             // Delete obsolete PSEs
  515.             foreach ($pseToDelete->getData() as $pseId) {
  516.                 $this->eventDispatcher->dispatch(
  517.                     new ProductSaleElementDeleteEvent(
  518.                         $pseId,
  519.                         CurrencyModel::getDefaultCurrency()->getId()
  520.                     ),
  521.                     TheliaEvents::PRODUCT_DELETE_PRODUCT_SALE_ELEMENT
  522.                 );
  523.             }
  524.             // Update the product template
  525.             $template_id $event->getTemplateId();
  526.             // Set it to null if it's zero.
  527.             if ($template_id <= 0) {
  528.                 $template_id null;
  529.             }
  530.             $product->setTemplateId($template_id)->save($con);
  531.             $product->clearProductSaleElementss();
  532.             $event->setProduct($product);
  533.             // Store all the stuff !
  534.             $con->commit();
  535.         } catch (\Exception $ex) {
  536.             $con->rollBack();
  537.             throw $ex;
  538.         }
  539.     }
  540.     /**
  541.      * Changes accessry position, selecting absolute ou relative change.
  542.      *
  543.      * @return object
  544.      */
  545.     public function updateAccessoryPosition(UpdatePositionEvent $event$eventNameEventDispatcherInterface $dispatcher)
  546.     {
  547.         return $this->genericUpdatePosition(AccessoryQuery::create(), $event$dispatcher);
  548.     }
  549.     /**
  550.      * Changes position, selecting absolute ou relative change.
  551.      *
  552.      * @return object
  553.      */
  554.     public function updateContentPosition(UpdatePositionEvent $event$eventNameEventDispatcherInterface $dispatcher)
  555.     {
  556.         return $this->genericUpdatePosition(ProductAssociatedContentQuery::create(), $event$dispatcher);
  557.     }
  558.     /**
  559.      * Update the value of a product feature.
  560.      */
  561.     public function updateFeatureProductValue(FeatureProductUpdateEvent $event): void
  562.     {
  563.         // Prepare the FeatureAv's ID
  564.         $featureAvId $event->getFeatureValue();
  565.         // Search for existing FeatureProduct
  566.         $featureProductQuery FeatureProductQuery::create()
  567.             ->filterByProductId($event->getProductId())
  568.             ->filterByFeatureId($event->getFeatureId())
  569.         ;
  570.         // If it's not a free text value, we can filter by the event's featureValue (which is an ID)
  571.         if ($event->getFeatureValue() !== null && $event->getIsTextValue() === false) {
  572.             $featureProductQuery->filterByFeatureAvId($featureAvId);
  573.         }
  574.         $featureProduct $featureProductQuery->findOne();
  575.         // If the FeatureProduct does not exist, create it
  576.         if ($featureProduct === null) {
  577.             $featureProduct = new FeatureProduct();
  578.             $featureProduct
  579.                 ->setProductId($event->getProductId())
  580.                 ->setFeatureId($event->getFeatureId())
  581.             ;
  582.             // If it's a free text value, create a FeatureAv to handle i18n
  583.             if ($event->getIsTextValue() === true) {
  584.                 $featureProduct->setIsFreeText(true);
  585.                 $createFeatureAvEvent = new FeatureAvCreateEvent();
  586.                 $createFeatureAvEvent
  587.                     ->setFeatureId($event->getFeatureId())
  588.                     ->setLocale($event->getLocale())
  589.                     ->setTitle($event->getFeatureValue());
  590.                 $this->eventDispatcher->dispatch($createFeatureAvEventTheliaEvents::FEATURE_AV_CREATE);
  591.                 $featureAvId $createFeatureAvEvent->getFeatureAv()->getId();
  592.             }
  593.         } // Else if the FeatureProduct exists and is a free text value
  594.         elseif ($featureProduct !== null && $event->getIsTextValue() === true) {
  595.             // Get the FeatureAv
  596.             $freeTextFeatureAv FeatureAvQuery::create()
  597.                 ->filterByFeatureProduct($featureProduct)
  598.                 ->findOneByFeatureId($event->getFeatureId());
  599.             // Get the FeatureAvI18n by locale
  600.             $freeTextFeatureAvI18n FeatureAvI18nQuery::create()
  601.                 ->filterById($freeTextFeatureAv->getId())
  602.                 ->findOneByLocale($event->getLocale());
  603.             // Nothing found for this lang and the new value is not empty : create FeatureAvI18n
  604.             if ($freeTextFeatureAvI18n === null && !empty($featureAvId)) {
  605.                 $featureAvI18n = new FeatureAvI18n();
  606.                 $featureAvI18n
  607.                     ->setId($freeTextFeatureAv->getId())
  608.                     ->setLocale($event->getLocale())
  609.                     ->setTitle($event->getFeatureValue())
  610.                     ->save();
  611.                 $featureAvId $featureAvI18n->getId();
  612.             } // Else if i18n exists but new value is empty : delete FeatureAvI18n
  613.             elseif ($freeTextFeatureAvI18n !== null && empty($featureAvId)) {
  614.                 $freeTextFeatureAvI18n->delete();
  615.                 // Check if there are still some FeatureAvI18n for this FeatureAv
  616.                 $freeTextFeatureAvI18ns FeatureAvI18nQuery::create()
  617.                     ->findById($freeTextFeatureAv->getId());
  618.                 // If there are no more FeatureAvI18ns for this FeatureAv, remove the corresponding FeatureProduct & FeatureAv
  619.                 if (\count($freeTextFeatureAvI18ns) == 0) {
  620.                     $deleteFeatureProductEvent = new FeatureProductDeleteEvent($event->getProductId(), $event->getFeatureId());
  621.                     $this->eventDispatcher->dispatch($deleteFeatureProductEventTheliaEvents::PRODUCT_FEATURE_DELETE_VALUE);
  622.                     $deleteFeatureAvEvent = new FeatureAvDeleteEvent($freeTextFeatureAv->getId());
  623.                     $this->eventDispatcher->dispatch($deleteFeatureAvEventTheliaEvents::FEATURE_AV_DELETE);
  624.                 }
  625.                 return;
  626.             } // Else if a FeatureAvI18n is found and the new value is not empty : update existing FeatureAvI18n
  627.             elseif ($freeTextFeatureAvI18n !== null && !empty($featureAvId)) {
  628.                 $freeTextFeatureAvI18n->setTitle($featureAvId);
  629.                 $freeTextFeatureAvI18n->save();
  630.                 $featureAvId $freeTextFeatureAvI18n->getId();
  631.             } // To prevent Integrity constraint violation
  632.             elseif (empty($featureAvId)) {
  633.                 return;
  634.             }
  635.         }
  636.         $featureProduct->setFeatureAvId($featureAvId);
  637.         $featureProduct->save();
  638.         $event->setFeatureProduct($featureProduct);
  639.     }
  640.     /**
  641.      * Delete a product feature value.
  642.      */
  643.     public function deleteFeatureProductValue(FeatureProductDeleteEvent $event): void
  644.     {
  645.         FeatureProductQuery::create()
  646.             ->filterByProductId($event->getProductId())
  647.             ->filterByFeatureId($event->getFeatureId())
  648.             ->delete()
  649.         ;
  650.     }
  651.     public function deleteImagePSEAssociations(FileDeleteEvent $event): void
  652.     {
  653.         $model $event->getFileToDelete();
  654.         if ($model instanceof ProductImage) {
  655.             $model->getProductSaleElementsProductImages()->delete();
  656.         }
  657.     }
  658.     public function deleteDocumentPSEAssociations(FileDeleteEvent $event): void
  659.     {
  660.         $model $event->getFileToDelete();
  661.         if ($model instanceof ProductDocument) {
  662.             $model->getProductSaleElementsProductDocuments()->delete();
  663.         }
  664.     }
  665.     /**
  666.      * When a feature is removed from a template, the products which are using this feature should be updated.
  667.      */
  668.     public function deleteTemplateFeature(TemplateDeleteFeatureEvent $eventstring $eventNameEventDispatcherInterface $dispatcher): void
  669.     {
  670.         // Detete the removed feature in all products which are using this template
  671.         $products ProductQuery::create()
  672.             ->filterByTemplateId($event->getTemplate()->getId())
  673.             ->find()
  674.         ;
  675.         foreach ($products as $product) {
  676.             $dispatcher->dispatch(
  677.                 new FeatureProductDeleteEvent($product->getId(), $event->getFeatureId()),
  678.                 TheliaEvents::PRODUCT_FEATURE_DELETE_VALUE
  679.             );
  680.         }
  681.     }
  682.     /**
  683.      * When an attribute is removed from a template, the conbinations and PSE of products which are using this template
  684.      * should be updated.
  685.      */
  686.     public function deleteTemplateAttribute(TemplateDeleteAttributeEvent $eventstring $eventNameEventDispatcherInterface $dispatcher): void
  687.     {
  688.         // Detete the removed attribute in all products which are using this template
  689.         $pseToDelete ProductSaleElementsQuery::create()
  690.             ->useProductQuery()
  691.                 ->filterByTemplateId($event->getTemplate()->getId())
  692.             ->endUse()
  693.             ->useAttributeCombinationQuery()
  694.                 ->filterByAttributeId($event->getAttributeId())
  695.             ->endUse()
  696.             ->select([ProductSaleElementsTableMap::COL_ID])
  697.             ->find();
  698.         $currencyId CurrencyModel::getDefaultCurrency()->getId();
  699.         foreach ($pseToDelete->getData() as $pseId) {
  700.             $dispatcher->dispatch(
  701.                 new ProductSaleElementDeleteEvent(
  702.                     $pseId,
  703.                     $currencyId
  704.                 ),
  705.                 TheliaEvents::PRODUCT_DELETE_PRODUCT_SALE_ELEMENT
  706.             );
  707.         }
  708.     }
  709.     /**
  710.      * Check if is a product view and if product_id is visible.
  711.      *
  712.      * @param string $eventName
  713.      */
  714.     public function viewCheck(ViewCheckEvent $event$eventNameEventDispatcherInterface $dispatcher): void
  715.     {
  716.         if ($event->getView() == 'product') {
  717.             $product ProductQuery::create()
  718.                 ->filterById($event->getViewId())
  719.                 ->filterByVisible(1)
  720.                 ->count();
  721.             if ($product == 0) {
  722.                 $dispatcher->dispatch($eventTheliaEvents::VIEW_PRODUCT_ID_NOT_VISIBLE);
  723.             }
  724.         }
  725.     }
  726.     /**
  727.      * @throws NotFoundHttpException
  728.      */
  729.     public function viewProductIdNotVisible(ViewCheckEvent $event): void
  730.     {
  731.         throw new NotFoundHttpException();
  732.     }
  733.     /**
  734.      * {@inheritDoc}
  735.      */
  736.     public static function getSubscribedEvents()
  737.     {
  738.         return [
  739.             TheliaEvents::PRODUCT_CREATE => ['create'128],
  740.             TheliaEvents::PRODUCT_CLONE => ['cloneProduct'128],
  741.             TheliaEvents::PRODUCT_UPDATE => ['update'128],
  742.             TheliaEvents::PRODUCT_DELETE => ['delete'128],
  743.             TheliaEvents::PRODUCT_TOGGLE_VISIBILITY => ['toggleVisibility'128],
  744.             TheliaEvents::PRODUCT_UPDATE_POSITION => ['updatePosition'128],
  745.             TheliaEvents::PRODUCT_UPDATE_SEO => ['updateSeo'128],
  746.             TheliaEvents::PRODUCT_ADD_CONTENT => ['addContent'128],
  747.             TheliaEvents::PRODUCT_REMOVE_CONTENT => ['removeContent'128],
  748.             TheliaEvents::PRODUCT_UPDATE_CONTENT_POSITION => ['updateContentPosition'128],
  749.             TheliaEvents::PRODUCT_ADD_ACCESSORY => ['addAccessory'128],
  750.             TheliaEvents::PRODUCT_REMOVE_ACCESSORY => ['removeAccessory'128],
  751.             TheliaEvents::PRODUCT_UPDATE_ACCESSORY_POSITION => ['updateAccessoryPosition'128],
  752.             TheliaEvents::PRODUCT_ADD_CATEGORY => ['addCategory'128],
  753.             TheliaEvents::PRODUCT_REMOVE_CATEGORY => ['removeCategory'128],
  754.             TheliaEvents::PRODUCT_SET_TEMPLATE => ['setProductTemplate'128],
  755.             TheliaEvents::PRODUCT_FEATURE_UPDATE_VALUE => ['updateFeatureProductValue'128],
  756.             TheliaEvents::PRODUCT_FEATURE_DELETE_VALUE => ['deleteFeatureProductValue'128],
  757.             TheliaEvents::TEMPLATE_DELETE_ATTRIBUTE => ['deleteTemplateAttribute'128],
  758.             TheliaEvents::TEMPLATE_DELETE_FEATURE => ['deleteTemplateFeature'128],
  759.             // Those two have to be executed before
  760.             TheliaEvents::IMAGE_DELETE => ['deleteImagePSEAssociations'192],
  761.             TheliaEvents::DOCUMENT_DELETE => ['deleteDocumentPSEAssociations'192],
  762.             TheliaEvents::VIEW_CHECK => ['viewCheck'128],
  763.             TheliaEvents::VIEW_PRODUCT_ID_NOT_VISIBLE => ['viewProductIdNotVisible'128],
  764.         ];
  765.     }
  766. }