From 36f8765aa340c364f7b7edfa967549c683f21747 Mon Sep 17 00:00:00 2001 From: Alexander Stehlik <alexander.stehlik@gmail.com> Date: Sat, 3 Dec 2016 18:52:06 +0100 Subject: [PATCH] [FEATURE] Reusable getCategoryFieldsForTable itemsProcFunc 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> --- .../EvaluateDisplayConditions.php | 328 +---------------- .../Utility/DisplayConditionEvaluator.php | 338 ++++++++++++++++++ .../Classes/Category/CategoryRegistry.php | 130 ++++++- ...TableMethodRemovedFromCategoryRegistry.rst | 36 ++ ...orTableItemsProcFuncInCategoryRegistry.rst | 58 +++ .../Unit/Category/CategoryRegistryTest.php | 152 ++++++++ .../frontend/Configuration/TCA/tt_content.php | 12 +- 7 files changed, 725 insertions(+), 329 deletions(-) create mode 100644 typo3/sysext/backend/Classes/Form/Utility/DisplayConditionEvaluator.php create mode 100644 typo3/sysext/core/Documentation/Changelog/master/Breaking-53045-GetCategoryFieldsForTableMethodRemovedFromCategoryRegistry.rst create mode 100644 typo3/sysext/core/Documentation/Changelog/master/Feature-53045-CategorizedFieldsForTableItemsProcFuncInCategoryRegistry.rst diff --git a/typo3/sysext/backend/Classes/Form/FormDataProvider/EvaluateDisplayConditions.php b/typo3/sysext/backend/Classes/Form/FormDataProvider/EvaluateDisplayConditions.php index 8c30e23b98a2..b75faa82eeaa 100644 --- a/typo3/sysext/backend/Classes/Form/FormDataProvider/EvaluateDisplayConditions.php +++ b/typo3/sysext/backend/Classes/Form/FormDataProvider/EvaluateDisplayConditions.php @@ -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 index 000000000000..e34021335ef8 --- /dev/null +++ b/typo3/sysext/backend/Classes/Form/Utility/DisplayConditionEvaluator.php @@ -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']; + } +} diff --git a/typo3/sysext/core/Classes/Category/CategoryRegistry.php b/typo3/sysext/core/Classes/Category/CategoryRegistry.php index 31a9a86c55da..34bd3c068a02 100644 --- a/typo3/sysext/core/Classes/Category/CategoryRegistry.php +++ b/typo3/sysext/core/Classes/Category/CategoryRegistry.php @@ -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) { @@ -162,6 +151,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. * 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 index 000000000000..7a0ec3fc05f3 --- /dev/null +++ b/typo3/sysext/core/Documentation/Changelog/master/Breaking-53045-GetCategoryFieldsForTableMethodRemovedFromCategoryRegistry.rst @@ -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 index 000000000000..0ec0cc3df139 --- /dev/null +++ b/typo3/sysext/core/Documentation/Changelog/master/Feature-53045-CategorizedFieldsForTableItemsProcFuncInCategoryRegistry.rst @@ -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 diff --git a/typo3/sysext/core/Tests/Unit/Category/CategoryRegistryTest.php b/typo3/sysext/core/Tests/Unit/Category/CategoryRegistryTest.php index b5b68e9de0eb..bc9aeea723ea 100644 --- a/typo3/sysext/core/Tests/Unit/Category/CategoryRegistryTest.php +++ b/typo3/sysext/core/Tests/Unit/Category/CategoryRegistryTest.php @@ -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); + } } diff --git a/typo3/sysext/frontend/Configuration/TCA/tt_content.php b/typo3/sysext/frontend/Configuration/TCA/tt_content.php index ba72c9e3c6bd..9f79707ef03f 100644 --- a/typo3/sysext/frontend/Configuration/TCA/tt_content.php +++ b/typo3/sysext/frontend/Configuration/TCA/tt_content.php @@ -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' => [ -- GitLab