vendor/isotope/isotope-core/system/modules/isotope/library/Isotope/Model/Product/Standard.php line 119

Open in your IDE?
  1. <?php
  2. /*
  3.  * Isotope eCommerce for Contao Open Source CMS
  4.  *
  5.  * Copyright (C) 2009 - 2019 terminal42 gmbh & Isotope eCommerce Workgroup
  6.  *
  7.  * @link       https://isotopeecommerce.org
  8.  * @license    https://opensource.org/licenses/lgpl-3.0.html
  9.  */
  10. namespace Isotope\Model\Product;
  11. use Contao\ContentElement;
  12. use Contao\Database;
  13. use Contao\Date;
  14. use Contao\Environment;
  15. use Contao\FrontendUser;
  16. use Contao\Input;
  17. use Contao\PageModel;
  18. use Contao\StringUtil;
  19. use Contao\System;
  20. use Contao\Widget;
  21. use Haste\Generator\RowClass;
  22. use Haste\Units\Mass\Weight;
  23. use Haste\Units\Mass\WeightAggregate;
  24. use Haste\Util\Url;
  25. use Isotope\Collection\ProductPrice as ProductPriceCollection;
  26. use Isotope\Frontend\ProductAction\ProductActionInterface;
  27. use Isotope\Frontend\ProductAction\Registry;
  28. use Isotope\Interfaces\IsotopeAttribute;
  29. use Isotope\Interfaces\IsotopeAttributeForVariants;
  30. use Isotope\Interfaces\IsotopeAttributeWithOptions;
  31. use Isotope\Interfaces\IsotopePrice;
  32. use Isotope\Interfaces\IsotopeProduct;
  33. use Isotope\Interfaces\IsotopeProductCollection;
  34. use Isotope\Interfaces\IsotopeProductWithOptions;
  35. use Isotope\Isotope;
  36. use Isotope\Model\Attribute;
  37. use Isotope\Model\Gallery;
  38. use Isotope\Model\Gallery\Standard as StandardGallery;
  39. use Isotope\Model\ProductCollectionItem;
  40. use Isotope\Model\ProductPrice;
  41. use Isotope\Model\ProductType;
  42. use Isotope\Template;
  43. /**
  44.  * Standard implementation of an Isotope product.
  45.  */
  46. class Standard extends AbstractProduct implements WeightAggregateIsotopeProductWithOptions
  47. {
  48.     /**
  49.      * Price model for the current product
  50.      * @var \Isotope\Model\ProductPrice
  51.      */
  52.     protected $objPrice false;
  53.     /**
  54.      * Attributes assigned to this product type
  55.      * @var array
  56.      * @deprecated
  57.      */
  58.     protected $arrAttributes;
  59.     /**
  60.      * Variant attributes assigned to this product type
  61.      * @var array
  62.      * @deprecated
  63.      */
  64.     protected $arrVariantAttributes;
  65.     /**
  66.      * Available variant IDs
  67.      * @var int[]
  68.      */
  69.     protected $arrVariantIds;
  70.     /**
  71.      * Customer defined configuration
  72.      * @var array
  73.      */
  74.     protected $arrCustomerConfig = array();
  75.     /**
  76.      * Default configuration (to predefine variant or customer editable attributes)
  77.      * @var array
  78.      */
  79.     protected $arrDefaults;
  80.     /**
  81.      * Unique form ID
  82.      * @var string
  83.      */
  84.     protected $strFormId 'iso_product';
  85.     /**
  86.      * For option widgets, helps determine the encoding type for a form
  87.      * @var bool
  88.      */
  89.     protected $hasUpload false;
  90.     /**
  91.      * For option widgets, don't submit if certain validation(s) fail
  92.      * @var bool
  93.      */
  94.     protected $doNotSubmit false;
  95.     /**
  96.      * @inheritdoc
  97.      */
  98.     public function isAvailableForCollection(IsotopeProductCollection $objCollection)
  99.     {
  100.         if (false === parent::isAvailableForCollection($objCollection)) {
  101.             return false;
  102.         }
  103.         // Check that the product is in any page of the current site
  104.         if (\count(\Isotope\Frontend::getPagesInCurrentRoot($this->getCategories(), $objCollection->getMember())) == 0) {
  105.             return false;
  106.         }
  107.         if ($this->hasVariants() && !count($this->getVariantIds())) {
  108.             return false;
  109.         }
  110.         // Check if "advanced price" is available
  111.         if ($this->getType()->hasAdvancedPrices()
  112.             && (\in_array('price'$this->getType()->getAttributes(), true) || $this->hasVariantPrices())
  113.             && null === $this->getPrice($objCollection)
  114.         ) {
  115.             return false;
  116.         }
  117.         return true;
  118.     }
  119.     /**
  120.      * Return true if the user should see lowest price tier as lowest price
  121.      *
  122.      * @return bool
  123.      */
  124.     public function canSeePriceTiers()
  125.     {
  126.         return $this->hasAdvancedPrices() && $this->getType()->show_price_tiers;
  127.     }
  128.     /**
  129.      * Return the unique form ID for the product
  130.      *
  131.      * @return string
  132.      */
  133.     public function getFormId()
  134.     {
  135.         return $this->strFormId;
  136.     }
  137.     /**
  138.      * Get product price model
  139.      *
  140.      * @param IsotopeProductCollection $objCollection
  141.      *
  142.      * @return \Isotope\Interfaces\IsotopePrice|ProductPrice
  143.      */
  144.     public function getPrice(IsotopeProductCollection $objCollection null)
  145.     {
  146.         if (null !== $objCollection && $objCollection !== Isotope::getCart()) {
  147.             return ProductPrice::findByProductAndCollection($this$objCollection);
  148.         }
  149.         if (false === $this->objPrice) {
  150.             if (null === $objCollection) {
  151.                 $objCollection Isotope::getCart();
  152.             }
  153.             $this->objPrice ProductPrice::findByProductAndCollection($this$objCollection);
  154.         }
  155.         return $this->objPrice;
  156.     }
  157.     public function setPrice(IsotopePrice $price)
  158.     {
  159.         $this->objPrice $price;
  160.         if ($price instanceof ProductPrice || $price instanceof ProductPriceCollection) {
  161.             $price->setProduct($this);
  162.         }
  163.     }
  164.     /**
  165.      * Return minimum quantity for the product (from advanced price tiers)
  166.      *
  167.      * @return int
  168.      */
  169.     public function getMinimumQuantity()
  170.     {
  171.         // Minimum quantity is only available for advanced pricing
  172.         if (!$this->hasAdvancedPrices() || null === $this->getPrice()) {
  173.             return 1;
  174.         }
  175.         $intLowest = (int) $this->getPrice()->getLowestTier();
  176.         if ($intLowest 1) {
  177.             return 1;
  178.         }
  179.         return $intLowest;
  180.     }
  181.     /**
  182.      * Return the product attributes
  183.      *
  184.      * @return array
  185.      *
  186.      * @deprecated Deprecated since Isotope 2.4, to be removed in Isotope 3. Use ProductType::getAttributes()
  187.      */
  188.     public function getAttributes()
  189.     {
  190.         if (null === $this->arrAttributes) {
  191.             $this->arrAttributes $this->getType()->getAttributes();
  192.         }
  193.         return $this->arrAttributes;
  194.     }
  195.     /**
  196.      * Return the product variant attributes
  197.      *
  198.      * @return array
  199.      *
  200.      * @deprecated Deprecated since Isotope 2.4, to be removed in Isotope 3. Use ProductType::getVariantAttributes()
  201.      */
  202.     public function getVariantAttributes()
  203.     {
  204.         if (null === $this->arrVariantAttributes) {
  205.             $this->arrVariantAttributes $this->getType()->getVariantAttributes();
  206.         }
  207.         return $this->arrVariantAttributes;
  208.     }
  209.     /**
  210.      * Return all available variant IDs of this product
  211.      *
  212.      * @return int[]
  213.      */
  214.     public function getVariantIds()
  215.     {
  216.         if (null === $this->arrVariantIds) {
  217.             $this->arrVariantIds = [];
  218.             // Nothing to do if we have no variants
  219.             if (!$this->hasVariants()) {
  220.                 return $this->arrVariantIds;
  221.             }
  222.             $time            Date::floorToMinute();
  223.             $blnHasProtected false;
  224.             $blnHasGuests    false;
  225.             $strQuery        '
  226.                 SELECT tl_iso_product.id, tl_iso_product.protected, tl_iso_product.groups
  227.                 FROM tl_iso_product
  228.                 WHERE
  229.                     pid=' $this->getProductId() . "
  230.                     AND language=''
  231.                     AND published='1'
  232.                     AND (start='' OR start<'$time')
  233.                     AND (stop='' OR stop>'" . ($time 60) . "')
  234.             ";
  235.             if (BE_USER_LOGGED_IN !== true) {
  236.                 $arrAttributes   $this->getType()->getVariantAttributes();
  237.                 $blnHasProtected \in_array('protected'$arrAttributestrue);
  238.                 $blnHasGuests \in_array('guests'$arrAttributestrue);
  239.                 // Hide guests-only products when logged in
  240.                 if (FE_USER_LOGGED_IN === true && $blnHasGuests) {
  241.                     $strQuery .= " AND (guests=''" . ($blnHasProtected " OR protected='1'" '') . ')';
  242.                 } // Hide protected if no user is logged in
  243.                 elseif (FE_USER_LOGGED_IN !== true && $blnHasProtected) {
  244.                     $strQuery .= " AND (protected=''" . ($blnHasGuests " OR guests='1'" '') . ")";
  245.                 }
  246.             }
  247.             /** @var object $objVariants */
  248.             $objVariants Database::getInstance()->query($strQuery);
  249.             while ($objVariants->next()) {
  250.                 if (FE_USER_LOGGED_IN !== true
  251.                     && $blnHasProtected
  252.                     && $objVariants->protected
  253.                     && (!$blnHasGuests || !$objVariants->guests)
  254.                 ) {
  255.                     continue;
  256.                 }
  257.                 if (FE_USER_LOGGED_IN === true
  258.                     && $blnHasGuests
  259.                     && $objVariants->guests
  260.                     && (!$blnHasProtected || $objVariants->protected)
  261.                 ) {
  262.                     continue;
  263.                 }
  264.                 if ($blnHasProtected && $objVariants->protected) {
  265.                     $groups StringUtil::deserialize($objVariants->groups);
  266.                     if (empty($groups) || !\is_array($groups) || !\count(array_intersect($groupsFrontendUser::getInstance()->groups))) {
  267.                         continue;
  268.                     }
  269.                 }
  270.                 $this->arrVariantIds[] = $objVariants->id;
  271.             }
  272.             // Only show variants where a price is available
  273.             if (!== \count($this->arrVariantIds) && $this->hasVariantPrices()) {
  274.                 if ($this->hasAdvancedPrices()) {
  275.                     $objPrices ProductPrice::findAdvancedByProductIdsAndCollection($this->arrVariantIdsIsotope::getCart());
  276.                 } else {
  277.                     $objPrices ProductPrice::findPrimaryByProductIds($this->arrVariantIds);
  278.                 }
  279.                 if (null === $objPrices) {
  280.                     $this->arrVariantIds = [];
  281.                 } else {
  282.                     $this->arrVariantIds $objPrices->fetchEach('pid');
  283.                 }
  284.             }
  285.         }
  286.         return $this->arrVariantIds;
  287.     }
  288.     /**
  289.      * Get the weight of the product (as object)
  290.      *
  291.      * @return Weight
  292.      */
  293.     public function getWeight()
  294.     {
  295.         if (!isset($this->arrData['shipping_weight'])) {
  296.             return null;
  297.         }
  298.         return Weight::createFromTimePeriod($this->arrData['shipping_weight']);
  299.     }
  300.     /**
  301.      * @inheritdoc
  302.      */
  303.     public function getOptions()
  304.     {
  305.         return array_merge($this->getVariantConfig(), $this->getCustomerConfig());
  306.     }
  307.     /**
  308.      * @inheritdoc
  309.      */
  310.     public function setOptions(array $options)
  311.     {
  312.         if (!$this->blnPreventSaving) {
  313.             throw new \RuntimeException('Do not modify a product object that is in the model registry!');
  314.         }
  315.         if (!$this->isVariant()) {
  316.             $this->arrCustomerConfig $options;
  317.             return;
  318.         }
  319.         $attributes array_intersect($this->getType()->getVariantAttributes(), Attribute::getVariantOptionFields());
  320.         $this->arrCustomerConfig = [];
  321.         foreach ($options as $k => $v) {
  322.             if (\in_array($k$attributestrue)) {
  323.                 if ($this->arrData[$k] != $v) {
  324.                     throw new \RuntimeException(
  325.                         sprintf('"%s" for attribute "%s" does not match current variant.'$v$k)
  326.                     );
  327.                 }
  328.                 // Ignore variant data, that's already stored
  329.                 continue;
  330.             }
  331.             $this->arrCustomerConfig[$k] = $v;
  332.         }
  333.     }
  334.     /**
  335.      * Get the product configuration
  336.      * This includes customer defined fields and variant options
  337.      *
  338.      * @return array
  339.      *
  340.      * @deprecated Deprecated since Isotope 2.4, to be removed in Isotope 3.0. Use getOptions() instead.
  341.      */
  342.     public function getConfiguration()
  343.     {
  344.         return Isotope::formatProductConfiguration($this->getOptions(), $this);
  345.     }
  346.     /**
  347.      * Get customer defined field values
  348.      *
  349.      * @return array
  350.      */
  351.     public function getCustomerConfig()
  352.     {
  353.         return $this->arrCustomerConfig;
  354.     }
  355.     /**
  356.      * Get variant option field values
  357.      *
  358.      * @return array
  359.      */
  360.     public function getVariantConfig()
  361.     {
  362.         if (!$this->isVariant()) {
  363.             return array();
  364.         }
  365.         $arrVariantConfig = array();
  366.         $arrAttributes array_intersect($this->getType()->getVariantAttributes(), Attribute::getVariantOptionFields());
  367.         foreach (array_unique($arrAttributes) as $attribute) {
  368.             $arrVariantConfig[$attribute] = $this->arrData[$attribute];
  369.         }
  370.         return $arrVariantConfig;
  371.     }
  372.     /**
  373.      * Generate a product template
  374.      *
  375.      * @param array $arrConfig
  376.      *
  377.      * @return string
  378.      *
  379.      * @throws \InvalidArgumentException
  380.      */
  381.     public function generate(array $arrConfig)
  382.     {
  383.         $objProduct $this;
  384.         $loadFallback = isset($arrConfig['loadFallback']) ? (bool) $arrConfig['loadFallback'] : true;
  385.         $this->strFormId = (($arrConfig['module'] instanceof ContentElement) ? 'cte' 'fmd') . $arrConfig['module']->id '_product_' $this->getProductId();
  386.         if (!$arrConfig['disableOptions']) {
  387.             $objProduct $this->validateVariant($loadFallback);
  388.             // A variant has been loaded, generate the variant
  389.             if ($objProduct->getId() != $this->getId()) {
  390.                 return $objProduct->generate($arrConfig);
  391.             }
  392.         }
  393.         /** @var Template|\stdClass $objTemplate */
  394.         $objTemplate = new Template($arrConfig['template']);
  395.         $objTemplate->setData($this->arrData);
  396.         $objTemplate->product $this;
  397.         $objTemplate->config  $arrConfig;
  398.         $objTemplate->highlightKeywords = function($text) {
  399.             $keywords Input::get('keywords');
  400.             if (empty($keywords)) {
  401.                 return $text;
  402.             }
  403.             $keywords StringUtil::trimsplit(' |-'$keywords);
  404.             $keywords array_filter(array_unique($keywords));
  405.             foreach ($keywords as $word) {
  406.                 $text StringUtil::highlight($text$word'<em>''</em>');
  407.             }
  408.             return $text;
  409.         };
  410.         $objTemplate->hasAttribute = function ($strAttribute) use ($objProduct) {
  411.             return \in_array($strAttribute$objProduct->getType()->getAttributes(), true)
  412.                 || \in_array($strAttribute$objProduct->getType()->getVariantAttributes(), true);
  413.         };
  414.         $objTemplate->generateAttribute = function ($strAttribute, array $arrOptions = array()) use ($objProduct) {
  415.             $objAttribute $GLOBALS['TL_DCA']['tl_iso_product']['attributes'][$strAttribute];
  416.             if (!($objAttribute instanceof IsotopeAttribute)) {
  417.                 throw new \InvalidArgumentException($strAttribute ' is not a valid attribute');
  418.             }
  419.             return $objAttribute->generate($objProduct$arrOptions);
  420.         };
  421.         $objTemplate->generatePrice = function() use ($objProduct) {
  422.             $objPrice $objProduct->getPrice();
  423.             /** @var ProductType $objType */
  424.             $objType $objProduct->getType();
  425.             if (null === $objPrice) {
  426.                 return '';
  427.             }
  428.             return $objPrice->generate($objType->showPriceTiers(), 1$objProduct->getOptions());
  429.         };
  430.         /** @var StandardGallery $currentGallery */
  431.         $currentGallery          null;
  432.         $objTemplate->getGallery = function ($strAttribute) use ($objProduct$arrConfig, &$currentGallery) {
  433.             if (null === $currentGallery
  434.                 || $currentGallery->getName() !== $objProduct->getFormId() . '_' $strAttribute
  435.             ) {
  436.                 $currentGallery Gallery::createForProductAttribute(
  437.                     $objProduct,
  438.                     $strAttribute,
  439.                     $arrConfig
  440.                 );
  441.             }
  442.             return $currentGallery;
  443.         };
  444.         $arrVariantOptions = array();
  445.         $arrProductOptions = array();
  446.         $arrAjaxOptions    = array();
  447.         if (!$arrConfig['disableOptions']) {
  448.             foreach (array_unique(array_merge($this->getType()->getAttributes(), $this->getType()->getVariantAttributes())) as $attribute) {
  449.                 $arrData $GLOBALS['TL_DCA']['tl_iso_product']['fields'][$attribute];
  450.                 if (($arrData['attributes']['customer_defined'] ?? null) || ($arrData['attributes']['variant_option'] ?? null)) {
  451.                     $strWidget $this->generateProductOptionWidget($attribute$arrVariantOptions$arrAjaxOptions$objWidget);
  452.                     if ($strWidget != '') {
  453.                         $arrProductOptions[$attribute] = array_merge($arrData, array
  454.                         (
  455.                             'name'    => $attribute,
  456.                             'html'    => $strWidget,
  457.                             'widget'  => $objWidget,
  458.                         ));
  459.                     }
  460.                     unset($objWidget);
  461.                 }
  462.             }
  463.         }
  464.         /** @var ProductActionInterface[] $actions */
  465.         $handleButtons false;
  466.         $actions array_filter(
  467.             Registry::all(true$this),
  468.             function (ProductActionInterface $action) use ($arrConfig) {
  469.                 return \in_array($action->getName(), $arrConfig['buttons'] ?? []) && $action->isAvailable($this$arrConfig);
  470.             }
  471.         );
  472.         // Sort actions by order in module configuration
  473.         $buttonOrder array_values($arrConfig['buttons'] ?? []);
  474.         usort($actions, function (ProductActionInterface $aProductActionInterface $b) use ($buttonOrder) {
  475.             return array_search($a->getName(), $buttonOrder) - array_search($b->getName(), $buttonOrder);
  476.         });
  477.         if (Input::post('FORM_SUBMIT') == $this->getFormId() && !$this->doNotSubmit) {
  478.             $handleButtons true;
  479.             foreach ($actions as $action) {
  480.                 if ($action->handleSubmit($this$arrConfig)) {
  481.                     $handleButtons false;
  482.                     break;
  483.                 }
  484.             }
  485.         }
  486.         /**
  487.          * @deprecated Deprecated since Isotope 2.5
  488.          */
  489.         $objTemplate->buttons = function() use ($arrConfig$handleButtons) {
  490.             $arrButtons = array();
  491.             // !HOOK: retrieve buttons
  492.             if (isset($arrConfig['buttons'], $GLOBALS['ISO_HOOKS']['buttons'])
  493.                 && \is_array($arrConfig['buttons'])
  494.                 && \is_array($GLOBALS['ISO_HOOKS']['buttons'])
  495.             ) {
  496.                 foreach ($GLOBALS['ISO_HOOKS']['buttons'] as $callback) {
  497.                     $arrButtons System::importStatic($callback[0])->{$callback[1]}($arrButtons$this);
  498.                 }
  499.             }
  500.             $arrButtons array_intersect_key($arrButtonsarray_flip($arrConfig['buttons'] ?? []));
  501.             if ($handleButtons) {
  502.                 foreach ($arrButtons as $button => $data) {
  503.                     if (isset($_POST[$button])) {
  504.                         if (isset($data['callback'])) {
  505.                             System::importStatic($data['callback'][0])->{$data['callback'][1]}($this$arrConfig);
  506.                         }
  507.                         break;
  508.                     }
  509.                 }
  510.             }
  511.             return $arrButtons;
  512.         };
  513.         RowClass::withKey('rowClass')->addCustom('product_option')->addFirstLast()->addEvenOdd()->applyTo($arrProductOptions);
  514.         $objTemplate->actions $actions;
  515.         $objTemplate->useQuantity $arrConfig['useQuantity'] && null === $this->getCollectionItem();
  516.         $objTemplate->minimum_quantity $this->getMinimumQuantity();
  517.         $objTemplate->raw $this->arrData;
  518.         $objTemplate->raw_options $this->getConfiguration();
  519.         $objTemplate->configuration $this->getConfiguration();
  520.         $objTemplate->href '';
  521.         $objTemplate->label_detail $GLOBALS['TL_LANG']['MSC']['detailLabel'];
  522.         $objTemplate->options $arrProductOptions;
  523.         $objTemplate->hasOptions \count($arrProductOptions) > 0;
  524.         $objTemplate->enctype $this->hasUpload 'multipart/form-data' 'application/x-www-form-urlencoded';
  525.         $objTemplate->formId $this->getFormId();
  526.         $objTemplate->formSubmit $this->getFormId();
  527.         $objTemplate->product_id $this->getProductId();
  528.         $objTemplate->module_id $arrConfig['module']->id;
  529.         if (!$arrConfig['jumpTo'] instanceof PageModel || $arrConfig['jumpTo']->iso_readerMode !== 'none') {
  530.             $objTemplate->href $this->generateUrl($arrConfig['jumpTo']);
  531.         }
  532.         if (!$arrConfig['disableOptions']) {
  533.             $GLOBALS['AJAX_PRODUCTS'][] = array('formId' => $this->getFormId(), 'attributes' => $arrAjaxOptions);
  534.         }
  535.         // !HOOK: alter product data before output
  536.         if (isset($GLOBALS['ISO_HOOKS']['generateProduct']) && \is_array($GLOBALS['ISO_HOOKS']['generateProduct'])) {
  537.             foreach ($GLOBALS['ISO_HOOKS']['generateProduct'] as $callback) {
  538.                 System::importStatic($callback[0])->{$callback[1]}($objTemplate$this);
  539.             }
  540.         }
  541.         return trim($objTemplate->parse());
  542.     }
  543.     /**
  544.      * Return a widget object based on a product attribute's properties
  545.      *
  546.      * @param string $strField
  547.      * @param array  $arrVariantOptions
  548.      * @param array  $arrAjaxOptions
  549.      *
  550.      * @return string
  551.      */
  552.     protected function generateProductOptionWidget($strField, &$arrVariantOptions, &$arrAjaxOptions, &$objWidget null)
  553.     {
  554.         $arrDefaults $this->getOptionsDefaults();
  555.         /** @var IsotopeAttribute|IsotopeAttributeWithOptions|IsotopeAttributeForVariants|Attribute $objAttribute */
  556.         $objAttribute $GLOBALS['TL_DCA']['tl_iso_product']['attributes'][$strField];
  557.         $arrData $GLOBALS['TL_DCA']['tl_iso_product']['fields'][$strField];
  558.         /** @var Widget $strClass */
  559.         $strClass $objAttribute->getFrontendWidget();
  560.         $arrData['eval']['required'] = $arrData['eval']['mandatory'];
  561.         // Value can be predefined in the URL, e.g. to preselect a variant
  562.         if (!empty($arrDefaults[$strField])) {
  563.             $arrData['default'] = $arrDefaults[$strField];
  564.         }
  565.         $arrField $strClass::getAttributesFromDca($arrData$strField$arrData['default'] ?? null$strField, static::$strTable$this);
  566.         // Prepare variant selection field
  567.         // @todo in 3.0: $objAttribute instanceof IsotopeAttributeForVariants
  568.         if ($objAttribute->isVariantOption()) {
  569.             $arrOptions $objAttribute->getOptionsForVariants($this->getVariantIds(), $arrVariantOptions);
  570.             // Hide selection if only one option is available (and "force_variant_options" is not set in product type)
  571.             if (\count($arrOptions) == && !$this->getType()->force_variant_options) {
  572.                 $arrVariantOptions[$strField] = $arrOptions[0];
  573.                 return '';
  574.             }
  575.             if ($arrField['value'] != '' && \in_array($arrField['value'], $arrOptions)) {
  576.                 $arrVariantOptions[$strField] = $arrField['value'];
  577.             }
  578.             // Remove options not available in any product variant
  579.             if (\is_array($arrField['options'])) {
  580.                 $blankOption null;
  581.                 foreach ($arrField['options'] as $k => $option) {
  582.                     // Keep groups and blankOptionLabels
  583.                     if ($option['value'] == '') {
  584.                         if (null !== $blankOption) {
  585.                             // Last blank option wins
  586.                             $arrField['options'][$blankOption] = $option;
  587.                             unset($arrField['options'][$k]);
  588.                         } else {
  589.                             $blankOption $k;
  590.                         }
  591.                     } elseif (!\in_array($option['value'], $arrOptions) && !$option['group']) {
  592.                         unset($arrField['options'][$k]);
  593.                     }
  594.                 }
  595.                 $arrField['options'] = array_values($arrField['options']);
  596.             }
  597.             $arrField['value'] = $this->$strField;
  598.         } elseif ($objAttribute instanceof IsotopeAttributeWithOptions && empty($arrField['options'])) {
  599.             return '';
  600.         }
  601.         if ($objAttribute->isVariantOption()
  602.             || ($objAttribute instanceof IsotopeAttributeWithOptions && $objAttribute->canHavePrices())
  603.             || $arrData['attributes']['ajax_option']
  604.             || $arrField['attributes']['ajax_option'// see https://github.com/isotope/core/issues/2096
  605.         ) {
  606.             $arrAjaxOptions[] = $strField;
  607.         }
  608.         // Convert optgroups so they work with FormSelectMenu
  609.         // @deprecated Remove in Isotope 3.0, the options should match for frontend if attribute is customer defined
  610.         if (
  611.             \is_array($arrField['options'])
  612.             && array_is_assoc($arrField['options'])
  613.             && \count(
  614.                 array_filter(
  615.                     $arrField['options'], function($v) {
  616.                         return !isset($v['label']);
  617.                     }
  618.                 )
  619.             ) > 0
  620.         ) {
  621.             $arrOptions $arrField['options'];
  622.             $arrField['options'] = array();
  623.             foreach ($arrOptions as $k => $v) {
  624.                 if (isset($v['label'])) {
  625.                     $arrField['options'][] = $v;
  626.                 } else {
  627.                     $arrField['options'][] = array(
  628.                         'label'     => $k,
  629.                         'value'     => $k,
  630.                         'group'     => '1',
  631.                     );
  632.                     foreach ($v as $vv) {
  633.                         $arrField['options'][] = $vv;
  634.                     }
  635.                 }
  636.             }
  637.         }
  638.         $arrField['storeValues'] = true;
  639.         $arrField['tableless'] = true;
  640.         $arrField['product'] = $this;
  641.         $arrField['id'] .= '_' $this->getFormId();
  642.         /** @var Widget|\stdClass $objWidget */
  643.         $objWidget = new $strClass($arrField);
  644.         // Validate input
  645.         if (Input::post('FORM_SUBMIT') == $this->getFormId()) {
  646.             $objWidget->validate();
  647.             if ($objWidget->hasErrors()) {
  648.                 $this->doNotSubmit true;
  649.             } elseif ($objWidget->submitInput() || $objWidget instanceof \uploadable) {
  650.                 $varValue $objWidget->value;
  651.                 // Convert date formats into timestamps
  652.                 if ($varValue != '' && \in_array($arrData['eval']['rgxp'], ['date''time''datim'], true)) {
  653.                     try {
  654.                         /** @var Date|object $objDate */
  655.                         $objDate = new Date($varValue$GLOBALS['TL_CONFIG'][$arrData['eval']['rgxp'] . 'Format']);
  656.                         $varValue $objDate->tstamp;
  657.                     } catch (\OutOfBoundsException $e) {
  658.                         $objWidget->addError(sprintf($GLOBALS['TL_LANG']['ERR'][$arrData['eval']['rgxp']], $GLOBALS['TL_CONFIG'][$arrData['eval']['rgxp'] . 'Format']));
  659.                     }
  660.                 }
  661.                 // Trigger the save_callback
  662.                 if (\is_array($arrData['save_callback'] ?? null)) {
  663.                     foreach ($arrData['save_callback'] as $callback) {
  664.                         try {
  665.                             if (\is_array($callback)) {
  666.                                 $varValue System::importStatic($callback[0])->{$callback[1]}($varValue$this$objWidget);
  667.                             } else {
  668.                                 $varValue $objAttribute->{$callback}($varValue$this$objWidget);
  669.                             }
  670.                         } catch (\Exception $e) {
  671.                             $objWidget->class 'error';
  672.                             $objWidget->addError($e->getMessage());
  673.                             $this->doNotSubmit true;
  674.                         }
  675.                     }
  676.                 }
  677.                 if (!$objWidget->hasErrors() && $varValue != '') {
  678.                     // @todo in 3.0: $objAttribute instanceof IsotopeAttributeForVariants
  679.                     if ($objAttribute->isVariantOption()) {
  680.                         $arrVariantOptions[$strField] = $varValue;
  681.                     } else {
  682.                         $this->arrCustomerConfig[$strField] = $varValue;
  683.                     }
  684.                 }
  685.             }
  686.         } elseif (isset($_GET[$strField]) && empty(Input::post('FORM_SUBMIT')) && !$objAttribute->isVariantOption()) {
  687.             $this->arrCustomerConfig[$strField] = $objWidget->value Input::get($strField);
  688.         }
  689.         $wizard '';
  690.         // Datepicker
  691.         if ($arrData['eval']['datepicker']) {
  692.             $GLOBALS['TL_JAVASCRIPT'][] = 'assets/datepicker/js/datepicker.min.js';
  693.             $GLOBALS['TL_CSS'][] = 'assets/datepicker/css/datepicker.min.css';
  694.             $icon 'assets/datepicker/images/icon.svg';
  695.             $rgxp   $arrData['eval']['rgxp'];
  696.             $format Date::formatToJs($GLOBALS['TL_CONFIG'][$rgxp 'Format']);
  697.             switch ($rgxp) {
  698.                 case 'datim':
  699.                     $time ",\n      timePicker:true";
  700.                     break;
  701.                 case 'time':
  702.                     $time ",\n      pickOnly:\"time\"";
  703.                     break;
  704.                 default:
  705.                     $time '';
  706.                     break;
  707.             }
  708.             $wizard .= ' <img src="'.$icon.'" width="20" height="20" alt="" id="toggle_' $objWidget->id '" style="vertical-align:-6px">
  709.   <script>
  710.   window.addEvent("domready", function() {
  711.     new Picker.Date($$("#ctrl_' $objWidget->id '"), {
  712.       draggable:false,
  713.       toggle:$$("#toggle_' $objWidget->id '"),
  714.       format:"' $format '",
  715.       positionOffset:{x:-197,y:-182}' $time ',
  716.       pickerClass:"datepicker_bootstrap",
  717.       useFadeInOut:!Browser.ie,
  718.       startDay:' $GLOBALS['TL_LANG']['MSC']['weekOffset'] . ',
  719.       titleFormat:"' $GLOBALS['TL_LANG']['MSC']['titleFormat'] . '"
  720.     });
  721.   });
  722.   </script>';
  723.         }
  724.         // Add a custom wizard
  725.         if (\is_array($arrData['wizard'] ?? null)) {
  726.             foreach ($arrData['wizard'] as $callback) {
  727.                 $wizard .= System::importStatic($callback[0])->{$callback[1]}($this);
  728.             }
  729.         }
  730.         if ($objWidget instanceof \uploadable) {
  731.             $this->hasUpload true;
  732.         }
  733.         return $objWidget->parse() . $wizard;
  734.     }
  735.     /**
  736.      * Load data of a product variant if the options match one
  737.      *
  738.      * @param bool $loadDefaultVariant
  739.      *
  740.      * @return IsotopeProduct|$this
  741.      */
  742.     public function validateVariant($loadDefaultVariant true)
  743.     {
  744.         if (!$this->hasVariants()) {
  745.             return $this;
  746.         }
  747.         $hasOptions null;
  748.         $arrOptions = array();
  749.         $arrDefaults $this->getOptionsDefaults();
  750.         // We don't need to validate IsotopeAttributeForVariants interface here, because Attribute::getVariantOptionFields will check it
  751.         foreach (array_intersect($this->getType()->getVariantAttributes(), Attribute::getVariantOptionFields()) as $attribute) {
  752.             /** @var IsotopeAttribute|Attribute $objAttribute */
  753.             $objAttribute $GLOBALS['TL_DCA']['tl_iso_product']['attributes'][$attribute];
  754.             $arrValues    $objAttribute->getOptionsForVariants($this->getVariantIds(), $arrOptions);
  755.             if (Input::post('FORM_SUBMIT') == $this->getFormId() && \in_array(Input::post($attribute), $arrValues)) {
  756.                 $arrOptions[$attribute] = Input::post($attribute);
  757.             } elseif (Input::post('FORM_SUBMIT') == '' && \in_array($arrDefaults[$attribute], $arrValues)) {
  758.                 $arrOptions[$attribute] = $arrDefaults[$attribute];
  759.             } elseif (\count($arrValues) == 1) {
  760.                 $arrOptions[$attribute] = $arrValues[0];
  761.             } else {
  762.                 // Abort if any attribute does not have a value, we can't find a variant
  763.                 $hasOptions false;
  764.                 break;
  765.             }
  766.             if (Input::post('FORM_SUBMIT') === $this->getFormId() && Input::post($attribute) === '') {
  767.                 Input::setPost($attribute$arrOptions[$attribute]);
  768.             }
  769.         }
  770.         $hasOptions false !== $hasOptions && \count($arrOptions) > 0;
  771.         if ($hasOptions && ($objVariant = static::findVariantOfProduct($this$arrOptions)) !== null) {
  772.             return $objVariant;
  773.         }
  774.         if (!$hasOptions && $loadDefaultVariant && ($objVariant = static::findDefaultVariantOfProduct($this)) !== null) {
  775.             return $objVariant;
  776.         }
  777.         return $this;
  778.     }
  779.     /**
  780.      * Validate data and remove non-available attributes
  781.      *
  782.      * @param array $arrData
  783.      *
  784.      * @return $this
  785.      */
  786.     public function setRow(array $arrData)
  787.     {
  788.         if ($arrData['pid'] > 0) {
  789.             // Do not use the model, it would trigger setRow and generate too much
  790.             // @deprecated use static::buildFindQuery once we drop BC support for buildQueryString
  791.             /** @var object $objParent */
  792.             $objParent Database::getInstance()->prepare(static::buildQueryString(array('table' => static::$strTable'column' => 'id')))->execute($arrData['pid']);
  793.             if (null === $objParent) {
  794.                 throw new \UnderflowException('Parent record of product variant ID ' $arrData['id'] . ' not found');
  795.             }
  796.             $this->setRow($objParent->row());
  797.             // Must be set before call to getInheritedFields()
  798.             $this->arrData['id'] = $arrData['id'];
  799.             $this->arrData['pid'] = $arrData['pid'];
  800.             $this->arrData['inherit'] = $arrData['inherit'];
  801.             // Set all variant attributes, except if they are inherited
  802.             $arrFallbackFields Attribute::getFetchFallbackFields();
  803.             $arrVariantFields array_diff($this->getType()->getVariantAttributes(), $this->getInheritedFields());
  804.             foreach ($arrData as $attribute => $value) {
  805.                 if (
  806.                     \in_array($attribute$arrVariantFieldstrue)
  807.                     || (($GLOBALS['TL_DCA']['tl_iso_product']['fields'][$attribute]['attributes']['legend'] ?? '') == ''
  808.                         && !\in_array(str_replace('_fallback'''$attribute), $arrFallbackFieldstrue))
  809.                 ) {
  810.                     $this->arrData[$attribute] = $arrData[$attribute];
  811.                     if (\in_array($attribute$arrFallbackFieldstrue)) {
  812.                         $this->arrData[$attribute '_fallback'] = $arrData[$attribute '_fallback'];
  813.                     }
  814.                 }
  815.             }
  816.             // Make sure publishing settings match product and variant (see #1120)
  817.             $this->arrData['published'] = $objParent->published $arrData['published'] : '';
  818.             $this->arrData['start'] = ($objParent->start != '' && ($arrData['start'] == '' || $objParent->start $arrData['start'])) ? $objParent->start $arrData['start'];
  819.             $this->arrData['stop'] = ($objParent->stop != '' && ($arrData['stop'] == '' || $objParent->stop $arrData['stop'])) ? $objParent->stop $arrData['stop'];
  820.             return $this;
  821.         }
  822.         // Empty cache
  823.         $this->objPrice             false;
  824.         $this->arrAttributes        null;
  825.         $this->arrVariantAttributes null;
  826.         $this->arrVariantIds        null;
  827.         $this->arrRelated           = [];
  828.         // Must initialize product type to have attributes etc.
  829.         if (($this->arrRelated['type'] = ProductType::findByPk($arrData['type'])) === null) {
  830.             throw new \UnderflowException('Product type for product ID ' $arrData['id'] . ' not found');
  831.         }
  832.         $this->strFormId 'iso_product_' $arrData['id'];
  833.         // Remove attributes not in this product type
  834.         foreach ($arrData as $attribute => $value) {
  835.             if ((
  836.                     !\in_array($attribute$this->getType()->getAttributes(), true)
  837.                     && !\in_array($attribute$this->getType()->getVariantAttributes(), true)
  838.                     && isset($GLOBALS['TL_DCA']['tl_iso_product']['fields'][$attribute]['attributes']['legend'])
  839.                     && $GLOBALS['TL_DCA']['tl_iso_product']['fields'][$attribute]['attributes']['legend'] != ''
  840.                 )
  841.                 || \in_array($attributeAttribute::getVariantOptionFields(), true)
  842.             ) {
  843.                 unset($arrData[$attribute]);
  844.             }
  845.         }
  846.         return parent::setRow($arrData);
  847.     }
  848.     /**
  849.      * Prevent reload of the database record
  850.      * We would need to fetch parent data etc. again, pretty useless
  851.      *
  852.      * @param array $arrData
  853.      *
  854.      * @return $this
  855.      */
  856.     public function mergeRow(array $arrData)
  857.     {
  858.         // do not allow to reset the whole record
  859.         if (isset($arrData['id'])) {
  860.             return $this;
  861.         }
  862.         return parent::mergeRow($arrData);
  863.     }
  864.     /**
  865.      * In a variant, only variant and non-inherited fields can be marked as modified
  866.      *
  867.      * @param string $strKey
  868.      */
  869.     public function markModified($strKey)
  870.     {
  871.         if ($this->isVariant()) {
  872.             $arrAttributes array_diff(
  873.                 $this->getType()->getVariantAttributes(),
  874.                 $this->getInheritedFields(),
  875.                 Attribute::getCustomerDefinedFields()
  876.             );
  877.         } else {
  878.             $arrAttributes array_diff($this->getType()->getAttributes(), Attribute::getCustomerDefinedFields());
  879.         }
  880.         if (!\in_array($strKey$arrAttributestrue)
  881.             && '' !== (string) ($GLOBALS['TL_DCA'][static::$strTable]['fields'][$strKey]['attributes']['legend'] ?? '')
  882.         ) {
  883.             return;
  884.         }
  885.         parent::markModified($strKey);
  886.     }
  887.     /**
  888.      * Generate url
  889.      *
  890.      * @return string
  891.      *
  892.      * @throws \InvalidArgumentException
  893.      */
  894.     public function generateUrl(PageModel $objJumpTo null/*, bool $absolute = false*/)
  895.     {
  896.         $absolute false;
  897.         if (func_num_args() >= 2) {
  898.             $absolute = (bool) func_get_arg(1);
  899.         }
  900.         if (null === $objJumpTo) {
  901.             global $objPage;
  902.             global $objIsotopeListPage;
  903.             $objJumpTo $objIsotopeListPage ?: $objPage;
  904.         }
  905.         if (!$objJumpTo instanceof PageModel) {
  906.             return '';
  907.         }
  908.         $strParams '';
  909.         if ($objJumpTo->iso_readerMode !== 'none') {
  910.             $strParams '/'.($this->arrData['alias'] ?: $this->getProductId());
  911.             if (!$GLOBALS['TL_CONFIG']['useAutoItem'] || !\in_array('product'$GLOBALS['TL_AUTO_ITEM'], true)) {
  912.                 $strParams '/product'.$strParams;
  913.             }
  914.         }
  915.         $url $absolute $objJumpTo->getAbsoluteUrl($strParams) : $objJumpTo->getFrontendUrl($strParams);
  916.         return Url::addQueryString(http_build_query($this->getOptions()), $url);
  917.     }
  918.     /**
  919.      * Return array of inherited attributes
  920.      *
  921.      * @return array
  922.      */
  923.     protected function getInheritedFields()
  924.     {
  925.         // Not a variant, no inherited fields
  926.         if (!$this->isVariant()) {
  927.             return array();
  928.         }
  929.         return array_merge(StringUtil::deserialize($this->arrData['inherit'], true), Attribute::getInheritFields());
  930.     }
  931.     private function getCollectionItem()
  932.     {
  933.         if (Input::get('collection_item') > 0) {
  934.             $item ProductCollectionItem::findByPk(Input::get('collection_item'));
  935.             if (null !== $item
  936.                 && $item->hasProduct()
  937.                 && $item->getProduct()->getProductId() == $this->getProductId()
  938.             ) {
  939.                 return $item;
  940.             }
  941.         }
  942.         return null;
  943.     }
  944.     /**
  945.      * Load default values from URL
  946.      */
  947.     private function getOptionsDefaults()
  948.     {
  949.         if (\is_array($this->arrDefaults)) {
  950.             return $this->arrDefaults;
  951.         }
  952.         $this->arrDefaults = array();
  953.         if (($item $this->getCollectionItem()) !== null) {
  954.             $this->arrDefaults $item->getOptions();
  955.         } else {
  956.             foreach ($_GET as $k => $v) {
  957.                 $this->arrDefaults[$k] = Input::get($k);
  958.             }
  959.         }
  960.         return $this->arrDefaults;
  961.     }
  962. }