vendor/isotope/isotope-core/system/modules/isotope/library/Isotope/Model/Attribute.php line 446

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;
  11. use Contao\Controller;
  12. use Contao\Database;
  13. use Contao\FilesModel;
  14. use Contao\Model;
  15. use Contao\StringUtil;
  16. use Haste\Util\Format;
  17. use Isotope\Interfaces\IsotopeAttribute;
  18. use Isotope\Interfaces\IsotopeAttributeWithOptions;
  19. use Isotope\Interfaces\IsotopeProduct;
  20. use Isotope\Isotope;
  21. use Isotope\Translation;
  22. /**
  23.  * Attribute represents a product attribute in Isotope eCommerce
  24.  *
  25.  * @property int           $id
  26.  * @property int           $tstamp
  27.  * @property string        $name
  28.  * @property string        $field_name
  29.  * @property string        $type
  30.  * @property string        $legend
  31.  * @property string        $description
  32.  * @property string        $optionsSource
  33.  * @property string|array  $options
  34.  * @property string        $foreignKey
  35.  * @property bool          $includeBlankOption
  36.  * @property string        $blankOptionLabel
  37.  * @property bool          $variant_option
  38.  * @property bool          $customer_defined
  39.  * @property bool          $be_search
  40.  * @property bool          $be_filter
  41.  * @property bool          $mandatory
  42.  * @property bool          $fe_filter
  43.  * @property bool          $fe_search
  44.  * @property bool          $fe_sorting
  45.  * @property bool          $multiple
  46.  * @property int           $size
  47.  * @property string        $extensions
  48.  * @property string        $rte
  49.  * @property bool          $multilingual
  50.  * @property bool          $rgxp
  51.  * @property bool          $placeholder
  52.  * @property int           $minlength
  53.  * @property int           $maxlength
  54.  * @property string        $conditionField
  55.  * @property string        $fieldType
  56.  * @property bool          $files
  57.  * @property bool          $filesOnly
  58.  * @property string        $sortBy
  59.  * @property string        $path
  60.  * @property bool          $storeFile
  61.  * @property string        $uploadFolder
  62.  * @property bool          $useHomeDir
  63.  * @property bool          $doNotOverwrite
  64.  * @property bool          $checkoutRelocate
  65.  * @property string        $checkoutTargetFolder
  66.  * @property string        $checkoutTargetFile
  67.  * @property bool          $datepicker
  68.  */
  69. abstract class Attribute extends TypeAgent implements IsotopeAttribute
  70. {
  71.     /**
  72.      * Table name
  73.      * @var string
  74.      */
  75.     protected static $strTable 'tl_iso_attribute';
  76.     /**
  77.      * Interface to validate attribute
  78.      * @var string
  79.      */
  80.     protected static $strInterface '\Isotope\Interfaces\IsotopeAttribute';
  81.     /**
  82.      * List of types (classes) for this model
  83.      * @var array
  84.      */
  85.     protected static $arrModelTypes = array();
  86.     /**
  87.      * Holds a map for field name to ID
  88.      * @var array
  89.      */
  90.     protected static $arrFieldNameMap = array();
  91.     /**
  92.      * Options for variants cache
  93.      * @var array
  94.      */
  95.     private $arrOptionsForVariants = array();
  96.     /**
  97.      * Return true if attribute is a variant option
  98.      *
  99.      * @return bool
  100.      *
  101.      * @deprecated will only be available when IsotopeAttributeForVariants interface is implemented
  102.      */
  103.     public function isVariantOption()
  104.     {
  105.         return (bool) $this->variant_option;
  106.     }
  107.     /**
  108.      * @inheritdoc
  109.      */
  110.     public function getFieldName()
  111.     {
  112.         return $this->field_name;
  113.     }
  114.     /**
  115.      * @inheritdoc
  116.      */
  117.     public function isCustomerDefined()
  118.     {
  119.         /* @todo in 3.0: $this instanceof IsotopeAttributeForVariants */
  120.         if ($this->isVariantOption()) {
  121.             return false;
  122.         }
  123.         return (bool) $this->customer_defined;
  124.     }
  125.     /**
  126.      * @inheritdoc
  127.      */
  128.     public function getBackendWidget()
  129.     {
  130.         if (!isset($GLOBALS['BE_FFL'][$this->type])) {
  131.             throw new \LogicException('Backend widget for attribute type "' $this->type '" does not exist.');
  132.         }
  133.         return $GLOBALS['BE_FFL'][$this->type];
  134.     }
  135.     /**
  136.      * @inheritdoc
  137.      */
  138.     public function getFrontendWidget()
  139.     {
  140.         if (!isset($GLOBALS['TL_FFL'][$this->type])) {
  141.             throw new \LogicException('Frontend widget for attribute type "' $this->type '" does not exist.');
  142.         }
  143.         return $GLOBALS['TL_FFL'][$this->type];
  144.     }
  145.     /**
  146.      * @inheritdoc
  147.      */
  148.     public function loadFromDCA(array &$arrData$strName)
  149.     {
  150.         $arrField = &$arrData['fields'][$strName];
  151.         $this->arrData \is_array($arrField['attributes']) ? $arrField['attributes'] : array();
  152.         if (\is_array($arrField['eval'] ?? null)) {
  153.             $this->arrData array_merge($arrField['eval'], $this->arrData);
  154.         }
  155.         $this->field_name  $strName;
  156.         $this->type        array_search(\get_called_class(), static::getModelTypes(), true);
  157.         $this->name        \is_array($arrField['label'] ?? null) ? $arrField['label'][0] : ($arrField['label'] ?? $strName);
  158.         $this->description \is_array($arrField['label'] ?? null) ? $arrField['label'][1] : '';
  159.         $this->be_filter   = ($arrField['filter'] ?? false) ? '1' '';
  160.         $this->be_search   = ($arrField['search'] ?? false) ? '1' '';
  161.         $this->foreignKey  $arrField['foreignKey'] ?? null;
  162.         $this->optionsSource '';
  163.     }
  164.     /**
  165.      * @inheritdoc
  166.      */
  167.     public function saveToDCA(array &$arrData)
  168.     {
  169.         // Keep field settings made through DCA code
  170.         $arrField \is_array($arrData['fields'][$this->field_name] ?? null) ? $arrData['fields'][$this->field_name] : [];
  171.         $arrField['label']                          = Translation::get(array($this->name$this->description));
  172.         $arrField['exclude']                        = true;
  173.         $arrField['inputType']                      = '';
  174.         $arrField['attributes']                     = $this->row();
  175.         $arrField['attributes']['variant_option']   = $this->isVariantOption(); /* @todo in 3.0: $this instanceof IsotopeAttributeForVariants */
  176.         $arrField['attributes']['customer_defined'] = $this->isCustomerDefined();
  177.         $arrField['eval']                           = \is_array($arrField['eval'] ?? null) ? array_merge($arrField['eval'], $arrField['attributes']) : $arrField['attributes'];
  178.         if ('' !== (string) $this->placeholder) {
  179.             $arrField['eval']['placeholder'] = Translation::get($this->placeholder);
  180.         }
  181.         if (!$this->isCustomerDefined()) {
  182.             $arrField['inputType'] = (string) array_search($this->getBackendWidget(), $GLOBALS['BE_FFL'], true);
  183.         }
  184.         // Support numeric paths (fileTree)
  185.         unset($arrField['eval']['path']);
  186.         if ($this->path != '' && ($objFile FilesModel::findByPk($this->path)) !== null) {
  187.             $arrField['eval']['path'] = $objFile->path;
  188.         }
  189.         // Only enable RTE config in the TextArea attribute
  190.         unset($arrField['eval']['rte']);
  191.         if ($this->be_filter) {
  192.             $arrField['filter'] = true;
  193.         }
  194.         if ($this->be_search) {
  195.             $arrField['search'] = true;
  196.         }
  197.         // Variant selection is always mandatory
  198.         /* @todo in 3.0: $this instanceof IsotopeAttributeForVariants */
  199.         if ($this->isVariantOption()) {
  200.             $arrField['eval']['mandatory'] = true;
  201.         }
  202.         if ($this->blankOptionLabel != '') {
  203.             $arrField['eval']['blankOptionLabel'] = Translation::get($this->blankOptionLabel);
  204.         }
  205.         // Prepare options
  206.         if (IsotopeAttributeWithOptions::SOURCE_FOREIGNKEY === $this->optionsSource && !$this->isVariantOption()) {
  207.             $arrField['foreignKey'] = $this->parseForeignKey($this->foreignKey$GLOBALS['TL_LANGUAGE']);
  208.             unset($arrField['options'], $arrField['reference']);
  209.         } else {
  210.             $arrOptions null;
  211.             switch ($this->optionsSource) {
  212.                 case IsotopeAttributeWithOptions::SOURCE_ATTRIBUTE:
  213.                     $arrOptions StringUtil::deserialize($this->options);
  214.                     break;
  215.                 case IsotopeAttributeWithOptions::SOURCE_FOREIGNKEY:
  216.                     $foreignKey $this->parseForeignKey($this->foreignKey$GLOBALS['TL_LANGUAGE']);
  217.                     $arrKey     explode('.'$foreignKey2);
  218.                     if ('' !== (string) $arrKey[0] && '' !== $arrKey[1]) {
  219.                         $arrOptions Database::getInstance()
  220.                             ->execute("SELECT id AS value, {$arrKey[1]} AS label FROM {$arrKey[0]} ORDER BY label")
  221.                             ->fetchAllAssoc()
  222.                         ;
  223.                     }
  224.                     break;
  225.                 case IsotopeAttributeWithOptions::SOURCE_TABLE:
  226.                     $arrOptions = [];
  227.                     if ($this instanceof IsotopeAttributeWithOptions && null !== ($options AttributeOption::findByAttribute($this, ['order' => AttributeOption::getTable().'.label']))) {
  228.                         foreach ($options as $model) {
  229.                             $arrOptions[] = [
  230.                                 'value' => $model->getLanguageId(),
  231.                                 'label' => $model->label,
  232.                             ];
  233.                         }
  234.                     }
  235.                     break;
  236.                 case IsotopeAttributeWithOptions::SOURCE_PRODUCT:
  237.                     $arrOptions = [];
  238.                     if ($this instanceof IsotopeAttributeWithOptions && null !== ($options AttributeOption::findByProducts($this, ['order' => AttributeOption::getTable().'.label']))) {
  239.                         foreach ($options as $model) {
  240.                             $arrOptions[] = [
  241.                                 'value' => $model->getLanguageId(),
  242.                                 'label' => $model->label,
  243.                             ];
  244.                         }
  245.                     }
  246.                     break;
  247.                 default:
  248.                     if ($this instanceof IsotopeAttributeWithOptions) {
  249.                         unset($arrField['options'], $arrField['reference']);
  250.                     }
  251.             }
  252.             if (!empty($arrOptions) && \is_array($arrOptions)) {
  253.                 $arrField['default'] = array();
  254.                 $arrField['options'] = array();
  255.                 $arrField['eval']['isAssociative'] = true;
  256.                 unset($arrField['reference']);
  257.                 $strGroup '';
  258.                 foreach ($arrOptions as $option) {
  259.                     if ($option['group'] ?? false) {
  260.                         $strGroup Translation::get($option['label']);
  261.                         continue;
  262.                     }
  263.                     if ($strGroup != '') {
  264.                         $arrField['options'][$strGroup][$option['value']] = Translation::get($option['label']);
  265.                     } else {
  266.                         $arrField['options'][$option['value']] = Translation::get($option['label']);
  267.                     }
  268.                     if ($option['default'] ?? false) {
  269.                         $arrField['default'][] = $option['value'];
  270.                     }
  271.                 }
  272.                 if (empty($arrField['default']) || $this->isCustomerDefined()) {
  273.                     unset($arrField['default']);
  274.                 } else if (!$arrField['eval']['multiple']) {
  275.                     $arrField['default'] = reset($arrField['default']);
  276.                 }
  277.             }
  278.         }
  279.         unset($arrField['eval']['foreignKey'], $arrField['eval']['options']);
  280.         // Add field to the current DCA table
  281.         $arrData['fields'][$this->field_name] = $arrField;
  282.     }
  283.     /**
  284.      * Get field options
  285.      * @return  array
  286.      * @deprecated  will only be available when IsotopeAttributeWithOptions interface is implemented
  287.      */
  288.     public function getOptions()
  289.     {
  290.         $arrOptions StringUtil::deserialize($this->options);
  291.         if (!\is_array($arrOptions)) {
  292.             return array();
  293.         }
  294.         return $arrOptions;
  295.     }
  296.     /**
  297.      * Get available variant options for a product
  298.      *
  299.      * @param int[] $arrIds
  300.      * @param array $arrOptions
  301.      *
  302.      * @return array
  303.      * @deprecated will only be available when IsotopeAttributeForVariants interface is implemented
  304.      */
  305.     public function getOptionsForVariants(array $arrIds, array $arrOptions = array())
  306.     {
  307.         if (=== \count($arrIds)) {
  308.             return [];
  309.         }
  310.         sort($arrIds);
  311.         ksort($arrOptions);
  312.         $strKey md5(implode('-'$arrIds) . '_' json_encode($arrOptions));
  313.         if (!isset($this->arrOptionsForVariants[$strKey])) {
  314.             $strWhere '';
  315.             foreach ($arrOptions as $field => $value) {
  316.                 $strWhere .= " AND $field=?";
  317.             }
  318.             $this->arrOptionsForVariants[$strKey] = Database::getInstance()->prepare('
  319.                 SELECT DISTINCT ' $this->field_name ' FROM tl_iso_product WHERE id IN (' implode(','$arrIds) . ')
  320.                 ' $strWhere
  321.             )->execute($arrOptions)->fetchEach($this->field_name);
  322.         }
  323.         return $this->arrOptionsForVariants[$strKey];
  324.     }
  325.     /**
  326.      * @inheritdoc
  327.      */
  328.     public function getValue(IsotopeProduct $product)
  329.     {
  330.         return $product->{$this->field_name};
  331.     }
  332.     /**
  333.      * @inheritdoc
  334.      */
  335.     public function getLabel()
  336.     {
  337.         return Format::dcaLabel('tl_iso_product'$this->field_name);
  338.     }
  339.     /**
  340.      * Generate HTML markup of product data for this attribute
  341.      *
  342.      * @param IsotopeProduct $objProduct
  343.      * @param array          $arrOptions
  344.      *
  345.      * @return mixed
  346.      */
  347.     public function generate(IsotopeProduct $objProduct, array $arrOptions = array())
  348.     {
  349.         $varValue $this->getValue($objProduct);
  350.         $arrOptions['product'] = $objProduct;
  351.         if (!\is_array($varValue)) {
  352.             return $this->generateValue($varValue$arrOptions);
  353.         }
  354.         // Generate a HTML table for associative arrays
  355.         if (!array_is_assoc($varValue) && \is_array($varValue[0])) {
  356.             return $arrOptions['noHtml'] ? $varValue $this->generateTable($varValue$objProduct);
  357.         }
  358.         if ($arrOptions['noHtml']) {
  359.             $result = array();
  360.             foreach ($varValue as $v1) {
  361.                 $result[$v1] = $this->generateValue($v1$arrOptions);
  362.             }
  363.             return $result;
  364.         }
  365.         // Generate ul/li listing for simple arrays
  366.         foreach ($varValue as &$v2) {
  367.             $v2 $this->generateValue($v2$arrOptions);
  368.         }
  369.         return $this->generateList($varValue);
  370.     }
  371.     /**
  372.      * @param mixed $value
  373.      * @param array $options
  374.      *
  375.      * @return string
  376.      */
  377.     public function generateValue($value, array $options = [])
  378.     {
  379.         return Format::dcaValue('tl_iso_product'$this->field_name$value);
  380.     }
  381.     /**
  382.      * Returns the foreign key for a certain language with a fallback option
  383.      *
  384.      * @param string $strSettings
  385.      * @param bool   $strLanguage
  386.      *
  387.      * @return mixed
  388.      */
  389.     protected function parseForeignKey($strSettings$strLanguage false)
  390.     {
  391.         $strFallback null;
  392.         $arrLines    StringUtil::trimsplit('@\r\n|\n|\r@'$strSettings);
  393.         // Return false if there are no lines
  394.         if ($strSettings == '' || !\is_array($arrLines) || empty($arrLines)) {
  395.             return null;
  396.         }
  397.         // Loop over the lines
  398.         foreach ($arrLines as $strLine) {
  399.             // Ignore empty lines and comments
  400.             if ($strLine == '' || strpos($strLine'#') === 0) {
  401.                 continue;
  402.             }
  403.             // Check for a language1
  404.             if (preg_match('/^([a-z]{2}(-[A-Z]{2})?)=(.+)$/'$strLine$matches)) {
  405.                 $foreignKey $matches[3];
  406.                 if ($matches[1] === $strLanguage) {
  407.                     return $foreignKey;
  408.                 }
  409.                 if (null === $strFallback) {
  410.                     $strFallback $foreignKey;
  411.                 }
  412.             } elseif (null === $strFallback) {
  413.                 // The row without language is the fallback
  414.                 $strFallback $strLine;
  415.             }
  416.         }
  417.         return $strFallback;
  418.     }
  419.     /**
  420.      * Generate HTML table for associative array values
  421.      *
  422.      * @param array          $arrValues
  423.      * @param IsotopeProduct $objProduct
  424.      *
  425.      * @return string
  426.      */
  427.     protected function generateTable(array $arrValuesIsotopeProduct $objProduct)
  428.     {
  429.         $arrFormat $GLOBALS['TL_DCA']['tl_iso_product']['fields'][$this->field_name]['tableformat'];
  430.         $last \count($arrValues[0]) - 1;
  431.         $strBuffer '
  432. <table class="' $this->field_name '">
  433.   <thead>
  434.     <tr>';
  435.         foreach (array_keys($arrValues[0]) as $i => $name) {
  436.             if ($arrFormat[$name]['doNotShow']) {
  437.                 continue;
  438.             }
  439.             $label $arrFormat[$name]['label'] ?: $name;
  440.             $strBuffer .= '
  441.       <th class="head_' $i . ($i == ' head_first' '') . ($i == $last ' head_last' '') . (!is_numeric($name) ? ' ' StringUtil::standardize($name) : '') . '">' $label '</th>';
  442.         }
  443.         $strBuffer .= '
  444.     </tr>
  445.   </thead>
  446.   <tbody>';
  447.         foreach ($arrValues as $r => $row) {
  448.             $strBuffer .= '
  449.     <tr class="row_' $r . ($r == ' row_first' '') . ($r == $last ' row_last' '') . ' ' . ($r 'odd' 'even') . '">';
  450.             $c = -1;
  451.             foreach ($row as $name => $value) {
  452.                 if ($arrFormat[$name]['doNotShow']) {
  453.                     continue;
  454.                 }
  455.                 if ('price' === $arrFormat[$name]['rgxp']) {
  456.                     $intTax = (int) $row['tax_class'];
  457.                     $value Isotope::formatPriceWithCurrency(Isotope::calculatePrice($value$objProduct$this->field_name$intTax));
  458.                 } else {
  459.                     $value $arrFormat[$name]['format'] ? sprintf($arrFormat[$name]['format'], $value) : $value;
  460.                 }
  461.                 $strBuffer .= '
  462.       <td class="col_' . ++$c . ($c == ' col_first' '') . ($c == $last ' col_last' '') . ' ' StringUtil::standardize($name) . '">' $value '</td>';
  463.             }
  464.             $strBuffer .= '
  465.     </tr>';
  466.         }
  467.         $strBuffer .= '
  468.   </tbody>
  469. </table>';
  470.         return $strBuffer;
  471.     }
  472.     /**
  473.      * Generate HTML list for array values
  474.      *
  475.      * @param array $arrValues
  476.      *
  477.      * @return string
  478.      */
  479.     protected function generateList(array $arrValues)
  480.     {
  481.         $strBuffer "\n<ul>";
  482.         $current 0;
  483.         $last    \count($arrValues) - 1;
  484.         foreach ($arrValues as $value) {
  485.             $class trim(($current == 'first' '') . ($current == $last ' last' ''));
  486.             $strBuffer .= "\n<li" . ($class != '' ' class="' $class '"' '') . '>' $value '</li>';
  487.             ++$current;
  488.         }
  489.         $strBuffer .= "\n</ul>";
  490.         return $strBuffer;
  491.     }
  492.     /**
  493.      * Get list of system columns
  494.      *
  495.      * @return array
  496.      */
  497.     public static function getSystemColumnsFields()
  498.     {
  499.         static $arrFields;
  500.         if (null === $arrFields) {
  501.             Controller::loadDataContainer('tl_iso_product');
  502.             $arrFields = array();
  503.             $arrDCA    = &$GLOBALS['TL_DCA']['tl_iso_product']['fields'];
  504.             foreach ($arrDCA as $field => $config) {
  505.                 if ($config['attributes']['systemColumn']) {
  506.                     $arrFields[] = $field;
  507.                 }
  508.             }
  509.         }
  510.         return $arrFields;
  511.     }
  512.     /**
  513.      * Return list of variant option fields
  514.      *
  515.      * @return array
  516.      */
  517.     public static function getVariantOptionFields()
  518.     {
  519.         static $arrFields;
  520.         if (null === $arrFields) {
  521.             Controller::loadDataContainer('tl_iso_product');
  522.             $arrFields = array();
  523.             $arrAttributes = &$GLOBALS['TL_DCA']['tl_iso_product']['attributes'];
  524.             /** @var Attribute $objAttribute */
  525.             foreach ($arrAttributes as $field => $objAttribute) {
  526.                 /* @todo in 3.0: $objAttribute instanceof IsotopeAttributeForVariants */
  527.                 if ($objAttribute->isVariantOption()) {
  528.                     $arrFields[] = $field;
  529.                 }
  530.             }
  531.         }
  532.         return $arrFields;
  533.     }
  534.     /**
  535.      * Return list of fields that are customer defined
  536.      *
  537.      * @return array
  538.      */
  539.     public static function getCustomerDefinedFields()
  540.     {
  541.         static $arrFields;
  542.         if (null === $arrFields) {
  543.             Controller::loadDataContainer('tl_iso_product');
  544.             $arrFields = array();
  545.             $arrDCA    = &$GLOBALS['TL_DCA']['tl_iso_product']['fields'];
  546.             foreach ($arrDCA as $field => $config) {
  547.                 if ($config['attributes']['customer_defined'] ?? false) {
  548.                     $arrFields[] = $field;
  549.                 }
  550.             }
  551.         }
  552.         return $arrFields;
  553.     }
  554.     /**
  555.      * Return array of attributes that have price relevant information
  556.      *
  557.      * @return array
  558.      */
  559.     public static function getPricedFields()
  560.     {
  561.         static $arrFields;
  562.         if (null === $arrFields) {
  563.             $arrFields Database::getInstance()->query("
  564.                 SELECT a.field_name
  565.                 FROM tl_iso_attribute a
  566.                 JOIN tl_iso_attribute_option o ON a.id=o.pid
  567.                 WHERE
  568.                   a.optionsSource='table'
  569.                   AND o.ptable='tl_iso_attribute'
  570.                   AND o.published='1'
  571.                   AND o.price!=''
  572.                 UNION
  573.                 SELECT a.field_name
  574.                 FROM tl_iso_attribute a
  575.                 JOIN tl_iso_attribute_option o ON a.field_name=o.field_name
  576.                 WHERE
  577.                   a.optionsSource='product'
  578.                   AND o.ptable='tl_iso_product'
  579.                   AND o.published='1'
  580.                   AND o.price!=''
  581.             ")->fetchEach('field_name');
  582.         }
  583.         return $arrFields;
  584.     }
  585.     /**
  586.      * Return list of fields that are multilingual
  587.      *
  588.      * @return array
  589.      */
  590.     public static function getMultilingualFields()
  591.     {
  592.         static $arrFields;
  593.         if (null === $arrFields) {
  594.             Controller::loadDataContainer('tl_iso_product');
  595.             $arrFields = array();
  596.             $arrDCA    = &$GLOBALS['TL_DCA']['tl_iso_product']['fields'];
  597.             foreach ($arrDCA as $field => $config) {
  598.                 if ($config['attributes']['multilingual'] ?? null) {
  599.                     $arrFields[] = $field;
  600.                 }
  601.             }
  602.         }
  603.         return $arrFields;
  604.     }
  605.     /**
  606.      * Return list of fields that have fetch_fallback set
  607.      *
  608.      * @return array
  609.      */
  610.     public static function getFetchFallbackFields()
  611.     {
  612.         static $arrFields;
  613.         if (null === $arrFields) {
  614.             Controller::loadDataContainer('tl_iso_product');
  615.             $arrFields = array();
  616.             $arrDCA    = &$GLOBALS['TL_DCA']['tl_iso_product']['fields'];
  617.             foreach ($arrDCA as $field => $config) {
  618.                 if ($config['attributes']['fetch_fallback'] ?? null) {
  619.                     $arrFields[] = $field;
  620.                 }
  621.             }
  622.         }
  623.         return $arrFields;
  624.     }
  625.     /**
  626.      * Return list of dynamic fields
  627.      * Dynamic fields cannot be filtered on database level (e.g. product price)
  628.      *
  629.      * @return array
  630.      */
  631.     public static function getDynamicAttributeFields()
  632.     {
  633.         static $arrFields;
  634.         if (null === $arrFields) {
  635.             Controller::loadDataContainer('tl_iso_product');
  636.             $arrFields = array();
  637.             $arrDCA    = &$GLOBALS['TL_DCA']['tl_iso_product']['fields'];
  638.             foreach ($arrDCA as $field => $config) {
  639.                 if (($config['attributes']['dynamic'] ?? null)
  640.                     || (($config['eval']['multiple'] ?? null) && !($config['eval']['csv'] ?? null))
  641.                 ) {
  642.                     $arrFields[] = $field;
  643.                 }
  644.             }
  645.         }
  646.         return $arrFields;
  647.     }
  648.     /**
  649.      * Return list of fixed fields
  650.      * Fixed fields cannot be disabled in product type config
  651.      *
  652.      * @param string|null $class
  653.      *
  654.      * @return array
  655.      */
  656.     public static function getFixedFields($class null)
  657.     {
  658.         Controller::loadDataContainer('tl_iso_product');
  659.         $arrFields = array();
  660.         $arrDCA    = &$GLOBALS['TL_DCA']['tl_iso_product']['fields'];
  661.         foreach ($arrDCA as $field => $config) {
  662.             $fixed = ($config['attributes']['fixed'] ?? null);
  663.             $isArray \is_array($fixed);
  664.             if ((!$isArray && $fixed) || (null !== $class && $isArray && \in_array($class$fixedtrue))) {
  665.                 $arrFields[] = $field;
  666.             }
  667.         }
  668.         return $arrFields;
  669.     }
  670.     /**
  671.      * Return list of variant fixed fields
  672.      * Fixed fields cannot be disabled in product type config
  673.      *
  674.      * @param string|null $class
  675.      *
  676.      * @return array
  677.      */
  678.     public static function getVariantFixedFields($class null)
  679.     {
  680.         Controller::loadDataContainer('tl_iso_product');
  681.         $arrFields = array();
  682.         $arrDCA = &$GLOBALS['TL_DCA']['tl_iso_product']['fields'];
  683.         foreach ($arrDCA as $field => $config) {
  684.             $fixed   $config['attributes']['variant_fixed'] ?? null;
  685.             $isArray \is_array($fixed);
  686.             if ((!$isArray && $fixed) || (null !== $class && $isArray && \in_array($class$fixedtrue))) {
  687.                 $arrFields[] = $field;
  688.             }
  689.         }
  690.         return $arrFields;
  691.     }
  692.     /**
  693.      * Return list of excluded fields
  694.      * Excluded fields cannot be enabled in product type config
  695.      *
  696.      * @return array
  697.      */
  698.     public static function getExcludedFields()
  699.     {
  700.         static $arrFields;
  701.         if (null === $arrFields) {
  702.             Controller::loadDataContainer('tl_iso_product');
  703.             $arrFields = array();
  704.             $arrDCA    = &$GLOBALS['TL_DCA']['tl_iso_product']['fields'];
  705.             foreach ($arrDCA as $field => $config) {
  706.                 if (($config['attributes']['excluded'] ?? false)) {
  707.                     $arrFields[] = $field;
  708.                 }
  709.             }
  710.         }
  711.         return $arrFields;
  712.     }
  713.     /**
  714.      * Return list of variant excluded fields
  715.      * Excluded fields cannot be disabled in product type config
  716.      *
  717.      * @return array
  718.      */
  719.     public static function getVariantExcludedFields()
  720.     {
  721.         static $arrFields;
  722.         if (null === $arrFields) {
  723.             Controller::loadDataContainer('tl_iso_product');
  724.             $arrFields = array();
  725.             $arrDCA    = &$GLOBALS['TL_DCA']['tl_iso_product']['fields'];
  726.             foreach ($arrDCA as $field => $config) {
  727.                 if ($config['attributes']['variant_excluded'] ?? false) {
  728.                     $arrFields[] = $field;
  729.                 }
  730.             }
  731.         }
  732.         return $arrFields;
  733.     }
  734.     /**
  735.      * Return list of singular fields
  736.      * Singular fields must not be enabled in product AND variant configuration.
  737.      *
  738.      * @return array
  739.      */
  740.     public static function getSingularFields()
  741.     {
  742.         static $arrFields;
  743.         if (null === $arrFields) {
  744.             Controller::loadDataContainer('tl_iso_product');
  745.             $arrFields = array();
  746.             $arrDCA    = &$GLOBALS['TL_DCA']['tl_iso_product']['fields'];
  747.             foreach ($arrDCA as $field => $config) {
  748.                 if ($config['attributes']['singular'] ?? false) {
  749.                     $arrFields[] = $field;
  750.                 }
  751.             }
  752.         }
  753.         return $arrFields;
  754.     }
  755.     /**
  756.      * Return list of fields that must be inherited by variants
  757.      *
  758.      * @return array
  759.      */
  760.     public static function getInheritFields()
  761.     {
  762.         static $arrFields;
  763.         if (null === $arrFields) {
  764.             Controller::loadDataContainer('tl_iso_product');
  765.             $arrFields = array();
  766.             $arrDCA    = &$GLOBALS['TL_DCA']['tl_iso_product']['fields'];
  767.             foreach ($arrDCA as $field => $config) {
  768.                 if ($config['attributes']['inherit'] ?? false) {
  769.                     $arrFields[] = $field;
  770.                 }
  771.             }
  772.         }
  773.         return $arrFields;
  774.     }
  775.     /**
  776.      * Find all valid attributes
  777.      *
  778.      * @param array $arrOptions An optional options array
  779.      *
  780.      * @return \Isotope\Model\Attribute[]|null The model collection or null if the result is empty
  781.      */
  782.     public static function findValid(array $arrOptions = array())
  783.     {
  784.         $t = static::getTable();
  785.         // Allow to set custom option conditions
  786.         if (!isset($arrOptions['column'])) {
  787.             $arrOptions['column'] = [];
  788.         } elseif (!\is_array($arrOptions['column'])) {
  789.             $arrOptions['column'] = [$t.'.'.$arrOptions['column'].'=?'];
  790.         }
  791.         $arrOptions['column'][] = "$t.type!=''";
  792.         $arrOptions['column'][] = "$t.field_name!=''";
  793.         return static::findAll($arrOptions);
  794.     }
  795.     /**
  796.      * Get an attribute by database field name
  797.      *
  798.      * @param string $strField
  799.      * @param array  $arrOptions
  800.      *
  801.      * @return Model|null
  802.      */
  803.     public static function findByFieldName($strField, array $arrOptions = [])
  804.     {
  805.         if (!isset(static::$arrFieldNameMap[$strField])) {
  806.             $objAttribute = static::findOneBy('field_name'$strField$arrOptions);
  807.             if (null === $objAttribute) {
  808.                 static::$arrFieldNameMap[$strField] = false;
  809.             } else {
  810.                 static::$arrFieldNameMap[$strField] = $objAttribute->id;
  811.             }
  812.             return $objAttribute;
  813.         }
  814.         if (static::$arrFieldNameMap[$strField] === false) {
  815.             return null;
  816.         }
  817.         return static::findByPk(static::$arrFieldNameMap[$strField], $arrOptions);
  818.     }
  819. }