$fieldConfig) { // Placeholders are only valid for input-like and text-like fields. if (!isset($fieldConfig['config']['placeholder'], $fieldConfig['config']['type']) || ( $fieldConfig['config']['type'] !== 'input' && $fieldConfig['config']['type'] !== 'text' && $fieldConfig['config']['type'] !== 'number' && $fieldConfig['config']['type'] !== 'email' && $fieldConfig['config']['type'] !== 'link' && $fieldConfig['config']['type'] !== 'password' && $fieldConfig['config']['type'] !== 'datetime' && $fieldConfig['config']['type'] !== 'color' && $fieldConfig['config']['type'] !== 'json' ) ) { continue; } // Resolve __row|field type placeholders if (str_starts_with((string)$fieldConfig['config']['placeholder'], '__row|')) { // split field names into array and remove the __row indicator $fieldNameArray = array_slice( GeneralUtility::trimExplode('|', $fieldConfig['config']['placeholder'], true), 1 ); $result['processedTca']['columns'][$fieldName]['config']['placeholder'] = $this->getPlaceholderValue($fieldNameArray, $result); } elseif (!empty($fieldConfig['config']['placeholder'])) { // Resolve placeholders from language files $result['processedTca']['columns'][$fieldName]['config']['placeholder'] = $this->getLanguageService()->sL($fieldConfig['config']['placeholder']); } // Remove empty placeholders if (empty($result['processedTca']['columns'][$fieldName]['config']['placeholder'])) { unset($result['processedTca']['columns'][$fieldName]['config']['placeholder']); } } return $result; } /** * Recursively resolve the placeholder value. A placeholder string with a * syntax of __row|field1|field2|field3 will be recursively resolved to a * final value. * * @param array $fieldNameArray * @param array $result * @param int $recursionLevel * @return string */ protected function getPlaceholderValue($fieldNameArray, $result, $recursionLevel = 0) { if ($recursionLevel > 99) { // This should not happen, treat as misconfiguration return ''; } $fieldName = array_shift($fieldNameArray); // Skip if a defined field was actually not present in the database row // Using array_key_exists here, since NULL values are valid as well. if (!array_key_exists($fieldName, $result['databaseRow'])) { return ''; } $value = $result['databaseRow'][$fieldName]; if (!isset($result['processedTca']['columns'][$fieldName]['config']) || !is_array($result['processedTca']['columns'][$fieldName]['config']) ) { return (string)$value; } $fieldConfig = $result['processedTca']['columns'][$fieldName]['config']; switch ($fieldConfig['type']) { case 'select': case 'category': // The FormDataProviders already resolved the select items to an array of uids, // filter out empty values that occur when no related record has been selected. $possibleUids = array_filter($value); $foreignTableName = $fieldConfig['foreign_table'] ?? ''; break; case 'group': $possibleUids = $this->getRelatedGroupFieldUids($fieldConfig, $value); $foreignTableName = $this->getAllowedTableForGroupField($fieldConfig); break; case 'inline': case 'file': $possibleUids = array_filter(GeneralUtility::trimExplode(',', $value, true)); $foreignTableName = $fieldConfig['foreign_table']; break; default: $possibleUids = []; $foreignTableName = ''; } if (!empty($possibleUids) && !empty($fieldNameArray)) { if (count($possibleUids) > 1 && $result['tcaSchemata']->get($foreignTableName)->isLanguageAware() && isset($result['currentSysLanguage']) ) { $possibleUids = $this->getPossibleUidsByCurrentSysLanguage($result, $possibleUids, $foreignTableName, $result['currentSysLanguage']); } $relatedFormData = $this->getRelatedFormData($result, $foreignTableName, $possibleUids[0], $fieldNameArray[0]); if ($result['tcaSchemata']->get($result['tableName'])->isLanguageAware() && isset($result['databaseRow'][$result['tcaSchemata']->get($result['tableName'])->getCapability(TcaSchemaCapability::Language)->getLanguageField()->getName()]) ) { $relatedFormData['currentSysLanguage'] = $result['databaseRow'][$result['tcaSchemata']->get($result['tableName'])->getCapability(TcaSchemaCapability::Language)->getLanguageField()->getName()]; } $value = $this->getPlaceholderValue($fieldNameArray, $relatedFormData, $recursionLevel + 1); } if ($recursionLevel === 0 && is_array($value)) { $value = implode(', ', $value); } return (string)$value; } /** * Compile a formdata result set based on the tablename and record uid. * * @param string $tableName Name of the table for which to compile formdata * @param int $uid UID of the record for which to compile the formdata * @param string $columnToProcess The column that is required from the record * @return array The compiled formdata */ protected function getRelatedFormData(array $result, $tableName, $uid, $columnToProcess) { $fakeDataInput = [ 'request' => $result['request'], 'command' => 'edit', 'vanillaUid' => (int)$uid, 'tableName' => $tableName, 'inlineCompileExistingChildren' => false, 'columnsToProcess' => [$columnToProcess], // pass through schemata as they are immutable once they are set 'tcaSchemata' => $result['tcaSchemata'], // pass through fullTca as it is immutable once set 'fullTca' => $result['fullTca'], ]; $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class); return $formDataCompiler->compile($fakeDataInput, GeneralUtility::makeInstance(TcaInputPlaceholderRecord::class)); } /** * Return uids of related records for group type fields. Uids consisting of * multiple parts like [table]_[uid]|[title] will be reduced to integers and * validated against the allowed table. Uids without a table prefix are * accepted in any case. * * @param array $fieldConfig TCA "config" section for the group type field. * @param array $value Related group field values prepared by TcaGroup data provider */ protected function getRelatedGroupFieldUids(array $fieldConfig, $value): array { $relatedUids = []; $allowedTable = $this->getAllowedTableForGroupField($fieldConfig); // Skip if it's not a resolvable foreign table if (!$allowedTable) { return []; } // Related group values have been prepared by TcaGroup data provider, an array is expected here foreach ($value as $singleValue) { $relatedUids[] = $singleValue['uid']; } return $relatedUids; } /** * Will read the "allowed" value from the given field configuration * and returns FALSE if none or more than one has been defined. * Otherwise the name of the allowed table will be returned. * * @param array $fieldConfig TCA "config" section for the group type field. * @return bool|string */ protected function getAllowedTableForGroupField(array $fieldConfig) { $allowedTable = false; $allowedTables = GeneralUtility::trimExplode(',', $fieldConfig['allowed'], true); if (count($allowedTables) === 1) { $allowedTable = $allowedTables[0]; } return $allowedTable; } /** * E.g. sys_file is not translatable, thus the uid of the translation of it's metadata has to be retrieved here. * * Get the uid of e.g. a file metadata entry for a given sys_language_uid and the possible translated data. * If there is no translation available, return the uid of default language. * If there is no value at all, return the "possible uids". * * @param array $result * @param array $possibleUids * @param string $foreignTableName * @param int $currentLanguage * @return array */ protected function getPossibleUidsByCurrentSysLanguage(array $result, array $possibleUids, $foreignTableName, $currentLanguage) { $languageField = $result['tcaSchemata']->get($foreignTableName)->getCapability(TcaSchemaCapability::Language)->getLanguageField()->getName(); $queryBuilder = $this->connectionPool->getQueryBuilderForTable($foreignTableName); $possibleRecords = $queryBuilder->select('uid', $languageField) ->from($foreignTableName) ->where( $queryBuilder->expr()->in( 'uid', $queryBuilder->createNamedParameter($possibleUids, Connection::PARAM_INT_ARRAY) ), $queryBuilder->expr()->in( $languageField, $queryBuilder->createNamedParameter([$currentLanguage, 0], Connection::PARAM_INT_ARRAY) ) ) ->groupBy($languageField, 'uid') ->executeQuery() ->fetchAllAssociative(); if (!empty($possibleRecords)) { // Either only one record or first record matches language if (count($possibleRecords) === 1 || (int)$possibleRecords[0][$languageField] === (int)$currentLanguage ) { return [$possibleRecords[0]['uid']]; } // Language of second record matches language return [$possibleRecords[1]['uid']]; } return $possibleUids; } protected function getLanguageService(): LanguageService { return $GLOBALS['LANG']; } }