classes/XLite/Model/Product.php line 80

Open in your IDE?
  1. <?php
  2. /**
  3.  * Copyright (c) 2001-present X-Cart Holdings LLC. All rights reserved.
  4.  * See https://www.x-cart.com/license-agreement.html for license details.
  5.  */
  6. namespace XLite\Model;
  7. use ApiPlatform\Core\Annotation as ApiPlatform;
  8. use Doctrine\ORM\Mapping as ORM;
  9. use XCart\Framework\ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\IntegerDateFilter;
  10. use XLite\API\Endpoint\Product\DTO\ProductInput as ProductInput;
  11. use XLite\API\Endpoint\Product\DTO\ProductOutput as ProductOutput;
  12. use XLite\API\Endpoint\ProductInventory\DTO\ProductInventoryOutput;
  13. use XLite\API\Filter\TranslationAwareOrderFilter;
  14. use XLite\Core\Cache\ExecuteCachedTrait;
  15. use XLite\Core\Converter;
  16. use XLite\Core\Database;
  17. use XLite\Core\Model\EntityVersion\EntityVersionInterface;
  18. use XLite\Core\Model\EntityVersion\EntityVersionTrait;
  19. use XLite\Model\Base\IMark;
  20. use XLite\Model\Product\ProductStockAvailabilityPolicy;
  21. /**
  22.  * The "product" model class
  23.  *
  24.  * @ORM\Entity
  25.  * @ORM\Table  (name="products",
  26.  *      indexes={
  27.  *          @ORM\Index (name="sku", columns={"sku"}),
  28.  *          @ORM\Index (name="price", columns={"price"}),
  29.  *          @ORM\Index (name="weight", columns={"weight"}),
  30.  *          @ORM\Index (name="free_shipping", columns={"free_shipping"}),
  31.  *          @ORM\Index (name="customerArea", columns={"enabled","arrivalDate"}),
  32.  *          @ORM\Index (name="getSearchParamsTypeBetween", columns={"arrivalDate"}),
  33.  *          @ORM\Index (name="prepareCndQtyRange", columns={"amount"}),
  34.  *          @ORM\Index (name="bestsellers", columns={"sales"}),
  35.  *          @ORM\Index (name="isDemo", columns={"isDemoProduct"})
  36.  *      }
  37.  * )
  38.  * @ApiPlatform\ApiResource(
  39.  *     input=ProductInput::class,
  40.  *     output=ProductOutput::class,
  41.  *     itemOperations={
  42.  *         "get"={
  43.  *             "method"="GET",
  44.  *             "path"="/products/{product_id}",
  45.  *             "identifiers"={"product_id"},
  46.  *         },
  47.  *         "put"={
  48.  *             "method"="PUT",
  49.  *             "path"="/products/{product_id}",
  50.  *             "identifiers"={"product_id"},
  51.  *         },
  52.  *         "delete"={
  53.  *             "method"="DELETE",
  54.  *             "path"="/products/{product_id}",
  55.  *             "identifiers"={"product_id"},
  56.  *         }
  57.  *     },
  58.  *     collectionOperations={
  59.  *         "get"={
  60.  *             "method"="GET",
  61.  *             "path"="/products",
  62.  *             "identifiers"={"product_id"},
  63.  *         },
  64.  *         "post"={
  65.  *             "method"="POST",
  66.  *             "path"="/products",
  67.  *             "identifiers"={"product_id"},
  68.  *         },
  69.  *         "get_product_inventory"={
  70.  *              "method"="GET",
  71.  *              "path"="/products/inventory",
  72.  *              "identifiers"={"product_id"},
  73.  *              "output"=ProductInventoryOutput::class,
  74.  *              "openapi_context"={
  75.  *                  "summary"="Retrieve a product inventory list",
  76.  *              },
  77.  *          },
  78.  *     }
  79.  * )
  80.  * @ApiPlatform\ApiFilter(IntegerDateFilter::class, properties={"updateDate"})
  81.  * @ApiPlatform\ApiFilter(TranslationAwareOrderFilter::class, properties={"name"="ASC","price"="ASC","arrivalDate"="ASC"})
  82.  */
  83. class Product extends \XLite\Model\Base\Catalog implements \XLite\Model\Base\IOrderItemEntityVersionInterface
  84. {
  85.     use EntityVersionTrait;
  86.     use ExecuteCachedTrait;
  87.     /**
  88.      * Default amounts
  89.      */
  90.     public const AMOUNT_DEFAULT_INV_TRACK 1000;
  91.     public const AMOUNT_DEFAULT_LOW_LIMIT 10;
  92.     /**
  93.      * Meta description type
  94.      */
  95.     public const META_DESC_TYPE_AUTO   'A';
  96.     public const META_DESC_TYPE_CUSTOM 'C';
  97.     /**
  98.      * Product unique ID
  99.      *
  100.      * @var integer
  101.      *
  102.      * @ORM\Id
  103.      * @ORM\GeneratedValue (strategy="AUTO")
  104.      * @ORM\Column         (type="integer", options={ "unsigned": true })
  105.      */
  106.     protected $product_id;
  107.     /**
  108.      * Product price
  109.      *
  110.      * @var float
  111.      *
  112.      * @ORM\Column (type="decimal", precision=14, scale=4)
  113.      */
  114.     protected $price 0.0000;
  115.     /**
  116.      * Product SKU
  117.      *
  118.      * @var string
  119.      *
  120.      * @ORM\Column (type="string", length=32, nullable=true)
  121.      */
  122.     protected $sku;
  123.     /**
  124.      * Is product available or not
  125.      *
  126.      * @var boolean
  127.      *
  128.      * @ORM\Column (type="boolean")
  129.      */
  130.     protected $enabled true;
  131.     /**
  132.      * Product weight
  133.      *
  134.      * @var float
  135.      *
  136.      * @ORM\Column (type="decimal", precision=14, scale=4)
  137.      */
  138.     protected $weight 0.0000;
  139.     /**
  140.      * Is product shipped in separate box
  141.      *
  142.      * @var boolean
  143.      *
  144.      * @ORM\Column (type="boolean")
  145.      */
  146.     protected $useSeparateBox false;
  147.     /**
  148.      * Product box width
  149.      *
  150.      * @var float
  151.      *
  152.      * @ORM\Column (type="decimal", precision=14, scale=4)
  153.      */
  154.     protected $boxWidth 0.0000;
  155.     /**
  156.      * Product box length
  157.      *
  158.      * @var float
  159.      *
  160.      * @ORM\Column (type="decimal", precision=14, scale=4)
  161.      */
  162.     protected $boxLength 0.0000;
  163.     /**
  164.      * Product box height
  165.      *
  166.      * @var float
  167.      *
  168.      * @ORM\Column (type="decimal", precision=14, scale=4)
  169.      */
  170.     protected $boxHeight 0.0000;
  171.     /**
  172.      * How many product items can be placed in a box
  173.      *
  174.      * @var integer
  175.      *
  176.      * @ORM\Column (type="integer")
  177.      */
  178.     protected $itemsPerBox 1;
  179.     /**
  180.      * Flag: false - product is shippable, true - product is not shippable
  181.      *
  182.      * @var boolean
  183.      *
  184.      * @ORM\Column (type="boolean")
  185.      */
  186.     protected $free_shipping false;
  187.     /**
  188.      * If false then the product is free from any taxes
  189.      *
  190.      * @var boolean
  191.      *
  192.      * @ORM\Column (type="boolean")
  193.      */
  194.     protected $taxable true;
  195.     /**
  196.      * Arrival date (UNIX timestamp)
  197.      *
  198.      * @var integer
  199.      *
  200.      * @ORM\Column (type="integer")
  201.      */
  202.     protected $arrivalDate 0;
  203.     /**
  204.      * Creation date (UNIX timestamp)
  205.      *
  206.      * @var integer
  207.      *
  208.      * @ORM\Column (type="integer")
  209.      */
  210.     protected $date 0;
  211.     /**
  212.      * Update date (UNIX timestamp)
  213.      *
  214.      * @var integer
  215.      *
  216.      * @ORM\Column (type="integer", options={ "unsigned": true })
  217.      */
  218.     protected $updateDate 0;
  219.     /**
  220.      * Is product need process or not
  221.      *
  222.      * @var boolean
  223.      *
  224.      * @ORM\Column (type="boolean")
  225.      */
  226.     protected $needProcess true;
  227.     /**
  228.      * Relation to a CategoryProducts entities
  229.      *
  230.      * @var \Doctrine\ORM\PersistentCollection
  231.      *
  232.      * @ORM\OneToMany (targetEntity="XLite\Model\CategoryProducts", mappedBy="product", cascade={"all"})
  233.      * @ORM\OrderBy   ({"orderbyInProduct" = "ASC"})
  234.      */
  235.     protected $categoryProducts;
  236.     /**
  237.      * Product order items
  238.      *
  239.      * @var \XLite\Model\OrderItem
  240.      *
  241.      * @ORM\OneToMany (targetEntity="XLite\Model\OrderItem", mappedBy="object")
  242.      */
  243.     protected $order_items;
  244.     /**
  245.      * Product images
  246.      *
  247.      * @var \Doctrine\Common\Collections\Collection
  248.      *
  249.      * @ORM\OneToMany (targetEntity="XLite\Model\Image\Product\Image", mappedBy="product", cascade={"all"})
  250.      * @ORM\OrderBy   ({"orderby" = "ASC"})
  251.      */
  252.     protected $images;
  253.     // {{{ Inventory properties
  254.     /**
  255.      * Is inventory tracking enabled or not
  256.      *
  257.      * @var boolean
  258.      *
  259.      * @ORM\Column (type="boolean")
  260.      */
  261.     protected $inventoryEnabled true;
  262.     /**
  263.      * Amount
  264.      *
  265.      * @var integer
  266.      *
  267.      * @ORM\Column (type="integer", options={ "unsigned": true })
  268.      */
  269.     protected $amount self::AMOUNT_DEFAULT_INV_TRACK;
  270.     /**
  271.      * Is low limit notification enabled for customer or not
  272.      *
  273.      * @var boolean
  274.      *
  275.      * @ORM\Column (type="boolean")
  276.      */
  277.     protected $lowLimitEnabledCustomer true;
  278.     /**
  279.      * Is low limit notification enabled for admin or not
  280.      *
  281.      * @var boolean
  282.      *
  283.      * @ORM\Column (type="boolean")
  284.      */
  285.     protected $lowLimitEnabled false;
  286.     /**
  287.      * Low limit amount
  288.      *
  289.      * @var integer
  290.      *
  291.      * @ORM\Column (type="integer", options={ "unsigned": true })
  292.      */
  293.     protected $lowLimitAmount self::AMOUNT_DEFAULT_LOW_LIMIT;
  294.     // }}}
  295.     /**
  296.      * Product class (relation)
  297.      *
  298.      * @var \XLite\Model\ProductClass
  299.      *
  300.      * @ORM\ManyToOne  (targetEntity="XLite\Model\ProductClass")
  301.      * @ORM\JoinColumn (name="product_class_id", referencedColumnName="id", onDelete="SET NULL")
  302.      */
  303.     protected $productClass;
  304.     /**
  305.      * Tax class (relation)
  306.      *
  307.      * @var \XLite\Model\TaxClass
  308.      *
  309.      * @ORM\ManyToOne  (targetEntity="XLite\Model\TaxClass")
  310.      * @ORM\JoinColumn (name="tax_class_id", referencedColumnName="id", onDelete="SET NULL")
  311.      */
  312.     protected $taxClass;
  313.     /**
  314.      * Attributes
  315.      *
  316.      * @var \Doctrine\Common\Collections\Collection
  317.      *
  318.      * @ORM\OneToMany (targetEntity="XLite\Model\Attribute", mappedBy="product", cascade={"all"})
  319.      * @ORM\OrderBy   ({"position" = "ASC"})
  320.      */
  321.     protected $attributes;
  322.     /**
  323.      * Attribute value (checkbox)
  324.      *
  325.      * @var \Doctrine\Common\Collections\Collection
  326.      *
  327.      * @ORM\OneToMany (targetEntity="XLite\Model\AttributeValue\AttributeValueCheckbox", mappedBy="product", cascade={"all"})
  328.      */
  329.     protected $attributeValueC;
  330.     /**
  331.      * Attribute value (text)
  332.      *
  333.      * @var \Doctrine\Common\Collections\Collection
  334.      *
  335.      * @ORM\OneToMany (targetEntity="XLite\Model\AttributeValue\AttributeValueText", mappedBy="product", cascade={"all"})
  336.      */
  337.     protected $attributeValueT;
  338.     /**
  339.      * Attribute value (select)
  340.      *
  341.      * @var \Doctrine\Common\Collections\Collection
  342.      *
  343.      * @ORM\OneToMany (targetEntity="XLite\Model\AttributeValue\AttributeValueSelect", mappedBy="product", cascade={"all"})
  344.      */
  345.     protected $attributeValueS;
  346.     /**
  347.      * Attribute value (hidden)
  348.      *
  349.      * @var \Doctrine\Common\Collections\Collection
  350.      *
  351.      * @ORM\OneToMany (targetEntity="XLite\Model\AttributeValue\AttributeValueHidden", mappedBy="product", cascade={"all"})
  352.      */
  353.     protected $attributeValueH;
  354.     /**
  355.      * Show product attributes in a separate tab
  356.      *
  357.      * @var boolean
  358.      *
  359.      * @ORM\Column (type="boolean")
  360.      */
  361.     protected $attrSepTab true;
  362.     /**
  363.      * How much product is sold (used in Top selling products statistics)
  364.      *
  365.      * @var integer
  366.      */
  367.     protected $sold 0;
  368.     /**
  369.      * Quick data
  370.      *
  371.      * @var \Doctrine\Common\Collections\Collection
  372.      *
  373.      * @ORM\OneToMany (targetEntity="XLite\Model\QuickData", mappedBy="product", cascade={"all"})
  374.      */
  375.     protected $quickData;
  376.     /**
  377.      * Storage of current attribute values according to the clear price will be calculated
  378.      *
  379.      * @var array
  380.      */
  381.     protected $attrValues = [];
  382.     /**
  383.      * Memberships
  384.      *
  385.      * @var \Doctrine\Common\Collections\ArrayCollection
  386.      *
  387.      * @ORM\ManyToMany (targetEntity="XLite\Model\Membership", inversedBy="products")
  388.      * @ORM\JoinTable (name="product_membership_links",
  389.      *      joinColumns={@ORM\JoinColumn (name="product_id", referencedColumnName="product_id", onDelete="CASCADE")},
  390.      *      inverseJoinColumns={@ORM\JoinColumn (name="membership_id", referencedColumnName="membership_id", onDelete="CASCADE")}
  391.      * )
  392.      */
  393.     protected $memberships;
  394.     /**
  395.      * Clean URLs
  396.      *
  397.      * @var \Doctrine\Common\Collections\Collection
  398.      *
  399.      * @ORM\OneToMany (targetEntity="XLite\Model\CleanURL", mappedBy="product", cascade={"all"})
  400.      * @ORM\OrderBy   ({"id" = "ASC"})
  401.      */
  402.     protected $cleanURLs;
  403.     /**
  404.      * Meta description type
  405.      *
  406.      * @var string
  407.      *
  408.      * @ORM\Column (type="string", length=1)
  409.      */
  410.     protected $metaDescType 'A';
  411.     /**
  412.      * Sales
  413.      *
  414.      * @var integer
  415.      *
  416.      * @ORM\Column (type="integer", options={ "unsigned": true })
  417.      */
  418.     protected $sales 0;
  419.     /**
  420.      * Flag to exporting entities
  421.      *
  422.      * @var boolean
  423.      *
  424.      * @ORM\Column (type="boolean")
  425.      */
  426.     protected $xcPendingExport false;
  427.     /**
  428.      * @var \Doctrine\Common\Collections\Collection
  429.      *
  430.      * @ORM\OneToMany (targetEntity="XLite\Model\ProductTranslation", mappedBy="owner", cascade={"all"})
  431.      */
  432.     protected $translations;
  433.     /**
  434.      * Flag for demo products.
  435.      *
  436.      * @ORM\Column (type="boolean")
  437.      */
  438.     protected bool $isDemoProduct false;
  439.     /**
  440.      * Return GlobalTabs
  441.      *
  442.      * @return \XLite\Model\Product\GlobalTab[]
  443.      */
  444.     public function getGlobalTabs()
  445.     {
  446.         return $this->executeCachedRuntime(static function () {
  447.             return \XLite\Core\Database::getRepo('\XLite\Model\Product\GlobalTab')->findAll();
  448.         }, ['getGlobalTabs']);
  449.     }
  450.     /**
  451.      * Clone
  452.      *
  453.      * @return static
  454.      */
  455.     public function cloneEntity()
  456.     {
  457.         /** @var Product $newProduct */
  458.         $newProduct parent::cloneEntity();
  459.         $this->cloneEntityScalar($newProduct);
  460.         $this->cloneEntityModels($newProduct);
  461.         $this->cloneEntityCategories($newProduct);
  462.         $this->cloneEntityAttributes($newProduct);
  463.         $this->cloneEntityImages($newProduct);
  464.         $this->cloneEntityMemberships($newProduct);
  465.         Database::getEM()->persist($newProduct);
  466.         foreach ($newProduct->getCleanURLs() as $url) {
  467.             $newProduct->getCleanURLs()->removeElement($url);
  468.             \XLite\Core\Database::getEM()->remove($url);
  469.         }
  470.         $newProduct->setSales(0);
  471.         return $newProduct;
  472.     }
  473.     /**
  474.      * Clone entity (scalar fields)
  475.      *
  476.      * @param Product $newProduct New product
  477.      *
  478.      * @return void
  479.      */
  480.     protected function cloneEntityScalar(Product $newProduct)
  481.     {
  482.         $newProduct->setSku(\XLite\Core\Database::getRepo('XLite\Model\Product')->assembleUniqueSKU($this->getSku()));
  483.         $newProduct->setName($this->getCloneName($this->getName()));
  484.     }
  485.     /**
  486.      * Clone entity (model fields)
  487.      *
  488.      * @param Product $newProduct New product
  489.      *
  490.      * @return void
  491.      */
  492.     protected function cloneEntityModels(Product $newProduct)
  493.     {
  494.         $newProduct->setTaxClass($this->getTaxClass());
  495.         $newProduct->setProductClass($this->getProductClass());
  496.     }
  497.     /**
  498.      * Clone entity (categories)
  499.      *
  500.      * @param Product $newProduct New product
  501.      *
  502.      * @return void
  503.      */
  504.     protected function cloneEntityCategories(Product $newProduct)
  505.     {
  506.         foreach ($this->getCategories() as $category) {
  507.             $link = new \XLite\Model\CategoryProducts();
  508.             $link->setProduct($newProduct);
  509.             $link->setCategory($category);
  510.             $newProduct->addCategoryProducts($link);
  511.             \XLite\Core\Database::getEM()->persist($link);
  512.         }
  513.     }
  514.     /**
  515.      * Clone entity (attributes)
  516.      *
  517.      * @param Product $newProduct New product
  518.      *
  519.      * @return void
  520.      */
  521.     protected function cloneEntityAttributes(Product $newProduct)
  522.     {
  523.         foreach (\XLite\Model\Attribute::getTypes() as $type => $name) {
  524.             $methodGet 'getAttributeValue' $type;
  525.             $methodAdd 'addAttributeValue' $type;
  526.             $groups    = [];
  527.             foreach ($this->$methodGet() as $value) {
  528.                 if (!$value->getAttribute()->getProduct()) {
  529.                     $newValue $value->cloneEntity();
  530.                     $newValue->setProduct($newProduct);
  531.                     $newProduct->$methodAdd($newValue);
  532.                     \XLite\Core\Database::getEM()->persist($newValue);
  533.                     $attribute $value->getAttribute();
  534.                     $property  $attribute->getProperty($value->getProduct());
  535.                     if ($property && $value instanceof \XLite\Model\AttributeValue\Multiple) {
  536.                         $groups[$attribute->getId()] = [
  537.                             'attr'     => $attribute,
  538.                             'property' => $property,
  539.                         ];
  540.                     }
  541.                 }
  542.             }
  543.             foreach ($groups as $group) {
  544.                 $newProperty $group['property']->cloneEntity();
  545.                 $newProperty->setProduct($newProduct);
  546.                 $newProperty->setAttribute($group['attr']);
  547.                 \XLite\Core\Database::getEM()->persist($newProperty);
  548.             }
  549.         }
  550.         foreach ($this->getAttributes() as $attribute) {
  551.             $class        $attribute->getAttributeValueClass($attribute->getType());
  552.             $repo         \XLite\Core\Database::getRepo($class);
  553.             $methodAdd    'addAttributeValue' $attribute->getType();
  554.             $newAttribute $attribute->cloneEntity();
  555.             $newAttribute->setProduct($newProduct);
  556.             $newProduct->addAttributes($newAttribute);
  557.             \XLite\Core\Database::getEM()->persist($newAttribute);
  558.             foreach ($repo->findBy(['attribute' => $attribute'product' => $this]) as $value) {
  559.                 $newValue $value->cloneEntity();
  560.                 $newValue->setProduct($newProduct);
  561.                 $newValue->setAttribute($newAttribute);
  562.                 $newProduct->$methodAdd($newValue);
  563.                 \XLite\Core\Database::getEM()->persist($newValue);
  564.             }
  565.         }
  566.     }
  567.     /**
  568.      * Clone entity (images)
  569.      *
  570.      * @param Product $newProduct New product
  571.      *
  572.      * @return void
  573.      */
  574.     protected function cloneEntityImages(Product $newProduct)
  575.     {
  576.         foreach ($this->getImages() as $image) {
  577.             $newImage $image->cloneEntity();
  578.             $newImage->setProduct($newProduct);
  579.             $newProduct->addImages($newImage);
  580.         }
  581.     }
  582.     /**
  583.      * Clone entity (memberships)
  584.      *
  585.      * @param Product $newProduct New product
  586.      *
  587.      * @return void
  588.      */
  589.     protected function cloneEntityMemberships(Product $newProduct)
  590.     {
  591.         foreach ($this->getMemberships() as $membership) {
  592.             $newProduct->addMemberships($membership);
  593.         }
  594.     }
  595.     /**
  596.      * Constructor
  597.      *
  598.      * @param array $data Entity properties OPTIONAL
  599.      */
  600.     public function __construct(array $data = [])
  601.     {
  602.         $this->categoryProducts = new \Doctrine\Common\Collections\ArrayCollection();
  603.         $this->images           = new \Doctrine\Common\Collections\ArrayCollection();
  604.         $this->order_items      = new \Doctrine\Common\Collections\ArrayCollection();
  605.         $this->memberships      = new \Doctrine\Common\Collections\ArrayCollection();
  606.         $this->attributeValueC  = new \Doctrine\Common\Collections\ArrayCollection();
  607.         $this->attributeValueS  = new \Doctrine\Common\Collections\ArrayCollection();
  608.         $this->attributeValueT  = new \Doctrine\Common\Collections\ArrayCollection();
  609.         $this->attributes       = new \Doctrine\Common\Collections\ArrayCollection();
  610.         $this->quickData        = new \Doctrine\Common\Collections\ArrayCollection();
  611.         $this->free_shipping    = !\XLite\Core\Config::getInstance()->General->requires_shipping_default;
  612.         parent::__construct($data);
  613.     }
  614.     /**
  615.      * Get object unique id
  616.      *
  617.      * @return integer
  618.      */
  619.     public function getId()
  620.     {
  621.         return $this->getProductId();
  622.     }
  623.     /**
  624.      * Get weight
  625.      *
  626.      * @return float
  627.      */
  628.     public function getWeight()
  629.     {
  630.         return $this->weight;
  631.     }
  632.     /**
  633.      * Get price: modules should never overwrite this method
  634.      *
  635.      * @return float
  636.      */
  637.     public function getPrice()
  638.     {
  639.         return $this->price;
  640.     }
  641.     /**
  642.      * Get clear price: this price can be overwritten by modules
  643.      *
  644.      * @return float
  645.      */
  646.     public function getClearPrice()
  647.     {
  648.         return $this->getPrice();
  649.     }
  650.     /**
  651.      * Get net Price
  652.      *
  653.      * @return float
  654.      */
  655.     public function getNetPrice()
  656.     {
  657.         return \XLite\Logic\Price::getInstance()->apply($this'getClearPrice', ['taxable'], 'net');
  658.     }
  659.     /**
  660.      * Get display Price
  661.      *
  662.      * @return float
  663.      */
  664.     public function getDisplayPrice()
  665.     {
  666.         return \XLite\Logic\Price::getInstance()->apply($this'getNetPrice', ['taxable'], 'display');
  667.     }
  668.     /**
  669.      * Get quick data price
  670.      *
  671.      * @return float
  672.      */
  673.     public function getQuickDataPrice()
  674.     {
  675.         $price $this->getClearPrice();
  676.         foreach ($this->prepareAttributeValues() as $av) {
  677.             if (is_object($av)) {
  678.                 $price += $av->getAbsoluteValue('price');
  679.             }
  680.         }
  681.         return $price;
  682.     }
  683.     /**
  684.      * Get clear weight
  685.      *
  686.      * @return float
  687.      */
  688.     public function getClearWeight()
  689.     {
  690.         return $this->getWeight();
  691.     }
  692.     /**
  693.      * Get name
  694.      *
  695.      * @return string
  696.      */
  697.     public function getName()
  698.     {
  699.         return $this->getSoftTranslation()->getName();
  700.     }
  701.     /**
  702.      * Get SKU
  703.      *
  704.      * @return string
  705.      */
  706.     public function getSku()
  707.     {
  708.         return $this->sku !== null ? (string) $this->sku null;
  709.     }
  710.     /**
  711.      * Get quantity of the product
  712.      *
  713.      * @return integer
  714.      */
  715.     public function getQty()
  716.     {
  717.         return $this->getPublicAmount();
  718.     }
  719.     /**
  720.      * Get image
  721.      *
  722.      * @return \XLite\Model\Image\Product\Image
  723.      */
  724.     public function getImage()
  725.     {
  726.         return $this->getImages()->get(0);
  727.     }
  728.     /**
  729.      * Get public images
  730.      *
  731.      * @return array
  732.      */
  733.     public function getPublicImages()
  734.     {
  735.         return $this->getImages()->toArray();
  736.     }
  737.     /**
  738.      * Get free shipping flag
  739.      *
  740.      * @return boolean
  741.      */
  742.     public function getFreeShipping()
  743.     {
  744.         return $this->free_shipping;
  745.     }
  746.     /**
  747.      * Get shippable flag
  748.      *
  749.      * @return boolean
  750.      */
  751.     public function getShippable()
  752.     {
  753.         return !$this->getFreeShipping();
  754.     }
  755.     /**
  756.      * Set shippable flag
  757.      *
  758.      * @param boolean $value Value
  759.      *
  760.      * @return void
  761.      */
  762.     public function setShippable($value)
  763.     {
  764.         $this->setFreeShipping(!$value);
  765.     }
  766.     /**
  767.      * Return true if product can be purchased
  768.      *
  769.      * @return boolean
  770.      */
  771.     public function isAvailable()
  772.     {
  773.         return \XLite::isAdminZone() || $this->isPublicAvailable();
  774.     }
  775.     /**
  776.      * Return true if product can be purchased in customer interface
  777.      *
  778.      * @return boolean
  779.      */
  780.     public function isPublicAvailable()
  781.     {
  782.         return $this->isVisible()
  783.             && $this->availableInDate()
  784.             && !$this->isOutOfStock();
  785.     }
  786.     /**
  787.      * Check product visibility
  788.      *
  789.      * @return boolean
  790.      */
  791.     public function isVisible()
  792.     {
  793.         return $this->getEnabled()
  794.             && $this->hasAvailableMembership();
  795.     }
  796.     /**
  797.      * Get membership Ids
  798.      *
  799.      * @return array
  800.      */
  801.     public function getMembershipIds()
  802.     {
  803.         $result = [];
  804.         foreach ($this->getMemberships() as $membership) {
  805.             $result[] = $membership->getMembershipId();
  806.         }
  807.         return $result;
  808.     }
  809.     /**
  810.      * Flag if the category and active profile have the same memberships. (when category is displayed or hidden)
  811.      *
  812.      * @return boolean
  813.      */
  814.     public function hasAvailableMembership()
  815.     {
  816.         return $this->getMemberships()->count() === 0
  817.             || in_array(\XLite\Core\Auth::getInstance()->getMembershipId(), $this->getMembershipIds());
  818.     }
  819.     /**
  820.      * Flag if the product is available according date/time
  821.      *
  822.      * @return boolean
  823.      */
  824.     public function availableInDate()
  825.     {
  826.         return !$this->getArrivalDate()
  827.             || \XLite\Core\Converter::getDayEnd(static::getUserTime()) > $this->getArrivalDate();
  828.     }
  829.     /**
  830.      * Check if product has image or not
  831.      *
  832.      * @return boolean
  833.      */
  834.     public function hasImage()
  835.     {
  836.         return $this->getImage() !== null && $this->getImage()->isPersistent();
  837.     }
  838.     /**
  839.      * Return image URL
  840.      *
  841.      * @return string|void
  842.      */
  843.     public function getImageURL()
  844.     {
  845.         return $this->getImage() ? $this->getImage()->getURL() : null;
  846.     }
  847.     /**
  848.      * Return random product category
  849.      *
  850.      * @param integer|null $categoryId Category ID OPTIONAL
  851.      *
  852.      * @return \XLite\Model\Category
  853.      */
  854.     public function getCategory($categoryId null)
  855.     {
  856.         $result $this->getLink($categoryId)->getCategory();
  857.         if (empty($result)) {
  858.             $result = new \XLite\Model\Category();
  859.         }
  860.         return $result;
  861.     }
  862.     /**
  863.      * Return random product category ID
  864.      *
  865.      * @param integer|null $categoryId Category ID OPTIONAL
  866.      *
  867.      * @return integer
  868.      */
  869.     public function getCategoryId($categoryId null)
  870.     {
  871.         return $this->getCategory($categoryId)->getCategoryId();
  872.     }
  873.     /**
  874.      * Return list of product categories
  875.      *
  876.      * @return array
  877.      */
  878.     public function getCategories()
  879.     {
  880.         $result = [];
  881.         foreach ($this->getCategoryProducts() as $cp) {
  882.             $result[] = $cp->getCategory();
  883.         }
  884.         return $result;
  885.     }
  886.     // {{{ Inventory methods
  887.     /**
  888.      * Setter
  889.      *
  890.      * @param int $value Value to set
  891.      *
  892.      * @return void
  893.      */
  894.     public function setAmount($value)
  895.     {
  896.         $this->amount $this->correctAmount($value);
  897.     }
  898.     /**
  899.      * Setter
  900.      *
  901.      * @param integer $amount Amount to set
  902.      *
  903.      * @return void
  904.      */
  905.     public function setLowLimitAmount($amount)
  906.     {
  907.         $this->lowLimitAmount $this->correctAmount($amount);
  908.     }
  909.     /**
  910.      * Increase / decrease product inventory amount
  911.      *
  912.      * @param integer $delta Amount delta
  913.      *
  914.      * @return void
  915.      */
  916.     public function changeAmount($delta)
  917.     {
  918.         if ($this->getInventoryEnabled()) {
  919.             $this->setAmount($this->getPublicAmount() + $delta);
  920.         }
  921.     }
  922.     /**
  923.      * Get public amount
  924.      *
  925.      * @return integer
  926.      */
  927.     public function getPublicAmount()
  928.     {
  929.         return $this->getAmount();
  930.     }
  931.     /**
  932.      * Get low available amount
  933.      *
  934.      * @return integer
  935.      */
  936.     public function getLowAvailableAmount()
  937.     {
  938.         return $this->getInventoryEnabled()
  939.             ? min($this->getLowDefaultAmount(), $this->getAvailableAmount())
  940.             : $this->getLowDefaultAmount();
  941.     }
  942.     /**
  943.      * Check if product amount is less than its low limit
  944.      *
  945.      * @return boolean
  946.      */
  947.     public function isLowLimitReached()
  948.     {
  949.         return $this->getInventoryEnabled()
  950.             && $this->getLowLimitEnabled()
  951.             && $this->getPublicAmount() <= $this->getLowLimitAmount();
  952.     }
  953.     /**
  954.      * List of controllers which should not send notifications
  955.      *
  956.      * @return array
  957.      */
  958.     protected function getForbiddenControllers()
  959.     {
  960.         return [
  961.             '\XLite\Controller\Admin\EventTask',
  962.             '\XLite\Controller\Admin\ProductList',
  963.             '\XLite\Controller\Admin\Product',
  964.         ];
  965.     }
  966.     /**
  967.      * Check if notifications should be sended in current situation
  968.      *
  969.      * @return boolean
  970.      */
  971.     public function isShouldSend()
  972.     {
  973.         $result false;
  974.         if (!defined('LC_CACHE_BUILDING')) {
  975.             $currentController     \XLite::getInstance()->getController();
  976.             $isControllerForbidden array_reduce(
  977.                 $this->getForbiddenControllers(),
  978.                 static function ($carry$controllerName) use ($currentController) {
  979.                     return $carry ?: ($currentController instanceof $controllerName);
  980.                 },
  981.                 false
  982.             );
  983.             $result \XLite\Core\Request::getInstance()->event !== 'import'
  984.                 && !$isControllerForbidden;
  985.         }
  986.         return $result;
  987.     }
  988.     /**
  989.      * Check and (if needed) correct amount value
  990.      *
  991.      * @param integer $amount Value to check
  992.      *
  993.      * @return integer
  994.      */
  995.     protected function correctAmount($amount)
  996.     {
  997.         return Converter::toUnsigned32BitInt($amount);
  998.     }
  999.     /**
  1000.      * Get a low default amount
  1001.      *
  1002.      * @return integer
  1003.      */
  1004.     protected function getLowDefaultAmount()
  1005.     {
  1006.         return 1;
  1007.     }
  1008.     /**
  1009.      * Default qty value to show to customers
  1010.      *
  1011.      * @return integer
  1012.      */
  1013.     public function getDefaultAmount()
  1014.     {
  1015.         return static::AMOUNT_DEFAULT_INV_TRACK;
  1016.     }
  1017.     /**
  1018.      * Get ProductStockAvailabilityPolicy associated with this product
  1019.      *
  1020.      * @return ProductStockAvailabilityPolicy
  1021.      */
  1022.     public function getStockAvailabilityPolicy()
  1023.     {
  1024.         return new ProductStockAvailabilityPolicy($this);
  1025.     }
  1026.     /**
  1027.      * Return product amount available to add to cart
  1028.      *
  1029.      * @return integer
  1030.      */
  1031.     public function getAvailableAmount()
  1032.     {
  1033.         return $this->getStockAvailabilityPolicy()->getAvailableAmount(Cart::getInstance());
  1034.     }
  1035.     /**
  1036.      * Alias: is product in stock or not
  1037.      *
  1038.      * @return boolean
  1039.      */
  1040.     public function isOutOfStock()
  1041.     {
  1042.         return $this->getAmount() <= && $this->getInventoryEnabled();
  1043.     }
  1044.     /**
  1045.      * Alias: is all product items in cart
  1046.      *
  1047.      * @return boolean
  1048.      */
  1049.     public function isAllStockInCart()
  1050.     {
  1051.         return $this->getStockAvailabilityPolicy()->isAllStockInCart(Cart::getInstance());
  1052.     }
  1053.     /**
  1054.      * How many product items added to cart
  1055.      *
  1056.      * @return boolean
  1057.      */
  1058.     public function getItemsInCart()
  1059.     {
  1060.         return $this->getStockAvailabilityPolicy()->getInCartAmount(Cart::getInstance());
  1061.     }
  1062.     /**
  1063.      * How many product items added to cart
  1064.      *
  1065.      * @return boolean
  1066.      */
  1067.     public function getItemsInCartMessage()
  1068.     {
  1069.         $count $this->getStockAvailabilityPolicy()->getInCartAmount(Cart::getInstance());
  1070.         return \XLite\Core\Translation::getInstance()->translate(
  1071.             'Items in your cart: X',
  1072.             ['count' => $count]
  1073.         );
  1074.     }
  1075.     /**
  1076.      * Check if the product is out-of-stock
  1077.      *
  1078.      * @return boolean
  1079.      */
  1080.     public function isShowStockWarning()
  1081.     {
  1082.         return $this->getInventoryEnabled()
  1083.             && $this->getLowLimitEnabledCustomer()
  1084.             && $this->getPublicAmount() <= $this->getLowLimitAmount()
  1085.             && !$this->isOutOfStock();
  1086.     }
  1087.     /**
  1088.      * Check if the product is out-of-stock
  1089.      *
  1090.      * @return boolean
  1091.      */
  1092.     public function isShowOutOfStockWarning()
  1093.     {
  1094.         return $this->getInventoryEnabled()
  1095.             && $this->isOutOfStock();
  1096.     }
  1097.     /**
  1098.      * Send notification to admin about product low limit
  1099.      *
  1100.      * @return void
  1101.      */
  1102.     public function sendLowLimitNotification()
  1103.     {
  1104.         \XLite\Core\Mailer::sendLowLimitWarningAdmin($this->prepareDataForNotification());
  1105.     }
  1106.     /**
  1107.      * Prepare data for 'low limit warning' email notifications
  1108.      *
  1109.      * @return array
  1110.      */
  1111.     public function prepareDataForNotification()
  1112.     {
  1113.         $data = [];
  1114.         $data['product'] = $this;
  1115.         $data['name']    = $this->getName();
  1116.         $data['sku']     = $this->getSKU();
  1117.         $data['amount']  = $this->getAmount();
  1118.         $params           = [
  1119.             'product_id' => $this->getProductId(),
  1120.             'page'       => 'inventory',
  1121.         ];
  1122.         $data['adminURL'] = \XLite\Core\Converter::buildFullURL('product'''$params\XLite::getAdminScript(), false);
  1123.         return $data;
  1124.     }
  1125.     // }}}
  1126.     /**
  1127.      * Set sku and trim it to max length
  1128.      *
  1129.      * @param string $sku
  1130.      *
  1131.      * @return void
  1132.      */
  1133.     public function setSku($sku)
  1134.     {
  1135.         $this->sku substr(
  1136.             $sku,
  1137.             0,
  1138.             \XLite\Core\Database::getRepo('XLite\Model\Product')->getFieldInfo('sku''length')
  1139.         );
  1140.     }
  1141.     /**
  1142.      * Get product Url
  1143.      *
  1144.      * @return string
  1145.      */
  1146.     public function getURL()
  1147.     {
  1148.         return $this->getProductId()
  1149.             ? \XLite\Core\Converter::makeURLValid(
  1150.                 \XLite\Core\Converter::buildURL('product''', ['product_id' => $this->getProductId()])
  1151.             )
  1152.             : null;
  1153.     }
  1154.     /**
  1155.      * Get front URL
  1156.      *
  1157.      * @return string
  1158.      */
  1159.     public function getFrontURL($withAttributes false$buildCuInAdminZone false)
  1160.     {
  1161.         return $this->getProductId()
  1162.             ? \XLite\Core\Converter::makeURLValid(
  1163.                 \XLite::getInstance()->getShopURL(
  1164.                     \XLite\Core\Converter::buildURL(
  1165.                         'product',
  1166.                         '',
  1167.                         $this->getParamsForFrontURL($withAttributes),
  1168.                         \XLite::getCustomerScript(),
  1169.                         $buildCuInAdminZone
  1170.                     )
  1171.                 )
  1172.             )
  1173.             : null;
  1174.     }
  1175.     /**
  1176.      * @return array
  1177.      */
  1178.     protected function getParamsForFrontURL($withAttributes false)
  1179.     {
  1180.         $result = [
  1181.             'product_id' => $this->getProductId(),
  1182.         ];
  1183.         if ($withAttributes) {
  1184.             $result['attribute_values'] = $this->getAttributeValuesParams();
  1185.         }
  1186.         return $result;
  1187.     }
  1188.     /**
  1189.      * @return string
  1190.      */
  1191.     protected function getAttributeValuesParams()
  1192.     {
  1193.         $validAttributes array_filter(
  1194.             $this->getAttrValues(),
  1195.             static function ($attr) {
  1196.                 return $attr && $attr->getAttribute();
  1197.             }
  1198.         );
  1199.         $paramsStrings array_map(
  1200.             static function ($attr) {
  1201.                 return $attr->getAttribute()->getId() . '_' $attr->getId();
  1202.             },
  1203.             $validAttributes
  1204.         );
  1205.         return trim(join(','$paramsStrings), ',');
  1206.     }
  1207.     /**
  1208.      * Minimal available amount
  1209.      *
  1210.      * @return integer
  1211.      */
  1212.     public function getMinPurchaseLimit()
  1213.     {
  1214.         return 1;
  1215.     }
  1216.     /**
  1217.      * Maximal available amount
  1218.      *
  1219.      * @return integer
  1220.      */
  1221.     public function getMaxPurchaseLimit()
  1222.     {
  1223.         return (int) \XLite\Core\Config::getInstance()->General->default_purchase_limit;
  1224.     }
  1225.     /**
  1226.      * Return product position in category
  1227.      *
  1228.      * @param integer|null $categoryId Category ID OPTIONAL
  1229.      *
  1230.      * @return integer|void
  1231.      */
  1232.     public function getOrderBy($categoryId null)
  1233.     {
  1234.         $link $this->getLink($categoryId);
  1235.         return $link $link->getOrderBy() : null;
  1236.     }
  1237.     /**
  1238.      * Count product images
  1239.      *
  1240.      * @return integer
  1241.      */
  1242.     public function countImages()
  1243.     {
  1244.         return count($this->getPublicImages());
  1245.     }
  1246.     /**
  1247.      * Try to fetch product description
  1248.      *
  1249.      * @return string
  1250.      */
  1251.     public function getCommonDescription()
  1252.     {
  1253.         return $this->getBriefDescription() ?: $this->getDescription();
  1254.     }
  1255.     /**
  1256.      * Get processed product brief description
  1257.      *
  1258.      * @return string
  1259.      */
  1260.     public function getProcessedBriefDescription()
  1261.     {
  1262.         $value $this->getBriefDescription();
  1263.         return $value
  1264.             ? static::getPreprocessedValue($value)
  1265.             : $value;
  1266.     }
  1267.     /**
  1268.      * Get processed product description
  1269.      *
  1270.      * @return string
  1271.      */
  1272.     public function getProcessedDescription()
  1273.     {
  1274.         $value $this->getDescription();
  1275.         return $value
  1276.             ? static::getPreprocessedValue($value)
  1277.             : $value;
  1278.     }
  1279.     /**
  1280.      * Get taxable basis
  1281.      *
  1282.      * @return float
  1283.      */
  1284.     public function getTaxableBasis()
  1285.     {
  1286.         return $this->getNetPrice();
  1287.     }
  1288.     /**
  1289.      * Check if product inventory changed
  1290.      *
  1291.      * @return boolean
  1292.      */
  1293.     public function isInventoryChanged()
  1294.     {
  1295.         $changeset \XLite\Core\Database::getEM()
  1296.             ->getUnitOfWork()
  1297.             ->getEntityChangeSet($this);
  1298.         return isset($changeset['amount']);
  1299.     }
  1300.     /**
  1301.      * Set product class
  1302.      *
  1303.      * @param ProductClass|null $productClass     Product class OPTIONAL
  1304.      * @param boolean           $preprocessChange Flag if use preprocessChangeProductClass() OPTIONAL
  1305.      *
  1306.      * @return Product
  1307.      */
  1308.     public function setProductClass(\XLite\Model\ProductClass $productClass null$preprocessChange true)
  1309.     {
  1310.         if (
  1311.             $preprocessChange
  1312.             && $this->productClass
  1313.             && (
  1314.                 !$productClass
  1315.                 || $productClass->getId() !== $this->productClass->getId()
  1316.             )
  1317.         ) {
  1318.             $this->preprocessChangeProductClass();
  1319.         }
  1320.         $this->productClass $productClass;
  1321.         return $this;
  1322.     }
  1323.     /**
  1324.      * Get attr values
  1325.      *
  1326.      * @return array
  1327.      */
  1328.     public function getAttrValues()
  1329.     {
  1330.         return $this->attrValues;
  1331.     }
  1332.     /**
  1333.      * Set attr values
  1334.      *
  1335.      * @param array $value Value
  1336.      *
  1337.      * @return void
  1338.      */
  1339.     public function setAttrValues($value)
  1340.     {
  1341.         $this->attrValues $value;
  1342.     }
  1343.     /**
  1344.      * Sort editable attributes
  1345.      *
  1346.      * @param array $a Attribute A
  1347.      * @param array $b Attribute B
  1348.      *
  1349.      * @return int
  1350.      */
  1351.     protected function sortEditableAttributes($a$b)
  1352.     {
  1353.         return $a['position'] > $b['position'] ? : -1;
  1354.     }
  1355.     /**
  1356.      * Get editable attributes
  1357.      *
  1358.      * @return list<\XLite\Model\Attribute>
  1359.      */
  1360.     public function getEditableAttributes()
  1361.     {
  1362.         return $this->executeCachedRuntime(function () {
  1363.             return $this->defineEditableAttributes();
  1364.         }, ['getEditableAttributes'$this->getProductId()]);
  1365.     }
  1366.     public function sortMyAttributes(array $attributes): array
  1367.     {
  1368.         /** @var \XLite\Model\Attribute $attribute */
  1369.         $minPos array_reduce($attributes, fn ($carry$attribute) => max($carry$attribute->getPosition($this)), 0);
  1370.         if ($minPos !== 0) {
  1371.             // at least one of them has non-default position
  1372.             usort($attributes, [$this'sortVisibleAttributes']);
  1373.         } else {
  1374.             usort($attributes, [$this'sortVisibleAttributesByGroup']);
  1375.         }
  1376.         return $attributes;
  1377.     }
  1378.     /**
  1379.      * @return array
  1380.      */
  1381.     protected function defineEditableAttributes()
  1382.     {
  1383.         $result = [];
  1384.         foreach ((array) \XLite\Model\Attribute::getTypes() as $type => $name) {
  1385.             $class \XLite\Model\Attribute::getAttributeValueClass($type);
  1386.             if (is_subclass_of($class'XLite\Model\AttributeValue\Multiple')) {
  1387.                 $result[] = \XLite\Core\Database::getRepo($class)->findMultipleAttributes($this);
  1388.             } elseif ($class === '\XLite\Model\AttributeValue\AttributeValueText') {
  1389.                 $result[] = \XLite\Core\Database::getRepo($class)->findEditableAttributes($this);
  1390.             }
  1391.         }
  1392.         $result = (array) call_user_func_array('array_merge'$result);
  1393.         usort($result, [$this'sortEditableAttributes']);
  1394.         if ($result) {
  1395.             foreach ($result as $k => $v) {
  1396.                 $result[$k] = $v[0];
  1397.             }
  1398.         }
  1399.         return $result;
  1400.     }
  1401.     /**
  1402.      * Sort visible attributes
  1403.      *
  1404.      * @param \XLite\Model\Attribute $a Attribute A
  1405.      * @param \XLite\Model\Attribute $b Attribute B
  1406.      *
  1407.      * @return int
  1408.      */
  1409.     protected function sortVisibleAttributes($a$b)
  1410.     {
  1411.         // fallback0 to product specific
  1412.         $aProductSpecific $a->getPosition($this);
  1413.         $bProductSpecific $b->getPosition($this);
  1414.         if ($aProductSpecific !== $bProductSpecific) {
  1415.             return $aProductSpecific <=> $bProductSpecific;
  1416.         }
  1417.         // fallback1 to global ordering
  1418.         $aGlobal $a->getPosition();
  1419.         $bGlobal $b->getPosition();
  1420.         if ($aGlobal !== $bGlobal) {
  1421.             return $aGlobal <=> $bGlobal;
  1422.         }
  1423.         // fallback2 to database insert ordering
  1424.         return $a->getId() <=> $b->getId();
  1425.     }
  1426.     /**
  1427.      * Sort visible attributes when all of them have default position
  1428.      */
  1429.     protected function sortVisibleAttributesByGroup(\XLite\Model\Attribute $a\XLite\Model\Attribute $b): int
  1430.     {
  1431.         // Firstly, compare IDs only to avoid Hydration from database
  1432.         $aGroupId $a->getAttributeGroup()?->getId() ?: 0;
  1433.         $bGroupId $b->getAttributeGroup()?->getId() ?: 0;
  1434.         if ($aGroupId !== $bGroupId) {
  1435.             // Group IDs are not the same therefore we have no choice except hydration
  1436.             $aGroupPosition $a->getAttributeGroup()?->getPosition() ?: $aGroupId;
  1437.             $bGroupPosition $b->getAttributeGroup()?->getPosition() ?: $bGroupId;
  1438.             if ($aGroupPosition !== $bGroupPosition) {
  1439.                 return $aGroupPosition <=> $bGroupPosition;
  1440.             }
  1441.         }
  1442.         // fallback1 to global ordering
  1443.         $aGlobal $a->getPosition();
  1444.         $bGlobal $b->getPosition();
  1445.         if ($aGlobal !== $bGlobal) {
  1446.             return $aGlobal <=> $bGlobal;
  1447.         }
  1448.         // fallback2 to database insert ordering
  1449.         return $a->getId() <=> $b->getId();
  1450.     }
  1451.     /**
  1452.      * Get all visible attributes with values
  1453.      *
  1454.      * @return list<\XLite\Model\Attribute>
  1455.      */
  1456.     public function getVisibleAttributes()
  1457.     {
  1458.         return $this->executeCachedRuntime(function () {
  1459.             return $this->defineVisibleAttributes();
  1460.         }, ['getVisibleAttributes'$this->getProductId()]);
  1461.     }
  1462.     /**
  1463.      * @return array
  1464.      */
  1465.     protected function defineVisibleAttributes()
  1466.     {
  1467.         $result = [];
  1468.         foreach ((array) \XLite\Model\Attribute::getTypes() as $type => $name) {
  1469.             if ($type === \XLite\Model\Attribute::TYPE_HIDDEN) {
  1470.                 continue;
  1471.             }
  1472.             $result array_merge(
  1473.                 $result,
  1474.                 \XLite\Core\Database::getRepo('XLite\Model\Attribute')
  1475.                     ->getAttributesWithValues($this$type)
  1476.             );
  1477.         }
  1478.         return $this->sortMyAttributes($result);
  1479.     }
  1480.     /**
  1481.      * Check - product has visible attrbiutes or not
  1482.      *
  1483.      * @return boolean
  1484.      */
  1485.     public function hasVisibleAttributes()
  1486.     {
  1487.         return count($this->getVisibleAttributes());
  1488.     }
  1489.     /**
  1490.      * Get editable attributes ids
  1491.      *
  1492.      * @return array
  1493.      */
  1494.     public function getEditableAttributesIds()
  1495.     {
  1496.         $result = [];
  1497.         foreach ($this->getEditableAttributes() as $a) {
  1498.             $result[] = $a->getId();
  1499.         }
  1500.         sort($result);
  1501.         return $result;
  1502.     }
  1503.     /**
  1504.      * Check - product has editable attrbiutes or not
  1505.      *
  1506.      * @return boolean
  1507.      */
  1508.     public function hasEditableAttributes()
  1509.     {
  1510.         return count($this->getEditableAttributes());
  1511.     }
  1512.     /**
  1513.      * Get multiple attributes
  1514.      *
  1515.      * @return array
  1516.      */
  1517.     public function getMultipleAttributes()
  1518.     {
  1519.         $result = [];
  1520.         foreach (\XLite\Model\Attribute::getTypes() as $type => $name) {
  1521.             $class \XLite\Model\Attribute::getAttributeValueClass($type);
  1522.             if (is_subclass_of($class'XLite\Model\AttributeValue\Multiple')) {
  1523.                 $result array_merge(
  1524.                     $result,
  1525.                     \XLite\Core\Database::getRepo($class)->findMultipleAttributes($this)
  1526.                 );
  1527.             }
  1528.         }
  1529.         if ($result) {
  1530.             foreach ($result as $k => $v) {
  1531.                 $result[$k] = $v[0];
  1532.             }
  1533.         }
  1534.         return $result;
  1535.     }
  1536.     /**
  1537.      * Get multiple attributes ids
  1538.      *
  1539.      * @return array
  1540.      */
  1541.     public function getMultipleAttributesIds()
  1542.     {
  1543.         $result = [];
  1544.         foreach ($this->getMultipleAttributes() as $a) {
  1545.             $result[] = $a->getId();
  1546.         }
  1547.         sort($result);
  1548.         return $result;
  1549.     }
  1550.     /**
  1551.      * Check - product has multiple attributes or not
  1552.      *
  1553.      * @return boolean
  1554.      */
  1555.     public function hasMultipleAttributes()
  1556.     {
  1557.         return count($this->getMultipleAttributes());
  1558.     }
  1559.     /**
  1560.      * Update quick data
  1561.      *
  1562.      * @return void
  1563.      */
  1564.     public function updateQuickData()
  1565.     {
  1566.         if ($this->isPersistent()) {
  1567.             \XLite\Core\QuickData::getInstance()->updateProductData($this);
  1568.         }
  1569.     }
  1570.     /**
  1571.      * @return boolean
  1572.      */
  1573.     protected function showPlaceholderOption()
  1574.     {
  1575.         if (\XLite::isAdminZone()) {
  1576.             return false;
  1577.         } elseif (\XLite\Core\Config::getInstance()->General->force_choose_product_options === 'quicklook') {
  1578.             return \XLite::getController()->getTarget() !== 'product';
  1579.         } elseif (\XLite\Core\Config::getInstance()->General->force_choose_product_options === 'product_page') {
  1580.             return true;
  1581.         }
  1582.         return false;
  1583.     }
  1584.     /**
  1585.      * Prepare attribute values
  1586.      *
  1587.      * @param array $ids Request-based selected attribute values OPTIONAL
  1588.      *
  1589.      * @return array
  1590.      */
  1591.     public function prepareAttributeValues($ids = [])
  1592.     {
  1593.         return $this->executeCachedRuntime(function () use ($ids) {
  1594.             $attributeValues = [];
  1595.             foreach ($this->getEditableAttributes() as $a) {
  1596.                 if ($a->getType() === \XLite\Model\Attribute::TYPE_TEXT) {
  1597.                     $value     $ids[$a->getId()] ?? $a->getAttributeValue($this)->getValue();
  1598.                     $attrValue $a->getAttributeValue($this);
  1599.                     $attributeValues[$a->getId()] = [
  1600.                         'attributeValue' => $attrValue,
  1601.                         'value'          => $value,
  1602.                     ];
  1603.                 } elseif ($a->getType() === \XLite\Model\Attribute::TYPE_CHECKBOX) {
  1604.                     $found null;
  1605.                     if (isset($ids[$a->getId()])) {
  1606.                         foreach ($a->getAttributeValue($this) as $av) {
  1607.                             if ($av->getId() === (int) $ids[$a->getId()]) {
  1608.                                 $found $av;
  1609.                                 break;
  1610.                             }
  1611.                         }
  1612.                     }
  1613.                     $value $found ?: $a->getDefaultAttributeValue($this);
  1614.                     $attributeValues[$a->getId()] = $value;
  1615.                 } else {
  1616.                     if ($this->showDefaultAttrubuteValue($a)) {
  1617.                         $attributeValues[$a->getId()] = $a->getDefaultAttributeValue($this);
  1618.                     }
  1619.                     if (isset($ids[$a->getId()])) {
  1620.                         foreach ($a->getAttributeValue($this) as $av) {
  1621.                             if ($av->getId() === (int) $ids[$a->getId()]) {
  1622.                                 $attributeValues[$a->getId()] = $av;
  1623.                                 break;
  1624.                             }
  1625.                         }
  1626.                     }
  1627.                 }
  1628.             }
  1629.             return $attributeValues;
  1630.         }, ['prepareAttributeValues'$ids]);
  1631.     }
  1632.     protected function showDefaultAttrubuteValue(\XLite\Model\Attribute $attribute)
  1633.     {
  1634.         return !$this->showPlaceholderOption();
  1635.     }
  1636.     /**
  1637.      * Define the specific clone name for the product
  1638.      *
  1639.      * @param string $name Product name
  1640.      *
  1641.      * @return string
  1642.      */
  1643.     protected function getCloneName($name)
  1644.     {
  1645.         return $name ' [ clone ]';
  1646.     }
  1647.     /**
  1648.      * Preprocess change product class
  1649.      *
  1650.      * @return void
  1651.      */
  1652.     protected function preprocessChangeProductClass()
  1653.     {
  1654.         if ($this->productClass) {
  1655.             foreach ($this->productClass->getAttributes() as $a) {
  1656.                 $class $a->getAttributeValueClass($a->getType());
  1657.                 $repo  \XLite\Core\Database::getRepo($class);
  1658.                 foreach ($repo->findBy(['product' => $this'attribute' => $a]) as $v) {
  1659.                     $repo->delete($v);
  1660.                 }
  1661.             }
  1662.         }
  1663.     }
  1664.     /**
  1665.      * Return certain Product <--> Category association
  1666.      *
  1667.      * @param integer|null $categoryId Category ID
  1668.      *
  1669.      * @return \XLite\Model\CategoryProducts|void
  1670.      */
  1671.     protected function findLinkByCategoryId($categoryId)
  1672.     {
  1673.         $result null;
  1674.         foreach ($this->getCategoryProducts() as $cp) {
  1675.             if ($cp->getCategory() && $cp->getCategory()->getCategoryId() == $categoryId) {
  1676.                 $result $cp;
  1677.             }
  1678.         }
  1679.         return $result;
  1680.     }
  1681.     /**
  1682.      * Return certain Product <--> Category association
  1683.      *
  1684.      * @param integer|null $categoryId Category ID OPTIONAL
  1685.      *
  1686.      * @return \XLite\Model\CategoryProducts
  1687.      */
  1688.     protected function getLink($categoryId null)
  1689.     {
  1690.         $result = empty($categoryId)
  1691.             ? $this->getCategoryProducts()->first()
  1692.             : $this->findLinkByCategoryId($categoryId);
  1693.         if (empty($result)) {
  1694.             $result = new \XLite\Model\CategoryProducts();
  1695.         }
  1696.         return $result;
  1697.     }
  1698.     /**
  1699.      * Returns position of the product in the given category
  1700.      *
  1701.      * @param integer $category
  1702.      *
  1703.      * @return integer
  1704.      */
  1705.     public function getPosition($category)
  1706.     {
  1707.         return $this->getOrderBy($category);
  1708.     }
  1709.     /**
  1710.      * Sets the position of the product in the given category
  1711.      *
  1712.      * @param array $value
  1713.      *
  1714.      * @return void
  1715.      */
  1716.     public function setPosition($value)
  1717.     {
  1718.         $link $this->getLink($value['category']);
  1719.         $link->setProduct($this);
  1720.         $link->setOrderby($value['position']);
  1721.         \XLite\Core\Database::getEM()->persist($link);
  1722.         \XLite\Core\Database::getEM()->flush($link);
  1723.     }
  1724.     /**
  1725.      * Returns meta description
  1726.      *
  1727.      * @return string
  1728.      */
  1729.     public function getMetaDesc()
  1730.     {
  1731.         return $this->getMetaDescType() === static::META_DESC_TYPE_AUTO
  1732.             ? static::generateMetaDescription($this->getCommonDescription())
  1733.             : $this->getSoftTranslation()->getMetaDesc();
  1734.     }
  1735.     /**
  1736.      * Generate meta description
  1737.      *
  1738.      * @param $description
  1739.      *
  1740.      * @return string
  1741.      */
  1742.     public static function generateMetaDescription($description)
  1743.     {
  1744.         return static::postprocessMetaDescription($description);
  1745.     }
  1746.     /**
  1747.      * Returns meta description type
  1748.      *
  1749.      * @return string
  1750.      */
  1751.     public function getMetaDescType()
  1752.     {
  1753.         $result $this->metaDescType;
  1754.         if (!$result) {
  1755.             $metaDescPresent array_reduce($this->getTranslations()->toArray(), static function ($carry$item) {
  1756.                 return $carry ?: (bool) $item->getMetaDesc();
  1757.             }, false);
  1758.             $result $metaDescPresent
  1759.                 ? static::META_DESC_TYPE_CUSTOM
  1760.                 : static::META_DESC_TYPE_AUTO;
  1761.         }
  1762.         return $result;
  1763.     }
  1764.     // {{{ Sales statistics
  1765.     /**
  1766.      * Set sales
  1767.      *
  1768.      * @param integer $sales Sales
  1769.      */
  1770.     public function setSales($sales)
  1771.     {
  1772.         $this->sales max(0$sales);
  1773.     }
  1774.     /**
  1775.      * Return sales
  1776.      *
  1777.      * @return integer
  1778.      */
  1779.     public function getSales()
  1780.     {
  1781.         return $this->sales;
  1782.     }
  1783.     /**
  1784.      * Update sales
  1785.      */
  1786.     public function updateSales()
  1787.     {
  1788.         $this->setSales(
  1789.             $this->getRepository()->findSalesByProduct($this)
  1790.         );
  1791.     }
  1792.     // }}}
  1793.     /**
  1794.      * Get product_id
  1795.      *
  1796.      * @return integer
  1797.      */
  1798.     public function getProductId()
  1799.     {
  1800.         return $this->product_id;
  1801.     }
  1802.     /**
  1803.      * Set price
  1804.      *
  1805.      * @param float $price
  1806.      *
  1807.      * @return Product
  1808.      */
  1809.     public function setPrice($price)
  1810.     {
  1811.         $this->price Converter::toUnsigned32BitFloat($price);
  1812.         return $this;
  1813.     }
  1814.     /**
  1815.      * Set enabled
  1816.      *
  1817.      * @param boolean $enabled
  1818.      *
  1819.      * @return Product
  1820.      */
  1821.     public function setEnabled($enabled)
  1822.     {
  1823.         $this->enabled = (bool) $enabled;
  1824.         return $this;
  1825.     }
  1826.     /**
  1827.      * Get enabled
  1828.      *
  1829.      * @return boolean
  1830.      */
  1831.     public function getEnabled()
  1832.     {
  1833.         return $this->enabled;
  1834.     }
  1835.     /**
  1836.      * Set weight
  1837.      *
  1838.      * @param float $weight
  1839.      *
  1840.      * @return Product
  1841.      */
  1842.     public function setWeight($weight)
  1843.     {
  1844.         $this->weight Converter::toUnsigned32BitFloat($weight);
  1845.         return $this;
  1846.     }
  1847.     /**
  1848.      * Set useSeparateBox
  1849.      *
  1850.      * @param boolean $useSeparateBox
  1851.      *
  1852.      * @return Product
  1853.      */
  1854.     public function setUseSeparateBox($useSeparateBox)
  1855.     {
  1856.         $this->useSeparateBox $useSeparateBox;
  1857.         return $this;
  1858.     }
  1859.     /**
  1860.      * Get useSeparateBox
  1861.      *
  1862.      * @return boolean
  1863.      */
  1864.     public function getUseSeparateBox()
  1865.     {
  1866.         return $this->useSeparateBox;
  1867.     }
  1868.     /**
  1869.      * Set boxWidth
  1870.      *
  1871.      * @param float $boxWidth
  1872.      *
  1873.      * @return Product
  1874.      */
  1875.     public function setBoxWidth($boxWidth)
  1876.     {
  1877.         $this->boxWidth Converter::toUnsigned32BitFloat($boxWidth);
  1878.         return $this;
  1879.     }
  1880.     /**
  1881.      * Get boxWidth
  1882.      *
  1883.      * @return float
  1884.      */
  1885.     public function getBoxWidth()
  1886.     {
  1887.         return $this->boxWidth;
  1888.     }
  1889.     /**
  1890.      * Set boxLength
  1891.      *
  1892.      * @param float $boxLength
  1893.      *
  1894.      * @return Product
  1895.      */
  1896.     public function setBoxLength($boxLength)
  1897.     {
  1898.         $this->boxLength Converter::toUnsigned32BitFloat($boxLength);
  1899.         return $this;
  1900.     }
  1901.     /**
  1902.      * Get boxLength
  1903.      *
  1904.      * @return float
  1905.      */
  1906.     public function getBoxLength()
  1907.     {
  1908.         return $this->boxLength;
  1909.     }
  1910.     /**
  1911.      * Set boxHeight
  1912.      *
  1913.      * @param float $boxHeight
  1914.      *
  1915.      * @return Product
  1916.      */
  1917.     public function setBoxHeight($boxHeight)
  1918.     {
  1919.         $this->boxHeight Converter::toUnsigned32BitFloat($boxHeight);
  1920.         return $this;
  1921.     }
  1922.     /**
  1923.      * Get boxHeight
  1924.      *
  1925.      * @return float
  1926.      */
  1927.     public function getBoxHeight()
  1928.     {
  1929.         return $this->boxHeight;
  1930.     }
  1931.     /**
  1932.      * Set itemsPerBox
  1933.      *
  1934.      * @param integer $itemsPerBox
  1935.      *
  1936.      * @return Product
  1937.      */
  1938.     public function setItemsPerBox($itemsPerBox)
  1939.     {
  1940.         $this->itemsPerBox Converter::toUnsigned32BitInt($itemsPerBox);
  1941.         return $this;
  1942.     }
  1943.     /**
  1944.      * Get itemsPerBox
  1945.      *
  1946.      * @return integer
  1947.      */
  1948.     public function getItemsPerBox()
  1949.     {
  1950.         return $this->itemsPerBox;
  1951.     }
  1952.     /**
  1953.      * Set free_shipping
  1954.      *
  1955.      * @param boolean $freeShipping
  1956.      *
  1957.      * @return Product
  1958.      */
  1959.     public function setFreeShipping($freeShipping)
  1960.     {
  1961.         $this->free_shipping = (bool) $freeShipping;
  1962.         return $this;
  1963.     }
  1964.     /**
  1965.      * Set taxable
  1966.      *
  1967.      * @param boolean $taxable
  1968.      *
  1969.      * @return Product
  1970.      */
  1971.     public function setTaxable($taxable)
  1972.     {
  1973.         $this->taxable $taxable;
  1974.         return $this;
  1975.     }
  1976.     /**
  1977.      * Get taxable
  1978.      *
  1979.      * @return boolean
  1980.      */
  1981.     public function getTaxable()
  1982.     {
  1983.         return $this->taxable;
  1984.     }
  1985.     /**
  1986.      * Set arrivalDate
  1987.      *
  1988.      * @param integer $arrivalDate
  1989.      *
  1990.      * @return Product
  1991.      */
  1992.     public function setArrivalDate($arrivalDate)
  1993.     {
  1994.         $this->arrivalDate min(MAX_TIMESTAMPmax(0$arrivalDate));
  1995.         return $this;
  1996.     }
  1997.     /**
  1998.      * Get arrivalDate
  1999.      *
  2000.      * @return integer
  2001.      */
  2002.     public function getArrivalDate()
  2003.     {
  2004.         return $this->arrivalDate;
  2005.     }
  2006.     /**
  2007.      * Returns true if product is classified as an upcoming product
  2008.      *
  2009.      * @return boolean
  2010.      */
  2011.     public function isUpcomingProduct()
  2012.     {
  2013.         return $this->getArrivalDate()
  2014.             && $this->getArrivalDate() > Converter::getDayEnd(static::getUserTime());
  2015.     }
  2016.     /**
  2017.      * Check if upcoming product is available
  2018.      *
  2019.      * @return boolean
  2020.      */
  2021.     public function isAllowedUpcomingProduct()
  2022.     {
  2023.         return false;
  2024.     }
  2025.     /**
  2026.      * Set date
  2027.      *
  2028.      * @param integer $date
  2029.      *
  2030.      * @return Product
  2031.      */
  2032.     public function setDate($date)
  2033.     {
  2034.         $this->date $date;
  2035.         return $this;
  2036.     }
  2037.     /**
  2038.      * Get date
  2039.      *
  2040.      * @return integer
  2041.      */
  2042.     public function getDate()
  2043.     {
  2044.         return $this->date;
  2045.     }
  2046.     /**
  2047.      * Set updateDate
  2048.      *
  2049.      * @param integer $updateDate
  2050.      *
  2051.      * @return Product
  2052.      */
  2053.     public function setUpdateDate($updateDate)
  2054.     {
  2055.         $this->updateDate $updateDate;
  2056.         return $this;
  2057.     }
  2058.     /**
  2059.      * Get updateDate
  2060.      *
  2061.      * @return integer
  2062.      */
  2063.     public function getUpdateDate()
  2064.     {
  2065.         return $this->updateDate;
  2066.     }
  2067.     /**
  2068.      * Set needProcess
  2069.      *
  2070.      * @param boolean $needProcess
  2071.      *
  2072.      * @return Product
  2073.      */
  2074.     public function setNeedProcess($needProcess)
  2075.     {
  2076.         $this->needProcess $needProcess;
  2077.         return $this;
  2078.     }
  2079.     /**
  2080.      * Get needProcess
  2081.      *
  2082.      * @return boolean
  2083.      */
  2084.     public function getNeedProcess()
  2085.     {
  2086.         return $this->needProcess;
  2087.     }
  2088.     /**
  2089.      * Set attrSepTab
  2090.      *
  2091.      * @param boolean $attrSepTab
  2092.      *
  2093.      * @return Product
  2094.      */
  2095.     public function setAttrSepTab($attrSepTab)
  2096.     {
  2097.         $this->attrSepTab $attrSepTab;
  2098.         return $this;
  2099.     }
  2100.     /**
  2101.      * Get attrSepTab
  2102.      *
  2103.      * @return boolean
  2104.      */
  2105.     public function getAttrSepTab()
  2106.     {
  2107.         return $this->attrSepTab;
  2108.     }
  2109.     /**
  2110.      * Set metaDescType
  2111.      *
  2112.      * @param string $metaDescType
  2113.      *
  2114.      * @return Product
  2115.      */
  2116.     public function setMetaDescType($metaDescType)
  2117.     {
  2118.         $this->metaDescType $metaDescType;
  2119.         return $this;
  2120.     }
  2121.     /**
  2122.      * Set xcPendingExport
  2123.      *
  2124.      * @param boolean $xcPendingExport
  2125.      *
  2126.      * @return Product
  2127.      */
  2128.     public function setXcPendingExport($xcPendingExport)
  2129.     {
  2130.         $this->xcPendingExport $xcPendingExport;
  2131.         return $this;
  2132.     }
  2133.     /**
  2134.      * Get xcPendingExport
  2135.      *
  2136.      * @return boolean
  2137.      */
  2138.     public function getXcPendingExport()
  2139.     {
  2140.         return $this->xcPendingExport;
  2141.     }
  2142.     /**
  2143.      * Add categoryProducts
  2144.      *
  2145.      * @param \XLite\Model\CategoryProducts $categoryProducts
  2146.      *
  2147.      * @return Product
  2148.      */
  2149.     public function addCategoryProducts(\XLite\Model\CategoryProducts $categoryProducts)
  2150.     {
  2151.         $this->categoryProducts[] = $categoryProducts;
  2152.         return $this;
  2153.     }
  2154.     /**
  2155.      * Get categoryProducts
  2156.      *
  2157.      * @return \Doctrine\Common\Collections\Collection
  2158.      */
  2159.     public function getCategoryProducts()
  2160.     {
  2161.         return $this->categoryProducts;
  2162.     }
  2163.     /**
  2164.      * @param \XLite\Model\Category[] $categories
  2165.      */
  2166.     public function addCategoryProductsLinksByCategories($categories)
  2167.     {
  2168.         foreach ($categories as $category) {
  2169.             if (!$this->hasCategoryProductsLinkByCategory($category)) {
  2170.                 $categoryProduct = new \XLite\Model\CategoryProducts();
  2171.                 $categoryProduct->setProduct($this);
  2172.                 $categoryProduct->setCategory($category);
  2173.                 $this->addCategoryProducts($categoryProduct);
  2174.             }
  2175.         }
  2176.     }
  2177.     /**
  2178.      * @param \XLite\Model\Category $category
  2179.      */
  2180.     public function addCategory($category)
  2181.     {
  2182.         $categoryProduct = new \XLite\Model\CategoryProducts();
  2183.         $categoryProduct->setProduct($this);
  2184.         $categoryProduct->setCategory($category);
  2185.         $this->addCategoryProducts($categoryProduct);
  2186.     }
  2187.     /**
  2188.      * @param \XLite\Model\Category[] $categories
  2189.      */
  2190.     public function removeCategoryProductsLinksByCategories($categories)
  2191.     {
  2192.         $categoryProductsLinks = [];
  2193.         foreach ($categories as $category) {
  2194.             $categoryProductsLink $this->findCategoryProductsLinkByCategory($category);
  2195.             if ($categoryProductsLink) {
  2196.                 $categoryProductsLinks[] = $categoryProductsLink;
  2197.             }
  2198.         }
  2199.         if ($categoryProductsLinks) {
  2200.             \XLite\Core\Database::getRepo('XLite\Model\CategoryProducts')->deleteInBatch(
  2201.                 $categoryProductsLinks
  2202.             );
  2203.         }
  2204.     }
  2205.     /**
  2206.      * @param \XLite\Model\Category[] $categories
  2207.      */
  2208.     public function replaceCategoryProductsLinksByCategories($categories)
  2209.     {
  2210.         $categoriesToAdd = [];
  2211.         foreach ($categories as $category) {
  2212.             if (!$this->hasCategoryProductsLinkByCategory($category)) {
  2213.                 $categoriesToAdd[] = $category;
  2214.             }
  2215.         }
  2216.         $categoriesIds array_map(static function ($item) {
  2217.             /** @var \XLite\Model\Category $item */
  2218.             return (int) $item->getCategoryId();
  2219.         }, $categories);
  2220.         $categoryProductsLinksToDelete = [];
  2221.         foreach ($this->getCategoryProducts() as $categoryProduct) {
  2222.             if (!in_array((int) $categoryProduct->getCategory()->getCategoryId(), $categoriesIdstrue)) {
  2223.                 $categoryProductsLinksToDelete[] = $categoryProduct;
  2224.             }
  2225.         }
  2226.         if ($categoryProductsLinksToDelete) {
  2227.             \XLite\Core\Database::getRepo('XLite\Model\CategoryProducts')->deleteInBatch(
  2228.                 $categoryProductsLinksToDelete
  2229.             );
  2230.         }
  2231.         if ($categoriesToAdd) {
  2232.             $this->addCategoryProductsLinksByCategories($categoriesToAdd);
  2233.         }
  2234.     }
  2235.     /**
  2236.      * @param \XLite\Model\Category $category
  2237.      *
  2238.      * @return bool
  2239.      */
  2240.     public function hasCategoryProductsLinkByCategory($category)
  2241.     {
  2242.         return (bool) $this->findCategoryProductsLinkByCategory($category);
  2243.     }
  2244.     /**
  2245.      * @param \XLite\Model\Category $category
  2246.      *
  2247.      * @return \XLite\Model\CategoryProducts
  2248.      */
  2249.     public function findCategoryProductsLinkByCategory($category)
  2250.     {
  2251.         /** @var \XLite\Model\CategoryProducts $categoryProduct */
  2252.         foreach ($this->getCategoryProducts() as $categoryProduct) {
  2253.             if ((int) $categoryProduct->getCategory()->getCategoryId() === (int) $category->getCategoryId()) {
  2254.                 return $categoryProduct;
  2255.             }
  2256.         }
  2257.         return null;
  2258.     }
  2259.     /**
  2260.      * Add order_items
  2261.      *
  2262.      * @param \XLite\Model\OrderItem $orderItems
  2263.      *
  2264.      * @return Product
  2265.      */
  2266.     public function addOrderItems(\XLite\Model\OrderItem $orderItems)
  2267.     {
  2268.         $this->order_items[] = $orderItems;
  2269.         return $this;
  2270.     }
  2271.     /**
  2272.      * Get order_items
  2273.      *
  2274.      * @return \Doctrine\Common\Collections\Collection
  2275.      */
  2276.     public function getOrderItems()
  2277.     {
  2278.         return $this->order_items;
  2279.     }
  2280.     /**
  2281.      * Add images
  2282.      *
  2283.      * @param \XLite\Model\Image\Product\Image $images
  2284.      *
  2285.      * @return Product
  2286.      */
  2287.     public function addImages(\XLite\Model\Image\Product\Image $images)
  2288.     {
  2289.         $this->images[] = $images;
  2290.         return $this;
  2291.     }
  2292.     /**
  2293.      * Get images
  2294.      *
  2295.      * @return \Doctrine\Common\Collections\Collection
  2296.      */
  2297.     public function getImages()
  2298.     {
  2299.         return $this->images;
  2300.     }
  2301.     /**
  2302.      * Get productClass
  2303.      *
  2304.      * @return \XLite\Model\ProductClass
  2305.      */
  2306.     public function getProductClass()
  2307.     {
  2308.         return $this->productClass;
  2309.     }
  2310.     /**
  2311.      * Set taxClass
  2312.      *
  2313.      * @param \XLite\Model\TaxClass $taxClass
  2314.      *
  2315.      * @return Product
  2316.      */
  2317.     public function setTaxClass(\XLite\Model\TaxClass $taxClass null)
  2318.     {
  2319.         $this->taxClass $taxClass;
  2320.         return $this;
  2321.     }
  2322.     /**
  2323.      * Get taxClass
  2324.      *
  2325.      * @return \XLite\Model\TaxClass
  2326.      */
  2327.     public function getTaxClass()
  2328.     {
  2329.         return $this->taxClass;
  2330.     }
  2331.     /**
  2332.      * Add attributes
  2333.      *
  2334.      * @param \XLite\Model\Attribute $attributes
  2335.      *
  2336.      * @return Product
  2337.      */
  2338.     public function addAttributes(\XLite\Model\Attribute $attributes)
  2339.     {
  2340.         $this->attributes[] = $attributes;
  2341.         return $this;
  2342.     }
  2343.     /**
  2344.      * Get attributes
  2345.      *
  2346.      * @return \Doctrine\Common\Collections\Collection<int, \XLite\Model\Attribute>
  2347.      */
  2348.     public function getAttributes()
  2349.     {
  2350.         return $this->attributes;
  2351.     }
  2352.     /**
  2353.      * Add attributeValueC
  2354.      *
  2355.      * @param \XLite\Model\AttributeValue\AttributeValueCheckbox $attributeValueC
  2356.      *
  2357.      * @return Product
  2358.      */
  2359.     public function addAttributeValueC(\XLite\Model\AttributeValue\AttributeValueCheckbox $attributeValueC)
  2360.     {
  2361.         $this->attributeValueC[] = $attributeValueC;
  2362.         return $this;
  2363.     }
  2364.     /**
  2365.      * Get attributeValueC
  2366.      *
  2367.      * @return \Doctrine\Common\Collections\Collection
  2368.      */
  2369.     public function getAttributeValueC()
  2370.     {
  2371.         return $this->attributeValueC;
  2372.     }
  2373.     /**
  2374.      * Add attributeValueT
  2375.      *
  2376.      * @param \XLite\Model\AttributeValue\AttributeValueText $attributeValueT
  2377.      *
  2378.      * @return Product
  2379.      */
  2380.     public function addAttributeValueT(\XLite\Model\AttributeValue\AttributeValueText $attributeValueT)
  2381.     {
  2382.         $this->attributeValueT[] = $attributeValueT;
  2383.         return $this;
  2384.     }
  2385.     /**
  2386.      * Get attributeValueT
  2387.      *
  2388.      * @return \Doctrine\Common\Collections\Collection
  2389.      */
  2390.     public function getAttributeValueT()
  2391.     {
  2392.         return $this->attributeValueT;
  2393.     }
  2394.     /**
  2395.      * Add attributeValueS
  2396.      *
  2397.      * @param \XLite\Model\AttributeValue\AttributeValueSelect $attributeValueS
  2398.      *
  2399.      * @return Product
  2400.      */
  2401.     public function addAttributeValueS(\XLite\Model\AttributeValue\AttributeValueSelect $attributeValueS)
  2402.     {
  2403.         $this->attributeValueS[] = $attributeValueS;
  2404.         return $this;
  2405.     }
  2406.     /**
  2407.      * Get attributeValueS
  2408.      *
  2409.      * @return \Doctrine\Common\Collections\Collection
  2410.      */
  2411.     public function getAttributeValueS()
  2412.     {
  2413.         return $this->attributeValueS;
  2414.     }
  2415.     /**
  2416.      * Add attributeValueH
  2417.      *
  2418.      * @param \XLite\Model\AttributeValue\AttributeValueHidden $attributeValueH
  2419.      *
  2420.      * @return Product
  2421.      */
  2422.     public function addAttributeValueH(\XLite\Model\AttributeValue\AttributeValueHidden $attributeValueH)
  2423.     {
  2424.         $this->attributeValueH[] = $attributeValueH;
  2425.         return $this;
  2426.     }
  2427.     /**
  2428.      * Get attributeValueH
  2429.      *
  2430.      * @return \Doctrine\Common\Collections\Collection
  2431.      */
  2432.     public function getAttributeValueH()
  2433.     {
  2434.         return $this->attributeValueH;
  2435.     }
  2436.     /**
  2437.      * Add quickData
  2438.      *
  2439.      * @param \XLite\Model\QuickData $quickData
  2440.      *
  2441.      * @return Product
  2442.      */
  2443.     public function addQuickData(\XLite\Model\QuickData $quickData)
  2444.     {
  2445.         $this->quickData[] = $quickData;
  2446.         return $this;
  2447.     }
  2448.     /**
  2449.      * Get quickData
  2450.      *
  2451.      * @return \Doctrine\Common\Collections\Collection
  2452.      */
  2453.     public function getQuickData()
  2454.     {
  2455.         return $this->quickData;
  2456.     }
  2457.     /**
  2458.      * Add memberships
  2459.      *
  2460.      * @param \XLite\Model\Membership $memberships
  2461.      *
  2462.      * @return Product
  2463.      */
  2464.     public function addMemberships(\XLite\Model\Membership $memberships)
  2465.     {
  2466.         $this->memberships[] = $memberships;
  2467.         return $this;
  2468.     }
  2469.     /**
  2470.      * Get memberships
  2471.      *
  2472.      * @return \Doctrine\Common\Collections\Collection
  2473.      */
  2474.     public function getMemberships()
  2475.     {
  2476.         return $this->memberships;
  2477.     }
  2478.     /**
  2479.      * @param \XLite\Model\Membership[] $memberships
  2480.      */
  2481.     public function addMembershipsByMemberships($memberships)
  2482.     {
  2483.         foreach ($memberships as $membership) {
  2484.             if (!$this->hasMembershipByMembership($membership)) {
  2485.                 $this->addMemberships($membership);
  2486.             }
  2487.         }
  2488.     }
  2489.     /**
  2490.      * @param \XLite\Model\Membership[] $memberships
  2491.      */
  2492.     public function removeMembershipsByMemberships($memberships)
  2493.     {
  2494.         foreach ($memberships as $membership) {
  2495.             if ($this->hasMembershipByMembership($membership)) {
  2496.                 $this->getMemberships()->removeElement($membership);
  2497.             }
  2498.         }
  2499.     }
  2500.     /**
  2501.      * @param \XLite\Model\Membership[] $memberships
  2502.      */
  2503.     public function replaceMembershipsByMemberships($memberships)
  2504.     {
  2505.         $ids array_map(static function ($item) {
  2506.             /** @var \XLite\Model\Membership $item */
  2507.             return (int) $item->getMembershipId();
  2508.         }, $memberships);
  2509.         $toRemove = [];
  2510.         foreach ($this->getMemberships() as $membership) {
  2511.             if (!in_array((int) $membership->getMembershipId(), $idstrue)) {
  2512.                 $toRemove[] = $membership;
  2513.             }
  2514.         }
  2515.         $this->addMembershipsByMemberships($memberships);
  2516.         $this->removeMembershipsByMemberships($toRemove);
  2517.     }
  2518.     /**
  2519.      * @param \XLite\Model\Membership $membership
  2520.      *
  2521.      * @return boolean
  2522.      */
  2523.     public function hasMembershipByMembership($membership)
  2524.     {
  2525.         return (bool) $this->getMembershipByMembership($membership);
  2526.     }
  2527.     /**
  2528.      * @param \XLite\Model\Membership $membership
  2529.      *
  2530.      * @return mixed|null
  2531.      */
  2532.     public function getMembershipByMembership($membership)
  2533.     {
  2534.         foreach ($this->getMemberships() as $membershipObject) {
  2535.             if (
  2536.                 (
  2537.                     $membership->isPersistent()
  2538.                     && (int) $membership->getMembershipId() === (int) $membershipObject->getMembershipId()
  2539.                 )
  2540.                 || $membership === $membershipObject
  2541.             ) {
  2542.                 return $membershipObject;
  2543.             }
  2544.         }
  2545.         return null;
  2546.     }
  2547.     /**
  2548.      * Add cleanURLs
  2549.      *
  2550.      * @param \XLite\Model\CleanURL $cleanURLs
  2551.      *
  2552.      * @return Product
  2553.      */
  2554.     public function addCleanURLs(\XLite\Model\CleanURL $cleanURLs)
  2555.     {
  2556.         $this->cleanURLs[] = $cleanURLs;
  2557.         return $this;
  2558.     }
  2559.     /**
  2560.      * Get cleanURLs
  2561.      *
  2562.      * @return \Doctrine\Common\Collections\Collection
  2563.      */
  2564.     public function getCleanURLs()
  2565.     {
  2566.         return $this->cleanURLs;
  2567.     }
  2568.     /**
  2569.      * Set inventoryEnabled
  2570.      *
  2571.      * @param boolean $inventoryEnabled
  2572.      *
  2573.      * @return Product
  2574.      */
  2575.     public function setInventoryEnabled($inventoryEnabled)
  2576.     {
  2577.         $this->inventoryEnabled $inventoryEnabled;
  2578.         return $this;
  2579.     }
  2580.     /**
  2581.      * Get inventoryEnabled
  2582.      *
  2583.      * @return boolean
  2584.      */
  2585.     public function getInventoryEnabled()
  2586.     {
  2587.         return $this->inventoryEnabled;
  2588.     }
  2589.     /**
  2590.      * Get amount
  2591.      *
  2592.      * @return integer
  2593.      */
  2594.     public function getAmount()
  2595.     {
  2596.         return $this->amount;
  2597.     }
  2598.     /**
  2599.      * Set lowLimitEnabledCustomer
  2600.      *
  2601.      * @param boolean $lowLimitEnabledCustomer
  2602.      *
  2603.      * @return Product
  2604.      */
  2605.     public function setLowLimitEnabledCustomer($lowLimitEnabledCustomer)
  2606.     {
  2607.         $this->lowLimitEnabledCustomer $lowLimitEnabledCustomer;
  2608.         return $this;
  2609.     }
  2610.     /**
  2611.      * Get lowLimitEnabledCustomer
  2612.      *
  2613.      * @return boolean
  2614.      */
  2615.     public function getLowLimitEnabledCustomer()
  2616.     {
  2617.         return $this->lowLimitEnabledCustomer;
  2618.     }
  2619.     /**
  2620.      * Set lowLimitEnabled
  2621.      *
  2622.      * @param boolean $lowLimitEnabled
  2623.      *
  2624.      * @return Product
  2625.      */
  2626.     public function setLowLimitEnabled($lowLimitEnabled)
  2627.     {
  2628.         $this->lowLimitEnabled $lowLimitEnabled;
  2629.         return $this;
  2630.     }
  2631.     /**
  2632.      * Get lowLimitEnabled
  2633.      *
  2634.      * @return boolean
  2635.      */
  2636.     public function getLowLimitEnabled()
  2637.     {
  2638.         return $this->lowLimitEnabled;
  2639.     }
  2640.     /**
  2641.      * Get lowLimitAmount
  2642.      *
  2643.      * @return integer
  2644.      */
  2645.     public function getLowLimitAmount()
  2646.     {
  2647.         return $this->lowLimitAmount;
  2648.     }
  2649.     /**
  2650.      * @return integer
  2651.      */
  2652.     protected function getAvailableAmountForTooltipText()
  2653.     {
  2654.         return $this->getAvailableAmount();
  2655.     }
  2656.     /**
  2657.      * Get help text for "all stock in cart" product
  2658.      *
  2659.      * @return string
  2660.      */
  2661.     public function getAllStockInCartTooltipText()
  2662.     {
  2663.         $text '';
  2664.         if ($this->getInventoryEnabled()) {
  2665.             $text = static::t(
  2666.                 'Sorry, we only have {{qty}} of that item available',
  2667.                 ['qty' => $this->getAvailableAmountForTooltipText()]
  2668.             );
  2669.         }
  2670.         return $text;
  2671.     }
  2672.     // {{{ Translation Getters / setters
  2673.     /**
  2674.      * @return string
  2675.      */
  2676.     public function getDescription()
  2677.     {
  2678.         return $this->getTranslationField(__FUNCTION__);
  2679.     }
  2680.     /**
  2681.      * @param string $description
  2682.      *
  2683.      * @return \XLite\Model\Base\Translation
  2684.      */
  2685.     public function setDescription($description)
  2686.     {
  2687.         return $this->setTranslationField(__FUNCTION__$description);
  2688.     }
  2689.     /**
  2690.      * @return string
  2691.      */
  2692.     public function getBriefDescription()
  2693.     {
  2694.         return $this->getTranslationField(__FUNCTION__);
  2695.     }
  2696.     /**
  2697.      * @param string $briefDescription
  2698.      *
  2699.      * @return \XLite\Model\Base\Translation
  2700.      */
  2701.     public function setBriefDescription($briefDescription)
  2702.     {
  2703.         return $this->setTranslationField(__FUNCTION__$briefDescription);
  2704.     }
  2705.     /**
  2706.      * @return string
  2707.      */
  2708.     public function getMetaTags()
  2709.     {
  2710.         return $this->getTranslationField(__FUNCTION__);
  2711.     }
  2712.     /**
  2713.      * @param string $metaTags
  2714.      *
  2715.      * @return \XLite\Model\Base\Translation
  2716.      */
  2717.     public function setMetaTags($metaTags)
  2718.     {
  2719.         return $this->setTranslationField(__FUNCTION__$metaTags);
  2720.     }
  2721.     /**
  2722.      * @param string $metaDesc
  2723.      *
  2724.      * @return \XLite\Model\Base\Translation
  2725.      */
  2726.     public function setMetaDesc($metaDesc)
  2727.     {
  2728.         return $this->setTranslationField(__FUNCTION__$metaDesc);
  2729.     }
  2730.     /**
  2731.      * @return string
  2732.      */
  2733.     public function getMetaTitle()
  2734.     {
  2735.         return $this->getTranslationField(__FUNCTION__);
  2736.     }
  2737.     /**
  2738.      * @param string $metaTitle
  2739.      *
  2740.      * @return \XLite\Model\Base\Translation
  2741.      */
  2742.     public function setMetaTitle($metaTitle)
  2743.     {
  2744.         return $this->setTranslationField(__FUNCTION__$metaTitle);
  2745.     }
  2746.     // }}}
  2747.     public function isDemoProduct(): bool
  2748.     {
  2749.         return $this->isDemoProduct;
  2750.     }
  2751.     public function setIsDemoProduct(bool $isDemoProduct): self
  2752.     {
  2753.         $this->isDemoProduct $isDemoProduct;
  2754.         return $this;
  2755.     }
  2756.     /**
  2757.      * Product marks
  2758.      *
  2759.      * @return IMark[]
  2760.      */
  2761.     public function getMarks(): array
  2762.     {
  2763.         return [];
  2764.     }
  2765.     public function setProductClassBy(int $newProductClassIdstring $newProductClassName ''): bool
  2766.     {
  2767.         $currentProductClassId $this->getProductClass()?->getId() ?? 0;
  2768.         if ($newProductClassId === $currentProductClassId) {
  2769.             return false;
  2770.         }
  2771.         return $newProductClassId === -1
  2772.             $this->createAndSetProductClass($newProductClassName)
  2773.             : $this->updateProductClass($newProductClassId);
  2774.     }
  2775.     public function updateProductClass(int $productClassId): bool
  2776.     {
  2777.         /** @var ProductClass $productClass */
  2778.         $productClass \XLite\Core\Database::getRepo(ProductClass::class)->find($productClassId);
  2779.         $this->setNewProductClass($productClass);
  2780.         return true;
  2781.     }
  2782.     public function createAndSetProductClass(string $newProductClassName): bool
  2783.     {
  2784.         $productClass = static::createProductClass($newProductClassName);
  2785.         if ($productClass) {
  2786.             $this->setNewProductClass($productClass);
  2787.             return true;
  2788.         }
  2789.         return false;
  2790.     }
  2791.     public static function createProductClass(string $newProductClassName): ?ProductClass
  2792.     {
  2793.         $name trim($newProductClassName);
  2794.         if ($name === '') {
  2795.             return null;
  2796.         }
  2797.         $productClass = new ProductClass();
  2798.         $productClass->setName($name);
  2799.         \XLite\Core\Database::getRepo(ProductClass::class)->insert($productClass);
  2800.         return $productClass;
  2801.     }
  2802.     public function setNewProductClass(?ProductClass $productClass)
  2803.     {
  2804.         $this->setProductClass($productClass);
  2805.         if ($productClass) {
  2806.             \XLite\Core\Database::getRepo(Attribute::class)->generateAttributeValues(
  2807.                 $this,
  2808.                 true
  2809.             );
  2810.             \XLite\Core\Database::getRepo(AttributeProperty::class)->generateClassAttributeProperties(
  2811.                 $this,
  2812.                 $productClass
  2813.             );
  2814.         }
  2815.     }
  2816. }