TYPO3 v15 dev-main snapshot ()

This commit is contained in:
2026-08-10 22:31:09 +02:00
commit af8cc155b5
6818 changed files with 642608 additions and 0 deletions
@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
/*
* 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\Core\Configuration\FlexForm\Exception;
use TYPO3\CMS\Core\Exception;
/**
* Abstract exception thrown if data structures can not be resolved, found or parsed.
*/
abstract class AbstractInvalidDataStructureException extends Exception {}
@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
/*
* 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\Core\Configuration\FlexForm\Exception;
/**
* Thrown if parsed data structure contains invalid config like references in sections
*/
class InvalidDataStructureException extends AbstractInvalidDataStructureException {}
@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
/*
* 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\Core\Configuration\FlexForm\Exception;
/**
* Thrown if parseFlexFormDataStructureByIdentifier() is given an empty string
*/
class InvalidIdentifierException extends AbstractInvalidDataStructureException {}
@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
/*
* 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\Core\Configuration\FlexForm\Exception;
/**
* Thrown if TCA is invalid.
* This may happen if a record is opened that points to a data structure of
* a no longer loaded extension
*/
class InvalidTcaException extends AbstractInvalidDataStructureException {}
@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
/*
* 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\Core\Configuration\FlexForm\Exception;
/**
* Thrown if data structure can not be determined due to missing TCA schema
*/
class InvalidTcaSchemaException extends AbstractInvalidDataStructureException {}
@@ -0,0 +1,891 @@
<?php
declare(strict_types=1);
/*
* 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\Core\Configuration\FlexForm;
use Psr\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
use TYPO3\CMS\Core\Configuration\Event\AfterFlexFormDataStructureIdentifierInitializedEvent;
use TYPO3\CMS\Core\Configuration\Event\AfterFlexFormDataStructureParsedEvent;
use TYPO3\CMS\Core\Configuration\Event\AfterTcaCompilationEvent;
use TYPO3\CMS\Core\Configuration\Event\BeforeFlexFormDataStructureIdentifierInitializedEvent;
use TYPO3\CMS\Core\Configuration\Event\BeforeFlexFormDataStructureParsedEvent;
use TYPO3\CMS\Core\Configuration\Event\BeforeTcaOverridesEvent;
use TYPO3\CMS\Core\Configuration\FlexForm\Exception\InvalidDataStructureException;
use TYPO3\CMS\Core\Configuration\FlexForm\Exception\InvalidIdentifierException;
use TYPO3\CMS\Core\Configuration\FlexForm\Exception\InvalidTcaException;
use TYPO3\CMS\Core\Configuration\FlexForm\Exception\InvalidTcaSchemaException;
use TYPO3\CMS\Core\Configuration\Tca\TcaMigration;
use TYPO3\CMS\Core\Configuration\Tca\TcaPreparation;
use TYPO3\CMS\Core\Schema\Field\FlexFormFieldType;
use TYPO3\CMS\Core\Schema\TcaSchema;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Unified service class for TCA type="flex" operations.
*
* This service provides comprehensive FlexForm handling capabilities that work with both:
* - TCA Schema objects (high-level, schema-aware operations)
* - Raw TCA configuration arrays (low-level operations during schema building)
*
* Note: Using the raw TCA configuration is not recommended and only available to support
* FlexFormTools during schema building. For extensions this might be the case on using
* {@see BeforeTcaOverridesEvent} or {@see AfterTcaCompilationEvent}.
*
* Usage examples:
* ```php
* // With TCA Schema (typical application usage)
* $flexFormTools->getDataStructureIdentifier($fieldTca, $table, $field, $row, $tcaSchema);
*
* // With raw TCA array (only during schema)
* $flexFormTools->getDataStructureIdentifier($fieldTca, $table, $field, $row, $rawTcaArray);
* ```
*
* The service automatically detects the input type and uses the appropriate resolution strategy.
*/
#[Autoconfigure(public: true)]
readonly class FlexFormTools
{
public function __construct(
private EventDispatcherInterface $eventDispatcher,
private TcaMigration $tcaMigration,
private TcaPreparation $tcaPreparation,
) {}
/**
* The method locates a specific data structure from given TCA and row combination
* and returns an identifier string that can be handed around, and can be resolved
* to a single data structure later without giving $row and $tca data again.
*
* Note: The returned syntax is meant to only specify the target location of the data structure.
* It SHOULD NOT be abused and enriched with data from the record that is dealt with. For
* instance, it is not allowed to add source record specific date like the "uid" or the "pid"!
* If that is done, it is up to the hook consumer to take care of possible side effects, e.g. if
* the DataHandler copies or moves records around and those references change.
*
* This method gets: Source data that influences the target location of a data structure
* This method returns: Target specification of the data structure
*
* This method is "paired" with method parseDataStructureByIdentifier() that
* will resolve the returned syntax again and returns the data structure itself.
*
* Both methods can be extended via events to return and accept additional
* identifier strings if needed, and to transmit further information within the identifier strings.
*
* Important: The TCA for data structure definitions MUST be overridden by 'columnsOverrides'
* as the "ds" config is a string, containing the data structure or a file pointer.
*
* Note: This method and the resolving methods below are well unit tested and document all
* nasty details this way.
*
* @param array $fieldTca Full TCA of the field in question that has type=flex set
* @param string $tableName The table name of the TCA field
* @param string $fieldName The field name
* @param array $row The data row
* @param array|TcaSchema|null $schema Either be the Tca Schema object or raw TCA configuration. Only omit in
* case handling is done via events. Otherwise, this will throw an exception
* on resolving the default identifier {@see InvalidTcaSchemaException}.
* Using the raw TCA configuration is furthermore not recommended and only
* available to support FlexFormTools during schema building. For extensions
* this might be the case on using {@see BeforeTcaOverridesEvent} or
* {@see AfterTcaCompilationEvent}.
*
* @return string Identifier JSON string
* @throws \RuntimeException If TCA is misconfigured
* @throws InvalidTcaException
*/
public function getDataStructureIdentifier(array $fieldTca, string $tableName, string $fieldName, array $row, array|TcaSchema|null $schema = null): string
{
$dataStructureIdentifier = $this->eventDispatcher
->dispatch(new BeforeFlexFormDataStructureIdentifierInitializedEvent($fieldTca, $tableName, $fieldName, $row))
->getIdentifier() ?? $this->getDefaultDataStructureIdentifier($tableName, $fieldName, $row, $schema);
$dataStructureIdentifier = $this->eventDispatcher
->dispatch(new AfterFlexFormDataStructureIdentifierInitializedEvent($fieldTca, $tableName, $fieldName, $row, $dataStructureIdentifier))
->getIdentifier();
return json_encode($dataStructureIdentifier, JSON_THROW_ON_ERROR);
}
/**
* Parse a data structure identified by $identifier to the final data structure array.
* This method is called after getDataStructureIdentifier(), finds the data structure
* and returns it.
*
* Events allow to manipulate the find logic and to post process the data structure array.
*
* Important: The TCA for data structure definitions MUST be overridden by 'columnsOverrides'
* as the "ds" config is a string, containing the data structure or a file pointer.
*
* After the data structure definition is found, the method resolves:
* - FILE:EXT: prefix of the data structure itself - the ds is in a file
* - FILE:EXT: prefix for sheets - if single sheets are in files
* - Create a sDEF sheet if the data structure has non, yet.
* - TCA Migration and Preparation is done for the resolved fields
*
* After that method is run, the data structure is fully resolved to an array,
* and same base normalization is done: If the ds did not contain a sheet,
* it will have one afterward as "sDEF".
*
* This method gets: Target specification of the data structure.
* This method returns: The normalized data structure parsed to an array.
*
* @param string $identifier JSON string to find the data structure location
* @param array|TcaSchema|null $schema Either be the Tca Schema object or raw TCA configuration. Only omit in
* case handling is done via events. Otherwise, this will throw an exception
* on resolving the default identifier {@see InvalidTcaSchemaException}.
* Using the raw TCA configuration is furthermore not recommended and only
* available to support FlexFormTools during schema building. For extensions
* this might be the case on using {@see BeforeTcaOverridesEvent} or
* {@see AfterTcaCompilationEvent}.
*
* @return array Parsed and normalized data structure
* @throws InvalidIdentifierException
* @throws InvalidTcaSchemaException
* @throws InvalidDataStructureException
*/
public function parseDataStructureByIdentifier(string $identifier, array|TcaSchema|null $schema = null): array
{
// Throw an exception for an empty string. This might be a valid use case for new
// records in some situations, so this is catchable to give callers a chance to deal with that.
if ($identifier === '') {
throw new InvalidIdentifierException(
'Empty string given to parseDataStructureByIdentifier(). This exception might '
. ' be caught to handle some new record situations properly',
1478100828
);
}
$parsedIdentifier = json_decode($identifier, true);
if (!is_array($parsedIdentifier) || $parsedIdentifier === []) {
// If there is some identifier and it can't be decoded, programming error -> not catchable
throw new \RuntimeException(
'Identifier could not be decoded to an array.',
1478345642
);
}
$dataStructure = $this->eventDispatcher
->dispatch(new BeforeFlexFormDataStructureParsedEvent($parsedIdentifier))
->getDataStructure() ?? $this->getDefaultStructureForIdentifier($parsedIdentifier, $schema);
$dataStructure = $this->convertDataStructureToArray($dataStructure);
$dataStructure = $this->ensureDefaultSheet($dataStructure);
$dataStructure = $this->resolveFileDirectives($dataStructure);
$dataStructure = $this->checkMigratePrepareFlexTca($dataStructure);
return $this->eventDispatcher
->dispatch(new AfterFlexFormDataStructureParsedEvent($dataStructure, $parsedIdentifier))
->getDataStructure();
}
/**
* Clean up FlexForm value XML to hold only the values it may according to its Data Structure.
* The order of tags will follow that of the data structure.
*
* @param array|TcaSchema $schema Main schema only, no sub schema! Using the raw TCA configuration is
* furthermore not recommended and only available to support FlexFormTools
* during schema building. For extensions this might be the case on
* using {@see BeforeTcaOverridesEvent} or {@see AfterTcaCompilationEvent}.
*
* @internal Signature may change, for instance to split 'DS finding' and flexArray2Xml(),
* which would allow broader use of the method. It is currently consumed by
* cleanup:flexforms CLI only.
*/
public function cleanFlexFormXML(string $table, string $field, array $row, array|TcaSchema $schema): string
{
if ((is_array($schema) && !isset($schema['columns'][$field]['config'])) || ($schema instanceof TcaSchema && !$schema->hasField($field)) || !isset($row[$field])) {
throw new \RuntimeException('Can not clean up FlexForm XML for a column not declared in TCA or not in record.', 1697554398);
}
try {
$fieldTca = is_array($schema) ? ['config' => $schema['columns'][$field]['config']] : ['config' => $schema->getField($field)->getConfiguration()];
$dataStructureArray = $this->parseDataStructureByIdentifier($this->getDataStructureIdentifier($fieldTca, $table, $field, $row, $schema), $schema);
} catch (InvalidIdentifierException) {
// Data structure can not be resolved or parsed. Reset value to empty string.
return '';
}
$valueArray = GeneralUtility::xml2array($row[$field]);
if (!is_array($valueArray)) {
// Current flex form values can not be parsed to an array. The entire thing is invalid. Reset to empty string.
return '';
}
if (!is_array($dataStructureArray['sheets'] ?? false)) {
// We might return empty string instead of throwing here, unsure.
throw new \RuntimeException('Data structure should always declare at least one sheet', 1697555523);
}
$newValueArray = [];
foreach ($dataStructureArray['sheets'] as $sheetKey => $sheetData) {
foreach (($sheetData['ROOT']['el'] ?? []) as $sheetElementKey => $sheetElementData) {
// For all elements allowed in Data Structure.
if (($sheetElementData['type'] ?? '') === 'array') {
// This is a section.
if (!is_array($sheetElementData['el'] ?? false) || !is_array($valueArray['data'][$sheetKey]['lDEF'][$sheetElementKey]['el'] ?? false)) {
// No possible containers defined for this section in DS, or no values set for this section.
continue;
}
foreach ($valueArray['data'][$sheetKey]['lDEF'][$sheetElementKey]['el'] as $valueSectionContainerKey => $valueSectionContainers) {
// We have containers for this section in values.
if (!is_array($valueSectionContainers ?? false)) {
// Values don't validate to an array, skip.
continue;
}
foreach ($valueSectionContainers as $valueContainerType => $valueContainerElements) {
// For all value containers in this section.
if (!is_array($sheetElementData['el'][$valueContainerType]['el'] ?? false)) {
// There is no DS for this container type, skip.
continue;
}
foreach (array_keys($sheetElementData['el'][$valueContainerType]['el']) as $containerElement) {
// Container type of this value container exists in DS. Iterate DS container to pick allowed single elements.
if (isset($valueContainerElements['el'][$containerElement]['vDEF'])) {
$newValueArray['data'][$sheetKey]['lDEF'][$sheetElementKey]['el'][$valueSectionContainerKey][$valueContainerType]['el'][$containerElement]['vDEF']
= $valueContainerElements['el'][$containerElement]['vDEF'];
}
}
}
if (isset($valueSectionContainers['_TOGGLE'])) {
// This was removed in TYPO3 v13, see #102551
unset($newValueArray['data'][$sheetKey]['lDEF'][$sheetElementKey]['el'][$valueSectionContainerKey]['_TOGGLE']);
}
}
} elseif (isset($valueArray['data'][$sheetKey]['lDEF'][$sheetElementKey]['vDEF'])) {
// Not a section but a simple field. Keep value if set.
$newValueArray['data'][$sheetKey]['lDEF'][$sheetElementKey]['vDEF'] = $valueArray['data'][$sheetKey]['lDEF'][$sheetElementKey]['vDEF'];
}
}
}
return $this->flexArray2Xml($newValueArray);
}
/**
* Convert FlexForm data array to XML
*
* @internal
*/
public function flexArray2Xml(array $array): string
{
// Map the weird keys from the internal array to tags and attributes.
$options = [
'parentTagMap' => [
'data' => 'sheet',
'sheet' => 'language',
'language' => 'field',
'el' => 'field',
'field' => 'value',
'field:el' => 'el',
'el:_IS_NUM' => 'section',
'section' => 'itemType',
],
'disableTypeAttrib' => 2,
];
return '<?xml version="1.0" encoding="utf-8" standalone="yes" ?>' . LF
. GeneralUtility::array2xml($array, '', 0, 'T3FlexForms', 4, $options);
}
/**
* Parses the flexForm XML string and converts it to an array.
* The resulting array will be multidimensional, as a value "bla.blubb"
* results in two levels, and a value "bla.blubb.bla" results in three levels.
*/
public function convertFlexFormContentToArray(string $flexFormContent): array
{
$settings = [];
$flexFormArray = GeneralUtility::xml2array($flexFormContent);
$flexFormArray = $flexFormArray['data'] ?? [];
foreach ($flexFormArray as $languages) {
if (!is_array($languages['lDEF'] ?? false)) {
continue;
}
foreach ($languages['lDEF'] as $valueKey => $valueDefinition) {
if (!str_contains($valueKey, '.')) {
$settings[$valueKey] = $this->walkFlexFormNode($valueDefinition);
} else {
$valueKeyParts = explode('.', $valueKey);
$currentNode = &$settings;
foreach ($valueKeyParts as $valueKeyPart) {
$currentNode = &$currentNode[$valueKeyPart];
}
if (is_array($valueDefinition)) {
if (array_key_exists('vDEF', $valueDefinition)) {
$currentNode = $valueDefinition['vDEF'];
} else {
$currentNode = $this->walkFlexFormNode($valueDefinition);
}
} else {
$currentNode = $valueDefinition;
}
}
}
}
return $settings;
}
/**
* Parses the flexForm XML string and converts it to an array.
* The resulting array will be multidimensional. Sheets are
* respected to support property paths in multiple sheets.
*
* A value such as "settings.pageId" results in three levels:
* "'sDEF' => ['settings' => ['pageId' => 123]]" and a value such
* as "settings.storages.newsPid" results in four levels:
* "'sDEF' => ['settings' => ['storages' => ['newsPid' => 123]]]"
*
* @param string $flexFormContent flexForm xml string
*/
public function convertFlexFormContentToSheetsArray(string $flexFormContent): array
{
$settings = [];
$flexFormArray = GeneralUtility::xml2array($flexFormContent);
$flexFormArray = $flexFormArray['data'] ?? [];
foreach ($flexFormArray as $sheetName => $sheet) {
foreach ($sheet as $language => $fields) {
if ($language !== 'lDEF') {
continue;
}
foreach ($fields as $valueKey => $valueDefinition) {
if (!str_contains($valueKey, '.')) {
$settings[$sheetName][$valueKey] = $this->walkFlexFormNode($valueDefinition);
} else {
$valueKeyParts = explode('.', $valueKey);
$currentNode = &$settings[$sheetName];
foreach ($valueKeyParts as $valueKeyPart) {
$currentNode = &$currentNode[$valueKeyPart];
}
if (is_array($valueDefinition)) {
if (array_key_exists('vDEF', $valueDefinition)) {
$currentNode = $valueDefinition['vDEF'];
} else {
$currentNode = $this->walkFlexFormNode($valueDefinition);
}
} else {
$currentNode = $valueDefinition;
}
}
}
}
}
return $settings;
}
/**
* Finds data structure in TCA, defined in column config 'ds'
*
* fieldTca = [
* 'config' => [
* 'type' => 'flex',
* 'ds' => '<T3DataStructure>...' OR 'FILE:...',
* ]
* ]
*
* This method returns an array of the form:
* [
* 'type' => 'tca',
* 'tableName' => $tableName,
* 'fieldName' => $fieldName,
* 'dataStructureKey' => $key,
* ];
*
* Example:
* [
* 'type' => 'tca',
* 'tableName' => 'tt_content',
* 'fieldName' => 'pi_flexform',
* 'dataStructureKey' => 'default',
* ];
*
* In case the TCA table supports record types and the given $row uses a record type with a custom
* data structure (via columnsOverrides) the record type is used as "dataStructureKey".
*
* Example:
* [
* 'type' => 'tca',
* 'tableName' => 'tt_content',
* 'fieldName' => 'pi_flexform',
* 'dataStructureKey' => 'powermail_pi1',
* ];
*
* @return array Identifier as array, see example above
* @throws InvalidTcaException
* @throws InvalidTcaSchemaException
*/
protected function getDefaultDataStructureIdentifier(string $tableName, string $fieldName, array $row, array|TcaSchema|null $schema = null): array
{
if ($schema === null) {
throw new InvalidTcaSchemaException('Can not resolve default data structure without TCA.', 1753182123);
}
$defaultIdentifier = [
'type' => 'tca',
'tableName' => $tableName,
'fieldName' => $fieldName,
'dataStructureKey' => null,
];
return is_array($schema)
? $this->getDataStructureIdentifierFromRawTca($schema, $tableName, $fieldName, $row, $defaultIdentifier)
: $this->getDataStructureIdentifierFromTcaSchema($schema, $tableName, $fieldName, $row, $defaultIdentifier);
}
/**
* Finds and returns the data structure from TCA - defined in column config 'ds'
*
* fieldTca = [
* 'config' => [
* 'type' => 'flex',
* 'ds' => '<T3DataStructure>...' OR 'FILE:...',
* ]
* ]
*
* Based on an identifier, e.g.:
* [
* 'type' => 'tca',
* 'tableName' => 'tt_content',
* 'fieldName' => 'pi_flexform',
* 'dataStructureKey' => 'default',
* ];
*
* this method returns '<T3DataStructure>...' OR 'FILE:...'.
*
* In case the TCA table supports record types and the "dataStructureKey" points to a record type,
* which is only the case if a record type defines a custom flex config (via columnsOverrides), this
* custom data structure is returned.
*
* @return string resolved data structure
* @throws InvalidTcaSchemaException
*/
protected function getDefaultStructureForIdentifier(array $identifier, array|TcaSchema|null $schema = null): string
{
// For the default only type "tca" is handled. Custom types need to be handled by corresponding events.
if (($identifier['type'] ?? '') !== 'tca') {
throw new InvalidIdentifierException(
'Identifier ' . json_encode($identifier) . ' could not be resolved',
1478104554
);
}
$tableName = (string)($identifier['tableName'] ?? '');
$fieldName = (string)($identifier['fieldName'] ?? '');
$dataStructureKey = (string)($identifier['dataStructureKey'] ?? '');
if ($tableName === '' || $fieldName === '' || $dataStructureKey === '') {
throw new \RuntimeException(
'Incomplete "tca" based identifier: ' . json_encode($identifier),
1478113471
);
}
if ($schema === null) {
throw new InvalidTcaSchemaException('Can not resolve default data structure without TCA.', 1753182125);
}
$dataStructure = is_array($schema)
? $this->resolveDataStructureFromRawTca($schema, $fieldName, $dataStructureKey)
: $this->resolveDataStructureFromTcaSchema($schema, $tableName, $fieldName, $dataStructureKey);
if ($dataStructure === '') {
throw new InvalidIdentifierException(
'Specified identifier ' . json_encode($identifier) . ' does not resolve to a valid data structure',
1732199538
);
}
return $dataStructure;
}
protected function convertDataStructureToArray(string|array $dataStructure): array
{
if (is_array($dataStructure)) {
return $dataStructure;
}
// Resolve FILE: prefix pointing to a DS in a file
if (str_starts_with(trim($dataStructure), 'FILE:')) {
$fileName = substr(trim($dataStructure), 5);
$file = GeneralUtility::getFileAbsFileName($fileName);
if (empty($file) || !is_file($file)) {
throw new InvalidIdentifierException(
'Data structure file "' . $fileName . '" could not be resolved to an existing file',
1478105826
);
}
$dataStructure = (string)file_get_contents($file);
}
// Parse main structure
$dataStructure = GeneralUtility::xml2array($dataStructure);
// Throw if it still is not an array, probably because GeneralUtility::xml2array() failed.
// This also may happen if artificial identifiers were constructed which don't resolve. The
// flex form "exclude" access rights systems does that -> catchable
if (!is_array($dataStructure)) {
throw new InvalidIdentifierException(
'Parse error: Data structure could not be resolved to a valid structure.',
1478106090
);
}
return $dataStructure;
}
/**
* Ensures a data structure has a default sheet, and no duplicate data
*/
protected function ensureDefaultSheet(array $dataStructure): array
{
if (isset($dataStructure['ROOT']) && isset($dataStructure['sheets'])) {
throw new \RuntimeException(
'Parsed data structure has both ROOT and sheets on top level. That is invalid.',
1440676540
);
}
if (isset($dataStructure['ROOT']) && is_array($dataStructure['ROOT'])) {
$dataStructure['sheets']['sDEF']['ROOT'] = $dataStructure['ROOT'];
unset($dataStructure['ROOT']);
}
return $dataStructure;
}
/**
* Resolve FILE:EXT and EXT: for single sheets
*/
protected function resolveFileDirectives(array $dataStructure): array
{
if (isset($dataStructure['sheets']) && is_array($dataStructure['sheets'])) {
foreach ($dataStructure['sheets'] as $sheetName => $sheetStructure) {
if (!is_array($sheetStructure)) {
if (str_starts_with(trim($sheetStructure), 'FILE:')) {
$file = GeneralUtility::getFileAbsFileName(substr(trim($sheetStructure), 5));
} else {
$file = GeneralUtility::getFileAbsFileName(trim($sheetStructure));
}
if ($file && @is_file($file)) {
$sheetStructure = GeneralUtility::xml2array((string)file_get_contents($file));
}
}
$dataStructure['sheets'][$sheetName] = $sheetStructure;
}
}
return $dataStructure;
}
/**
* Check for invalid flex form structures, migrate and prepare single fields.
* @throws InvalidDataStructureException
*/
private function checkMigratePrepareFlexTca(array $dataStructure): array
{
if (!is_array($dataStructure['sheets'] ?? null)) {
return $dataStructure;
}
$newStructure = $dataStructure;
foreach ($dataStructure['sheets'] as $sheetName => $sheetStructure) {
if (!is_array($sheetStructure['ROOT']['el'])) {
continue;
}
foreach ($sheetStructure['ROOT']['el'] as $sheetElementName => $sheetElementConfig) {
if (!is_array($sheetElementConfig)) {
continue;
}
if (($sheetElementConfig['type'] ?? null) === 'array' xor ($sheetElementConfig['section'] ?? null) === '1') {
// Section element, but type=array without section=1 or vice versa is not ok
throw new InvalidDataStructureException(
'Broken data structure on field name ' . $sheetElementName . '. section without type or vice versa is not allowed',
1440685208
);
}
if (($sheetElementConfig['type'] ?? null) === 'array' && ($sheetElementConfig['section'] ?? null) === '1') {
// Section element
if (!is_array($sheetElementConfig['el'] ?? null)) {
continue;
}
foreach ($sheetElementConfig['el'] as $containerName => $containerConfig) {
if (!is_array($containerConfig['el'] ?? null)) {
continue;
}
foreach ($containerConfig['el'] as $containerElementName => $containerElementConfig) {
if (!is_array($containerElementConfig)) {
continue;
}
if (
// inline, file, group and category are always DB relations
in_array($containerElementConfig['config']['type'] ?? [], ['inline', 'file', 'folder', 'group', 'category'], true)
// MM is not allowed (usually type=select, otherwise the upper check should kick in)
|| isset($containerElementConfig['config']['MM'])
// foreign_table is not allowed (usually type=select, otherwise the upper check should kick in)
|| isset($containerElementConfig['config']['foreign_table'])
) {
// Nesting types that use DB relations in container sections is not supported.
throw new InvalidDataStructureException(
'Invalid flex form data structure on field name "' . $containerElementName . '" with element "' . $sheetElementName . '"'
. ' in section container "' . $containerName . '": Nesting elements that have database relations in flex form'
. ' sections is not allowed.',
1458745468
);
}
if (($containerElementConfig['type'] ?? null) === 'array' && ($containerElementConfig['section'] ?? null) === '1') {
// Nesting sections is not supported. Throw an exception if configured.
throw new InvalidDataStructureException(
'Invalid flex form data structure on field name "' . $containerElementName . '" with element "' . $sheetElementName . '"'
. ' in section container "' . $containerName . '": Nesting sections in container elements'
. ' sections is not allowed.',
1458745712
);
}
$containerElementConfig = $this->migrateFlexField($containerElementName, $containerElementConfig);
$containerElementConfig = $this->prepareFlexField($containerElementName, $containerElementConfig);
$newStructure['sheets'][$sheetName]['ROOT']['el'][$sheetElementName]['el'][$containerName]['el'][$containerElementName] = $containerElementConfig;
}
}
} else {
// Normal element
$sheetElementConfig = $this->migrateFlexField($sheetElementName, $sheetElementConfig);
$sheetElementConfig = $this->prepareFlexField($sheetElementName, $sheetElementConfig);
$newStructure['sheets'][$sheetName]['ROOT']['el'][$sheetElementName] = $sheetElementConfig;
}
}
}
return $newStructure;
}
private function migrateFlexField(string $fieldName, array $fieldConfig): array
{
// TcaMigration of this field. Call the TcaMigration and log any deprecations.
$dummyTca = [
'dummyTable' => [
'columns' => [
$fieldName => $fieldConfig,
],
],
];
$tcaProcessingResult = $this->tcaMigration->migrate($dummyTca);
// Messages are reset on each `migrate()` execution
$messages = $tcaProcessingResult->getMessages();
if (!empty($messages)) {
$context = 'FlexFormTools did an on-the-fly migration of a flex form data structure. This is deprecated and will be removed.'
. ' Merge the following changes into the flex form definition "' . $fieldName . '":';
array_unshift($messages, $context);
trigger_error(implode(LF, $messages), E_USER_DEPRECATED);
}
return $tcaProcessingResult->getTca()['dummyTable']['columns'][$fieldName];
}
private function prepareFlexField(string $fieldName, array $fieldConfig): array
{
$dummyTca = [
'dummyTable' => [
'columns' => [
$fieldName => $fieldConfig,
],
],
];
$preparedTca = $this->tcaPreparation->prepare($dummyTca, true);
return $preparedTca['dummyTable']['columns'][$fieldName];
}
/**
* Resolve data structure identifier from raw TCA configuration.
*/
private function getDataStructureIdentifierFromRawTca(array $schema, string $tableName, string $fieldName, array $row, array $defaultIdentifier): array
{
// Check for record type specific configuration
if (isset($schema['ctrl']['type'])) {
$recordType = $row[$schema['ctrl']['type']] ?? '';
if (isset($schema['types'][$recordType]) && ($fieldConfig = $this->getRecordTypeSpecificFieldConfig($schema, $recordType, $fieldName)) !== []) {
if ($fieldConfig['config']['type'] === 'flex' && $fieldConfig['config']['ds'] !== '') {
$defaultIdentifier['dataStructureKey'] = $recordType;
return $defaultIdentifier;
}
throw new InvalidTcaException(
'TCA misconfiguration in table "' . $tableName . '" field "' . $fieldName . '" with record type "' . $recordType . '"'
. ' The field is either not configured as type="flex" or no valid data structure is defined for this record type.',
1751796941
);
}
}
// Fall back to base field configuration
$baseField = $schema['columns'][$fieldName]['config'] ?? [];
if (($baseField['type'] ?? '') === 'flex' && ($baseField['ds'] ?? '') !== '') {
$defaultIdentifier['dataStructureKey'] = 'default';
return $defaultIdentifier;
}
throw new InvalidTcaException(
'TCA misconfiguration in table "' . $tableName . '" field "' . $fieldName . '" config section:'
. ' The field is either not configured as type="flex" or no valid data structure is defined.',
1732198005
);
}
/**
* Resolve data structure identifier from TCA Schema.
*/
private function getDataStructureIdentifierFromTcaSchema(TcaSchema $schema, string $tableName, string $fieldName, array $row, array $defaultIdentifier): array
{
if ($schema->getName() !== $tableName) {
throw new InvalidTcaSchemaException('Given Tca Schema does not match table ' . $tableName . ' from data structure identifier.', 1753182124);
}
// Check for record type specific configuration
if ($schema->supportsSubSchema()) {
$recordType = (string)($row[$schema->getSubSchemaTypeInformation()->getFieldName()] ?? '');
if ($recordType !== '' && $schema->hasSubSchema($recordType) && ($subSchema = $schema->getSubSchema($recordType))->hasField($fieldName)) {
$flexField = $subSchema->getField($fieldName);
if ($flexField instanceof FlexFormFieldType && $flexField->getDataStructure() !== '') {
$defaultIdentifier['dataStructureKey'] = $recordType;
return $defaultIdentifier;
}
throw new InvalidTcaException(
'TCA misconfiguration in table "' . $tableName . '" field "' . $fieldName . '" with record type "' . $recordType . '"'
. ' The field is either not configured as type="flex" or no valid data structure is defined for this record type.',
1751796940
);
}
}
// Fall back to base field
$baseField = $schema->getField($fieldName);
if ($baseField instanceof FlexFormFieldType && $baseField->getDataStructure() !== '') {
$defaultIdentifier['dataStructureKey'] = 'default';
return $defaultIdentifier;
}
throw new InvalidTcaException(
'TCA misconfiguration in table "' . $tableName . '" field "' . $fieldName . '" config section:'
. ' The field is either not configured as type="flex" or no valid data structure is defined.',
1732198004
);
}
/**
* Resolve data structure from raw TCA configuration.
*/
private function resolveDataStructureFromRawTca(array $schema, string $fieldName, string $dataStructureKey): string
{
// Try record type specific configuration first
if (isset($schema['ctrl']['type'], $schema['types'][$dataStructureKey])
&& ($fieldConfig = $this->getRecordTypeSpecificFieldConfig($schema, $dataStructureKey, $fieldName)) !== []
&& ($fieldConfig['config']['type'] ?? '') === 'flex'
&& is_string($fieldConfig['config']['ds'] ?? false)
) {
return $fieldConfig['config']['ds'];
}
// Fall back to default configuration
if ($dataStructureKey === 'default') {
$baseField = $schema['columns'][$fieldName]['config'] ?? [];
if (($baseField['type'] ?? '') === 'flex' && is_string($baseField['ds'] ?? false)) {
return $baseField['ds'];
}
}
return '';
}
/**
* Resolve data structure from TCA Schema.
*/
private function resolveDataStructureFromTcaSchema(TcaSchema $schema, string $table, string $field, string $dataStructureKey): string
{
if ($schema->getName() !== $table) {
throw new InvalidTcaSchemaException('Given Tca Schema does not match table ' . $table . ' from data structure identifier.', 1753182126);
}
// Try record type specific configuration first
if ($schema->supportsSubSchema()
&& $schema->hasSubSchema($dataStructureKey)
&& ($subSchema = $schema->getSubSchema($dataStructureKey))->hasField($field)
&& ($flexField = $subSchema->getField($field)) instanceof FlexFormFieldType
) {
return $flexField->getDataStructure();
}
// Fall back to default configuration
if ($dataStructureKey === 'default' && ($flexField = $schema->getField($field)) instanceof FlexFormFieldType) {
return $flexField->getDataStructure();
}
return '';
}
/**
* Returns the record type specific configuration, also already taking columnsOverrides into account.
* In case the field is not defined for the record type, no configuration is returned.
*/
protected function getRecordTypeSpecificFieldConfig(array $tcaForTable, string $recordType, string $fieldName): array
{
$recordTypeConfig = $tcaForTable['types'][$recordType];
$showItemArray = GeneralUtility::trimExplode(',', $recordTypeConfig['showitem'] ?? '', true);
foreach ($showItemArray as $aShowItemFieldString) {
[$name, , $paletteName] = GeneralUtility::trimExplode(';', $aShowItemFieldString . ';;;');
if ($name === '--div--') {
continue;
}
if ($name === '--palette--' && !empty($paletteName)) {
if (!isset($tcaForTable['palettes'][$paletteName]['showitem'])) {
continue;
}
$palettesArray = GeneralUtility::trimExplode(',', $tcaForTable['palettes'][$paletteName]['showitem']);
foreach ($palettesArray as $aPalettesString) {
[$name] = GeneralUtility::trimExplode(';', $aPalettesString . ';;');
if ($name === $fieldName && isset($tcaForTable['columns'][$name])) {
return array_replace_recursive($tcaForTable['columns'][$name], $recordTypeConfig['columnsOverrides'][$name] ?? []);
}
}
} elseif ($name === $fieldName && isset($tcaForTable['columns'][$name])) {
return array_replace_recursive($tcaForTable['columns'][$name], $recordTypeConfig['columnsOverrides'][$name] ?? []);
}
}
return [];
}
/**
* Parses a flexForm node recursively and takes care of sections etc.
* Helper method of convertFlexFormContentToArray() and convertFlexFormContentToSheetsArray().
*/
private function walkFlexFormNode(mixed $nodeArray): mixed
{
if (!is_array($nodeArray)) {
return $nodeArray;
}
$result = [];
foreach ($nodeArray as $nodeKey => $nodeValue) {
if ($nodeKey === 'vDEF') {
return $nodeValue;
}
if (in_array($nodeKey, ['el', '_arrayContainer'])) {
return $this->walkFlexFormNode($nodeValue);
}
if (($nodeKey[0] ?? '') === '_') {
continue;
}
if (strpos((string)$nodeKey, '.')) {
$nodeKeyParts = explode('.', $nodeKey);
$currentNode = &$result;
$nodeKeyPartsCount = count($nodeKeyParts);
for ($i = 0; $i < $nodeKeyPartsCount - 1; $i++) {
$currentNode = &$currentNode[$nodeKeyParts[$i]];
}
$newNode = [next($nodeKeyParts) => $nodeValue];
$subVal = $this->walkFlexFormNode($newNode);
$currentNode[key($subVal)] = current($subVal);
} elseif (is_array($nodeValue)) {
if (array_key_exists('vDEF', $nodeValue)) {
$result[$nodeKey] = $nodeValue['vDEF'];
} else {
$result[$nodeKey] = $this->walkFlexFormNode($nodeValue);
}
} else {
$result[$nodeKey] = $nodeValue;
}
}
return $result;
}
}