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

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