[FEATURE] Reusable getCategoryFieldsForTable itemsProcFunc 68/24968/19
authorAlexander Stehlik <alexander.stehlik@gmail.com>
Sat, 3 Dec 2016 17:52:06 +0000 (18:52 +0100)
committerJan Helke <typo3@helke.de>
Sat, 3 Dec 2016 18:13:25 +0000 (19:13 +0100)
Allow the the method CategoryRegistry->getCategoryFieldItems()
to be used as itemsProcFunc for select fields in the TCA in
arbitary contexts by introducing a new categoryFieldsTable
configuration in the config section of a column in the TCA.

This configuration key can either consist of a single string containing
the name of the table or a configuration array to define additional
conditions that need to be true so that a configured table is used.

The condition matching is based on the displayCond functionality of
the TCA. To make use of the existing functionality the code for
matching the display conditions is extracted from the form data
provider to a new DisplayConditionEvaluator utility class.

Resolves: #53045
Releases: master
Change-Id: I128cbeb6747a8f83e68cdaaaafbc3ab5901353d4
Reviewed-on: https://review.typo3.org/24968
Tested-by: TYPO3com <no-reply@typo3.com>
Reviewed-by: Anja Leichsenring <aleichsenring@ab-softlab.de>
Tested-by: Anja Leichsenring <aleichsenring@ab-softlab.de>
Reviewed-by: Jan Helke <typo3@helke.de>
Tested-by: Jan Helke <typo3@helke.de>
typo3/sysext/backend/Classes/Form/FormDataProvider/EvaluateDisplayConditions.php
typo3/sysext/backend/Classes/Form/Utility/DisplayConditionEvaluator.php [new file with mode: 0644]
typo3/sysext/core/Classes/Category/CategoryRegistry.php
typo3/sysext/core/Documentation/Changelog/master/Breaking-53045-GetCategoryFieldsForTableMethodRemovedFromCategoryRegistry.rst [new file with mode: 0644]
typo3/sysext/core/Documentation/Changelog/master/Feature-53045-CategorizedFieldsForTableItemsProcFuncInCategoryRegistry.rst [new file with mode: 0644]
typo3/sysext/core/Tests/Unit/Category/CategoryRegistryTest.php
typo3/sysext/frontend/Configuration/TCA/tt_content.php

index 8c30e23..b75faa8 100644 (file)
@@ -15,6 +15,7 @@ namespace TYPO3\CMS\Backend\Form\FormDataProvider;
  */
 
 use TYPO3\CMS\Backend\Form\FormDataProviderInterface;
+use TYPO3\CMS\Backend\Form\Utility\DisplayConditionEvaluator;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 
 /**
@@ -52,7 +53,11 @@ class EvaluateDisplayConditions implements FormDataProviderInterface
                 continue;
             }
 
-            if (!$this->evaluateDisplayCondition($columnConfiguration['displayCond'], $result['databaseRow'])) {
+            $displayConditionValid = $this->getDisplayConditionEvaluator()->evaluateDisplayCondition(
+                $columnConfiguration['displayCond'],
+                $result['databaseRow']
+            );
+            if (!$displayConditionValid) {
                 unset($result['processedTca']['columns'][$columnName]);
             }
         }
@@ -86,7 +91,12 @@ class EvaluateDisplayConditions implements FormDataProviderInterface
                 if (!isset($sheetConfiguration['ROOT']['displayCond'])) {
                     continue;
                 }
-                if (!$this->evaluateDisplayCondition($sheetConfiguration['ROOT']['displayCond'], $flexFormRowData, true)) {
+                $displayConditionValid = $this->getDisplayConditionEvaluator()->evaluateDisplayCondition(
+                    $sheetConfiguration['ROOT']['displayCond'],
+                    $flexFormRowData,
+                    true
+                );
+                if (!$displayConditionValid) {
                     unset($result['processedTca']['columns'][$columnName]['config']['ds']['sheets'][$sheetName]);
                 }
             }
@@ -142,7 +152,7 @@ class EvaluateDisplayConditions implements FormDataProviderInterface
             if ($key === 'el' && is_array($value)) {
                 $newSubStructure = [];
                 foreach ($value as $subKey => $subValue) {
-                    if (!isset($subValue['displayCond']) || $this->evaluateDisplayCondition($subValue['displayCond'], $flexFormRowData, true)) {
+                    if (!isset($subValue['displayCond']) || $this->getDisplayConditionEvaluator()->evaluateDisplayCondition($subValue['displayCond'], $flexFormRowData, true)) {
                         $newSubStructure[$subKey] = $subValue;
                     }
                 }
@@ -176,316 +186,12 @@ class EvaluateDisplayConditions implements FormDataProviderInterface
     }
 
     /**
-     * Evaluates the provided condition and returns TRUE if the form
-     * element should be displayed.
-     *
-     * The condition string is separated by colons and the first part
-     * indicates what type of evaluation should be performed.
-     *
-     * @param string $displayCondition
-     * @param array $record
-     * @param bool $flexformContext
-     * @param int $recursionLevel Internal level of recursion
-     * @return bool TRUE if condition evaluates successfully
-     */
-    protected function evaluateDisplayCondition($displayCondition, array $record = [], $flexformContext = false, $recursionLevel = 0)
-    {
-        if ($recursionLevel > 99) {
-            // This should not happen, treat as misconfiguration
-            return true;
-        }
-        if (!is_array($displayCondition)) {
-            // DisplayCondition is not an array - just get its value
-            $result = $this->evaluateSingleDisplayCondition($displayCondition, $record, $flexformContext);
-        } else {
-            // Multiple conditions given as array ('AND|OR' => condition array)
-            $conditionEvaluations = [
-                'AND' => [],
-                'OR' => [],
-            ];
-            foreach ($displayCondition as $logicalOperator => $groupedDisplayConditions) {
-                $logicalOperator = strtoupper($logicalOperator);
-                if (($logicalOperator !== 'AND' && $logicalOperator !== 'OR') || !is_array($groupedDisplayConditions)) {
-                    // Invalid line. Skip it.
-                    continue;
-                } else {
-                    foreach ($groupedDisplayConditions as $key => $singleDisplayCondition) {
-                        $key = strtoupper($key);
-                        if (($key === 'AND' || $key === 'OR') && is_array($singleDisplayCondition)) {
-                            // Recursion statement: condition is 'AND' or 'OR' and is pointing to an array (should be conditions again)
-                            $conditionEvaluations[$logicalOperator][] = $this->evaluateDisplayCondition(
-                                [$key => $singleDisplayCondition],
-                                $record,
-                                $flexformContext,
-                                $recursionLevel + 1
-                            );
-                        } else {
-                            // Condition statement: collect evaluation of this single condition.
-                            $conditionEvaluations[$logicalOperator][] = $this->evaluateSingleDisplayCondition(
-                                $singleDisplayCondition,
-                                $record,
-                                $flexformContext
-                            );
-                        }
-                    }
-                }
-            }
-            if (!empty($conditionEvaluations['OR']) && in_array(true, $conditionEvaluations['OR'], true)) {
-                // There are OR conditions and at least one of them is TRUE
-                $result = true;
-            } elseif (!empty($conditionEvaluations['AND']) && !in_array(false, $conditionEvaluations['AND'], true)) {
-                // There are AND conditions and none of them is FALSE
-                $result = true;
-            } elseif (!empty($conditionEvaluations['OR']) || !empty($conditionEvaluations['AND'])) {
-                // There are some conditions. But no OR was TRUE and at least one AND was FALSE
-                $result = false;
-            } else {
-                // There are no proper conditions - misconfiguration. Return TRUE.
-                $result = true;
-            }
-        }
-        return $result;
-    }
-
-    /**
-     * Evaluates the provided condition and returns TRUE if the form
-     * element should be displayed.
-     *
-     * The condition string is separated by colons and the first part
-     * indicates what type of evaluation should be performed.
-     *
-     * @param string $displayCondition
-     * @param array $record
-     * @param bool $flexformContext
-     * @return bool
-     * @see evaluateDisplayCondition()
-     */
-    protected function evaluateSingleDisplayCondition($displayCondition, array $record = [], $flexformContext = false)
-    {
-        $result = false;
-        list($matchType, $condition) = explode(':', $displayCondition, 2);
-        switch ($matchType) {
-            case 'FIELD':
-                $result = $this->matchFieldCondition($condition, $record, $flexformContext);
-                break;
-            case 'HIDE_FOR_NON_ADMINS':
-                $result = $this->matchHideForNonAdminsCondition();
-                break;
-            case 'REC':
-                $result = $this->matchRecordCondition($condition, $record);
-                break;
-            case 'VERSION':
-                $result = $this->matchVersionCondition($condition, $record);
-                break;
-            case 'USER':
-                $result = $this->matchUserCondition($condition, $record);
-                break;
-        }
-        return $result;
-    }
-
-    /**
-     * Evaluates conditions concerning a field of the current record.
-     * Requires a record set via ->setRecord()
-     *
-     * Example:
-     * "FIELD:sys_language_uid:>:0" => TRUE, if the field 'sys_language_uid' is greater than 0
-     *
-     * @param string $condition
-     * @param array $record
-     * @param bool $flexformContext
-     * @return bool
-     */
-    protected function matchFieldCondition($condition, $record, $flexformContext = false)
-    {
-        list($fieldName, $operator, $operand) = explode(':', $condition, 3);
-        if ($flexformContext) {
-            if (strpos($fieldName, 'parentRec.') !== false) {
-                $fieldNameParts = explode('.', $fieldName, 2);
-                $fieldValue = $record['parentRec'][$fieldNameParts[1]];
-            } else {
-                $fieldValue = $record[$fieldName]['vDEF'];
-            }
-        } else {
-            $fieldValue = $record[$fieldName];
-        }
-        $result = false;
-        switch ($operator) {
-            case 'REQ':
-                if (is_array($fieldValue) && count($fieldValue) <= 1) {
-                    $fieldValue = array_shift($fieldValue);
-                }
-                if (strtoupper($operand) === 'TRUE') {
-                    $result = (bool)$fieldValue;
-                } else {
-                    $result = !$fieldValue;
-                }
-                break;
-            case '>':
-                if (is_array($fieldValue) && count($fieldValue) <= 1) {
-                    $fieldValue = array_shift($fieldValue);
-                }
-                $result = $fieldValue > $operand;
-                break;
-            case '<':
-                if (is_array($fieldValue) && count($fieldValue) <= 1) {
-                    $fieldValue = array_shift($fieldValue);
-                }
-                $result = $fieldValue < $operand;
-                break;
-            case '>=':
-                if (is_array($fieldValue) && count($fieldValue) <= 1) {
-                    $fieldValue = array_shift($fieldValue);
-                }
-                $result = $fieldValue >= $operand;
-                break;
-            case '<=':
-                if (is_array($fieldValue) && count($fieldValue) <= 1) {
-                    $fieldValue = array_shift($fieldValue);
-                }
-                $result = $fieldValue <= $operand;
-                break;
-            case '-':
-            case '!-':
-                if (is_array($fieldValue) && count($fieldValue) <= 1) {
-                    $fieldValue = array_shift($fieldValue);
-                }
-                list($minimum, $maximum) = explode('-', $operand);
-                $result = $fieldValue >= $minimum && $fieldValue <= $maximum;
-                if ($operator[0] === '!') {
-                    $result = !$result;
-                }
-                break;
-            case '=':
-            case '!=':
-                if (is_array($fieldValue) && count($fieldValue) <= 1) {
-                    $fieldValue = array_shift($fieldValue);
-                }
-                $result = $fieldValue == $operand;
-                if ($operator[0] === '!') {
-                    $result = !$result;
-                }
-                break;
-            case 'IN':
-            case '!IN':
-                if (is_array($fieldValue)) {
-                    $result = count(array_intersect($fieldValue, explode(',', $operand))) > 0;
-                } else {
-                    $result = GeneralUtility::inList($operand, $fieldValue);
-                }
-                if ($operator[0] === '!') {
-                    $result = !$result;
-                }
-                break;
-            case 'BIT':
-            case '!BIT':
-                $result = (bool)((int)$fieldValue & $operand);
-                if ($operator[0] === '!') {
-                    $result = !$result;
-                }
-                break;
-        }
-        return $result;
-    }
-
-    /**
-     * Evaluates TRUE if current backend user is an admin.
-     *
-     * @return bool
-     */
-    protected function matchHideForNonAdminsCondition()
-    {
-        return (bool)$this->getBackendUser()->isAdmin();
-    }
-
-    /**
-     * Evaluates conditions concerning the status of the current record.
-     * Requires a record set via ->setRecord()
-     *
-     * Example:
-     * "REC:NEW:FALSE" => TRUE, if the record is already persisted (has a uid > 0)
-     *
-     * @param string $condition
-     * @param array $record
-     * @return bool
-     */
-    protected function matchRecordCondition($condition, $record)
-    {
-        $result = false;
-        list($operator, $operand) = explode(':', $condition, 2);
-        if ($operator === 'NEW') {
-            if (strtoupper($operand) === 'TRUE') {
-                $result = !((int)$record['uid'] > 0);
-            } elseif (strtoupper($operand) === 'FALSE') {
-                $result = ((int)$record['uid'] > 0);
-            }
-        }
-        return $result;
-    }
-
-    /**
-     * Evaluates whether the current record is versioned.
-     * Requires a record set via ->setRecord()
-     *
-     * @param string $condition
-     * @param array $record
-     * @return bool
-     */
-    protected function matchVersionCondition($condition, $record)
-    {
-        $result = false;
-        list($operator, $operand) = explode(':', $condition, 2);
-        if ($operator === 'IS') {
-            $isNewRecord = !((int)$record['uid'] > 0);
-            // Detection of version can be done be detecting the workspace of the user
-            $isUserInWorkspace = $this->getBackendUser()->workspace > 0;
-            if ((int)$record['pid'] === -1 || (int)$record['_ORIG_pid'] === -1) {
-                $isRecordDetectedAsVersion = true;
-            } else {
-                $isRecordDetectedAsVersion = false;
-            }
-            // New records in a workspace are not handled as a version record
-            // if it's no new version, we detect versions like this:
-            // -- if user is in workspace: always TRUE
-            // -- if editor is in live ws: only TRUE if pid == -1
-            $isVersion = ($isUserInWorkspace || $isRecordDetectedAsVersion) && !$isNewRecord;
-            if (strtoupper($operand) === 'TRUE') {
-                $result = $isVersion;
-            } elseif (strtoupper($operand) === 'FALSE') {
-                $result = !$isVersion;
-            }
-        }
-        return $result;
-    }
-
-    /**
-     * Evaluates via the referenced user-defined method
-     *
-     * @param string $condition
-     * @param array $record
-     * @return bool
-     */
-    protected function matchUserCondition($condition, $record)
-    {
-        $conditionParameters = explode(':', $condition);
-        $userFunction = array_shift($conditionParameters);
-
-        $parameter = [
-            'record' => $record,
-            'flexformValueKey' => 'vDEF',
-            'conditionParameters' => $conditionParameters
-        ];
-
-        return (bool)GeneralUtility::callUserFunction($userFunction, $parameter, $this);
-    }
-
-    /**
-     * Get current backend user
+     * Returns the DisplayConditionEvaluator utility.
      *
-     * @return \TYPO3\CMS\Core\Authentication\BackendUserAuthentication
+     * @return DisplayConditionEvaluator
      */
-    protected function getBackendUser()
+    protected function getDisplayConditionEvaluator()
     {
-        return $GLOBALS['BE_USER'];
+        return GeneralUtility::makeInstance(DisplayConditionEvaluator::class);
     }
 }
diff --git a/typo3/sysext/backend/Classes/Form/Utility/DisplayConditionEvaluator.php b/typo3/sysext/backend/Classes/Form/Utility/DisplayConditionEvaluator.php
new file mode 100644 (file)
index 0000000..e340213
--- /dev/null
@@ -0,0 +1,338 @@
+<?php
+namespace TYPO3\CMS\Backend\Form\Utility;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\SingletonInterface;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+
+/**
+ * Utility class for evaluating TCA display conditions.
+ */
+class DisplayConditionEvaluator implements SingletonInterface
+{
+    /**
+     * Evaluates the provided condition and returns TRUE if the form
+     * element should be displayed.
+     *
+     * The condition string is separated by colons and the first part
+     * indicates what type of evaluation should be performed.
+     *
+     * @param string $displayCondition
+     * @param array $record
+     * @param bool $flexformContext
+     * @param int $recursionLevel Internal level of recursion
+     * @return bool TRUE if condition evaluates successfully
+     */
+    public function evaluateDisplayCondition($displayCondition, array $record = [], $flexformContext = false, $recursionLevel = 0)
+    {
+        if ($recursionLevel > 99) {
+            // This should not happen, treat as misconfiguration
+            return true;
+        }
+        if (!is_array($displayCondition)) {
+            // DisplayCondition is not an array - just get its value
+            $result = $this->evaluateSingleDisplayCondition($displayCondition, $record, $flexformContext);
+        } else {
+            // Multiple conditions given as array ('AND|OR' => condition array)
+            $conditionEvaluations = [
+                'AND' => [],
+                'OR' => [],
+            ];
+            foreach ($displayCondition as $logicalOperator => $groupedDisplayConditions) {
+                $logicalOperator = strtoupper($logicalOperator);
+                if (($logicalOperator !== 'AND' && $logicalOperator !== 'OR') || !is_array($groupedDisplayConditions)) {
+                    // Invalid line. Skip it.
+                    continue;
+                } else {
+                    foreach ($groupedDisplayConditions as $key => $singleDisplayCondition) {
+                        $key = strtoupper($key);
+                        if (($key === 'AND' || $key === 'OR') && is_array($singleDisplayCondition)) {
+                            // Recursion statement: condition is 'AND' or 'OR' and is pointing to an array (should be conditions again)
+                            $conditionEvaluations[$logicalOperator][] = $this->evaluateDisplayCondition(
+                                [$key => $singleDisplayCondition],
+                                $record,
+                                $flexformContext,
+                                $recursionLevel + 1
+                            );
+                        } else {
+                            // Condition statement: collect evaluation of this single condition.
+                            $conditionEvaluations[$logicalOperator][] = $this->evaluateSingleDisplayCondition(
+                                $singleDisplayCondition,
+                                $record,
+                                $flexformContext
+                            );
+                        }
+                    }
+                }
+            }
+            if (!empty($conditionEvaluations['OR']) && in_array(true, $conditionEvaluations['OR'], true)) {
+                // There are OR conditions and at least one of them is TRUE
+                $result = true;
+            } elseif (!empty($conditionEvaluations['AND']) && !in_array(false, $conditionEvaluations['AND'], true)) {
+                // There are AND conditions and none of them is FALSE
+                $result = true;
+            } elseif (!empty($conditionEvaluations['OR']) || !empty($conditionEvaluations['AND'])) {
+                // There are some conditions. But no OR was TRUE and at least one AND was FALSE
+                $result = false;
+            } else {
+                // There are no proper conditions - misconfiguration. Return TRUE.
+                $result = true;
+            }
+        }
+        return $result;
+    }
+
+    /**
+     * Evaluates the provided condition and returns TRUE if the form
+     * element should be displayed.
+     *
+     * The condition string is separated by colons and the first part
+     * indicates what type of evaluation should be performed.
+     *
+     * @param string $displayCondition
+     * @param array $record
+     * @param bool $flexformContext
+     * @return bool
+     * @see evaluateDisplayCondition()
+     */
+    protected function evaluateSingleDisplayCondition($displayCondition, array $record = [], $flexformContext = false)
+    {
+        $result = false;
+        list($matchType, $condition) = explode(':', $displayCondition, 2);
+        switch ($matchType) {
+            case 'FIELD':
+                $result = $this->matchFieldCondition($condition, $record, $flexformContext);
+                break;
+            case 'HIDE_FOR_NON_ADMINS':
+                $result = $this->matchHideForNonAdminsCondition();
+                break;
+            case 'REC':
+                $result = $this->matchRecordCondition($condition, $record);
+                break;
+            case 'VERSION':
+                $result = $this->matchVersionCondition($condition, $record);
+                break;
+            case 'USER':
+                $result = $this->matchUserCondition($condition, $record);
+                break;
+        }
+        return $result;
+    }
+
+    /**
+     * Evaluates conditions concerning a field of the current record.
+     * Requires a record set via ->setRecord()
+     *
+     * Example:
+     * "FIELD:sys_language_uid:>:0" => TRUE, if the field 'sys_language_uid' is greater than 0
+     *
+     * @param string $condition
+     * @param array $record
+     * @param bool $flexformContext
+     * @return bool
+     */
+    protected function matchFieldCondition($condition, $record, $flexformContext = false)
+    {
+        list($fieldName, $operator, $operand) = explode(':', $condition, 3);
+        if ($flexformContext) {
+            if (strpos($fieldName, 'parentRec.') !== false) {
+                $fieldNameParts = explode('.', $fieldName, 2);
+                $fieldValue = $record['parentRec'][$fieldNameParts[1]];
+            } else {
+                $fieldValue = $record[$fieldName]['vDEF'];
+            }
+        } else {
+            $fieldValue = $record[$fieldName];
+        }
+        $result = false;
+        switch ($operator) {
+            case 'REQ':
+                if (is_array($fieldValue) && count($fieldValue) <= 1) {
+                    $fieldValue = array_shift($fieldValue);
+                }
+                if (strtoupper($operand) === 'TRUE') {
+                    $result = (bool)$fieldValue;
+                } else {
+                    $result = !$fieldValue;
+                }
+                break;
+            case '>':
+                if (is_array($fieldValue) && count($fieldValue) <= 1) {
+                    $fieldValue = array_shift($fieldValue);
+                }
+                $result = $fieldValue > $operand;
+                break;
+            case '<':
+                if (is_array($fieldValue) && count($fieldValue) <= 1) {
+                    $fieldValue = array_shift($fieldValue);
+                }
+                $result = $fieldValue < $operand;
+                break;
+            case '>=':
+                if (is_array($fieldValue) && count($fieldValue) <= 1) {
+                    $fieldValue = array_shift($fieldValue);
+                }
+                $result = $fieldValue >= $operand;
+                break;
+            case '<=':
+                if (is_array($fieldValue) && count($fieldValue) <= 1) {
+                    $fieldValue = array_shift($fieldValue);
+                }
+                $result = $fieldValue <= $operand;
+                break;
+            case '-':
+            case '!-':
+                if (is_array($fieldValue) && count($fieldValue) <= 1) {
+                    $fieldValue = array_shift($fieldValue);
+                }
+                list($minimum, $maximum) = explode('-', $operand);
+                $result = $fieldValue >= $minimum && $fieldValue <= $maximum;
+                if ($operator[0] === '!') {
+                    $result = !$result;
+                }
+                break;
+            case '=':
+            case '!=':
+                if (is_array($fieldValue) && count($fieldValue) <= 1) {
+                    $fieldValue = array_shift($fieldValue);
+                }
+                $result = $fieldValue == $operand;
+                if ($operator[0] === '!') {
+                    $result = !$result;
+                }
+                break;
+            case 'IN':
+            case '!IN':
+                if (is_array($fieldValue)) {
+                    $result = count(array_intersect($fieldValue, explode(',', $operand))) > 0;
+                } else {
+                    $result = GeneralUtility::inList($operand, $fieldValue);
+                }
+                if ($operator[0] === '!') {
+                    $result = !$result;
+                }
+                break;
+            case 'BIT':
+            case '!BIT':
+                $result = (bool)((int)$fieldValue & $operand);
+                if ($operator[0] === '!') {
+                    $result = !$result;
+                }
+                break;
+        }
+        return $result;
+    }
+
+    /**
+     * Evaluates TRUE if current backend user is an admin.
+     *
+     * @return bool
+     */
+    protected function matchHideForNonAdminsCondition()
+    {
+        return (bool)$this->getBackendUser()->isAdmin();
+    }
+
+    /**
+     * Evaluates conditions concerning the status of the current record.
+     * Requires a record set via ->setRecord()
+     *
+     * Example:
+     * "REC:NEW:FALSE" => TRUE, if the record is already persisted (has a uid > 0)
+     *
+     * @param string $condition
+     * @param array $record
+     * @return bool
+     */
+    protected function matchRecordCondition($condition, $record)
+    {
+        $result = false;
+        list($operator, $operand) = explode(':', $condition, 2);
+        if ($operator === 'NEW') {
+            if (strtoupper($operand) === 'TRUE') {
+                $result = !((int)$record['uid'] > 0);
+            } elseif (strtoupper($operand) === 'FALSE') {
+                $result = ((int)$record['uid'] > 0);
+            }
+        }
+        return $result;
+    }
+
+    /**
+     * Evaluates whether the current record is versioned.
+     * Requires a record set via ->setRecord()
+     *
+     * @param string $condition
+     * @param array $record
+     * @return bool
+     */
+    protected function matchVersionCondition($condition, $record)
+    {
+        $result = false;
+        list($operator, $operand) = explode(':', $condition, 2);
+        if ($operator === 'IS') {
+            $isNewRecord = !((int)$record['uid'] > 0);
+            // Detection of version can be done be detecting the workspace of the user
+            $isUserInWorkspace = $this->getBackendUser()->workspace > 0;
+            if ((int)$record['pid'] === -1 || (int)$record['_ORIG_pid'] === -1) {
+                $isRecordDetectedAsVersion = true;
+            } else {
+                $isRecordDetectedAsVersion = false;
+            }
+            // New records in a workspace are not handled as a version record
+            // if it's no new version, we detect versions like this:
+            // -- if user is in workspace: always TRUE
+            // -- if editor is in live ws: only TRUE if pid == -1
+            $isVersion = ($isUserInWorkspace || $isRecordDetectedAsVersion) && !$isNewRecord;
+            if (strtoupper($operand) === 'TRUE') {
+                $result = $isVersion;
+            } elseif (strtoupper($operand) === 'FALSE') {
+                $result = !$isVersion;
+            }
+        }
+        return $result;
+    }
+
+    /**
+     * Evaluates via the referenced user-defined method
+     *
+     * @param string $condition
+     * @param array $record
+     * @return bool
+     */
+    protected function matchUserCondition($condition, $record)
+    {
+        $conditionParameters = explode(':', $condition);
+        $userFunction = array_shift($conditionParameters);
+
+        $parameter = [
+            'record' => $record,
+            'flexformValueKey' => 'vDEF',
+            'conditionParameters' => $conditionParameters
+        ];
+
+        return (bool)GeneralUtility::callUserFunction($userFunction, $parameter, $this);
+    }
+
+    /**
+     * Get current backend user
+     *
+     * @return \TYPO3\CMS\Core\Authentication\BackendUserAuthentication
+     */
+    protected function getBackendUser()
+    {
+        return $GLOBALS['BE_USER'];
+    }
+}
index 31a9a86..34bd3c0 100644 (file)
@@ -14,6 +14,7 @@ namespace TYPO3\CMS\Core\Category;
  * The TYPO3 project - inspiring people to share!
  */
 
+use TYPO3\CMS\Backend\Form\Utility\DisplayConditionEvaluator;
 use TYPO3\CMS\Core\SingletonInterface;
 use TYPO3\CMS\Core\Utility\ArrayUtility;
 use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
@@ -130,27 +131,15 @@ class CategoryRegistry implements SingletonInterface
     }
 
     /**
-     * Returns a list of category fields for a given table for populating selector "category_field"
-     * in tt_content table (called as itemsProcFunc).
+     * Returns a list of category fields for the table configured in the categoryFieldsTable setting.
+     * For use in an itemsProcFunc of a TCA select field.
      *
-     * @param array $configuration Current field configuration
-     * @throws \UnexpectedValueException
+     * @param array $configuration The TCA and row arrays passed to the itemsProcFunc.
      * @return void
      */
-    public function getCategoryFieldsForTable(array &$configuration)
+    public function getCategoryFieldItems(array &$configuration)
     {
-        $table = '';
-        $menuType = isset($configuration['row']['menu_type'][0]) ? $configuration['row']['menu_type'][0] : '';
-        // Define the table being looked up from the type of menu
-        if ($menuType === 'categorized_pages') {
-            $table = 'pages';
-        } elseif ($menuType === 'categorized_content') {
-            $table = 'tt_content';
-        }
-        // Return early if no table is defined
-        if (empty($table)) {
-            throw new \UnexpectedValueException('The given menu_type is not supported.', 1381823570);
-        }
+        $table = $this->getActiveCategoryFieldsTable($configuration);
         // Loop on all registries and find entries for the correct table
         foreach ($this->registry as $tableName => $fields) {
             if ($tableName === $table) {
@@ -163,6 +152,113 @@ class CategoryRegistry implements SingletonInterface
     }
 
     /**
+     * Tries to determine of which table the category fields should be collected.
+     * It looks in the categoryFieldsTable TCA entry in the config section of the current field.
+     *
+     * It is possible to pass a plain string with a table name or an array of table names
+     * that can be activated with an active condition. There must exactly be one active
+     * table at once. A possible array configuration might look like this:
+     *
+     * 'categoryFieldsTable' => array(
+     *     'categorized_pages' => array(
+     *         'table' => 'pages',
+     *         'activeCondition' => 'FIELD:menu_type:=:categorized_pages'
+     *     ),
+     *     'categorized_content' => array(
+     *         'table' => 'tt_content',
+     *         'activeCondition' => 'FIELD:menu_type:=:categorized_content'
+     *     )
+     * ),
+     *
+     * @param array $configuration The TCA and row arrays passed to the itemsProcFunc.
+     * @throws \RuntimeException In case of an invalid configuration.
+     * @return string
+     */
+    protected function getActiveCategoryFieldsTable(array $configuration)
+    {
+        $fieldAndTableInfo = sprintf(' (field: %s, table: %s)', $configuration['field'], $configuration['table']);
+
+        if (empty($configuration['config']['categoryFieldsTable'])) {
+            throw new \RuntimeException(
+                'The categoryFieldsTable setting is missing in the config section' . $fieldAndTableInfo,
+                1447273908
+            );
+        }
+
+        if (is_string($configuration['config']['categoryFieldsTable'])) {
+            return $configuration['config']['categoryFieldsTable'];
+        }
+
+        if (!is_array($configuration['config']['categoryFieldsTable'])) {
+            throw new \RuntimeException(
+                sprintf(
+                    'The categoryFieldsTable table setting must be a string or an array, %s given' . $fieldAndTableInfo,
+                    gettype($configuration['config']['categoryFieldsTable'])
+                ),
+                1447274126
+            );
+        }
+
+        $activeTable = null;
+
+        foreach ($configuration['config']['categoryFieldsTable'] as $configKey => $tableConfig) {
+            if (empty($tableConfig['table'])) {
+                throw new \RuntimeException(
+                    sprintf(
+                        'The table setting is missing for the categoryFieldsTable %s' . $fieldAndTableInfo,
+                        $configKey
+                    ),
+                    1447274131
+                );
+            }
+            if (empty($tableConfig['activeCondition'])) {
+                throw new \RuntimeException(
+                    sprintf(
+                        'The activeCondition setting is missing for the categoryFieldsTable %s' . $fieldAndTableInfo,
+                        $configKey
+                    ),
+                    1480786868
+                );
+            }
+
+            if ($this->getDisplayConditionEvaluator()->evaluateDisplayCondition(
+                $tableConfig['activeCondition'],
+                $configuration['row']
+            )
+            ) {
+                if (!empty($activeTable)) {
+                    throw new \RuntimeException(
+                        sprintf(
+                            'There must only be one active categoryFieldsTable. Multiple active tables (%s, %s) '
+                            . 'were found' . $fieldAndTableInfo,
+                            $activeTable,
+                            $tableConfig['table']
+                        ),
+                        1480787321
+                    );
+                }
+                $activeTable = $tableConfig['table'];
+            }
+        }
+
+        if (empty($activeTable)) {
+            throw new \RuntimeException('No active was found' . $fieldAndTableInfo, 1447274507);
+        }
+
+        return $activeTable;
+    }
+
+    /**
+     * Returns the display condition evaluator utility class.
+     *
+     * @return DisplayConditionEvaluator
+     */
+    protected function getDisplayConditionEvaluator()
+    {
+        return GeneralUtility::makeInstance(DisplayConditionEvaluator::class);
+    }
+
+    /**
      * Tells whether a table has a category configuration in the registry.
      *
      * @param string $tableName Name of the table to be looked up
diff --git a/typo3/sysext/core/Documentation/Changelog/master/Breaking-53045-GetCategoryFieldsForTableMethodRemovedFromCategoryRegistry.rst b/typo3/sysext/core/Documentation/Changelog/master/Breaking-53045-GetCategoryFieldsForTableMethodRemovedFromCategoryRegistry.rst
new file mode 100644 (file)
index 0000000..7a0ec3f
--- /dev/null
@@ -0,0 +1,36 @@
+.. include:: ../../Includes.txt
+
+===================================================================================
+Breaking: #53045 - getCategoryFieldsForTable() method removed from CategoryRegistry
+===================================================================================
+
+See :issue:`53045`
+
+Description
+===========
+
+The method :php:`getCategoryFieldsForTable()` is removed from the :php:`\TYPO3\CMS\Core\Category\CategoryRegistry`
+class.
+
+It could only handle the `tt_content` menus `categorized_pages` and `categorized_content`.
+
+
+Impact
+======
+
+The method :php:`getCategoryFieldsForTable()` is removed. Any third party code that uses it will break.
+
+
+Affected Installations
+======================
+
+All installations with third party code making using the removed method.
+
+
+Migration
+=========
+
+A new method  :php:`getCategoryFieldItems()` is added that can be used by third party code for any
+categorized table.
+
+.. index:: Backend, PHP-API, TCA
diff --git a/typo3/sysext/core/Documentation/Changelog/master/Feature-53045-CategorizedFieldsForTableItemsProcFuncInCategoryRegistry.rst b/typo3/sysext/core/Documentation/Changelog/master/Feature-53045-CategorizedFieldsForTableItemsProcFuncInCategoryRegistry.rst
new file mode 100644 (file)
index 0000000..0ec0cc3
--- /dev/null
@@ -0,0 +1,58 @@
+.. include:: ../../Includes.txt
+
+================================================================================
+Feature: #53045 - Categorized fields for table itemsProcFunc in CategoryRegistry
+================================================================================
+
+See :issue:`53045`
+
+Description
+===========
+
+A new method :php:`getCategoryFieldItems()` is added to the :php:`\TYPO3\CMS\Core\Category\CategoryRegistry` class.
+
+This method can be used as an `itemsProcFunc` in TCA and returns a list of all categorized fields of a table.
+
+The table for which the categorized fields should be returned can be specified in two ways.
+
+Static table
+------------
+
+You can provide a static table name in the config of your TCA field:
+
+.. code-block:: php
+
+    'itemsProcFunc' => \TYPO3\CMS\Core\Category\CategoryRegistry::class . '->getCategoryFieldItems',
+    'categoryFieldsTable' => 'my_table_name',
+
+
+Dynamic table selection
+-----------------------
+
+You can also provide a list of tables. The active table can be selected by using a display condition:
+
+.. code-block:: php
+
+    'itemsProcFunc' => \TYPO3\CMS\Core\Category\CategoryRegistry::class . '->getCategoryFieldItems',
+    'categoryFieldsTable' => [
+        'categorized_pages' => [
+            'table' => 'pages',
+            'activeCondition' => 'FIELD:menu_type:=:categorized_pages'
+        ],
+        'categorized_content' => [
+            'table' => 'tt_content',
+            'activeCondition' => 'FIELD:menu_type:=:categorized_content'
+        ]
+    ]
+
+
+Impact
+======
+
+The method :php:`getCategoryFieldsForTable()` is removed. It could only handle the `tt_content` menus
+`categorized_pages` and `categorized_content`.
+
+A new method  :php:`getCategoryFieldItems()` is added that can be used by third party code for any
+categorized table.
+
+.. index:: Backend, PHP-API, TCA
index b5b68e9..bc9aeea 100644 (file)
@@ -13,6 +13,8 @@ namespace TYPO3\CMS\Core\Tests\Unit\Category;
  *
  * The TYPO3 project - inspiring people to share!
  */
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Lang\LanguageService;
 
 /**
  * Testcase for CategoryRegistry
@@ -332,4 +334,154 @@ class CategoryRegistryTest extends \TYPO3\CMS\Core\Tests\UnitTestCase
         $sqlData = $this->subject->addExtensionCategoryDatabaseSchemaToTablesDefinition([], 'text_extension_a');
         $this->assertEmpty($sqlData['sqlString'][0]);
     }
+
+    /**
+     * @test
+     */
+    public function getCategoryFieldItemsReturnsFieldsForStaticTable()
+    {
+        $GLOBALS['LANG'] = GeneralUtility::makeInstance(LanguageService::class);
+        $this->subject->add('text_extension_a', $this->tables['first']);
+        $this->subject->add('text_extension_a', $this->tables['first'], 'categories2');
+        $configuration = [
+            'config' => [
+                'categoryFieldsTable' => $this->tables['first']
+            ]
+        ];
+        $this->subject->getCategoryFieldItems($configuration);
+        $this->assertEquals([['Categories', 'categories'], ['Categories', 'categories2']], $configuration['items']);
+    }
+
+    /**
+     * @test
+     */
+    public function getCategoryFieldItemsReturnsFieldsForDynamicTables()
+    {
+        $GLOBALS['LANG'] = GeneralUtility::makeInstance(LanguageService::class);
+        $this->subject->add('text_extension_a', $this->tables['first']);
+        $this->subject->add('text_extension_a', $this->tables['first'], 'categories2');
+        $configuration = [
+            'row' => [
+                'menu_type' => 'categorized_pages',
+            ],
+            'config' => [
+                'categoryFieldsTable' => [
+                    [
+                        'table' => 'othertable',
+                        'activeCondition' => 'FIELD:menu_type:=:categorized_content',
+                    ],
+                    [
+                        'table' => $this->tables['first'],
+                        'activeCondition' => 'FIELD:menu_type:=:categorized_pages',
+                    ],
+                ],
+            ],
+        ];
+        $this->subject->getCategoryFieldItems($configuration);
+        $this->assertEquals([['Categories', 'categories'], ['Categories', 'categories2']], $configuration['items']);
+    }
+
+    /**
+     * @test
+     */
+    public function getCategoryFieldItemsThrowsExceptionIfConfigMissing()
+    {
+        $this->expectException(\RuntimeException::class);
+        $this->expectExceptionCode(1447273908);
+        $configuration = [];
+        $this->subject->getCategoryFieldItems($configuration);
+    }
+
+    /**
+     * @test
+     */
+    public function getCategoryFieldItemsThrowsExceptionIfTypeIsInvalid()
+    {
+        $this->expectException(\RuntimeException::class);
+        $this->expectExceptionCode(1447274126);
+        $configuration = [
+            'config' => [
+                'categoryFieldsTable' => new \stdClass()
+            ]
+        ];
+        $this->subject->getCategoryFieldItems($configuration);
+    }
+
+    /**
+     * @test
+     */
+    public function getCategoryFieldItemsThrowsExceptionIfTableIsMissing()
+    {
+        $this->expectException(\RuntimeException::class);
+        $this->expectExceptionCode(1447274131);
+        $configuration = [
+            'config' => [
+                'categoryFieldsTable' => [['activeCondition' => 'TRUE']]
+            ]
+        ];
+        $this->subject->getCategoryFieldItems($configuration);
+    }
+
+    /**
+     * @test
+     */
+    public function getCategoryFieldItemsThrowsExceptionIfActiveConditionIsMissing()
+    {
+        $this->expectException(\RuntimeException::class);
+        $this->expectExceptionCode(1480786868);
+        $configuration = [
+            'config' => [
+                'categoryFieldsTable' => [['table' => 'testtable']]
+            ]
+        ];
+        $this->subject->getCategoryFieldItems($configuration);
+    }
+
+    /**
+     * @test
+     */
+    public function getCategoryFieldItemsThrowsExceptionIfNoActiveTableIsFound()
+    {
+        $this->expectException(\RuntimeException::class);
+        $this->expectExceptionCode(1447274507);
+        $configuration = [
+            'row' => [],
+            'config' => [
+                'categoryFieldsTable' => [
+                    [
+                        'table' => 'testtable',
+                        'activeCondition' => 'FALSE',
+                    ]
+                ],
+            ]
+        ];
+        $this->subject->getCategoryFieldItems($configuration);
+    }
+
+    /**
+     * @test
+     */
+    public function getCategoryFieldItemsThrowsExceptionIfMultipleTablesAreActive()
+    {
+        $this->expectException(\RuntimeException::class);
+        $this->expectExceptionCode(1480787321);
+        $configuration = [
+            'row' => [
+                'menu_type' => 'categorized_pages',
+            ],
+            'config' => [
+                'categoryFieldsTable' => [
+                    [
+                        'table' => 'testtable',
+                        'activeCondition' => 'FIELD:menu_type:=:categorized_pages'
+                    ],
+                    [
+                        'table' => 'testtable2',
+                        'activeCondition' => 'FIELD:menu_type:=:categorized_pages'
+                    ]
+                ],
+            ]
+        ];
+        $this->subject->getCategoryFieldItems($configuration);
+    }
 }
index ba72c9e..9f79707 100644 (file)
@@ -1034,7 +1034,17 @@ return [
                 'size' => 1,
                 'minitems' => 0,
                 'maxitems' => 1,
-                'itemsProcFunc' => \TYPO3\CMS\Core\Category\CategoryRegistry::class . '->getCategoryFieldsForTable',
+                'itemsProcFunc' => \TYPO3\CMS\Core\Category\CategoryRegistry::class . '->getCategoryFieldItems',
+                'categoryFieldsTable' => [
+                    'categorized_pages' => [
+                        'table' => 'pages',
+                        'activeCondition' => 'FIELD:menu_type:=:categorized_pages'
+                    ],
+                    'categorized_content' => [
+                        'table' => 'tt_content',
+                        'activeCondition' => 'FIELD:menu_type:=:categorized_content'
+                    ]
+                ]
             ]
         ],
         'table_caption' => [