parseDisplayConditions($result); $result = $this->evaluateConditions($result); return $result; } /** * Find all 'displayCond' in TCA and flex forms and substitute them with an * array representation that contains all relevant data to * evaluate the condition later. For "FIELD" conditions the helper methods * findFieldValue() is used to find the value of the referenced field to put * that value into the returned array, too. This is important since the referenced * field is "relative" to the position of the field that has the display condition. * For instance, "FIELD:aField:=:foo" within a flex form field references a field * value from the same sheet, and there are many more complex scenarios to resolve. * * @param array $result Incoming result array * @throws \RuntimeException * @return array Modified result array with all displayCond parsed into arrays */ protected function parseDisplayConditions(array $result): array { $flexColumns = []; foreach ($result['processedTca']['columns'] as $columnName => $columnConfiguration) { if (isset($columnConfiguration['config']['type']) && $columnConfiguration['config']['type'] === 'flex') { $flexColumns[$columnName] = $columnConfiguration; } if (!isset($columnConfiguration['displayCond'])) { continue; } $result['processedTca']['columns'][$columnName]['displayCond'] = $this->parseConditionRecursive( $columnConfiguration['displayCond'], $result['databaseRow'] ); } foreach ($flexColumns as $columnName => $flexColumn) { $sheetNameFieldNames = []; foreach ($flexColumn['config']['ds']['sheets'] as $sheetName => $sheetConfiguration) { // Create a list of all sheet names with field names combinations for later 'sheetName.fieldName' lookups // 'one.sheet.one.field' as key, with array of "sheetName" and "fieldName" as value if (isset($sheetConfiguration['ROOT']['el']) && is_array($sheetConfiguration['ROOT']['el'])) { foreach ($sheetConfiguration['ROOT']['el'] as $flexElementName => $flexElementConfiguration) { // section container have no value in its own if (isset($flexElementConfiguration['type']) && $flexElementConfiguration['type'] === 'array' && isset($flexElementConfiguration['section']) && $flexElementConfiguration['section'] == 1 ) { continue; } $combinedKey = $sheetName . '.' . $flexElementName; if (array_key_exists($combinedKey, $sheetNameFieldNames)) { throw new \RuntimeException( 'Ambiguous sheet name and field name combination: Sheet "' . $sheetNameFieldNames[$combinedKey]['sheetName'] . '" with field name "' . $sheetNameFieldNames[$combinedKey]['fieldName'] . '" overlaps with sheet "' . $sheetName . '" and field name "' . $flexElementName . '". Do not do that.', 1481483061 ); } $sheetNameFieldNames[$combinedKey] = [ 'sheetName' => $sheetName, 'fieldName' => $flexElementName, ]; } } } foreach ($flexColumn['config']['ds']['sheets'] as $sheetName => $sheetConfiguration) { if (isset($sheetConfiguration['ROOT']['displayCond'])) { // Condition on a flex sheet $flexContext = [ 'context' => 'flexSheet', 'sheetNameFieldNames' => $sheetNameFieldNames, 'currentSheetName' => $sheetName, 'flexFormRowData' => $result['databaseRow'][$columnName] ?? null, ]; $parsedDisplayCondition = $this->parseConditionRecursive( $sheetConfiguration['ROOT']['displayCond'], $result['databaseRow'], $flexContext ); $result['processedTca']['columns'][$columnName]['config']['ds'] ['sheets'][$sheetName]['ROOT']['displayCond'] = $parsedDisplayCondition; } if (isset($sheetConfiguration['ROOT']['el']) && is_array($sheetConfiguration['ROOT']['el'])) { foreach ($sheetConfiguration['ROOT']['el'] as $flexElementName => $flexElementConfiguration) { if (isset($flexElementConfiguration['displayCond'])) { // Condition on a flex element $flexContext = [ 'context' => 'flexField', 'sheetNameFieldNames' => $sheetNameFieldNames, 'currentSheetName' => $sheetName, 'currentFieldName' => $flexElementName, 'flexFormDataStructure' => $result['processedTca']['columns'][$columnName]['config']['ds'], 'flexFormRowData' => $result['databaseRow'][$columnName] ?? null, ]; $parsedDisplayCondition = $this->parseConditionRecursive( $flexElementConfiguration['displayCond'], $result['databaseRow'], $flexContext ); $result['processedTca']['columns'][$columnName]['config']['ds'] ['sheets'][$sheetName]['ROOT'] ['el'][$flexElementName]['displayCond'] = $parsedDisplayCondition; } if (isset($flexElementConfiguration['type']) && $flexElementConfiguration['type'] === 'array' && isset($flexElementConfiguration['section']) && $flexElementConfiguration['section'] == 1 && isset($flexElementConfiguration['children']) && is_array($flexElementConfiguration['children']) ) { // Conditions on flex container section elements foreach ($flexElementConfiguration['children'] as $containerIdentifier => $containerElements) { if (isset($containerElements['el']) && is_array($containerElements['el'])) { foreach ($containerElements['el'] as $containerElementName => $containerElementConfiguration) { if (isset($containerElementConfiguration['displayCond'])) { $flexContext = [ 'context' => 'flexContainerElement', 'sheetNameFieldNames' => $sheetNameFieldNames, 'currentSheetName' => $sheetName, 'currentFieldName' => $flexElementName, 'currentContainerIdentifier' => $containerIdentifier, 'currentContainerElementName' => $containerElementName, 'flexFormDataStructure' => $result['processedTca']['columns'][$columnName]['config']['ds'], 'flexFormRowData' => $result['databaseRow'][$columnName], ]; $parsedDisplayCondition = $this->parseConditionRecursive( $containerElementConfiguration['displayCond'], $result['databaseRow'], $flexContext ); $result['processedTca']['columns'][$columnName]['config']['ds'] ['sheets'][$sheetName]['ROOT'] ['el'][$flexElementName] ['children'][$containerIdentifier] ['el'][$containerElementName]['displayCond'] = $parsedDisplayCondition; } } } } } } } } } return $result; } /** * Parse a condition into an array representation and validate syntax. Handles nested conditions combined with AND and OR. * Calls itself recursive for nesting and logically combined conditions. * * @param mixed $condition Either an array with multiple conditions combined with AND or OR, or a single condition string * @param array $databaseRow Incoming full database row * @param array $flexContext Detailed flex context if display condition is within a flex field, needed to determine field value for "FIELD" conditions * @throws \RuntimeException * @return array Array representation of that condition, see unit tests for details on syntax */ protected function parseConditionRecursive($condition, array $databaseRow, array $flexContext = []): array { $conditionArray = []; if (is_string($condition)) { $conditionArray = $this->parseSingleConditionString($condition, $databaseRow, $flexContext); } elseif (is_array($condition)) { foreach ($condition as $logicalOperator => $groupedDisplayConditions) { $logicalOperator = strtoupper(is_string($logicalOperator) ? $logicalOperator : ''); if (($logicalOperator !== 'AND' && $logicalOperator !== 'OR') || !is_array($groupedDisplayConditions)) { throw new \RuntimeException( 'Multiple conditions must have boolean operator "OR" or "AND", "' . $logicalOperator . '" given.', 1481380393 ); } $conditionArray = [ 'type' => $logicalOperator, 'subConditions' => [], ]; foreach ($groupedDisplayConditions as $key => $singleDisplayCondition) { $key = strtoupper((string)$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) $conditionArray['subConditions'][] = $this->parseConditionRecursive( [$key => $singleDisplayCondition], $databaseRow, $flexContext ); } else { $conditionArray['subConditions'][] = $this->parseConditionRecursive( $singleDisplayCondition, $databaseRow, $flexContext ); } } } } else { throw new \RuntimeException( 'Condition must be either an array with sub conditions or a single condition string, type ' . gettype($condition) . ' given.', 1481381058 ); } return $conditionArray; } /** * Parse a single condition string into pieces, validate them and return * an array representation. * * @param string $conditionString Given condition string like "VERSION:IS:true" * @param array $databaseRow Incoming full database row * @param array $flexContext Detailed flex context if display condition is within a flex field, needed to determine field value for "FIELD" conditions * @return array Validated name array, example: [ type="VERSION", isVersion="true" ] * @throws \RuntimeException */ protected function parseSingleConditionString(string $conditionString, array $databaseRow, array $flexContext = []): array { $conditionArray = GeneralUtility::trimExplode(':', $conditionString, false, 4); $namedConditionArray = [ 'type' => $conditionArray[0], ]; switch ($namedConditionArray['type']) { case 'FIELD': if (empty($conditionArray[1])) { throw new \RuntimeException( 'Field condition "' . $conditionString . '" must have a field name as second part, none given.' . 'Example: "FIELD:myField:=:myValue"', 1481385695 ); } $fieldName = $conditionArray[1]; if (empty($conditionArray[2])) { throw new \RuntimeException( 'Field condition "' . $conditionString . '" must have a valid operator as third part, none given.', 1481386239 ); } $namedConditionArray['operator'] = $conditionArray[2]; if (!isset($conditionArray[3])) { throw new \RuntimeException( 'Field condition "' . $conditionString . '" must have an operand as fourth part, none given.' . ' Example: "FIELD:myField:=:4"', 1481401543 ); } $operand = $conditionArray[3]; if ($namedConditionArray['operator'] === 'REQ') { $operand = strtolower($operand); if ($operand === 'true') { $namedConditionArray['operand'] = true; } elseif ($operand === 'false') { $namedConditionArray['operand'] = false; } else { throw new \RuntimeException( 'Field condition "' . $conditionString . '" must have "true" or "false" as fourth part.' . ' Example: "FIELD:myField:REQ:true', 1481401892 ); } } elseif (in_array($namedConditionArray['operator'], ['>', '<', '>=', '<=', 'BIT', '!BIT'], true)) { if (!MathUtility::canBeInterpretedAsInteger($operand)) { throw new \RuntimeException( 'Field condition "' . $conditionString . '" with comparison operator ' . $namedConditionArray['operator'] . ' must have a number as fourth part, ' . $operand . ' given. Example: "FIELD:myField:>:42"', 1481456806 ); } $namedConditionArray['operand'] = (int)$operand; } elseif (in_array($namedConditionArray['operator'], ['-', '!-'], true)) { [$minimum, $maximum] = GeneralUtility::trimExplode('-', $operand); if (!MathUtility::canBeInterpretedAsInteger($minimum) || !MathUtility::canBeInterpretedAsInteger($maximum)) { throw new \RuntimeException( 'Field condition "' . $conditionString . '" with comparison operator ' . $namedConditionArray['operator'] . ' must have two numbers as fourth part, separated by dash, ' . $operand . ' given. Example: "FIELD:myField:-:1-3"', 1481457277 ); } $namedConditionArray['operand'] = ''; $namedConditionArray['min'] = (int)$minimum; $namedConditionArray['max'] = (int)$maximum; } elseif (in_array($namedConditionArray['operator'], ['IN', '!IN', '=', '!='], true)) { $namedConditionArray['operand'] = $operand; } else { $allowedOperators = ['REQ', '>', '<', '>=', '<=', '-', '!-', '=', '!=', 'IN', '!IN', 'BIT', '!BIT']; throw new \RuntimeException( 'Field condition "' . $conditionString . '" must have a valid operator as third part, invalid one given.' . ' Valid operators are: "' . implode('", "', $allowedOperators) . '".' . ' Example: "FIELD:myField:=:4"', 1745918372 ); } $namedConditionArray['fieldValue'] = $this->findFieldValue($fieldName, $databaseRow, $flexContext); break; case 'HIDE_FOR_NON_ADMINS': break; case 'REC': if (empty($conditionArray[1]) || $conditionArray[1] !== 'NEW') { throw new \RuntimeException( 'Record condition "' . $conditionString . '" must contain "NEW" keyword: either "REC:NEW:true" or "REC:NEW:false"', 1481384784 ); } if (empty($conditionArray[2])) { throw new \RuntimeException( 'Record condition "' . $conditionString . '" must have an operand "true" or "false", none given. Example: "REC:NEW:true"', 1481384947 ); } $operand = strtolower($conditionArray[2]); if ($operand === 'true') { $namedConditionArray['isNew'] = true; } elseif ($operand === 'false') { $namedConditionArray['isNew'] = false; } else { throw new \RuntimeException( 'Record condition "' . $conditionString . '" must have an operand "true" or "false, example "REC:NEW:true", given: ' . $operand, 1481385173 ); } // Programming error: There must be a uid available, other data providers should have taken care of that already if (!array_key_exists('uid', $databaseRow)) { throw new \RuntimeException( 'Required [\'databaseRow\'][\'uid\'] not found in data array', 1481467208 ); } // May contain "NEW123..." $namedConditionArray['uid'] = $databaseRow['uid']; break; case 'VERSION': if (empty($conditionArray[1]) || $conditionArray[1] !== 'IS') { throw new \RuntimeException( 'Version condition "' . $conditionString . '" must contain "IS" keyword: either "VERSION:IS:false" or "VERSION:IS:true"', 1481383660 ); } if (empty($conditionArray[2])) { throw new \RuntimeException( 'Version condition "' . $conditionString . '" must have an operand "true" or "false", none given. Example: "VERSION:IS:true', 1481383888 ); } $operand = strtolower($conditionArray[2]); if ($operand === 'true') { $namedConditionArray['isVersion'] = true; } elseif ($operand === 'false') { $namedConditionArray['isVersion'] = false; } else { throw new \RuntimeException( 'Version condition "' . $conditionString . '" must have a "true" or "false" operand, example "VERSION:IS:true", given: ' . $operand, 1481384123 ); } // Programming error: There must be a uid available, other data providers should have taken care of that already if (!array_key_exists('uid', $databaseRow)) { throw new \RuntimeException( 'Required [\'databaseRow\'][\'uid\'] not found in data array', 1481469854 ); } $namedConditionArray['uid'] = $databaseRow['uid']; if (array_key_exists('t3ver_oid', $databaseRow)) { $namedConditionArray['t3ver_oid'] = $databaseRow['t3ver_oid']; } if (array_key_exists('pid', $databaseRow)) { $namedConditionArray['pid'] = $databaseRow['pid']; } if (array_key_exists('_ORIG_pid', $databaseRow)) { $namedConditionArray['_ORIG_pid'] = $databaseRow['_ORIG_pid']; } break; case 'USER': if (empty($conditionArray[1])) { throw new \RuntimeException( 'User function condition "' . $conditionString . '" must have a user function defined a second part, none given.' . ' Correct format is USER:\My\User\Func->match:more:arguments,' . ' given: ' . $conditionString, 1481382954 ); } $namedConditionArray['function'] = $conditionArray[1]; array_shift($conditionArray); array_shift($conditionArray); $parameters = count($conditionArray) < 2 ? $conditionArray : array_merge( [$conditionArray[0]], GeneralUtility::trimExplode(':', $conditionArray[1]) ); $namedConditionArray['parameters'] = $parameters; $namedConditionArray['record'] = $databaseRow; $namedConditionArray['flexContext'] = $flexContext; break; default: throw new \RuntimeException( 'Unknown condition rule type "' . $namedConditionArray['type'] . '" with display condition "' . $conditionString . '".', 1481381950 ); } return $namedConditionArray; } /** * Find field value the condition refers to for "FIELD:" conditions. For "normal" TCA fields this is the value of * a "neighbor" field, but in flex form context it can be prepended with a sheet name. The method sorts out the * details and returns the current field value. * * @param string $givenFieldName The full name used in displayCond. Can have sheet names included in flex context * @param array $databaseRow Incoming database row values * @param array $flexContext Detailed flex context if display condition is within a flex field, needed to determine field value for "FIELD" conditions * @throws \RuntimeException * @return mixed The current field value from database row or a deeper flex form structure field. */ protected function findFieldValue(string $givenFieldName, array $databaseRow, array $flexContext = []) { $fieldValue = null; // Early return for "normal" tca fields if (empty($flexContext)) { if (array_key_exists($givenFieldName, $databaseRow)) { $fieldValue = $databaseRow[$givenFieldName]; } return $fieldValue; } if ($flexContext['context'] === 'flexSheet') { // A display condition on a flex form sheet. Relatively simple: fieldName is either // "parentRec.fieldName" pointing to a databaseRow field name, or "sheetName.fieldName" pointing // to a field value from a neighbor field. if (str_starts_with($givenFieldName, 'parentRec.')) { $fieldName = substr($givenFieldName, 10); if (array_key_exists($fieldName, $databaseRow)) { $fieldValue = $databaseRow[$fieldName]; } } else { if (array_key_exists($givenFieldName, $flexContext['sheetNameFieldNames'])) { if ($flexContext['currentSheetName'] === $flexContext['sheetNameFieldNames'][$givenFieldName]['sheetName']) { throw new \RuntimeException( 'Configuring displayCond to "' . $givenFieldName . '" on flex form sheet "' . $flexContext['currentSheetName'] . '" referencing a value from the same sheet does not make sense.', 1481485705 ); } } $sheetName = $flexContext['sheetNameFieldNames'][$givenFieldName]['sheetName'] ?? null; $fieldName = $flexContext['sheetNameFieldNames'][$givenFieldName]['fieldName'] ?? null; if (!isset($flexContext['flexFormRowData']['data'][$sheetName]['lDEF'][$fieldName]['vDEF'])) { throw new \RuntimeException( 'Flex form displayCond on sheet "' . $flexContext['currentSheetName'] . '" references field "' . $fieldName . '" of sheet "' . $sheetName . '", but that field does not exist in current data structure', 1481488492 ); } $fieldValue = $flexContext['flexFormRowData']['data'][$sheetName]['lDEF'][$fieldName]['vDEF']; } } elseif ($flexContext['context'] === 'flexField') { // A display condition on a flex field. Handle "parentRec." similar to sheet conditions, // get a list of "local" field names and see if they are used as reference, else see if a // "sheetName.fieldName" field reference is given if (str_starts_with($givenFieldName, 'parentRec.')) { $fieldName = substr($givenFieldName, 10); if (array_key_exists($fieldName, $databaseRow)) { $fieldValue = $databaseRow[$fieldName]; } } else { $listOfLocalFlexFieldNames = array_keys( $flexContext['flexFormDataStructure']['sheets'][$flexContext['currentSheetName']]['ROOT']['el'] ); if (in_array($givenFieldName, $listOfLocalFlexFieldNames, true)) { // Condition references field name of the same sheet $sheetName = $flexContext['currentSheetName']; if (!isset($flexContext['flexFormRowData']['data'][$sheetName]['lDEF'][$givenFieldName]['vDEF'])) { throw new \RuntimeException( 'Flex form displayCond on field "' . $flexContext['currentFieldName'] . '" on flex form sheet "' . $flexContext['currentSheetName'] . '" references field "' . $givenFieldName . '", but a field value' . ' does not exist in this sheet', 1481492953 ); } $fieldValue = $flexContext['flexFormRowData']['data'][$sheetName]['lDEF'][$givenFieldName]['vDEF']; } elseif (in_array($givenFieldName, array_keys($flexContext['sheetNameFieldNames'], true))) { // Condition references field name including a sheet name $sheetName = $flexContext['sheetNameFieldNames'][$givenFieldName]['sheetName']; $fieldName = $flexContext['sheetNameFieldNames'][$givenFieldName]['fieldName']; $fieldValue = $flexContext['flexFormRowData']['data'][$sheetName]['lDEF'][$fieldName]['vDEF']; } else { throw new \RuntimeException( 'Flex form displayCond on field "' . $flexContext['currentFieldName'] . '" on flex form sheet "' . $flexContext['currentSheetName'] . '" references a field or field / sheet combination "' . $givenFieldName . '" that might be defined in given data structure but is not found in data values.', 1481496170 ); } } } elseif ($flexContext['context'] === 'flexContainerElement') { // A display condition on a flex form section container element. Handle "parentRec.", compare to a // list of local field names, compare to a list of field names from same sheet, compare to a list // of sheet fields from other sheets. if (str_starts_with($givenFieldName, 'parentRec.')) { $fieldName = substr($givenFieldName, 10); if (array_key_exists($fieldName, $databaseRow)) { $fieldValue = $databaseRow[$fieldName]; } } else { $currentSheetName = $flexContext['currentSheetName']; $currentFieldName = $flexContext['currentFieldName']; $currentContainerIdentifier = $flexContext['currentContainerIdentifier']; $currentContainerElementName = $flexContext['currentContainerElementName']; $listOfLocalContainerElementNames = array_keys( $flexContext['flexFormDataStructure']['sheets'][$currentSheetName]['ROOT'] ['el'][$currentFieldName] ['children'][$currentContainerIdentifier] ['el'] ); $listOfLocalContainerElementNamesWithSheetName = []; foreach ($listOfLocalContainerElementNames as $aContainerElementName) { $listOfLocalContainerElementNamesWithSheetName[$currentSheetName . '.' . $aContainerElementName] = [ 'containerElementName' => $aContainerElementName, ]; } $listOfLocalFlexFieldNames = array_keys( $flexContext['flexFormDataStructure']['sheets'][$currentSheetName]['ROOT']['el'] ); if (in_array($givenFieldName, $listOfLocalContainerElementNames, true)) { // Condition references field of same container instance $containerType = current(array_keys( $flexContext['flexFormRowData']['data'][$currentSheetName] ['lDEF'][$currentFieldName] ['el'][$currentContainerIdentifier] )); $fieldValue = $flexContext['flexFormRowData']['data'][$currentSheetName] ['lDEF'][$currentFieldName] ['el'][$currentContainerIdentifier] [$containerType] ['el'][$givenFieldName]['vDEF']; } elseif (in_array($givenFieldName, array_keys($listOfLocalContainerElementNamesWithSheetName, true))) { // Condition references field name of same container instance and has sheet name included $containerType = current(array_keys( $flexContext['flexFormRowData']['data'][$currentSheetName] ['lDEF'][$currentFieldName] ['el'][$currentContainerIdentifier] )); $fieldName = $listOfLocalContainerElementNamesWithSheetName[$givenFieldName]['containerElementName']; $fieldValue = $flexContext['flexFormRowData']['data'][$currentSheetName] ['lDEF'][$currentFieldName] ['el'][$currentContainerIdentifier] [$containerType] ['el'][$fieldName]['vDEF']; } elseif (in_array($givenFieldName, $listOfLocalFlexFieldNames, true)) { // Condition reference field name of sheet this section container is in $fieldValue = $flexContext['flexFormRowData']['data'][$currentSheetName] ['lDEF'][$givenFieldName]['vDEF']; } elseif (in_array($givenFieldName, array_keys($flexContext['sheetNameFieldNames'], true))) { $sheetName = $flexContext['sheetNameFieldNames'][$givenFieldName]['sheetName']; $fieldName = $flexContext['sheetNameFieldNames'][$givenFieldName]['fieldName']; $fieldValue = $flexContext['flexFormRowData']['data'][$sheetName]['lDEF'][$fieldName]['vDEF']; } else { $containerType = current(array_keys( $flexContext['flexFormRowData']['data'][$currentSheetName] ['lDEF'][$currentFieldName] ['el'][$currentContainerIdentifier] )); throw new \RuntimeException( 'Flex form displayCond on section container field "' . $currentContainerElementName . '" of container type "' . $containerType . '" on flex form sheet "' . $flexContext['currentSheetName'] . '" references a field or field / sheet combination "' . $givenFieldName . '" that might be defined in given data structure but is not found in data values.', 1481634649 ); } } } return $fieldValue; } /** * Loop through TCA, find prepared conditions and evaluate them. Delete either the * field itself if the condition did not match, or the 'displayCond' in TCA. */ protected function evaluateConditions(array $result): array { // Evaluate normal tca fields first $listOfFlexFieldNames = []; foreach ($result['processedTca']['columns'] as $columnName => $columnConfiguration) { $conditionResult = true; if (isset($columnConfiguration['displayCond'])) { $conditionResult = $this->evaluateConditionRecursive($columnConfiguration['displayCond']); if (!$conditionResult) { unset($result['processedTca']['columns'][$columnName]); } else { // Always unset the whole parsed display condition to save some memory, we're done with them unset($result['processedTca']['columns'][$columnName]['displayCond']); } } // If field was not removed and if it is a flex field, add to list of flex fields to scan if ($conditionResult && ($columnConfiguration['config']['type'] ?? false) === 'flex') { $listOfFlexFieldNames[] = $columnName; } } // Search for flex fields and evaluate sheet conditions throwing them away if needed foreach ($listOfFlexFieldNames as $columnName) { $columnConfiguration = $result['processedTca']['columns'][$columnName] ?? []; foreach ($columnConfiguration['config']['ds']['sheets'] as $sheetName => $sheetConfiguration) { if (is_array($sheetConfiguration['ROOT']['displayCond'] ?? false)) { if (!$this->evaluateConditionRecursive($sheetConfiguration['ROOT']['displayCond'])) { unset($result['processedTca']['columns'][$columnName]['config']['ds']['sheets'][$sheetName]); } else { unset($result['processedTca']['columns'][$columnName]['config']['ds']['sheets'][$sheetName]['ROOT']['displayCond']); } } } } // With full sheets gone we loop over display conditions of single fields in flex to throw fields away if needed $listOfFlexSectionContainers = []; foreach ($listOfFlexFieldNames as $columnName) { $columnConfiguration = $result['processedTca']['columns'][$columnName]; if (is_array($columnConfiguration['config']['ds']['sheets'])) { foreach ($columnConfiguration['config']['ds']['sheets'] as $sheetName => $sheetConfiguration) { if (isset($sheetConfiguration['ROOT']['el']) && is_array($sheetConfiguration['ROOT']['el'])) { foreach ($sheetConfiguration['ROOT']['el'] as $flexField => $flexConfiguration) { $conditionResult = true; if (isset($flexConfiguration['displayCond']) && is_array($flexConfiguration['displayCond'])) { $conditionResult = $this->evaluateConditionRecursive($flexConfiguration['displayCond']); if (!$conditionResult) { unset( $result['processedTca']['columns'][$columnName]['config']['ds'] ['sheets'][$sheetName]['ROOT'] ['el'][$flexField] ); } else { unset( $result['processedTca']['columns'][$columnName]['config']['ds'] ['sheets'][$sheetName]['ROOT'] ['el'][$flexField]['displayCond'] ); } } // If it was not removed and if the field is a section container, add it to the section container list if ($conditionResult && isset($flexConfiguration['type']) && $flexConfiguration['type'] === 'array' && isset($flexConfiguration['section']) && $flexConfiguration['section'] == 1 && isset($flexConfiguration['children']) && is_array($flexConfiguration['children']) ) { $listOfFlexSectionContainers[] = [ 'columnName' => $columnName, 'sheetName' => $sheetName, 'flexField' => $flexField, ]; } } } } } } // Loop over found section container elements and evaluate their conditions foreach ($listOfFlexSectionContainers as $flexSectionContainerPosition) { $columnName = $flexSectionContainerPosition['columnName']; $sheetName = $flexSectionContainerPosition['sheetName']; $flexField = $flexSectionContainerPosition['flexField']; $sectionElement = $result['processedTca']['columns'][$columnName]['config']['ds'] ['sheets'][$sheetName]['ROOT'] ['el'][$flexField]; foreach ($sectionElement['children'] as $containerInstanceName => $containerDataStructure) { if (isset($containerDataStructure['el']) && is_array($containerDataStructure['el'])) { foreach ($containerDataStructure['el'] as $containerElementName => $containerElementConfiguration) { if (isset($containerElementConfiguration['displayCond']) && is_array($containerElementConfiguration['displayCond'])) { if (!$this->evaluateConditionRecursive($containerElementConfiguration['displayCond'])) { unset( $result['processedTca']['columns'][$columnName]['config']['ds'] ['sheets'][$sheetName]['ROOT'] ['el'][$flexField] ['children'][$containerInstanceName] ['el'][$containerElementName] ); } else { unset( $result['processedTca']['columns'][$columnName]['config']['ds'] ['sheets'][$sheetName]['ROOT'] ['el'][$flexField] ['children'][$containerInstanceName] ['el'][$containerElementName]['displayCond'] ); } } } } } } return $result; } /** * Evaluate a condition recursive by evaluating the single condition type * * @param array $conditionArray The condition to evaluate, possibly with subConditions for AND and OR types * @return bool true if the condition matched */ protected function evaluateConditionRecursive(array $conditionArray): bool { switch ($conditionArray['type']) { case 'AND': $result = true; foreach ($conditionArray['subConditions'] as $subCondition) { $result = $result && $this->evaluateConditionRecursive($subCondition); } return $result; case 'OR': $result = false; foreach ($conditionArray['subConditions'] as $subCondition) { $result = $result || $this->evaluateConditionRecursive($subCondition); } return $result; case 'FIELD': return $this->matchFieldCondition($conditionArray); case 'HIDE_FOR_NON_ADMINS': return (bool)$this->getBackendUser()->isAdmin(); case 'REC': return $this->matchRecordCondition($conditionArray); case 'VERSION': return $this->matchVersionCondition($conditionArray); case 'USER': return $this->matchUserCondition($conditionArray); } return false; } /** * Evaluates conditions concerning a field of the current record. * * Example: * "FIELD:sys_language_uid:>:0" => TRUE, if the field 'sys_language_uid' is greater than 0 * * @param array $condition Condition array */ protected function matchFieldCondition(array $condition): bool { $operator = $condition['operator']; $operand = $condition['operand']; $fieldValue = $condition['fieldValue']; $result = false; switch ($operator) { case 'REQ': if (is_array($fieldValue) && count($fieldValue) <= 1) { $fieldValue = array_shift($fieldValue); } if ($operand) { $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); } if ($fieldValue === null) { // If field value is null, this is NOT greater than or equal 0 // See test set "Field is not greater than or equal to zero if empty array given" $result = false; } else { $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); } $min = $condition['min']; $max = $condition['max']; $result = $fieldValue >= $min && $fieldValue <= $max; 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, GeneralUtility::trimExplode(',', $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 conditions concerning the status of the current record. * * Example: * "REC:NEW:FALSE" => TRUE, if the record is already persisted (has a uid > 0) * * @param array $condition Condition array */ protected function matchRecordCondition(array $condition): bool { if ($condition['isNew']) { return !((int)$condition['uid'] > 0); } return (int)$condition['uid'] > 0; } /** * Evaluates whether the current record is versioned. * * @param array $condition Condition array */ protected function matchVersionCondition(array $condition): bool { $isNewRecord = !((int)$condition['uid'] > 0); // Detection of version can be done by detecting the workspace of the user $isUserInWorkspace = $this->getBackendUser()->workspace > 0; if ((int)($condition['t3ver_oid'] ?? 0) > 0) { $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 t3ver_oid > 0 $result = ($isUserInWorkspace || $isRecordDetectedAsVersion) && !$isNewRecord; if (!$condition['isVersion']) { $result = !$result; } return $result; } /** * Evaluates via the referenced user-defined method * * @param array $condition Condition array */ protected function matchUserCondition(array $condition): bool { $parameter = [ 'record' => $condition['record'], 'flexContext' => $condition['flexContext'], 'flexformValueKey' => 'vDEF', 'conditionParameters' => $condition['parameters'], ]; return (bool)GeneralUtility::callUserFunction($condition['function'], $parameter, $this); } protected function getBackendUser(): BackendUserAuthentication { return $GLOBALS['BE_USER']; } }