Files

386 lines
16 KiB
PHP

<?php
/*
* 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!
*/
namespace TYPO3\CMS\Backend\Form\FormDataProvider;
use TYPO3\CMS\Backend\Form\FormDataProviderInterface;
use TYPO3\CMS\Core\Utility\ArrayUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
/**
* On "new" command, initialize new database row with default data
*/
class DatabaseRowInitializeNew implements FormDataProviderInterface
{
/**
* Initialize new row with default values from various sources
* There are 4 sources of default values. Mind the order, the last takes precedence.
*
* @return array
* @throws \UnexpectedValueException
*/
public function addData(array $result)
{
if ($result['command'] !== 'new') {
return $result;
}
if (!is_array($result['databaseRow'])) {
throw new \UnexpectedValueException(
'databaseRow of table ' . $result['tableName'] . ' is not an array',
1444431128
);
}
$result = $this->setDefaultsFromUserTsConfig($result);
$result = $this->setDefaultsFromPageTsConfig($result);
$result = $this->setDefaultsFromNeighborRow($result);
$result = $this->setDefaultsFromDefaultValues($result);
$result = $this->setDefaultsFromInlineRelations($result);
$result = $this->setDefaultsFromInlineParentLanguage($result);
$result = $this->setDefaultsFromInlineParentUid($result);
$result = $this->setPid($result);
return $result;
}
/**
* Set defaults defined by user ts "TCAdefaults"
* Supports both field-level defaults and type-specific defaults
* TCAdefaults.tt_content.header_layout = 1 (field-level)
* TCAdefaults.tt_content.header_layout.types.textmedia = 3 (type-specific)
*
* @param array $result Result array
* @return array Modified result array
*/
protected function setDefaultsFromUserTsConfig(array $result)
{
$tableNameWithDot = $result['tableName'] . '.';
// Apply default values from user typo script "TCAdefaults" if any
if (isset($result['userTsConfig']['TCAdefaults.'][$tableNameWithDot])
&& is_array($result['userTsConfig']['TCAdefaults.'][$tableNameWithDot])
) {
$recordType = $this->getRecordTypeFromRow($result);
$mergedDefaults = $this->mergeTypeSpecificTcaDefaults(
$result['userTsConfig']['TCAdefaults.'][$tableNameWithDot],
$recordType
);
foreach ($mergedDefaults as $fieldName => $fieldValue) {
if (isset($result['processedTca']['columns'][$fieldName])) {
$result['databaseRow'][$fieldName] = $fieldValue;
}
}
}
return $result;
}
/**
* Set defaults defined by page ts "TCAdefaults"
* Supports both field-level defaults and type-specific defaults
* TCAdefaults.tt_content.header_layout = 1 (field-level)
* TCAdefaults.tt_content.header_layout.types.textmedia = 3 (type-specific)
*
* @param array $result Result array
* @return array Modified result array
*/
protected function setDefaultsFromPageTsConfig(array $result)
{
$tableNameWithDot = $result['tableName'] . '.';
if (isset($result['pageTsConfig']['TCAdefaults.'][$tableNameWithDot])
&& is_array($result['pageTsConfig']['TCAdefaults.'][$tableNameWithDot])
) {
$recordType = $this->getRecordTypeFromRow($result);
$mergedDefaults = $this->mergeTypeSpecificTcaDefaults(
$result['pageTsConfig']['TCAdefaults.'][$tableNameWithDot],
$recordType
);
foreach ($mergedDefaults as $fieldName => $fieldValue) {
if (isset($result['processedTca']['columns'][$fieldName])) {
$result['databaseRow'][$fieldName] = $fieldValue;
}
}
}
return $result;
}
/**
* If a neighbor row is given (if vanillaUid was negative), field can be initialized with values
* from neighbor for fields registered in TCA['ctrl']['useColumnsForDefaultValues'].
*
* @param array $result Result array
* @return array Modified result array
*/
protected function setDefaultsFromNeighborRow(array $result)
{
if (is_array($result['neighborRow'])
&& !empty($result['processedTca']['ctrl']['useColumnsForDefaultValues'])
) {
$defaultColumns = GeneralUtility::trimExplode(',', $result['processedTca']['ctrl']['useColumnsForDefaultValues'], true);
foreach ($defaultColumns as $fieldName) {
if (isset($result['processedTca']['columns'][$fieldName])
&& isset($result['neighborRow'][$fieldName])
) {
$result['databaseRow'][$fieldName] = $result['neighborRow'][$fieldName];
}
}
}
return $result;
}
/**
* Apply default values.
* These are typically carried around as "defVals" GET vars and set by controllers
* in $result['defaultValues'] array as init values.
*
* @param array $result Result array
* @return array Modified result array
*/
protected function setDefaultsFromDefaultValues(array $result)
{
$tableName = $result['tableName'];
$defaultValues = $result['defaultValues'] ?? [];
if (isset($defaultValues[$tableName]) && is_array($defaultValues[$tableName])) {
foreach ($defaultValues[$tableName] as $fieldName => $fieldValue) {
if (isset($result['processedTca']['columns'][$fieldName])) {
$result['databaseRow'][$fieldName] = $fieldValue;
}
}
}
return $result;
}
/**
* Inline scenario if a new intermediate record to an existing child-child is
* compiled. Set "foreign_selector" field of this intermediate row to given
* "childChildUid". See TcaDataCompiler array comment of inlineChildChildUid
* for more details.
*
* @param array $result Result array
* @return array Modified result array
* @throws \UnexpectedValueException
*/
protected function setDefaultsFromInlineRelations(array $result)
{
if ($result['inlineChildChildUid'] === null) {
return $result;
}
if (!is_int($result['inlineChildChildUid'])) {
throw new \UnexpectedValueException(
'An inlineChildChildUid is given for table ' . $result['tableName'] . ', but is not an integer',
1444434103
);
}
if (!isset($result['inlineParentConfig']['foreign_selector'])) {
throw new \UnexpectedValueException(
'An inlineChildChildUid is given for table ' . $result['tableName'] . ', but no foreign_selector in inlineParentConfig',
1444434102
);
}
$selectorFieldName = $result['inlineParentConfig']['foreign_selector'];
$fieldType = (string)($result['processedTca']['columns'][$selectorFieldName]['config']['type'] ?? '');
if (!in_array($fieldType, ['select', 'category', 'group'], true)) {
throw new \UnexpectedValueException(
$selectorFieldName . ' is target type of a foreign_selector field to table ' . $result['tableName'] . ' and must be either a select, category or group type field',
1444434104
);
}
if ($result['inlineChildChildUid']) {
$result['databaseRow'][$selectorFieldName] = $result['inlineChildChildUid'];
}
return $result;
}
/**
* If a new child is created in an inline relation via ajax, and if the parent is a localized record,
* the child should have the same sys_language_uid set in the field declared in ['ctrl']['languageField']
* if the child is localizable itself.
* A localized parent transfers its sys_language_uid via inlineParentConfig['inline']['parentSysLanguageUid'],
* use that value as default for the child record languageField.
*
* @param array $result Result array
* @return array Modified result array
* @throws \UnexpectedValueException
*/
protected function setDefaultsFromInlineParentLanguage(array $result): array
{
if (!isset($result['inlineParentConfig']['inline']['parentSysLanguageUid'])
|| empty($result['processedTca']['ctrl']['languageField'])
|| empty($result['processedTca']['ctrl']['transOrigPointerField'])
) {
return $result;
}
if (!MathUtility::canBeInterpretedAsInteger($result['inlineParentConfig']['inline']['parentSysLanguageUid'])) {
throw new \UnexpectedValueException(
'A sys_language_uid is set from inline parent config but the value is no integer',
1490360772
);
}
$parentSysLanguageUid = (int)$result['inlineParentConfig']['inline']['parentSysLanguageUid'];
$languageFieldName = $result['processedTca']['ctrl']['languageField'];
$result['databaseRow'][$languageFieldName] = $parentSysLanguageUid;
return $result;
}
/**
* Set the parent uid of inline relations created via ajax to the corresponding foreign field
*
* @param array $result Result array
*/
protected function setDefaultsFromInlineParentUid(array $result): array
{
$isInlineChild = $result['isInlineChild'] ?? false;
$parentField = $result['inlineParentConfig']['foreign_field'] ?? false;
if ($isInlineChild && $parentField && !empty($result['inlineParentUid'])) {
$result['databaseRow'][$parentField] = $result['inlineParentUid'];
}
return $result;
}
/**
* Set the pid. This is either the vanillaUid (see description in FormDataCompiler),
* or a pid given by page TSconfig for inline children.
*
* @param array $result Result array
* @return array Modified result array
* @throws \UnexpectedValueException
*/
protected function setPid(array $result)
{
// Set pid to vanillaUid. This can be a negative value
// if the record is added relative to another record.
$result['databaseRow']['pid'] = $result['vanillaUid'];
// In case a new inline record is created, the pid can be set to a different value
// by page TSconfig, but not by user TSconfig. This overrides the above pid selection
// and forces the pid of new inline children.
$tableNameWithDot = $result['tableName'] . '.';
if ($result['isInlineChild'] && isset($result['pageTsConfig']['TCAdefaults.'][$tableNameWithDot]['pid'])) {
if (!MathUtility::canBeInterpretedAsInteger($result['pageTsConfig']['TCAdefaults.'][$tableNameWithDot]['pid'])) {
throw new \UnexpectedValueException(
'page TSconfig setting TCAdefaults.' . $tableNameWithDot . 'pid must be a number, but given string '
. $result['pageTsConfig']['TCAdefaults.'][$tableNameWithDot]['pid'] . ' can not be interpreted as integer',
1461598332
);
}
$result['databaseRow']['pid'] = (int)$result['pageTsConfig']['TCAdefaults.'][$tableNameWithDot]['pid'];
}
return $result;
}
/**
* Determine the record type from the current database row
* and additionally fall back to defaultValues as this is
* basically the only source we have for new records at this point.
*
* @param array $result The form data result array
* @return string The record type value
*/
protected function getRecordTypeFromRow(array $result): string
{
// If recordTypeValue is already set, use it
if (is_string($result['recordTypeValue'] ?? false) && $result['recordTypeValue'] !== '') {
return $result['recordTypeValue'];
}
$recordTypeValue = '0';
// Check if there's a type field defined in TCA
if (is_string($result['processedTca']['ctrl']['type'] ?? false) && $result['processedTca']['ctrl']['type'] !== '') {
$tcaTypeField = $result['processedTca']['ctrl']['type'];
// Handle simple type field (not foreign field reference)
if (!str_contains($tcaTypeField, ':')) {
// First check if the type field exists in the database row and has a value
if (array_key_exists($tcaTypeField, $result['databaseRow']) && $result['databaseRow'][$tcaTypeField] !== null) {
$recordTypeValue = (string)$result['databaseRow'][$tcaTypeField];
} else {
// Check if the type field is set in defaultValues
$tableName = $result['tableName'];
if (isset($result['defaultValues'][$tableName][$tcaTypeField])) {
$recordTypeValue = (string)$result['defaultValues'][$tableName][$tcaTypeField];
}
}
}
}
// Apply the same fallback logic as DatabaseRecordTypeValue:
// Check the determined value actually exists as types key, otherwise fall back to 0 or 1
if (empty($result['processedTca']['types'][$recordTypeValue])) {
$recordTypeValue = !empty($result['processedTca']['types']['0']) ? '0' : '1';
}
return $recordTypeValue;
}
/**
* Merge type-specific TCAdefaults over field-level defaults
* Similar to how TCEFORM type-specific settings work
*
* @param array $tcaDefaults The TCAdefaults configuration for the table
* @param string $recordType The current record type value
* @return array Merged defaults with type-specific values taking precedence
*/
protected function mergeTypeSpecificTcaDefaults(array $tcaDefaults, string $recordType): array
{
$mergedDefaults = [];
foreach ($tcaDefaults as $fieldKey => $fieldConfiguration) {
if (str_ends_with($fieldKey, '.')) {
// This is a field with sub-configuration (potentially types)
$fieldName = rtrim($fieldKey, '.');
if (!is_array($fieldConfiguration)) {
continue;
}
$fieldDefault = $fieldConfiguration;
// Check if there are type-specific overrides
if (!empty($fieldConfiguration['types.']) && is_array($fieldConfiguration['types.'])) {
$typeSpecificConfiguration = $fieldConfiguration['types.'];
unset($fieldDefault['types.']);
// If we have a matching type-specific configuration, merge it
if (!empty($typeSpecificConfiguration[$recordType . '.']) && is_array($typeSpecificConfiguration[$recordType . '.'])) {
ArrayUtility::mergeRecursiveWithOverrule($fieldDefault, $typeSpecificConfiguration[$recordType . '.']);
} elseif (!empty($typeSpecificConfiguration[$recordType])) {
// Simple value (not array)
$mergedDefaults[$fieldName] = $typeSpecificConfiguration[$recordType];
continue;
}
}
// If the field configuration is now empty (only had types.), skip it
// Otherwise, use the first non-array value or the whole configuration
foreach ($fieldDefault as $key => $value) {
if (!str_ends_with($key, '.') && !is_array($value)) {
$mergedDefaults[$fieldName] = $value;
break;
}
}
} else {
// Simple field-level default
$mergedDefaults[$fieldKey] = $fieldConfiguration;
}
}
return $mergedDefaults;
}
}