diff --git a/typo3/sysext/backend/Classes/Preview/FluidBasedContentPreviewRenderer.php b/typo3/sysext/backend/Classes/Preview/FluidBasedContentPreviewRenderer.php index 7c5babf68f1c906e11d1c21d7726863e9c9a68af..9a600be35095eed2f2811faa422b37b4f0af3614 100644 --- a/typo3/sysext/backend/Classes/Preview/FluidBasedContentPreviewRenderer.php +++ b/typo3/sysext/backend/Classes/Preview/FluidBasedContentPreviewRenderer.php @@ -24,6 +24,7 @@ use TYPO3\CMS\Backend\View\Event\PageContentPreviewRenderingEvent; use TYPO3\CMS\Core\Attribute\AsEventListener; use TYPO3\CMS\Core\Authentication\BackendUserAuthentication; use TYPO3\CMS\Core\Core\Environment; +use TYPO3\CMS\Core\Domain\RecordFactory; use TYPO3\CMS\Core\Service\FlexFormService; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Fluid\View\StandaloneView; @@ -42,7 +43,8 @@ final class FluidBasedContentPreviewRenderer implements LoggerAwareInterface use LoggerAwareTrait; public function __construct( - protected readonly FlexFormService $flexFormService + protected readonly FlexFormService $flexFormService, + protected readonly RecordFactory $recordFactory, ) {} #[AsEventListener('typo3-backend/fluid-preview/content')] @@ -89,6 +91,7 @@ final class FluidBasedContentPreviewRenderer implements LoggerAwareInterface if ($table === 'tt_content' && !empty($row['pi_flexform'])) { $view->assign('pi_flexform_transformed', $this->flexFormService->convertFlexFormContentToArray($row['pi_flexform'])); } + $view->assign('record', $this->recordFactory->createResolvedRecordFromDatabaseRow($table, $row)); return $view->render(); } catch (\Exception $e) { $this->logger->warning('The backend preview for content element {uid} can not be rendered using the Fluid template file "{file}"', [ diff --git a/typo3/sysext/backend/Classes/Preview/StandardContentPreviewRenderer.php b/typo3/sysext/backend/Classes/Preview/StandardContentPreviewRenderer.php index bed7e3c04181ab767fd062612ef570be5d2443a3..9338a6022dd1011b787357e252a5ceb111852e38 100644 --- a/typo3/sysext/backend/Classes/Preview/StandardContentPreviewRenderer.php +++ b/typo3/sysext/backend/Classes/Preview/StandardContentPreviewRenderer.php @@ -23,9 +23,16 @@ use TYPO3\CMS\Backend\Routing\UriBuilder; use TYPO3\CMS\Backend\Utility\BackendUtility; use TYPO3\CMS\Backend\View\BackendLayout\Grid\GridColumnItem; use TYPO3\CMS\Core\Authentication\BackendUserAuthentication; +use TYPO3\CMS\Core\Domain\RawRecord; +use TYPO3\CMS\Core\Domain\Record; +use TYPO3\CMS\Core\Domain\RecordFactory; use TYPO3\CMS\Core\Imaging\IconFactory; use TYPO3\CMS\Core\Imaging\IconSize; +use TYPO3\CMS\Core\Imaging\ImageManipulation\CropVariantCollection; use TYPO3\CMS\Core\Localization\LanguageService; +use TYPO3\CMS\Core\Resource\FileReference; +use TYPO3\CMS\Core\Resource\ProcessedFile; +use TYPO3\CMS\Core\Schema\TcaSchemaFactory; use TYPO3\CMS\Core\Type\Bitmask\Permission; use TYPO3\CMS\Core\Utility\GeneralUtility; @@ -39,6 +46,8 @@ use TYPO3\CMS\Core\Utility\GeneralUtility; * by changing this TCA configuration. * * See also PreviewRendererInterface documentation. + * + * @todo Evaluate class and streamline to properly use DI */ class StandardContentPreviewRenderer implements PreviewRendererInterface, LoggerAwareInterface { @@ -79,14 +88,15 @@ class StandardContentPreviewRenderer implements PreviewRendererInterface, Logger public function renderPageModulePreviewContent(GridColumnItem $item): string { - $recordType = $item->getRecordType(); $languageService = $this->getLanguageService(); $table = $item->getTable(); $record = $item->getRecord(); + $recordObj = GeneralUtility::makeInstance(RecordFactory::class)->createResolvedRecordFromDatabaseRow($table, $record); + $recordType = $recordObj->getRecordType(); $out = ''; // If record type is unknown, render warning message. - if ($item->getTypeColumn() !== '' && !is_array($GLOBALS['TCA'][$table]['types'][$recordType] ?? null)) { + if (!GeneralUtility::makeInstance(TcaSchemaFactory::class)->get($recordObj->getMainType())->hasSubSchema($recordType)) { $message = sprintf( $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.noMatchingValue'), $recordType @@ -105,29 +115,25 @@ class StandardContentPreviewRenderer implements PreviewRendererInterface, Logger case 'header': break; case 'uploads': - if ($record['media']) { - $out .= $this->linkEditContent($this->getThumbCodeUnlinked($record, $table, 'media'), $record); + if ($recordObj['media'] ?? false) { + $out .= $this->linkEditContent($this->getThumbCodeUnlinked($recordObj['media']), $record); } break; case 'shortcut': - if (!empty($record['records'])) { + if (!empty($recordObj['records'])) { $shortcutContent = ''; - $recordList = explode(',', $record['records']); - foreach ($recordList as $recordIdentifier) { - $split = BackendUtility::splitTable_Uid($recordIdentifier); - $shortcutTableName = empty($split[0]) ? $table : $split[0]; - $shortcutRecord = BackendUtility::getRecord($shortcutTableName, $split[1]); - if (is_array($shortcutRecord)) { - $shortcutRecord = $this->translateShortcutRecord($record, $shortcutRecord, $shortcutTableName, (int)$split[1]); - $icon = $this->getIconFactory()->getIconForRecord($shortcutTableName, $shortcutRecord, IconSize::SMALL)->render(); - $icon = BackendUtility::wrapClickMenuOnIcon( - $icon, - $shortcutTableName, - $shortcutRecord['uid'], - '1' - ); - $shortcutContent .= '<li class="list-group-item">' . $icon . ' ' . htmlspecialchars(BackendUtility::getRecordTitle($shortcutTableName, $shortcutRecord)) . '</li>'; - } + $shortcutRecords = $recordObj['records'] instanceof \Traversable ? $recordObj['records'] : [$recordObj['records']]; + foreach ($shortcutRecords as $shortcutRecord) { + $shortcutTableName = $shortcutRecord->getMainType(); + $shortcutRecord = $this->translateShortcutRecord($recordObj, $shortcutRecord, $shortcutTableName); + $icon = $this->getIconFactory()->getIconForRecord($shortcutTableName, $shortcutRecord->toArray(), IconSize::SMALL)->render(); + $icon = BackendUtility::wrapClickMenuOnIcon( + $icon, + $shortcutTableName, + $shortcutRecord->getUid(), + '1' + ); + $shortcutContent .= '<li class="list-group-item">' . $icon . ' ' . htmlspecialchars(BackendUtility::getRecordTitle($shortcutTableName, $shortcutRecord->toArray())) . '</li>'; } $out .= $shortcutContent ? '<ul class="list-group">' . $shortcutContent . '</ul>' : ''; } @@ -162,17 +168,17 @@ class StandardContentPreviewRenderer implements PreviewRendererInterface, Logger } break; default: - if ($record['bodytext']) { + if ($recordObj['bodytext'] ?? false) { $out .= $this->linkEditContent($this->renderText($record['bodytext']), $record); } - if ($record['image']) { - $out .= $this->linkEditContent($this->getThumbCodeUnlinked($record, $table, 'image'), $record); + if ($recordObj['image'] ?? false) { + $out .= $this->linkEditContent($this->getThumbCodeUnlinked($recordObj['image']), $record); } - if ($record['media']) { - $out .= $this->linkEditContent($this->getThumbCodeUnlinked($record, $table, 'media'), $record); + if ($recordObj['media'] ?? false) { + $out .= $this->linkEditContent($this->getThumbCodeUnlinked($recordObj['media']), $record); } - if ($record['assets']) { - $out .= $this->linkEditContent($this->getThumbCodeUnlinked($record, $table, 'assets'), $record); + if ($recordObj['assets'] ?? false) { + $out .= $this->linkEditContent($this->getThumbCodeUnlinked($recordObj['assets']), $record); } } @@ -232,26 +238,21 @@ class StandardContentPreviewRenderer implements PreviewRendererInterface, Logger return $preview; } - protected function translateShortcutRecord(array $targetRecord, array $shortcutRecord, string $tableName, int $uid): array + protected function translateShortcutRecord(Record $targetRecord, Record $shortcutRecord, string $tableName): RawRecord { - $targetLanguage = (int)($targetRecord['sys_language_uid'] ?? 0); - if ($targetLanguage === 0 || !BackendUtility::isTableLocalizable($tableName)) { - return $shortcutRecord; - } - - $languageField = $GLOBALS['TCA'][$tableName]['ctrl']['languageField']; - $shortcutLanguage = (int)($shortcutRecord[$languageField] ?? 0); - if ($targetLanguage === $shortcutLanguage) { - return $shortcutRecord; + $targetLanguage = ($targetRecord->getLanguageId() ?? 0); + if ($targetLanguage === 0 + || !GeneralUtility::makeInstance(TcaSchemaFactory::class)->get($tableName)->isLanguageAware() + || $targetLanguage === ($shortcutRecord->getLanguageId() ?? 0) + ) { + return $shortcutRecord->getRawRecord(); } // record is localized - fetch the shortcut record translation, if available - $shortcutRecordLocalization = BackendUtility::getRecordLocalization($tableName, $uid, $targetLanguage); - if (is_array($shortcutRecordLocalization) && !empty($shortcutRecordLocalization)) { - $shortcutRecord = $shortcutRecordLocalization[0]; - } - - return $shortcutRecord; + $shortcutRecordLocalization = BackendUtility::getRecordLocalization($tableName, $shortcutRecord->getUid(), $targetLanguage); + return is_array($shortcutRecordLocalization) && !empty($shortcutRecordLocalization) + ? GeneralUtility::makeInstance(RecordFactory::class)->createRawRecord($tableName, $shortcutRecordLocalization[0]) + : $shortcutRecord->getRawRecord(); } protected function getProcessedValue(GridColumnItem $item, string|array $fieldList, array &$info): void @@ -268,17 +269,60 @@ class StandardContentPreviewRenderer implements PreviewRendererInterface, Logger } } - /** - * Create thumbnail code for record/field but not linked - * - * @param mixed[] $row Record array - * @param string $table Table (record is from) - * @param string $field Field name for which thumbnail are to be rendered. - * @return string HTML for thumbnails, if any. - */ - protected function getThumbCodeUnlinked(array $row, string $table, string $field): string + protected function getThumbCodeUnlinked(iterable|FileReference $fileReferences): string { - return BackendUtility::thumbCode(row: $row, table: $table, field: $field, linkInfoPopup: false); + $thumbData = ''; + $fileReferences = $fileReferences instanceof FileReference ? [$fileReferences] : $fileReferences; + foreach ($fileReferences as $fileReferenceObject) { + // Do not show previews of hidden references + if ($fileReferenceObject->getProperty('hidden')) { + continue; + } + $fileObject = $fileReferenceObject->getOriginalFile(); + if ($fileObject->isMissing()) { + $missingFileIcon = $this->getIconFactory() + ->getIcon('mimetypes-other-other', IconSize::MEDIUM, 'overlay-missing') + ->setTitle(static::getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:warning.file_missing') . ' ' . $fileObject->getName()) + ->render(); + $thumbData .= '<div class="preview-thumbnails-element"><div class="preview-thumbnails-element-image">' . $missingFileIcon . '</div></div>'; + continue; + } + + // Preview web image or media elements + if ($GLOBALS['TYPO3_CONF_VARS']['GFX']['thumbnails'] + && ($fileReferenceObject->getOriginalFile()->isImage() || $fileReferenceObject->getOriginalFile()->isMediaFile()) + ) { + $cropVariantCollection = CropVariantCollection::create((string)$fileReferenceObject->getProperty('crop')); + $cropArea = $cropVariantCollection->getCropArea(); + $taskType = ProcessedFile::CONTEXT_IMAGEPREVIEW; + $processingConfiguration = [ + 'width' => 64, + 'height' => 64, + ]; + if (!$cropArea->isEmpty()) { + $taskType = ProcessedFile::CONTEXT_IMAGECROPSCALEMASK; + $processingConfiguration = [ + 'maxWidth' => 64, + 'maxHeight' => 64, + 'crop' => $cropArea->makeAbsoluteBasedOnFile($fileReferenceObject), + ]; + } + $processedImage = $fileObject->process($taskType, $processingConfiguration); + $attributes = [ + 'src' => $processedImage->getPublicUrl() ?? '', + 'width' => $processedImage->getProperty('width'), + 'height' => $processedImage->getProperty('height'), + 'alt' => $fileReferenceObject->getAlternative() ?: $fileReferenceObject->getName(), + 'loading' => 'lazy', + ]; + $imgTag = '<img ' . GeneralUtility::implodeAttributes($attributes, true) . '/>'; + } else { + $imgTag = $this->getIconFactory()->getIconForResource($fileObject)->setTitle($fileObject->getName())->render(); + } + $thumbData .= '<div class="preview-thumbnails-element"><div class="preview-thumbnails-element-image">' . $imgTag . '</div></div>'; + } + + return $thumbData ? '<div class="preview-thumbnails" style="--preview-thumbnails-size: 64px">' . $thumbData . '</div>' : ''; } /** @@ -324,12 +368,14 @@ class StandardContentPreviewRenderer implements PreviewRendererInterface, Logger } /** - * Will create a link on the input string and possibly a big button after the string which links to editing in the RTE. - * Used for content element content displayed so the user can click the content / "Edit in Rich Text Editor" button + * Will create a link on the input string and possibly a big button after the string which links to editing in the + * RTE. Used for content element content displayed so the user can click the content / "Edit in Rich Text Editor" + * button * * @param string $linkText String to link. Must be prepared for HTML output. * @param array $row The row. - * @return string If the whole thing was editable and $linkText is not empty $linkText is returned with link around. Otherwise just $linkText. + * @return string If the whole thing was editable and $linkText is not empty $linkText is returned with link + * around. Otherwise just $linkText. */ protected function linkEditContent(string $linkText, array $row, string $table = 'tt_content'): string { diff --git a/typo3/sysext/backend/Classes/Utility/BackendUtility.php b/typo3/sysext/backend/Classes/Utility/BackendUtility.php index 8c8399ccd0b5d23faca28f2b2635b5f6511a0753..7efab50781d3fd535b3f2bcd2d81759879ae54d2 100644 --- a/typo3/sysext/backend/Classes/Utility/BackendUtility.php +++ b/typo3/sysext/backend/Classes/Utility/BackendUtility.php @@ -1051,6 +1051,7 @@ class BackendUtility * @param int|string $size Optional: $size is [w]x[h] of the thumbnail. 64 is default. * @param bool $linkInfoPopup Whether to wrap with a link opening the info popup * @return string Thumbnail image tag. + * @todo Unused in Core deprecate it! */ public static function thumbCode( $row, diff --git a/typo3/sysext/backend/Classes/View/Drawing/BackendLayoutRenderer.php b/typo3/sysext/backend/Classes/View/Drawing/BackendLayoutRenderer.php index 7ae6c081843c58a4083d6fff7897b47056559c55..0dae4431c1636ff3551560817501ba42580eec66 100644 --- a/typo3/sysext/backend/Classes/View/Drawing/BackendLayoutRenderer.php +++ b/typo3/sysext/backend/Classes/View/Drawing/BackendLayoutRenderer.php @@ -74,7 +74,7 @@ class BackendLayoutRenderer // @todo: ideally we hand in the record object into the GridColumnItem in the future if (!$recordIdentityMap->hasIdentifier('tt_content', (int)($contentRecord['uid'] ?? null))) { try { - $recordObject = $this->recordFactory->createFromDatabaseRow('tt_content', $contentRecord); + $recordObject = $this->recordFactory->createResolvedRecordFromDatabaseRow('tt_content', $contentRecord); $recordIdentityMap->add($recordObject); } catch (UndefinedSchemaException) { } diff --git a/typo3/sysext/core/Classes/Collection/LazyRecordCollection.php b/typo3/sysext/core/Classes/Collection/LazyRecordCollection.php new file mode 100644 index 0000000000000000000000000000000000000000..c60e1ae259e0c1685cd5979ca4846f2479e458b5 --- /dev/null +++ b/typo3/sysext/core/Classes/Collection/LazyRecordCollection.php @@ -0,0 +1,98 @@ +<?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\Collection; + +use TYPO3\CMS\Core\Domain\RecordInterface; + +/** + * When first accessed, this class will initialize itself and find the relations + * for this record field. + * + * This class acts as a "Value holder", as it only fetches the related records + * when needed. + * + * @todo: Evaluate if we should use a Ghost object instead. + * + * @internal not part of public API, as this needs to be streamlined and proven + */ +class LazyRecordCollection implements \IteratorAggregate, \ArrayAccess, \Countable +{ + /** + * @var RecordInterface[]|\Closure + */ + private array|\Closure $items; + + public function __construct( + private readonly mixed $fieldValue, + \Closure $initialization + ) { + $this->items = $initialization; + } + + public function count(): int + { + $this->initialize(); + return count($this->items); + } + + private function initialize(): void + { + if ($this->items instanceof \Closure) { + $this->items = ($this->items)(); + } + } + + public function getIterator(): \Iterator + { + $this->initialize(); + return new \ArrayIterator($this->items); + } + + public function __toString(): string + { + return (string)$this->fieldValue; + } + + public function offsetExists(mixed $offset): bool + { + $this->initialize(); + return isset($this->items[$offset]); + } + + public function offsetGet(mixed $offset): mixed + { + $this->initialize(); + return $this->items[$offset] ?? null; + } + + public function offsetSet(mixed $offset, mixed $value): void + { + if ($value instanceof RecordInterface === false) { + throw new \InvalidArgumentException( + 'Modifying the record collection is only allowed by setting a value of type RecordInterface.', + 1723188315 + ); + } + $this->items[$offset] = $value; + } + + public function offsetUnset(mixed $offset): void + { + throw new \RuntimeException('Removing items from the record collection is not implemented.', 1723188316); + } +} diff --git a/typo3/sysext/core/Classes/Configuration/Tca/TcaPreparation.php b/typo3/sysext/core/Classes/Configuration/Tca/TcaPreparation.php index 13ea356e7a0e1743efb72ac432174e05ffbbf49e..9e477bad0a2f94514a3dbdf7fd34240fc6a071e9 100644 --- a/typo3/sysext/core/Classes/Configuration/Tca/TcaPreparation.php +++ b/typo3/sysext/core/Classes/Configuration/Tca/TcaPreparation.php @@ -47,6 +47,7 @@ readonly class TcaPreparation $tca = $this->configureEmailSoftReferences($tca); $tca = $this->configureLinkSoftReferences($tca); $tca = $this->configureSelectSingle($tca); + $tca = $this->configureRelationshipToOne($tca); return $tca; } @@ -330,7 +331,12 @@ readonly class TcaPreparation } /** - * Add "'maxitems' => 1" to all "'type' => 'select'" column fields with "'renderType' => 'selectSingle'". + * Add "'relationship' for TCA type "select" fields, having "selectSingle" set as renderType and are + * pointing to a "foreign_table". Depending on further configuration, this will set the "relationship" + * to either "manyToMany" (in case "MM" is set) or to "manyToOne". + * Already defined "relationship" is not overwritten! + * + * This is mainly done to prevent checks on the renderType, which should be avoided. */ protected function configureSelectSingle(array $tca): array { @@ -339,11 +345,40 @@ readonly class TcaPreparation continue; } foreach ($tableDefinition['columns'] as &$fieldConfig) { - if (($fieldConfig['config']['type'] ?? null) === 'select' && ($fieldConfig['config']['renderType'] ?? null) === 'selectSingle') { - // Hard set/override: 'maxitems' to 1, since selectSingle - as the name suggests - only - // allows a single item to be selected. Setting this config option allows to prevent - // further checks for the renderType, which should be avoided. - // @todo This should be solved by using a dedicated TCA type for selectSingle instead. + if (($fieldConfig['config']['type'] ?? null) !== 'select' + || ($fieldConfig['config']['renderType'] ?? null) !== 'selectSingle' + || !isset($fieldConfig['config']['foreign_table']) + || isset($fieldConfig['config']['relationship']) + ) { + continue; + } + + if (isset($fieldConfig['config']['MM'])) { + $fieldConfig['config']['relationship'] = 'manyToMany'; + } else { + $fieldConfig['config']['relationship'] = 'manyToOne'; + } + } + } + return $tca; + } + + /** + * Add "'maxitems' => 1" to all relation type column fields with 'relationship' set to 'oneToOne' or 'manyToOne'. + */ + protected function configureRelationshipToOne(array $tca): array + { + foreach ($tca as &$tableDefinition) { + if (!is_array($tableDefinition['columns'] ?? null)) { + continue; + } + foreach ($tableDefinition['columns'] as &$fieldConfig) { + $type = $fieldConfig['config']['type'] ?? null; + if (in_array($type, ['select', 'inline', 'group', 'folder', 'file'], true) + && in_array($fieldConfig['config']['relationship'] ?? null, ['oneToOne', 'manyToOne'], true) + ) { + // Hard set/override: 'maxitems' to 1, since relationship [x]ToOne - as the name suggests - + // only allows a single item to be selected. $fieldConfig['config']['maxitems'] = 1; } } diff --git a/typo3/sysext/core/Classes/DataHandling/DataHandler.php b/typo3/sysext/core/Classes/DataHandling/DataHandler.php index ee5da9a59ec84a5b0647699bea02c82799a0f8d6..6a7e569ca3d1bec0b0c4760a60733676914149f9 100644 --- a/typo3/sysext/core/Classes/DataHandling/DataHandler.php +++ b/typo3/sysext/core/Classes/DataHandling/DataHandler.php @@ -68,7 +68,6 @@ use TYPO3\CMS\Core\Schema\Capability\TcaSchemaCapability; use TYPO3\CMS\Core\Schema\Field\FieldTranslationBehaviour; use TYPO3\CMS\Core\Schema\Field\FileFieldType; use TYPO3\CMS\Core\Schema\Field\InlineFieldType; -use TYPO3\CMS\Core\Schema\RelationshipType; use TYPO3\CMS\Core\Schema\TcaSchemaFactory; use TYPO3\CMS\Core\Service\OpcodeCacheService; use TYPO3\CMS\Core\Site\Entity\SiteLanguage; @@ -6101,7 +6100,7 @@ class DataHandler ) { continue; } - if (in_array($fieldType->getRelationshipType(), [RelationshipType::List, RelationshipType::ForeignField], true)) { + if ($fieldType->getRelationshipType()->isSingularRelationship()) { $dbAnalysis = $this->createRelationHandlerInstance(); $dbAnalysis->start($value, $fieldConfig['foreign_table'], '', (int)$record['uid'], $table, $fieldConfig); $dbAnalysis->undeleteRecord = true; diff --git a/typo3/sysext/core/Classes/DataHandling/RecordFieldTransformer.php b/typo3/sysext/core/Classes/DataHandling/RecordFieldTransformer.php new file mode 100644 index 0000000000000000000000000000000000000000..607e595acb3c68671a50526501ac309ec7023ce4 --- /dev/null +++ b/typo3/sysext/core/Classes/DataHandling/RecordFieldTransformer.php @@ -0,0 +1,248 @@ +<?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\DataHandling; + +use Doctrine\DBAL\Types\Type; +use TYPO3\CMS\Core\Collection\LazyRecordCollection; +use TYPO3\CMS\Core\Context\Context; +use TYPO3\CMS\Core\Database\ConnectionPool; +use TYPO3\CMS\Core\Domain\Persistence\RecordIdentityMap; +use TYPO3\CMS\Core\Domain\RawRecord; +use TYPO3\CMS\Core\Domain\RecordFactory; +use TYPO3\CMS\Core\Domain\RecordInterface; +use TYPO3\CMS\Core\Domain\RecordPropertyClosure; +use TYPO3\CMS\Core\LinkHandling\LinkService; +use TYPO3\CMS\Core\LinkHandling\TypoLinkCodecService; +use TYPO3\CMS\Core\Resource\Collection\LazyFileReferenceCollection; +use TYPO3\CMS\Core\Resource\Collection\LazyFolderCollection; +use TYPO3\CMS\Core\Resource\FileReference; +use TYPO3\CMS\Core\Resource\Folder; +use TYPO3\CMS\Core\Resource\ResourceFactory; +use TYPO3\CMS\Core\Schema\Field\DateTimeFieldType; +use TYPO3\CMS\Core\Schema\Field\FieldTypeInterface; +use TYPO3\CMS\Core\Schema\Field\FileFieldType; +use TYPO3\CMS\Core\Schema\Field\FlexFormFieldType; +use TYPO3\CMS\Core\Schema\Field\RelationalFieldTypeInterface; +use TYPO3\CMS\Core\Schema\Field\StaticSelectFieldType; +use TYPO3\CMS\Core\Schema\FlexFormSchemaFactory; +use TYPO3\CMS\Core\Schema\RelationMap; +use TYPO3\CMS\Core\Service\FlexFormService; +use TYPO3\CMS\Core\Utility\ArrayUtility; +use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\CMS\Core\Utility\MathUtility; +use TYPO3\CMS\Frontend\Typolink\TypolinkParameter; + +/** + * This generic mapper takes a field value of a record, and maps the value + * of a field with a specific type (TCA column type) to an expanded property + * (e.g. a type "file" field to a collection of FileReference objects). + * + * Common examples are \DateTimeImmutable objects for TCA fields of type=datetime. + * + * In general, this class is very inflexible, and not configurable, + * but we hope to extend this further in the future to add custom mappings. + * + * This class also calls the RelationResolver for any kind of resolved relations, + * but tries to handle most of the logic on its own when no lazy-loading is needed. + * + * About lazy-laading: For all relation types, which do not have a "toOne" relationship, + * a lazy collection is used. The relations in this collection are only resolved once + * they are accessed. For the "toOne" relations, a RecordPropertyClosure is used, which + * also initializes the corresponding record only when accessed. While a collection could + * be empty after being resolved, a single record might resolve to NULL, in case of an + * invalid relation value. + * + * @internal This class is not part of the TYPO3 Core API. It might get moved or changed. + */ +readonly class RecordFieldTransformer +{ + public function __construct( + protected RelationResolver $relationResolver, + protected ResourceFactory $resourceFactory, + protected FlexFormService $flexFormService, + protected FlexFormSchemaFactory $flexFormSchemaFactory, + protected LinkService $linkService, + protected TypoLinkCodecService $typoLinkCodecService, + protected ConnectionPool $connectionPool, + protected RecordIdentityMap $recordIdentityMap, + ) {} + + public function transformField(FieldTypeInterface $fieldInformation, RawRecord $rawRecord, Context $context): mixed + { + $fieldValue = $rawRecord[$fieldInformation->getName()]; + + // type=file needs to be handled before RelationalFieldTypeInterface + if ($fieldInformation instanceof FileFieldType) { + if ($fieldInformation->getRelationshipType()->isToOne()) { + return new RecordPropertyClosure( + function () use ($rawRecord, $fieldInformation, $context): ?FileReference { + $fileReference = $this->relationResolver->resolveFileReferences($rawRecord, $fieldInformation, $context)[0] ?? null; + if ($fileReference === null) { + return null; + } + return new FileReference($fileReference->getProperties()); + } + ); + } + return new LazyFileReferenceCollection($fieldValue, function () use ($rawRecord, $fieldInformation, $context): array { + return $this->relationResolver->resolveFileReferences($rawRecord, $fieldInformation, $context); + }); + } + + if ($fieldInformation instanceof RelationalFieldTypeInterface) { + /** @var RecordFactory $recordFactory */ + // @todo This method is called by RecordFactory -> instantiating the factory here again shows, that those classes should actually be somehow belong together. + $recordFactory = GeneralUtility::makeInstance(RecordFactory::class); + if ($fieldInformation->getRelationshipType()->isToOne()) { + return new RecordPropertyClosure( + function () use ($rawRecord, $fieldInformation, $context, $recordFactory): ?RecordInterface { + $recordData = $this->relationResolver->resolve($rawRecord, $fieldInformation, $context)[0] ?? null; + if ($recordData === null) { + return null; + } + $dbTable = $recordData['table']; + $row = $recordData['row']; + // check RecordIdentityMap for already loaded records + if ($this->recordIdentityMap->hasIdentifier($dbTable, (int)$row['uid'])) { + $record = $this->recordIdentityMap->findByIdentifier($dbTable, (int)$row['uid']); + } else { + $record = $recordFactory->createResolvedRecordFromDatabaseRow($dbTable, $row, $context); + $this->recordIdentityMap->add($record); + } + return $record; + } + ); + } + return new LazyRecordCollection( + $fieldValue, + function () use ($rawRecord, $fieldInformation, $context, $recordFactory): array { + $relationalRecords = []; + $recordData = $this->relationResolver->resolve($rawRecord, $fieldInformation, $context); + foreach ($recordData as $singleRecordData) { + $dbTable = $singleRecordData['table']; + $row = $singleRecordData['row']; + // check RecordIdentityMap for already loaded records + if ($this->recordIdentityMap->hasIdentifier($dbTable, (int)$row['uid'])) { + $relationalRecords[] = $this->recordIdentityMap->findByIdentifier($dbTable, (int)$row['uid']); + } else { + $record = $recordFactory->createResolvedRecordFromDatabaseRow($dbTable, $row, $context); + $this->recordIdentityMap->add($record); + $relationalRecords[] = $record; + } + } + return $relationalRecords; + } + ); + } + + if ($fieldInformation->isType(TableColumnType::FOLDER)) { + if (in_array((string)($fieldInformation->getConfiguration()['relationship'] ?? ''), ['oneToOne', 'manyToOne'], true)) { + return new RecordPropertyClosure( + function () use ($fieldValue): ?Folder { + $folder = $this->resolveFoldersRecursive(GeneralUtility::trimExplode(',', (string)$fieldValue, true, 1))[0] ?? null; + if ($folder === null) { + return null; + } + return new Folder($folder->getStorage(), $folder->getIdentifier(), $folder->getName()); + } + ); + } + return new LazyFolderCollection($fieldValue, function () use ($fieldValue): array { + return $this->resolveFoldersRecursive(GeneralUtility::trimExplode(',', (string)$fieldValue, true)); + }); + } + + // Static select lists is transformed into an array of values + if ($fieldInformation instanceof StaticSelectFieldType) { + $selectForcedToSingle = (string)($fieldInformation->getConfiguration()['renderType'] ?? '') === 'selectSingle'; + return $selectForcedToSingle ? $fieldValue : GeneralUtility::trimExplode(',', (string)$fieldValue, true); + } + if ($fieldInformation->isType(TableColumnType::FLEX)) { + /** @var FlexFormFieldType $fieldInformation */ + return $this->processFlexForm($rawRecord, $fieldInformation, (string)$fieldValue, $context); + } + if ($fieldInformation->isType(TableColumnType::JSON)) { + return Type::getType('json')->convertToPHPValue((string)$fieldValue, $this->connectionPool->getConnectionForTable($rawRecord->getMainType())->getDatabasePlatform()); + } + if ($fieldInformation instanceof DateTimeFieldType) { + return $fieldValue === null || (!$fieldInformation->isNullable() && $fieldValue === 0) + ? null + : new \DateTimeImmutable((MathUtility::canBeInterpretedAsInteger($fieldValue) ? '@' : '') . $fieldValue); + } + if ($fieldInformation->isType(TableColumnType::LINK)) { + return TypolinkParameter::createFromTypolinkParts($this->typoLinkCodecService->decode($fieldValue)); + } + return $fieldValue; + } + + /** + * @return Folder[] + */ + protected function resolveFoldersRecursive(array $folders): array + { + $foldersRecursive = []; + foreach ($folders as $singleFolder) { + if ($singleFolder instanceof Folder === false) { + $singleFolder = $this->resourceFactory->getFolderObjectFromCombinedIdentifier($singleFolder); + } + $foldersRecursive[] = $singleFolder; + array_push($foldersRecursive, ...$this->resolveFoldersRecursive($singleFolder->getSubfolders())); + } + return $foldersRecursive; + } + + /** + * This method creates an array which contains all information which is valid from the + * selected Schema. Ideally, this should be "FlexRecord" objects, and also keep the original values. + * This functionality will likely change in the future. + */ + protected function processFlexForm(RawRecord $record, FlexFormFieldType $fieldInformation, mixed $fieldValue, Context $context): array + { + $plainValues = $this->flexFormService->convertFlexFormContentToArray((string)$fieldValue); + $usedSchema = $this->flexFormSchemaFactory->getSchemaForRecord( + $record, + $fieldInformation, + // @todo: RelationMap does not work in FlexForm currently, as we do not have this information persisted somewhere + new RelationMap() + ); + + if ($usedSchema !== null) { + $resolvedValues = []; + // Flatten keys (because we receive settings[mysetting] and we want settings.mysetting) + $plainValues = ArrayUtility::flattenPlain($plainValues); + $recordFactory = GeneralUtility::makeInstance(RecordFactory::class); + foreach ($plainValues as $fieldName => $plainFieldValue) { + // That's a "fun" workaround: In order to allow to process e.g. "sDEF/header", we need + // to add this to the "rawRecord" (thus, we clone it), so it is within the array + // and then set "sDEF/header" even though this is not a DB field. Then we keep it in "$fieldName" + // which actually is the plain field name (in this case "header") + $fieldInformationOfFlexField = $usedSchema->getField($fieldName); + // No field given, we just skip the value, as it is not properly defined + if ($fieldInformationOfFlexField === null) { + continue; + } + $rawRecordValues = array_replace($record->toArray(), [$fieldInformationOfFlexField->getName() => $plainFieldValue]); + $fakeRawRecordWithFlexField = $recordFactory->createRawRecord($record->getMainType(), $rawRecordValues); + $transformedValue = $this->transformField($fieldInformationOfFlexField, $fakeRawRecordWithFlexField, $context); + $resolvedValues[$fieldName] = $transformedValue; + } + return ArrayUtility::unflatten($resolvedValues); + } + return $plainValues; + } +} diff --git a/typo3/sysext/core/Classes/DataHandling/RelationResolver.php b/typo3/sysext/core/Classes/DataHandling/RelationResolver.php new file mode 100644 index 0000000000000000000000000000000000000000..72fd390e6f41ff29fb53fb3881c3aed184951b21 --- /dev/null +++ b/typo3/sysext/core/Classes/DataHandling/RelationResolver.php @@ -0,0 +1,148 @@ +<?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\DataHandling; + +use Symfony\Component\DependencyInjection\Attribute\Autowire; +use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface; +use TYPO3\CMS\Core\Context\Context; +use TYPO3\CMS\Core\Database\RelationHandler; +use TYPO3\CMS\Core\Domain\Persistence\GreedyDatabaseBackend; +use TYPO3\CMS\Core\Domain\RawRecord; +use TYPO3\CMS\Core\Domain\Record; +use TYPO3\CMS\Core\Domain\RecordInterface; +use TYPO3\CMS\Core\Resource\FileReference; +use TYPO3\CMS\Core\Resource\ResourceFactory; +use TYPO3\CMS\Core\Schema\Field\FieldTypeInterface; +use TYPO3\CMS\Core\Utility\GeneralUtility; + +/** + * Finds relations for a RelationalFieldType field such as: + * - inline + * - select with foreign_table or MM + * - group with allowed or MM + * - category + * - file + * + * Files are handled differently with specific File / FileReference objects + * instead of general Record objects. Therefore, resolveFileReferences() is used. + * + * What it does to the outside world: + * - You have a record and a field with relations and you get a collection of the related raw DB records. + * + * What it hides: + * - How the DB queries are made. + * + * The result is usually wrapped in a Closure, so it is only called when needed, + * however this piece of code does not care about how it is used. + * + * @internal not part of public API, as this needs to be streamlined and proven. + */ +readonly class RelationResolver +{ + public function __construct( + #[Autowire(service: 'cache.runtime')] + protected FrontendInterface $runtimeCache, + protected ResourceFactory $resourceFactory, + protected GreedyDatabaseBackend $greedyDatabaseBackend + ) {} + + /** + * This method currently returns an array with "table" and "row" pairs, + * but will probably return something else in the future. + * + * @return list<array{table: string, row: array<string, mixed>}> + */ + public function resolve(RecordInterface $record, FieldTypeInterface $fieldInformation, Context $context): array + { + $sortedAndGroupedIds = $this->getGroupedRelationIds($record, $fieldInformation, $context); + + $groupedByTable = []; + // group sorted items by table + foreach ($sortedAndGroupedIds as $item) { + $groupedByTable[$item['table']][] = (int)$item['id']; + } + $unorderedRows = $this->getRelationalRows($groupedByTable, $context); + $sortedRows = []; + // Sort the relation rows based on the field value + foreach ($sortedAndGroupedIds as $item) { + if (isset($unorderedRows[$item['table']][(int)$item['id']])) { + $sortedRows[] = $unorderedRows[$item['table']][(int)$item['id']]; + } + } + return $sortedRows; + } + + /** + * @return FileReference[] + */ + public function resolveFileReferences(RecordInterface $record, FieldTypeInterface $fieldInformation, Context $context): array + { + $sortedAndGroupedIds = $this->getGroupedRelationIds($record, $fieldInformation, $context); + $sortedFileReferenceIds = array_map(static fn(array $item) => (int)$item['id'], $sortedAndGroupedIds); + $rows = $this->greedyDatabaseBackend->getRows('sys_file_reference', $sortedFileReferenceIds, $context); + $fileReferenceObjects = []; + foreach ($rows as $row) { + $fileReferenceObjects[] = $this->resourceFactory->createFileReferenceObject($row); + } + return $fileReferenceObjects; + } + + /** + * We currently use the RelationHandler to resolve all records attached to a given field. + * @todo This will be replaced by querying the RefIndex directly in the future. + * + * @return array<int, array<string, mixed>> + */ + protected function getGroupedRelationIds(RecordInterface $record, FieldTypeInterface $fieldInformation, Context $context): array + { + $rawRecord = $record instanceof Record ? $record->getRawRecord() : $record; + $recordData = $rawRecord->toArray(); + $relationHandler = GeneralUtility::makeInstance(RelationHandler::class); + $relationHandler->setWorkspaceId($context->getPropertyFromAspect('workspace', 'id', 0)); + if ($rawRecord instanceof RawRecord && $rawRecord->getComputedProperties()->getLocalizedUid() > 0) { + $relationHandler->initializeForField($record->getMainType(), $fieldInformation, $rawRecord->getComputedProperties()->getLocalizedUid(), $recordData[$fieldInformation->getName()] ?? null); + } else { + $relationHandler->initializeForField($record->getMainType(), $fieldInformation, $recordData, $recordData[$fieldInformation->getName()] ?? null); + } + $relationHandler->processDeletePlaceholder(); + return $relationHandler->itemArray; + } + + /** + * Find the relations relevant for this field. This could be multiple tables! + * + * Note: While $necessaryRelationsOfRequestedField is sorted, the result will be the plain unsorted database rows. + * + * @return array<string, array<int, array{table: string, row: array<string, mixed>}>> + */ + protected function getRelationalRows(array $necessaryRelationsOfRequestedField, Context $context): array + { + $finalRows = []; + foreach ($necessaryRelationsOfRequestedField as $dbTable => $uids) { + // Let's loop over all tables, and fetch all records of the PIDs of the given UIDs in a greedy way + $rows = $this->greedyDatabaseBackend->getRows($dbTable, $uids, $context); + foreach ($rows as $row) { + $finalRows[$dbTable][(int)$row['uid']] = [ + 'table' => $dbTable, + 'row' => $row, + ]; + } + } + return $finalRows; + } +} diff --git a/typo3/sysext/core/Classes/Database/ReferenceIndex.php b/typo3/sysext/core/Classes/Database/ReferenceIndex.php index 6bc6ea034157813d2706eea1c8c3cd606fe1e46a..96be31409c71d978f19a87241e64a346120eb2b3 100644 --- a/typo3/sysext/core/Classes/Database/ReferenceIndex.php +++ b/typo3/sysext/core/Classes/Database/ReferenceIndex.php @@ -578,7 +578,7 @@ class ReferenceIndex $itemArray = $fieldRelations['itemArray']; if ($field->isType(TableColumnType::INLINE, TableColumnType::FILE) && $field instanceof RelationalFieldTypeInterface - && ($field->getRelationshipType() === RelationshipType::List || $field->getRelationshipType() === RelationshipType::ForeignField) + && $field->getRelationshipType()->isSingularRelationship() ) { // RelationHandler does not return info on hidden, starttime, endtime for inline non-MM, yet. Add this now. // @todo: Refactor RelationHandler / PlainDataResolver to (optionally?) return full child row record diff --git a/typo3/sysext/core/Classes/Database/RelationHandler.php b/typo3/sysext/core/Classes/Database/RelationHandler.php index 5f554a253d0faa37067aec9649175298001a0734..8a6837b2127209244c26f043a90ef30931959f5d 100644 --- a/typo3/sysext/core/Classes/Database/RelationHandler.php +++ b/typo3/sysext/core/Classes/Database/RelationHandler.php @@ -187,7 +187,7 @@ class RelationHandler string $tableName, array|FieldTypeInterface $fieldConfiguration, array|string|int|null $baseRecordOrUid, - string|array|null $currentValue = null + string|int|float|array|null $currentValue = null ): void { if ($fieldConfiguration instanceof FieldTypeInterface) { $fieldConfiguration = $fieldConfiguration->getConfiguration(); diff --git a/typo3/sysext/core/Classes/Domain/Persistence/GreedyDatabaseBackend.php b/typo3/sysext/core/Classes/Domain/Persistence/GreedyDatabaseBackend.php new file mode 100644 index 0000000000000000000000000000000000000000..d8f8ad2aa440693958298c1f164aa6d1d54f9154 --- /dev/null +++ b/typo3/sysext/core/Classes/Domain/Persistence/GreedyDatabaseBackend.php @@ -0,0 +1,163 @@ +<?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\Domain\Persistence; + +use Symfony\Component\DependencyInjection\Attribute\Autowire; +use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface; +use TYPO3\CMS\Core\Context\Context; +use TYPO3\CMS\Core\Context\DateTimeAspect; +use TYPO3\CMS\Core\Context\LanguageAspect; +use TYPO3\CMS\Core\Context\UserAspect; +use TYPO3\CMS\Core\Context\VisibilityAspect; +use TYPO3\CMS\Core\Database\Connection; +use TYPO3\CMS\Core\Database\ConnectionPool; +use TYPO3\CMS\Core\Domain\Access\RecordAccessVoter; +use TYPO3\CMS\Core\Domain\Repository\PageRepository; +use TYPO3\CMS\Core\Utility\GeneralUtility; + +/** + * Fetches all records of a single table of one / multiple PID(s) + * in one DB query, and stores them in a runtime cache. + * + * The records are neither grouped nor do we care about + * - sorting + * - limit / offset + * - or BE User permissions. + * + * It is "greedy" because it is meant to do simple query + * in a fast way, to reduce overlays on a "per record" basis. + * + * The records are fetched depending on + * - fe_group permissions + * - language (incl. overlays etc) + * - workspace + * - visibility restrictions (starttime / endtime / hidden / deleted) + * + * The class returns an array and does not handle objects, + * as it is very "lowlevel" and thus: internal. + * + * @internal not part of public API, as this needs to be streamlined and proven + */ +readonly class GreedyDatabaseBackend +{ + public function __construct( + #[Autowire(service: 'cache.runtime')] + protected FrontendInterface $runtimeCache, + protected RecordAccessVoter $recordAccessVoter, + protected ConnectionPool $connectionPool + ) {} + + public function getRows(string $tableName, array $uids, Context $context): array + { + // Check runtime cache first + // @todo: the runtime cache needs to be much more sophisticated + // as it needs to understand that a DB query for ID 4,5 has been made, because + // they have been fetched with IDs 2,3 in the call before. + $cacheIdentifier = $this->createRuntimeCacheIdentifier($tableName, $uids, $context); + if ($this->runtimeCache->has($cacheIdentifier)) { + $allRows = $this->runtimeCache->get($cacheIdentifier); + } else { + $queryBuilder = $this->connectionPool->getQueryBuilderForTable($tableName); + $queryBuilder + ->select('*') + ->from($tableName); + // @todo: consider a context-based query restriction container here! + + // Subselect is doing: give me the PID of the given UIDs + // So we can get a greedy query for all records of these PIDs + $queryBuilderForSubselect = $queryBuilder->getConnection()->createQueryBuilder(); + $queryBuilderForSubselect + ->select('pid') + ->from($tableName) + ->where( + $queryBuilderForSubselect->expr()->in( + 'uid', + $queryBuilder->createNamedParameter($uids, Connection::PARAM_INT_ARRAY) + ) + ); + + // Inject the subselect in the WHERE part of the main query + $queryBuilder->where( + $queryBuilder->expr()->comparison( + $queryBuilder->quoteIdentifier('pid'), + 'IN', + '(' . $queryBuilderForSubselect->getSQL() . ')' + ) + ); + + $allRows = $queryBuilder->executeQuery()->fetchAllAssociative(); + $this->runtimeCache->set($cacheIdentifier, $allRows); + } + + return $this->handleOverlays( + // Only use the records from the given UIDs + array_filter($allRows, static fn(array $row) => in_array((int)$row['uid'], $uids, true)), + $tableName, + $context + ); + } + + protected function handleOverlays(array $rows, string $dbTable, Context $context): array + { + $pageRepository = GeneralUtility::makeInstance(PageRepository::class, $context); + $finalRows = []; + foreach ($rows as $row) { + $pageRepository->versionOL($dbTable, $row); + if ($row === false) { + continue; + } + $row = $pageRepository->getLanguageOverlay($dbTable, $row); + if ($row === null) { + continue; + } + // Check fe_group, hidden, starttime, endtime etc. + if (!$this->recordAccessVoter->accessGranted($dbTable, $row, $context)) { + continue; + } + $finalRows[] = $row; + } + return $finalRows; + } + + protected function createRuntimeCacheIdentifier(string $tableName, array $uids, Context $context): string + { + sort($uids); + $cacheIdentifier = 'greedy_database_backend_' . $tableName . '-' . md5(implode('_', $uids)) . '-'; + $cacheIdentifier .= $context->getAspect('workspace')->getId() . '-'; + /** @var LanguageAspect $languageAspect */ + $languageAspect = $context->getAspect('language'); + $cacheIdentifier .= $languageAspect->getId() . '-' . $languageAspect->getOverlayType() . '-' . md5(implode('_', $languageAspect->getFallbackChain())) . '-'; + /** @var VisibilityAspect $visibilityAspect */ + $visibilityAspect = $context->getAspect('visibility'); + $cacheIdentifier .= $visibilityAspect->includeHiddenPages() ? '1' : '0'; + $cacheIdentifier .= $visibilityAspect->includeHiddenContent() ? '1' : '0'; + $cacheIdentifier .= $visibilityAspect->includeScheduledRecords() ? '1' : '0'; + $cacheIdentifier .= $visibilityAspect->includeDeletedRecords() ? '1' : '0'; + + /** @var DateTimeAspect $dateAspect */ + $dateAspect = $context->getAspect('date'); + $cacheIdentifier .= '-' . $dateAspect->get('timestamp'); + + /** @var UserAspect $userAspect */ + $userAspect = $context->getAspect('frontend.user'); + $groupIds = $userAspect->getGroupIds(); + $cacheIdentifier .= '-' . implode('_', $groupIds); + + return $cacheIdentifier; + } +} diff --git a/typo3/sysext/core/Classes/Domain/Record.php b/typo3/sysext/core/Classes/Domain/Record.php index 8ea67f0b8d532022225afc70027a824955cd743e..d0f2ba385372f4fce026688217b2baba91978a1d 100644 --- a/typo3/sysext/core/Classes/Domain/Record.php +++ b/typo3/sysext/core/Classes/Domain/Record.php @@ -27,12 +27,12 @@ use TYPO3\CMS\Core\Domain\Record\VersionInfo; * * @internal not part of public API, as this needs to be streamlined and proven */ -readonly class Record implements \ArrayAccess, RecordInterface +class Record implements \ArrayAccess, RecordInterface { public function __construct( - protected RawRecord $rawRecord, + protected readonly RawRecord $rawRecord, protected array $properties, - protected ?SystemProperties $systemProperties + protected readonly ?SystemProperties $systemProperties ) {} public function getUid(): int @@ -62,6 +62,11 @@ readonly class Record implements \ArrayAccess, RecordInterface public function toArray(bool $includeSpecialProperties = false): array { + foreach ($this->properties as $key => $property) { + if ($property instanceof RecordPropertyClosure) { + $this->properties[$key] = $property->instantiate(); + } + } if ($includeSpecialProperties) { return ['uid' => $this->getUid(), 'pid' => $this->getPid()] + $this->properties + ($this->systemProperties?->toArray() ?? []); } @@ -86,7 +91,17 @@ readonly class Record implements \ArrayAccess, RecordInterface public function offsetGet(mixed $offset): mixed { if (isset($this->properties[$offset])) { - return $this->properties[$offset]; + $property = $this->properties[$offset]; + if ($property instanceof RecordPropertyClosure) { + $property = $property->instantiate(); + $this->properties[$offset] = $property; + } + return $property; + } + + if (in_array($offset, ['uid', 'pid'], true)) { + // Enable access of uid and pid via array access + return $this->rawRecord[$offset]; } if ($this->getRecordType() === null && isset($this->rawRecord[$offset])) { diff --git a/typo3/sysext/core/Classes/Domain/RecordFactory.php b/typo3/sysext/core/Classes/Domain/RecordFactory.php index 8ae7404502b4345cfa7f24e6e4866c012ec5aefd..60c3e8b65f2d9f06ac5245f6459c7d3597b638a5 100644 --- a/typo3/sysext/core/Classes/Domain/RecordFactory.php +++ b/typo3/sysext/core/Classes/Domain/RecordFactory.php @@ -18,6 +18,8 @@ declare(strict_types=1); namespace TYPO3\CMS\Core\Domain; use Symfony\Component\DependencyInjection\Attribute\Autoconfigure; +use TYPO3\CMS\Core\Context\Context; +use TYPO3\CMS\Core\DataHandling\RecordFieldTransformer; use TYPO3\CMS\Core\Domain\Record\ComputedProperties; use TYPO3\CMS\Core\Domain\Record\LanguageInfo; use TYPO3\CMS\Core\Domain\Record\SystemProperties; @@ -32,10 +34,18 @@ use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Core\Versioning\VersionState; /** - * Creates record objects out of TCA-based database rows, - * by evaluating the TCA columns, and splits everything which is not a declared column - * for a TCA type. This is usually the case when a TCA table has a 'typeField' defined, - * such as "pages", "be_users" and "tt_content". + * Creates record objects out of TCA-based database rows by evaluating the TCA columns and splitting + * everything which is not a declared column for a TCA type. This is usually the case when a TCA table + * has a 'typeField' defined, such as "pages", "be_users" and "tt_content". + * + * In addition, the RecordFactory can create "Resolved records" by utilizing the RecordFieldTransformer. + * A "Resolved record" is checked for the actual type (TCA field column type) and is then resolved to + * - a relation (records, files or folders - wrapped in collections) + * - an exploded list (e.g. static select) + * - a FlexForm field + * - a DateTime field. + * + * This means that the field value of a "Resolved Record" is expanded to the actual types (Date objects etc.) * * @internal not part of TYPO3 Core API yet. */ @@ -43,14 +53,77 @@ use TYPO3\CMS\Core\Versioning\VersionState; readonly class RecordFactory { public function __construct( - protected TcaSchemaFactory $schemaFactory + protected TcaSchemaFactory $schemaFactory, + protected RecordFieldTransformer $fieldTransformer, ) {} /** - * Takes a full database record (the whole row), and creates a Record object out of it, based on the type - * of the record. + * Takes a full database record (the whole row), and creates a Record object out of it, + * based on the type of the record. + * + * This method does not handle special expansion of fields. + * @todo Now unused - we might want to remove this again */ public function createFromDatabaseRow(string $table, array $record): Record + { + $rawRecord = $this->createRawRecord($table, $record); + $schema = $this->schemaFactory->get($table); + $subSchema = null; + if ($schema->hasSubSchema($rawRecord->getRecordType() ?? '')) { + $subSchema = $schema->getSubSchema($rawRecord->getRecordType()); + } + + // Only use the fields that are defined in the schema + $properties = []; + foreach ($record as $fieldName => $fieldValue) { + if ($subSchema && !$subSchema->hasField($fieldName)) { + continue; + } + $properties[$fieldName] = $fieldValue; + } + return $this->createRecord($rawRecord, $properties); + } + + /** + * Create a "resolved" record. Resolved means that the fields will have + * their values resolved and extended. A typical use-case is resolving + * of related records, or using \DateTimeImmutable objects for datetime fields. + */ + public function createResolvedRecordFromDatabaseRow(string $table, array $record, ?Context $context = null): Record + { + $context = $context ?? GeneralUtility::makeInstance(Context::class); + $properties = []; + $rawRecord = $this->createRawRecord($table, $record); + $schema = $this->schemaFactory->get($table); + $subSchema = null; + if ($schema->hasSubSchema($rawRecord->getRecordType() ?? '')) { + $subSchema = $schema->getSubSchema($rawRecord->getRecordType()); + } + + // Only use the fields that are defined in the schema + foreach ($record as $fieldName => $fieldValue) { + if ($subSchema) { + if (!$subSchema->hasField($fieldName)) { + continue; + } + $schema = $subSchema; + } elseif (!$schema->hasField($fieldName)) { + continue; + } + $fieldInformation = $schema->getField($fieldName); + $properties[$fieldName] = $this->fieldTransformer->transformField( + $fieldInformation, + $rawRecord, + $context + ); + } + return $this->createRecord($rawRecord, $properties); + } + + /** + * Creates a raw record object from a table and a record array. + */ + public function createRawRecord(string $table, array $record): RawRecord { if (!$this->schemaFactory->has($table)) { throw new \InvalidArgumentException( @@ -60,30 +133,28 @@ readonly class RecordFactory } $schema = $this->schemaFactory->get($table); $fullType = $table; - $properties = []; - $subSchema = null; - $typeFieldDefinition = $schema->getSubSchemaDivisorField(); - if ($typeFieldDefinition !== null) { - if (!isset($record[$typeFieldDefinition->getName()])) { + $subSchemaDivisorField = $schema->getSubSchemaDivisorField(); + if ($subSchemaDivisorField !== null) { + $subSchemaDivisorFieldName = $subSchemaDivisorField->getName(); + if (!isset($record[$subSchemaDivisorFieldName])) { throw new \InvalidArgumentException( - 'Missing typeField "' . $typeFieldDefinition->getName() . '" in record of requested table "' . $table . '".', + 'Missing typeField "' . $subSchemaDivisorFieldName . '" in record of requested table "' . $table . '".', 1715267513, ); } - $recordType = (string)$record[$typeFieldDefinition->getName()]; + $recordType = (string)$record[$subSchemaDivisorFieldName]; $fullType .= '.' . $recordType; - $subSchema = $schema->getSubSchema($recordType); } $computedProperties = $this->extractComputedProperties($record); - $rawRecord = new RawRecord((int)$record['uid'], (int)$record['pid'], $record, $computedProperties, $fullType); + return new RawRecord((int)$record['uid'], (int)$record['pid'], $record, $computedProperties, $fullType); + } - // Only use the fields that are defined in the schema - foreach ($record as $fieldName => $fieldValue) { - if ($subSchema && !$subSchema->hasField($fieldName)) { - continue; - } - $properties[$fieldName] = $fieldValue; - } + /** + * Quick helper function in order to avoid duplicate code. + */ + protected function createRecord(RawRecord $rawRecord, array $properties): Record + { + $schema = $this->schemaFactory->get($rawRecord->getMainType()); [$properties, $systemProperties] = $this->extractSystemInformation( $schema, $rawRecord, diff --git a/typo3/sysext/core/Classes/Domain/RecordPropertyClosure.php b/typo3/sysext/core/Classes/Domain/RecordPropertyClosure.php new file mode 100644 index 0000000000000000000000000000000000000000..0d019bc17a5d3e178daa1744a5a80863ba0e3b1f --- /dev/null +++ b/typo3/sysext/core/Classes/Domain/RecordPropertyClosure.php @@ -0,0 +1,35 @@ +<?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\Domain; + +/** + * Used to initialize a record once a property is accessed + * + * @internal + */ +final readonly class RecordPropertyClosure +{ + public function __construct( + private \Closure $instantiator + ) {} + + public function instantiate(): ?object + { + return ($this->instantiator)(); + } +} diff --git a/typo3/sysext/core/Classes/Resource/Collection/LazyFileReferenceCollection.php b/typo3/sysext/core/Classes/Resource/Collection/LazyFileReferenceCollection.php new file mode 100644 index 0000000000000000000000000000000000000000..8ebb724d246d08fb3215d262089897818de7f9ff --- /dev/null +++ b/typo3/sysext/core/Classes/Resource/Collection/LazyFileReferenceCollection.php @@ -0,0 +1,96 @@ +<?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\Resource\Collection; + +use TYPO3\CMS\Core\Resource\FileReference; + +/** + * When first accessed, this class will initialize itself and find the file references + * for this record field. + * + * This class acts as a "Value holder", as it only fetches the related file references + * when needed. + * + * @internal not part of public API, as this needs to be streamlined and proven + */ +class LazyFileReferenceCollection implements \IteratorAggregate, \ArrayAccess, \Countable +{ + /** + * @var FileReference[]|\Closure + */ + private array|\Closure $items; + + public function __construct( + private readonly mixed $fieldValue, + \Closure $initialization + ) { + $this->items = $initialization; + } + + public function count(): int + { + $this->initialize(); + return count($this->items); + } + + private function initialize(): void + { + if ($this->items instanceof \Closure) { + $this->items = ($this->items)(); + } + } + + public function getIterator(): \Iterator + { + $this->initialize(); + return new \ArrayIterator($this->items); + } + + public function __toString(): string + { + return (string)$this->fieldValue; + } + + public function offsetExists(mixed $offset): bool + { + $this->initialize(); + return isset($this->items[$offset]); + } + + public function offsetGet(mixed $offset): mixed + { + $this->initialize(); + return $this->items[$offset] ?? null; + } + + public function offsetSet(mixed $offset, mixed $value): void + { + if ($value instanceof FileReference === false) { + throw new \InvalidArgumentException( + 'Modifying the file reference collection is only allowed by setting a value of type FileReference.', + 1723188317 + ); + } + $this->items[$offset] = $value; + } + + public function offsetUnset(mixed $offset): void + { + throw new \RuntimeException('Removing items from the file reference collection is not implemented.', 1723188318); + } +} diff --git a/typo3/sysext/core/Classes/Resource/Collection/LazyFolderCollection.php b/typo3/sysext/core/Classes/Resource/Collection/LazyFolderCollection.php new file mode 100644 index 0000000000000000000000000000000000000000..6a56aa7d8b98f7b10e142192a170e01c9d3eb4a9 --- /dev/null +++ b/typo3/sysext/core/Classes/Resource/Collection/LazyFolderCollection.php @@ -0,0 +1,96 @@ +<?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\Resource\Collection; + +use TYPO3\CMS\Core\Resource\Folder; + +/** + * When first accessed, this class will initialize itself and find the folders + * for this record field. + * + * This class acts as a "Value holder", as it only fetches the related folders + * when needed. + * + * @internal not part of public API, as this needs to be streamlined and proven + */ +class LazyFolderCollection implements \IteratorAggregate, \ArrayAccess, \Countable +{ + /** + * @var Folder[]|\Closure + */ + private array|\Closure $items; + + public function __construct( + private readonly mixed $fieldValue, + \Closure $initialization + ) { + $this->items = $initialization; + } + + public function count(): int + { + $this->initialize(); + return count($this->items); + } + + private function initialize(): void + { + if ($this->items instanceof \Closure) { + $this->items = ($this->items)(); + } + } + + public function getIterator(): \Iterator + { + $this->initialize(); + return new \ArrayIterator($this->items); + } + + public function __toString(): string + { + return (string)$this->fieldValue; + } + + public function offsetExists(mixed $offset): bool + { + $this->initialize(); + return isset($this->items[$offset]); + } + + public function offsetGet(mixed $offset): mixed + { + $this->initialize(); + return $this->items[$offset] ?? null; + } + + public function offsetSet(mixed $offset, mixed $value): void + { + if ($value instanceof Folder === false) { + throw new \InvalidArgumentException( + 'Modifying the folder collection is only allowed by setting a value of type Folder.', + 1724136133 + ); + } + $this->items[$offset] = $value; + } + + public function offsetUnset(mixed $offset): void + { + throw new \RuntimeException('Removing items from the folder collection is not implemented.', 1724136134); + } +} diff --git a/typo3/sysext/core/Classes/Schema/Field/DateTimeFieldType.php b/typo3/sysext/core/Classes/Schema/Field/DateTimeFieldType.php index 7c8e2f00bd87f3cf356dcb5a27d1e8fede42f492..e20639f638f52156a4529e59c89029cc6fd801a4 100644 --- a/typo3/sysext/core/Classes/Schema/Field/DateTimeFieldType.php +++ b/typo3/sysext/core/Classes/Schema/Field/DateTimeFieldType.php @@ -37,6 +37,11 @@ final readonly class DateTimeFieldType extends AbstractFieldType implements Fiel return $this->configuration['dbtype'] ?? null; } + public function isNullable(): bool + { + return (bool)($this->configuration['nullable'] ?? false); + } + public static function __set_state(array $state): self { return new self(...$state); diff --git a/typo3/sysext/core/Classes/Schema/FieldTypeFactory.php b/typo3/sysext/core/Classes/Schema/FieldTypeFactory.php index d6b927de128fc96f95d850684f9294e2573a1c76..de796efa460f5d9b50c30c2cdfc4a34f6773d06f 100644 --- a/typo3/sysext/core/Classes/Schema/FieldTypeFactory.php +++ b/typo3/sysext/core/Classes/Schema/FieldTypeFactory.php @@ -104,15 +104,16 @@ class FieldTypeFactory // Build all schemata first return $this->createFlexFormField($parentSchemaName ?? $schemaName, $fieldName, $configuration, $relationMap, $parentSchemaName ? $schemaName : null); case 'select': - if (RelationshipType::fromTcaConfiguration($configuration) !== RelationshipType::Static) { - return $this->createFromTca(SelectRelationFieldType::class, $fieldName, $configuration, $relationMap->getActiveRelations($parentSchemaName ?? $schemaName, $parentFieldName ?? $fieldName, $parentFieldName ? $fieldName : null)); + // In case type "select" is used without any relationship information, it's a static list + if (RelationshipType::fromTcaConfiguration($configuration) === RelationshipType::Undefined) { + return $this->createFromTca(StaticSelectFieldType::class, $fieldName, $configuration); } - return $this->createFromTca(StaticSelectFieldType::class, $fieldName, $configuration); + return $this->createFromTca(SelectRelationFieldType::class, $fieldName, $configuration, $relationMap->getActiveRelations($parentSchemaName ?? $schemaName, $parentFieldName ?? $fieldName)); default: if ($this->hasFieldType($fieldType)) { $fieldTypeClass = $this->availableFieldTypes[$fieldType]; if (is_a($fieldTypeClass, RelationalFieldTypeInterface::class, true)) { - return $this->createFromTca($fieldTypeClass, $fieldName, $configuration, $relationMap->getActiveRelations($parentSchemaName ?? $schemaName, $parentFieldName ?? $fieldName, $parentFieldName ? $fieldName : null)); + return $this->createFromTca($fieldTypeClass, $fieldName, $configuration, $relationMap->getActiveRelations($parentSchemaName ?? $schemaName, $parentFieldName ?? $fieldName)); } return $this->createFromTca($fieldTypeClass, $fieldName, $configuration); diff --git a/typo3/sysext/core/Classes/Schema/FlexFormSchema.php b/typo3/sysext/core/Classes/Schema/FlexFormSchema.php index f176c1bee66f4b5bcfc582c3da7d47eb9d47159c..5c5a3268d3f82509669f476f21463eef6aebcf69 100644 --- a/typo3/sysext/core/Classes/Schema/FlexFormSchema.php +++ b/typo3/sysext/core/Classes/Schema/FlexFormSchema.php @@ -17,6 +17,7 @@ declare(strict_types=1); namespace TYPO3\CMS\Core\Schema; +use TYPO3\CMS\Core\Schema\Field\FieldTypeInterface; use TYPO3\CMS\Core\Schema\Struct\FlexSheet; /** @@ -40,6 +41,23 @@ final readonly class FlexFormSchema implements SchemaInterface return $this->structIdentifier; } + public function getField(string $fieldName, string $sheetName = 'sDEF'): ?FieldTypeInterface + { + if (!isset($this->sheets[$sheetName])) { + return null; + } + if ($this->sheets[$sheetName]->hasField($sheetName . '/' . $fieldName)) { + return $this->sheets[$sheetName]->getField($sheetName . '/' . $fieldName); + } + // Look for any kind of field that has the same name, regardless of the sheet name + foreach ($this->sheets as $sheetName => $sheet) { + if ($sheet->hasField($sheetName . '/' . $fieldName)) { + return $sheet->getField($sheetName . '/' . $fieldName); + } + } + return null; + } + public static function __set_state(array $state): self { return new self(...$state); diff --git a/typo3/sysext/core/Classes/Schema/FlexFormSchemaFactory.php b/typo3/sysext/core/Classes/Schema/FlexFormSchemaFactory.php index 6c6fffe86d2a764a7f599d9c35eea6576b55c080..f1172295a90b65ac758314e4091d4153e5662929 100644 --- a/typo3/sysext/core/Classes/Schema/FlexFormSchemaFactory.php +++ b/typo3/sysext/core/Classes/Schema/FlexFormSchemaFactory.php @@ -19,12 +19,15 @@ namespace TYPO3\CMS\Core\Schema; use Symfony\Component\DependencyInjection\Attribute\Autoconfigure; use TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools; +use TYPO3\CMS\Core\Domain\RawRecord; use TYPO3\CMS\Core\Schema\Field\FieldCollection; +use TYPO3\CMS\Core\Schema\Field\FlexFormFieldType; use TYPO3\CMS\Core\Schema\Struct\FlexSectionContainer; use TYPO3\CMS\Core\Schema\Struct\FlexSheet; /** - * Parses all possibles schemas of all sheets of a field + * Parses all possibles schemas of all sheets of a field. + * * @internal This is an experimental implementation and might change until TYPO3 v13 LTS */ #[Autoconfigure(public: true, shared: true)] @@ -35,10 +38,44 @@ final readonly class FlexFormSchemaFactory protected FieldTypeFactory $fieldTypeFactory ) {} + /** + * Currently this mixes Schema and Record Information, and could be handled in a cleaner way, especially + * with the list_type resolving. This method signature will most likely change. + */ + public function getSchemaForRecord(RawRecord $record, FlexFormFieldType $field, RelationMap $relationMap): ?FlexFormSchema + { + $allSchemata = $this->createSchemataForFlexField( + $field->getConfiguration(), + $record->getMainType(), + $field->getName(), + $relationMap + ); + + $structIdentifierParts = [ + $record->getMainType(), + $field->getName(), + ]; + $alternativeIdentifierParts = $structIdentifierParts; + if ($record->getRecordType()) { + $structIdentifierParts[] = $record->getRecordType(); + $alternativeIdentifierParts[] = '*,' . $record->getRecordType(); + } + + $structIdentifier = implode('/', $structIdentifierParts); + $alternativeIdentifier = implode('/', $alternativeIdentifierParts); + + foreach ($allSchemata as $schema) { + if ($schema->getName() === $structIdentifier || $schema->getName() === $alternativeIdentifier) { + return $schema; + } + } + return null; + } + /** * @return FlexFormSchema[] */ - public function createSchemataForFlexField(array $tcaConfig, string $tableName, string $fieldName, RelationMap $relationMap): array + protected function createSchemataForFlexField(array $tcaConfig, string $tableName, string $fieldName, RelationMap $relationMap): array { // Create schema for each possibility we have $flexSchemas = []; diff --git a/typo3/sysext/core/Classes/Schema/RelationMap.php b/typo3/sysext/core/Classes/Schema/RelationMap.php index 91c6146fe3234a1317ff7f6267a6a0278c85e265..bd094d2cb588712bb4ca3fb481250dd8e7b73f46 100644 --- a/typo3/sysext/core/Classes/Schema/RelationMap.php +++ b/typo3/sysext/core/Classes/Schema/RelationMap.php @@ -44,7 +44,7 @@ final class RelationMap $fromFieldName, $toTable, $fieldConfig['MM'], - $fieldConfig['MM_opposite'] ?? null, + $fieldConfig['MM_opposite_field'] ?? null, $flexPointer ); } else { @@ -58,10 +58,10 @@ final class RelationMap $fromFieldName, $fieldConfig['foreign_table'], $fieldConfig['MM'], - $fieldConfig['MM_opposite'] ?? null, + $fieldConfig['MM_opposite_field'] ?? null, $flexPointer ); - } elseif (isset($fieldConfig['foreign_table']) && isset($fieldConfig['foreign_field'])) { + } elseif (isset($fieldConfig['foreign_table'], $fieldConfig['foreign_field'])) { $this->addActiveRelationWithField( $fromTable, $fromFieldName, @@ -81,12 +81,12 @@ final class RelationMap } } - protected function addMMRelation(string $fromTable, string $fromField, string $toTable, string $mm, ?string $mmOpposite = null, ?string $flexPointer = null): void + protected function addMMRelation(string $fromTable, string $fromField, string $toTable, string $mm, ?string $mmOppositeField = null, ?string $flexPointer = null): void { $this->relations[$fromTable][$fromField][] = [ 'target' => $toTable, 'mm' => $mm, - 'mmOpposite' => $mmOpposite, + 'mmOppositeField' => $mmOppositeField, 'flexPointer' => $flexPointer, ]; } @@ -111,21 +111,14 @@ final class RelationMap /** * @return ActiveRelation[] */ - public function getActiveRelations(string $tableName, ?string $fieldName = null, ?string $flexPointer = null): array + public function getActiveRelations(string $tableName, string $fieldName): array { - if ($fieldName === null) { - $relations = []; - foreach ($this->relations[$tableName] as $fieldRelations) { - $relations = array_merge($relations, $fieldRelations); - } - return array_map([$this, 'makeActiveRelation'], $relations); - } return array_map([$this, 'makeActiveRelation'], $this->relations[$tableName][$fieldName] ?? []); } protected function makeActiveRelation(array $relation): ActiveRelation { - return new ActiveRelation($relation['target'], $relation['targetField'] ?? null); + return new ActiveRelation($relation['mm'] ?? $relation['target'], $relation['mmOppositeField'] ?? $relation['targetField'] ?? null); } /** diff --git a/typo3/sysext/core/Classes/Schema/RelationshipType.php b/typo3/sysext/core/Classes/Schema/RelationshipType.php index 604dc654154551d85155695d3120c747cc61db20..541112fe4273c7a76bcca9debd04d4e64c4b878d 100644 --- a/typo3/sysext/core/Classes/Schema/RelationshipType.php +++ b/typo3/sysext/core/Classes/Schema/RelationshipType.php @@ -22,9 +22,15 @@ namespace TYPO3\CMS\Core\Schema; */ enum RelationshipType: string { - // The reference to the parent is stored in a pointer field in the child record - // Typically used when 'foreign_field' is set - case ForeignField = 'field'; + // A direct relation, e.g. sys_file.metadata => sys_file_metadata + case OneToOne = '1:1'; + + // A record with active relations, e.g. inline elements blog_article.comments => comment. The reference + // to the left side is stored in a pointer field in the right side. Typically used when 'foreign_field' is set. + case OneToMany = '1:n'; + + // One item is selected on the active site, e.g. be_users.avatar => file, while file can be selected by any user + case ManyToOne = 'n:1'; // Regular MM intermediate table is used to store data case ManyToMany = 'mm'; @@ -32,10 +38,7 @@ enum RelationshipType: string // An item list (separated by comma) is stored (like select type is doing) case List = 'list'; - // Do we really need this? - // @todo check if we need this - case Static = 'static'; - + // Type can not be defined case Undefined = ''; public static function fromTcaConfiguration(array $configuration): self @@ -46,18 +49,36 @@ enum RelationshipType: string if (isset($configuration['MM'])) { return self::ManyToMany; } + if (isset($configuration['relationship'])) { + return match ($configuration['relationship']) { + 'oneToOne' => self::OneToOne, + 'oneToMany' => self::OneToMany, + 'manyToOne' => self::ManyToOne, + default => throw new \UnexpectedValueException('Invalid relationship type: ' . $configuration['relationship'], 1724661829), + }; + } if (isset($configuration['foreign_field'])) { - return self::ForeignField; + return self::OneToMany; } if (isset($configuration['foreign_table'])) { + // ManyToOne (as with `renderType=selectSingle`) is + // handled by `relationship` configuration above. + // See `TcaPreparation::configureSelectSingle()`. return self::List; } if (($configuration['type'] ?? '') === 'group') { return self::List; } - if (($configuration['type'] ?? '') === 'select') { - return self::Static; - } return self::Undefined; } + + public function isToOne(): bool + { + return in_array($this, [self::OneToOne, self::ManyToOne], true); + } + + public function isSingularRelationship(): bool + { + return in_array($this, [self::OneToOne, self::ManyToOne, self::OneToMany, self::List], true); + } } diff --git a/typo3/sysext/core/Configuration/TCA/Overrides/sys_file_metadata.php b/typo3/sysext/core/Configuration/TCA/Overrides/sys_file_metadata.php index 68ae369174b4672b523efec1f19c29dc114d2501..e827e84629240681342f85b5f2636217656a70e0 100644 --- a/typo3/sysext/core/Configuration/TCA/Overrides/sys_file_metadata.php +++ b/typo3/sysext/core/Configuration/TCA/Overrides/sys_file_metadata.php @@ -10,6 +10,6 @@ $GLOBALS['TCA']['sys_file_metadata']['columns']['l10n_parent']['config'] = [ 'type' => 'group', 'allowed' => 'sys_file_metadata', 'size' => 1, - 'maxitems' => 1, + 'relationship' => 'manyToOne', 'default' => 0, ]; diff --git a/typo3/sysext/core/Configuration/TCA/Overrides/sys_file_reference.php b/typo3/sysext/core/Configuration/TCA/Overrides/sys_file_reference.php index fc11ef456ebeec413575d66bee652710cdee083a..3cc87e81ba2b6b61d391728a1398077ae114178b 100644 --- a/typo3/sysext/core/Configuration/TCA/Overrides/sys_file_reference.php +++ b/typo3/sysext/core/Configuration/TCA/Overrides/sys_file_reference.php @@ -10,6 +10,6 @@ $GLOBALS['TCA']['sys_file_reference']['columns']['l10n_parent']['config'] = [ 'type' => 'group', 'allowed' => 'sys_file_reference', 'size' => 1, - 'maxitems' => 1, + 'relationship' => 'manyToOne', 'default' => 0, ]; diff --git a/typo3/sysext/core/Configuration/TCA/be_users.php b/typo3/sysext/core/Configuration/TCA/be_users.php index 8600f260a055020d7ec6cb91a4870db729ed0c69..e0589c14ef607164122bca0c3c099f570d144012 100644 --- a/typo3/sysext/core/Configuration/TCA/be_users.php +++ b/typo3/sysext/core/Configuration/TCA/be_users.php @@ -92,7 +92,7 @@ return [ 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_tca.xlf:be_users.avatar', 'config' => [ 'type' => 'file', - 'maxitems' => 1, + 'relationship' => 'manyToOne', 'allowed' => 'common-image-types', ], ], diff --git a/typo3/sysext/core/Configuration/TCA/pages.php b/typo3/sysext/core/Configuration/TCA/pages.php index dc1214977b3fece5be9a3021917c79805f519177..32d4683a721e2f2f69206cb4cf3cd0b6f36131fc 100644 --- a/typo3/sysext/core/Configuration/TCA/pages.php +++ b/typo3/sysext/core/Configuration/TCA/pages.php @@ -392,7 +392,7 @@ return [ 'type' => 'group', 'allowed' => 'pages', 'size' => 1, - 'maxitems' => 1, + 'relationship' => 'manyToOne', 'suggestOptions' => [ 'default' => [ 'additionalSearchFields' => 'nav_title, url', @@ -442,7 +442,7 @@ return [ 'type' => 'group', 'allowed' => 'pages', 'size' => 1, - 'maxitems' => 1, + 'relationship' => 'manyToOne', 'suggestOptions' => [ 'default' => [ 'additionalSearchFields' => 'nav_title, url', @@ -462,7 +462,7 @@ return [ 'type' => 'group', 'allowed' => 'pages', 'size' => 1, - 'maxitems' => 1, + 'relationship' => 'manyToOne', 'default' => 0, ], ], diff --git a/typo3/sysext/core/Configuration/TCA/sys_file.php b/typo3/sysext/core/Configuration/TCA/sys_file.php index 4389aa75cf2f3b1ea3b9397cfa00c37ba8ca697c..6f12651232c8e241f18dcd4f949b1983178f2375 100644 --- a/typo3/sysext/core/Configuration/TCA/sys_file.php +++ b/typo3/sysext/core/Configuration/TCA/sys_file.php @@ -118,7 +118,7 @@ return [ 'foreign_field' => 'file', 'size' => 1, 'minitems' => 1, - 'maxitems' => 1, + 'relationship' => 'oneToOne', ], ], ], diff --git a/typo3/sysext/core/Configuration/TCA/sys_file_collection.php b/typo3/sysext/core/Configuration/TCA/sys_file_collection.php index 2157667599f440be69c52c351b4e372460d77ecb..bec526409de52ee1914b0beca5d48f3665a8f1d7 100644 --- a/typo3/sysext/core/Configuration/TCA/sys_file_collection.php +++ b/typo3/sysext/core/Configuration/TCA/sys_file_collection.php @@ -60,7 +60,7 @@ return [ 'config' => [ 'type' => 'folder', 'minitems' => 1, - 'maxitems' => 1, + 'relationship' => 'manyToOne', 'size' => 1, ], ], diff --git a/typo3/sysext/core/Configuration/TCA/sys_file_reference.php b/typo3/sysext/core/Configuration/TCA/sys_file_reference.php index d54b4f1b8a1ae3089b303b9cab746f6394a5c396..5f494738b3f52080775b0c9b94da9a42a09912e3 100644 --- a/typo3/sysext/core/Configuration/TCA/sys_file_reference.php +++ b/typo3/sysext/core/Configuration/TCA/sys_file_reference.php @@ -32,7 +32,7 @@ return [ 'config' => [ 'type' => 'group', 'size' => 1, - 'maxitems' => 1, + 'relationship' => 'manyToOne', 'allowed' => 'sys_file', 'hideSuggest' => true, ], diff --git a/typo3/sysext/core/Configuration/TCA/sys_filemounts.php b/typo3/sysext/core/Configuration/TCA/sys_filemounts.php index e3d256de0469ff68df672eadf03ed006be700753..d2077482d1b3a0c80a5e93827601ddb04a758806 100644 --- a/typo3/sysext/core/Configuration/TCA/sys_filemounts.php +++ b/typo3/sysext/core/Configuration/TCA/sys_filemounts.php @@ -38,7 +38,7 @@ return [ 'config' => [ 'type' => 'folder', 'required' => true, - 'maxitems' => 1, + 'relationship' => 'manyToOne', 'size' => 1, ], ], diff --git a/typo3/sysext/core/Documentation/Changelog/13.3/Feature-103581-AutomaticallyTransformTCAFieldValuesForRecordObjects.rst b/typo3/sysext/core/Documentation/Changelog/13.3/Feature-103581-AutomaticallyTransformTCAFieldValuesForRecordObjects.rst new file mode 100644 index 0000000000000000000000000000000000000000..6ecb8d0861e9059c95e23fff7168bd2bbe6ed334 --- /dev/null +++ b/typo3/sysext/core/Documentation/Changelog/13.3/Feature-103581-AutomaticallyTransformTCAFieldValuesForRecordObjects.rst @@ -0,0 +1,159 @@ +.. include:: /Includes.rst.txt + +.. _feature-103581-1723209131: + +============================================================================== +Feature: #103581 - Automatically transform TCA field values for Record objects +============================================================================== + +See :issue:`103581` + +Description +=========== + +With :issue:`103783` the new :php:`Record` object has been introduced. It's an +object representing a raw database record, based on TCA and is usually used in +the frontend (via Fluid Templates), when fetching records with the +:php:`RecordTransformationDataProcessor` (:typoscript:`record-transformation`) +or by collecting content elements with the :php:`PageContentFetchingProcessor` +(:typoscript:`page-content`). + +The Records API - introduced together with the Schema API in :issue:`104002` - +now expands the :php:`Records`'s values for most common field types (known +from the TCA Schema) from their raw database value into "rich-flavored" values, +which might be :php:`Record`, :php:`FileReference`, :php:`Folder` or +:php:`\DateTimeImmutable` objects. + +This works for the following "relation" TCA types: + +* :php:`category` +* :php:`file` +* :php:`folder` +* :php:`group` +* :php:`inline` +* :php:`select` with :php:`MM` and :php:`foreign_table` + +In addition, the values of following TCA types are also resolved and +expanded automatically: + +* :php:`datetime` +* :php:`flex` +* :php:`json` +* :php:`link` +* :php:`select` with a static list of entries + +Each of the fields receive a full-fledged resolved value, based on the field +configuration from TCA. + +In case of relations (:php:`category`, :php:`group`, :php:`inline`, +:php:`select` with :php:`MM` and :php:`foreign_table`), a collection +(:php:`LazyRecordCollection`) of new :php:`Record` objects is attached as +value. In case of :php:`file`, a collection (:php:`LazyFileReferenceCollection`) +of :php:`FileReference` objects and in case of type :php:`folder`, a collection +(:php:`LazyFolderCollection`) of :php:`Folder` objects are attached. + +.. note:: + + The relations are only resolved once they are accessed - also known as + "lazy loading". This allows for recursion and circular dependencies to be + managed automatically. It's therefore also possible that the collection + is actually empty. + + +Example +======= + +.. code-block:: html + + <f:for each="{myContent.main.records}" as="record"> + <f:for each="{record.image}" as="image"> + <f:image image="{image}" /> + </f:for> + </f:for> + +New TCA option `relationship` +============================= + +In order to define cardinality on TCA level, the option :php:`relationship` is +introduced for all "relation" TCA types listed above. If this option is set to +:php:`oneToOne` or :php:`manyToOne`, then relations are resolved directly +without being wrapped into collection objects. In case the relation can +not be resolved, :php:`NULL` is returned. + +.. code-block:: php + + 'image' => [ + 'config' => [ + 'type' => 'file', + 'relationship' => 'manyToOne', + ] + ] + +.. code-block:: html + + <f:for each="{myContent.main.records}" as="record"> + <f:image image="{record.image}" /> + </f:for> + +.. note:: + + The TCA option :php:`maxitems` does not influence this behavior. This means + it is possible to have a :php:`oneToMany` relation with maximum one value + allowed. This way, overrides of this value will not break functionality. + +Field expansion +=============== + +For TCA type :php:`flex`, the corresponding FlexForm is resolved and therefore +all values within this FlexForm are processed and expanded as well. + +Fields of TCA type :php:`datetime` will be transformed into a full +:php:`\DateTimeInterface` object. + +Fields of TCA type :php:`json` will provide the decoded JSON value. + +Fields of TCA type :php:`link` will provide the :php:`TypolinkParameter` object, +which is a object oriented representation of the corresponding TypoLink +:typoscript:`parameter` configuration. + +Fields of TCA type :php:`select` without a :php:`relationship` will always provide +an array of static values. + +.. note:: + + TYPO3 tries to automatically resolve the :php:`relationship` for type + :php:`select` fields, which use :php:`renderType=selectSingle` and + having a :php:`foreign_table` set. This means, in case no + :php:`relationship` has been defined yet, it is set to either :php:`manyToOne` + as the default or :php:`manyToMany` for fields with option :php:`MM`. + +Impact +====== + +When using :php:`Record` objects through the :php:`RecordFactory` API, e.g. via +:php:`RecordTransformationDataProcessor` (:typoscript:`record-transformation`) +or :php:`PageContentFetchingProcessor` (`page-content`), the corresponding +:php:`Record` objects are now automatically processed and enriched. + +Those can not only be used in the frontend but also for Backend Previews in +the page module. This is possible by configuring a Fluid Template via Page +TSconfig to be used for the page preview rendering: + + +.. code-block:: typoscript + + mod.web_layout.tt_content.preview { + textmedia = EXT:site/Resources/Private/Templates/Preview/Textmedia.html + } + +In such template the newly available variable :html:`{record}` can be used to +access the resolved field values. It is advised to migrate existing preview +templates to this new object, as the former values will probably vanish in the +next major version. + +By utilizing the new API for fetching records and content elements, the need +for further data processors, e.g. :php:`FilesProcessor` (:typoscript:`files`), +becomes superfluous since all relations are resolved automatically when +requested. + +.. index:: Backend, FlexForm, Frontend, TCA, ext:core diff --git a/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/category_many_to_many.csv b/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/category_many_to_many.csv new file mode 100644 index 0000000000000000000000000000000000000000..6617cca53ba9cc4ebcb92410d467e7b5570faf69 --- /dev/null +++ b/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/category_many_to_many.csv @@ -0,0 +1,18 @@ +"sys_category" +,"uid","pid","title","t3ver_oid","t3ver_wsid" +,2,1,"Category 1",0,0 +,11,1,"Category 2",0,0 +,12,1,"Category 1 ws",2,1 +,13,1,"Category 2 ws",11,1 +"sys_category_record_mm", +,"uid_local","uid_foreign","tablenames","fieldname","sorting" +,2,260,"tt_content","typo3tests_contentelementb_categories_mm",1 +,11,260,"tt_content","typo3tests_contentelementb_categories_mm",0 +,12,263,"tt_content","typo3tests_contentelementb_categories_mm",2 +,13,263,"tt_content","typo3tests_contentelementb_categories_mm",1 +"sys_refindex" +,"hash","tablename","recuid","field","sorting","workspace","ref_table","ref_uid","ref_field","ref_sorting" +,"2c5724d09ec962d1e2f91f95b8c09aca","sys_category",2,"items",1,0,"tt_content",260,"typo3tests_contentelementb_categories_mm",1 +,"92c238672948a46154e0d92e554863d8","sys_category",11,"items",0,0,"tt_content",260,"typo3tests_contentelementb_categories_mm",2 +,"9639b2368a6e342fb05b6973bf0d4e9b","sys_category",12,"items",0,1,"tt_content",263,"typo3tests_contentelementb_categories_mm",2 +,"aca02beed00d6e3f32efe78945bc9d63","sys_category",13,"items",0,1,"tt_content",263,"typo3tests_contentelementb_categories_mm",1 diff --git a/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/category_many_to_many_localized.csv b/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/category_many_to_many_localized.csv new file mode 100644 index 0000000000000000000000000000000000000000..066ff033877f43b0ffc3df4f746a82a49806e530 --- /dev/null +++ b/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/category_many_to_many_localized.csv @@ -0,0 +1,18 @@ +"sys_category" +,"uid","pid","title","sys_language_uid","l10n_parent" +,2,1,"Category 1",0,0 +,11,1,"Category 2",0,0 +,12,1,"Category 1 translated",1,2 +,13,1,"Category 2 translated",1,11 +"sys_category_record_mm" +,"uid_local","uid_foreign","tablenames","fieldname" +,2,260,"tt_content","typo3tests_contentelementb_categories_mm" +,11,260,"tt_content","typo3tests_contentelementb_categories_mm" +,12,381,"tt_content","typo3tests_contentelementb_categories_mm" +"tt_content" +,"uid","pid","typo3tests_contentelementb_categories_mm","sys_language_uid","l18n_parent" +,260,1,2,0,0 +,381,1,1,1,260 +"sys_refindex" +,"hash","tablename","recuid","field","sorting","workspace","ref_table","ref_uid","ref_field","ref_sorting" +,"f25cbb70441772d669dfc0052fc047f8","sys_category",2,"items",0,0,"tt_content",381,"typo3tests_contentelementb_categories_mm",1 diff --git a/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/category_one_to_many.csv b/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/category_one_to_many.csv new file mode 100644 index 0000000000000000000000000000000000000000..64c2edbdcf6e3bc0361e8256dd046b135e9e2779 --- /dev/null +++ b/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/category_one_to_many.csv @@ -0,0 +1,8 @@ +"sys_category" +,"uid","pid","title" +,2,1,"Category 1" +,11,1,"Category 2" +"sys_refindex" +,"hash","tablename","recuid","field","sorting","workspace","ref_table","ref_uid","ref_field","ref_sorting" +,"41d028c84304886e4e2e2e8800297890","tt_content",260,"typo3tests_contentelementb_categories_1m",0,0,"sys_category",2,"",0 +,"448bd2548af7b53f1ce3da31153fae47","tt_content",260,"typo3tests_contentelementb_categories_1m",1,0,"sys_category",11,"",0 diff --git a/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/category_one_to_one.csv b/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/category_one_to_one.csv new file mode 100644 index 0000000000000000000000000000000000000000..288cd9b72e02567be3f3d753faf2962d9eee151f --- /dev/null +++ b/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/category_one_to_one.csv @@ -0,0 +1,7 @@ +"sys_category" +,"uid","pid","title" +,2,1,"Category 1" +,3,1,"Category 2" +"sys_refindex" +,"hash","tablename","recuid","field","sorting","workspace","ref_table","ref_uid","ref_field","ref_sorting" +,"1b0c3b8f46a1290b073881cd58cd12b1","tt_content",260,"typo3tests_contentelementb_categories_11",0,0,"sys_category",2,"",0 diff --git a/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/circular_relation.csv b/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/circular_relation.csv new file mode 100644 index 0000000000000000000000000000000000000000..04a3306fe25a2b609d653592f3344a92b1f9f49e --- /dev/null +++ b/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/circular_relation.csv @@ -0,0 +1,6 @@ +"tt_content" +,"uid","pid","t3ver_oid","t3ver_wsid","typo3tests_contentelementb_circular_relation","CType" +,260,1,0,0,"260","typo3tests_contentelementb" +"sys_refindex" +,"hash","tablename","recuid","field","sorting","workspace","ref_table","ref_uid" +,"83f48fc0812d5b6a2f26df9a21617edd","tt_content",260,"typo3tests_contentelementb_circular_relation",1,0,"tt_content",260 diff --git a/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/collections.csv b/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/collections.csv new file mode 100644 index 0000000000000000000000000000000000000000..2f08c6e7427de0b7762e18f25d00ddc6f4407e0e --- /dev/null +++ b/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/collections.csv @@ -0,0 +1,16 @@ +"typo3tests_contentelementb_collection" +,"uid","pid","fieldA","foreign_table_parent_uid","t3ver_oid","t3ver_wsid" +,1,1,"lorem foo bar",260,0,0 +,2,1,"lorem foo bar 2",260,0,0 +,3,1,"lorem foo bar WS",260,1,1 +,4,1,"lorem foo bar 2 WS",260,2,1 +"tt_content" +,"uid","pid","t3ver_oid","t3ver_wsid","typo3tests_contentelementb_collection","CType" +,260,1,0,0,2,"typo3tests_contentelementb" +,261,1,1,1,2,"typo3tests_contentelementb" +"sys_refindex" +,"hash","tablename","recuid","field","sorting","workspace","ref_table","ref_uid" +,"4a0b3d6b5dbe434c6d0c4571a08a9ae4","tt_content",260,"typo3tests_contentelementb_collection",0,0,"typo3tests_contentelementb_collection",1 +,"92c238672948a46154e0d92e554863d8","tt_content",260,"typo3tests_contentelementb_collection",1,0,"typo3tests_contentelementb_collection",2 +,"5cff181b1eb5b95ef7d6459477d59b0f","tt_content",261,"typo3tests_contentelementb_collection",0,1,"typo3tests_contentelementb_collection",3 +,"0739f14b25a651517c31d1da0c41b751","tt_content",261,"typo3tests_contentelementb_collection",1,1,"typo3tests_contentelementb_collection",4 diff --git a/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/collections_recursive.csv b/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/collections_recursive.csv new file mode 100644 index 0000000000000000000000000000000000000000..2d4fce240805d373cd0fb9aa39c5e563416573c4 --- /dev/null +++ b/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/collections_recursive.csv @@ -0,0 +1,17 @@ +"typo3tests_contentelementb_collection_recursive" +,"uid","pid","fieldA","collection_inner","foreign_table_parent_uid" +,1,1,"lorem foo bar A",2,260 +,2,1,"lorem foo bar A2",0,260 +"collection_inner" +,"uid","pid","fieldB","foreign_table_parent_uid" +,1,1,"lorem foo bar B",1 +,2,1,"lorem foo bar B2",1 +"tt_content" +,"uid","pid","t3ver_oid","t3ver_wsid","typo3tests_contentelementb_collection_recursive","CType" +,1,1,0,0,2,"typo3tests_contentelementb" +"sys_refindex" +,"hash","tablename","recuid","field","sorting","workspace","ref_table","ref_uid" +,"bf519eea10aaf2c85976a6622a7ed88b","tt_content",260,"typo3tests_contentelementb_collection_recursive",0,0,"typo3tests_contentelementb_collection_recursive",1 +,"e8e5539ae8691d1045d5beca3ca7b480","tt_content",260,"typo3tests_contentelementb_collection_recursive",1,0,"typo3tests_contentelementb_collection_recursive",2 +,"7142cce84a4d688bca9a30075a7aea7d","typo3tests_contentelementb_collection_recursive",1,"collection_inner",0,0,"collection_inner",1 +,"b40eeb513311e4f465d9455466cfd410","typo3tests_contentelementb_collection_recursive",1,"collection_inner",1,0,"collection_inner",2 diff --git a/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/db_reference_hidden.csv b/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/db_reference_hidden.csv new file mode 100644 index 0000000000000000000000000000000000000000..f6877ae6819c9693a349b720faf39660b93f0ad2 --- /dev/null +++ b/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/db_reference_hidden.csv @@ -0,0 +1,10 @@ +"pages" +,"uid","pid","title","t3ver_oid","t3ver_wsid","hidden" +,1,1,"Page 1",0,0 +,2,1,"Page 2",0,0 +,3,1,"Page 1 ws",1,1 +,4,1,"Page 2 ws",2,1 +"tt_content" +,"uid","pid","t3ver_oid","t3ver_wsid","typo3tests_contentelementb_pages_relation" +,1,1,0,0,"1,2" +,2,1,1,1,"1,2" diff --git a/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/db_relation.csv b/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/db_relation.csv new file mode 100644 index 0000000000000000000000000000000000000000..2c570abd555ca116d38987a4e9cbc69cb5c46ccd --- /dev/null +++ b/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/db_relation.csv @@ -0,0 +1,9 @@ +"pages" +,"uid","pid","title","t3ver_oid","t3ver_wsid","hidden" +,1906,1,"Page 1",0,0,0 +"tt_content" +,"uid","pid","t3ver_oid","t3ver_wsid","typo3tests_contentelementb_pages_relation" +,260,1,0,0,"1" +"sys_refindex" +,"hash","tablename","recuid","field","sorting","workspace","ref_table","ref_uid" +,"367a388427279ea5b203c37fbfb6b71c","tt_content",260,"typo3tests_contentelementb_pages_relation",0,0,"pages",1906 diff --git a/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/db_relation_mm.csv b/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/db_relation_mm.csv new file mode 100644 index 0000000000000000000000000000000000000000..79f20322ba2c8a3f84689ba726e520c3928d9c71 --- /dev/null +++ b/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/db_relation_mm.csv @@ -0,0 +1,12 @@ +"pages" +,"uid","pid","title" +,3207,1,"Page 1" +,3208,1,"Page 2" +"block_pages_mm" +,"uid_local","uid_foreign","sorting" +,263,3207,1 +,263,3208,2 +"sys_refindex" +,"hash","tablename","recuid","field","sorting","workspace","ref_table","ref_uid" +,"6b6b81550679e1783f44989f4fb19193","tt_content",263,"typo3tests_contentelementb_pages_mm",1,0,"pages",3208 +,"f59806ad6a2c2d46ebe07df3a49e384b","tt_content",263,"typo3tests_contentelementb_pages_mm",0,0,"pages",3207 diff --git a/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/db_relation_multiple.csv b/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/db_relation_multiple.csv new file mode 100644 index 0000000000000000000000000000000000000000..a1c61ec87eaea35227bb399f3489222fff668efe --- /dev/null +++ b/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/db_relation_multiple.csv @@ -0,0 +1,8 @@ +"pages" +,"uid","pid","title" +,1,1,"Page 1" +,2,1,"Page 2" +"tt_content" +,"uid","pid","header" +,1,1,"Content 1" +,2,1,"Content 2" diff --git a/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/db_relation_recursive.csv b/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/db_relation_recursive.csv new file mode 100644 index 0000000000000000000000000000000000000000..1c0d5acea47efec602b73db6e25a23ccede37e9f --- /dev/null +++ b/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/db_relation_recursive.csv @@ -0,0 +1,14 @@ +"test_record" +,"uid","pid","title","record_collection" +,1,1,"Record 1",1 +,2,1,"Record 2",1 +"record_collection" +,"uid","pid","text","foreign_table_parent_uid" +,3,1,"Collection 1",1 +,2,1,"Collection 2",2 +"sys_refindex" +,"hash","tablename","recuid","field","sorting","workspace","ref_table","ref_uid" +,"c0900336e432959e26c567c08ee7c4a1","tt_content",260,"typo3tests_contentelementb_record_relation_recursive",0,0,"test_record",1 +,"978725a067d15a001f9770b19a70669d","tt_content",260,"typo3tests_contentelementb_record_relation_recursive",1,0,"test_record",2 +,"90e9e109fbcd06ef38430673574ca426","test_record",1,"record_collection",0,0,"record_collection",3 +,"e933eac5cddb5176f87431f03c78e18c","test_record",2,"record_collection",0,0,"record_collection",2 diff --git a/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/db_relations.csv b/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/db_relations.csv new file mode 100644 index 0000000000000000000000000000000000000000..db282d0b8e251a3eebd7bee217d4f6de085cd344 --- /dev/null +++ b/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/db_relations.csv @@ -0,0 +1,17 @@ +"pages" +,"uid","pid","title","t3ver_oid","t3ver_wsid","hidden" +,1906,1,"Page 1",0,0,0 +,3389,1,"Page 2",0,0,0 +,3391,1,"Page 1 ws",1906,1,0 +,3393,1,"Page 2 ws",3389,1,0 +,5,1,"Page 3",0,0,1 +"tt_content" +,"uid","pid","t3ver_oid","t3ver_wsid","typo3tests_contentelementb_pages_relations" +,260,1,0,0,"1,2" +,261,1,1,1,"1,2" +"sys_refindex" +,"hash","tablename","recuid","field","sorting","workspace","ref_table","ref_uid" +,"367a388427279ea5b203c37fbfb6b71c","tt_content",260,"typo3tests_contentelementb_pages_relations",0,0,"pages",1906 +,"978725a067d15a001f9770b19a70669d","tt_content",260,"typo3tests_contentelementb_pages_relations",1,0,"pages",3389 +,"0ea740965dd56db56d18acf0bdb0bc15","tt_content",261,"typo3tests_contentelementb_pages_relations",0,1,"pages",1906 +,"85acf9c1c591795353d8faf79544faf5","tt_content",261,"typo3tests_contentelementb_pages_relations",1,1,"pages",3389 diff --git a/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/file_reference.csv b/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/file_reference.csv new file mode 100644 index 0000000000000000000000000000000000000000..a16ed5251dc11b6f759fd89a6c3d7adb66961d28 --- /dev/null +++ b/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/file_reference.csv @@ -0,0 +1,9 @@ +"sys_file_reference" +,"uid","pid","uid_local","uid_foreign","tablenames","fieldname","sorting_foreign","t3ver_wsid","t3ver_state","t3ver_stage","t3ver_oid" +,2161,1,1,260,"tt_content","image",1,0,0,0,0 +"sys_file",,,,,,,,,,,,,,,,,,, +,"uid","pid","type","storage","identifier","extension","mime_type","name","sha1","size","creation_date","modification_date","missing","metadata","identifier_hash","folder_hash","last_indexed" +,1,0,2,1,"/kasper-skarhoj1.jpg","jpg","image/jpeg","kasper-skarhoj1.jpg","05d8c6dda534a0b9e7023c3031e60e4b49c3da40",39037,1677330452,1658395918,0,0,"98576a90d58d51fcfe91c41f4c571fd600e1190e","42099b4af021e53fd8fd4e056c2568d7c2e3ffa8",0 +"sys_refindex" +,"hash","tablename","recuid","field","sorting","workspace","ref_table","ref_uid" +,"01df57e9d30506cb7ec9cd2724b985eb","tt_content",260,"image",0,0,"sys_file_reference",2161 diff --git a/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/file_references.csv b/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/file_references.csv new file mode 100644 index 0000000000000000000000000000000000000000..d8f56fab41317e30c1305a72536fe46f4a9ad368 --- /dev/null +++ b/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/file_references.csv @@ -0,0 +1,17 @@ +"sys_file_reference" +,"uid","pid","uid_local","uid_foreign","tablenames","fieldname","sorting_foreign","t3ver_wsid","t3ver_state","t3ver_stage","t3ver_oid" +,2161,1,1,260,"tt_content","image",1,0,0,0,0 +,2162,1,1,260,"tt_content","image",2,0,0,0,0 +,2163,1,1,260,"tt_content","media",1,0,0,0,0 +,2164,1,1,260,"tt_content","media",2,0,0,0,0 +,2165,1,1,260,"tt_content","assets",1,0,0,0,0 +"sys_file",,,,,,,,,,,,,,,,,,, +,"uid","pid","type","storage","identifier","extension","mime_type","name","sha1","size","creation_date","modification_date","missing","metadata","identifier_hash","folder_hash","last_indexed" +,1,0,2,1,"/kasper-skarhoj1.jpg","jpg","image/jpeg","kasper-skarhoj1.jpg","05d8c6dda534a0b9e7023c3031e60e4b49c3da40",39037,1677330452,1658395918,0,0,"98576a90d58d51fcfe91c41f4c571fd600e1190e","42099b4af021e53fd8fd4e056c2568d7c2e3ffa8",0 +"sys_refindex" +,"hash","tablename","recuid","field","sorting","workspace","ref_table","ref_uid" +,"01df57e9d30506cb7ec9cd2724b985eb","tt_content",260,"image",0,0,"sys_file_reference",2161 +,"1cac14244432b984e356dd3a7a1e6cbf","tt_content",260,"image",0,0,"sys_file_reference",2162 +,"bd99dcda6d449ad66d1be20499f31056","tt_content",260,"media",0,0,"sys_file_reference",2163 +,"3032602f676415aee093600bb17a3fd6","tt_content",260,"media",0,0,"sys_file_reference",2164 +,"2fe787ec710ceeb6470cb3da40c66f81","tt_content",260,"assets",0,0,"sys_file_reference",2165 diff --git a/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/folder_files.csv b/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/folder_files.csv new file mode 100644 index 0000000000000000000000000000000000000000..7c90beb26910d8749d6539f14de51870a2437768 --- /dev/null +++ b/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/folder_files.csv @@ -0,0 +1,3 @@ +"sys_file",,,,,,,,,,,,,,,,,,, +,"uid","pid","type","storage","identifier","extension","mime_type","name","sha1","size","creation_date","modification_date","missing","metadata","identifier_hash","folder_hash","last_indexed" +,1,0,2,1,"/kasper-skarhoj1.jpg","jpg","image/jpeg","kasper-skarhoj1.jpg","05d8c6dda534a0b9e7023c3031e60e4b49c3da40",39037,1677330452,1658395918,0,0,"98576a90d58d51fcfe91c41f4c571fd600e1190e","42099b4af021e53fd8fd4e056c2568d7c2e3ffa8",0 diff --git a/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/foreign_table_select_multiple.csv b/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/foreign_table_select_multiple.csv new file mode 100644 index 0000000000000000000000000000000000000000..e886ab33f337dd86d05598030d120fa741ffa706 --- /dev/null +++ b/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/foreign_table_select_multiple.csv @@ -0,0 +1,14 @@ +"pages" +,"uid","pid","title","doktype" +,2,1,"Record Storage",254 +"test_record" +,"uid","pid","title","record_collection" +,1,2,"Record 1",1 +"record_collection" +,"uid","pid","text","foreign_table_parent_uid", +,1,2,"Collection 1",1 +"sys_refindex" +,"hash","tablename","recuid","field","sorting","workspace","ref_table","ref_uid" +,"1cac14244432b984e356dd3a7a1e6cbf","tt_content",260,"typo3tests_contentelementb_select_foreign_multiple",0,0,"test_record",1 +,"493dc67b404141c0ca3aa1b43409647c","tt_content",260,"typo3tests_contentelementb_select_foreign_multiple",1,0,"test_record",1 +,"90e9e109fbcd06ef38430673574ca426","test_record",1,"record_collection",0,0,"record_collection",1 diff --git a/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/select_foreign.csv b/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/select_foreign.csv new file mode 100644 index 0000000000000000000000000000000000000000..ffba73da9d395891bde60a276a89db0aba748ca0 --- /dev/null +++ b/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/select_foreign.csv @@ -0,0 +1,9 @@ +"test_record" +,"uid","pid","title","record_collection" +,1,1,"Record 1",0 +,2,1,"Record 2",0 +"sys_refindex" +,"hash","tablename","recuid","field","sorting","workspace","ref_table","ref_uid" +,"f36643910656b91d77a0640a92dec881","tt_content",260,"typo3tests_contentelementb_select_foreign",0,0,"test_record",1 +,"1cac14244432b984e356dd3a7a1e6cbf","tt_content",260,"typo3tests_contentelementb_select_foreign_multiple",0,0,"test_record",1 +,"5e6ed22d795e28d598703907f1d7574f","tt_content",260,"typo3tests_contentelementb_select_foreign_multiple",1,0,"test_record",2 diff --git a/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/select_foreign_native.csv b/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/select_foreign_native.csv new file mode 100644 index 0000000000000000000000000000000000000000..78bcc25383e5426b9a6d60558554beb82439eb51 --- /dev/null +++ b/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/select_foreign_native.csv @@ -0,0 +1,4 @@ +"native_record" +,"uid","pid","title", +,1,1,"Record 1",0 +,2,1,"Record 2",0 diff --git a/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/select_foreign_recursive.csv b/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/select_foreign_recursive.csv new file mode 100644 index 0000000000000000000000000000000000000000..fba12fb4178e3dc403faa4ae9bc381c3a9333f74 --- /dev/null +++ b/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/select_foreign_recursive.csv @@ -0,0 +1,13 @@ +"test_record" +,"uid","pid","title","record_collection" +,1,1,"Record 1",1 +,2,1,"Record 2",1 +"record_collection" +,"uid","pid","text","foreign_table_parent_uid", +,3,1,"Collection 1",1 +,2,1,"Collection 2",2, +"sys_refindex" +,"hash","tablename","recuid","field","sorting","workspace","ref_table","ref_uid" +,"f36643910656b91d77a0640a92dec881","tt_content",260,"typo3tests_contentelementb_select_foreign",0,0,"test_record",1 +,"90e9e109fbcd06ef38430673574ca426","test_record",1,"record_collection",0,0,"record_collection",3 +,"e933eac5cddb5176f87431f03c78e18c","test_record",2,"record_collection",0,0,"record_collection",2 diff --git a/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/select_one_to_one.csv b/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/select_one_to_one.csv new file mode 100644 index 0000000000000000000000000000000000000000..26bce422c6e821855e94a1ec775a70a3db4c95ea --- /dev/null +++ b/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/select_one_to_one.csv @@ -0,0 +1,9 @@ +"test_record" +,"uid","pid","title" +,1,1,"Record 1" +"tt_content" +,"uid","pid","t3ver_oid","t3ver_wsid","typo3tests_contentelementb_select_one_to_one" +,260,1,0,0,"1" +"sys_refindex" +,"hash","tablename","recuid","field","sorting","workspace","ref_table","ref_uid" +,"f36643910656b91d77a0640a92dec881","tt_content",260,"typo3tests_contentelementb_select_one_to_one",0,0,"test_record",1 diff --git a/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/TestFolder/file.jpg b/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/TestFolder/file.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/TestFolder/sub/subfile.jpg b/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/TestFolder/sub/subfile.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/typo3/sysext/core/Tests/Functional/DataHandling/RecordFieldTransformerTest.php b/typo3/sysext/core/Tests/Functional/DataHandling/RecordFieldTransformerTest.php new file mode 100644 index 0000000000000000000000000000000000000000..75f11ff40db7f1168369f120f9c7cf272a1ad7ad --- /dev/null +++ b/typo3/sysext/core/Tests/Functional/DataHandling/RecordFieldTransformerTest.php @@ -0,0 +1,1210 @@ +<?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\Tests\Functional\DataHandling; + +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Test; +use TYPO3\CMS\Core\Collection\LazyRecordCollection; +use TYPO3\CMS\Core\Context\Context; +use TYPO3\CMS\Core\Context\LanguageAspect; +use TYPO3\CMS\Core\Context\WorkspaceAspect; +use TYPO3\CMS\Core\DataHandling\RecordFieldTransformer; +use TYPO3\CMS\Core\Domain\Record; +use TYPO3\CMS\Core\Domain\RecordFactory; +use TYPO3\CMS\Core\Domain\RecordPropertyClosure; +use TYPO3\CMS\Core\Resource\Collection\LazyFileReferenceCollection; +use TYPO3\CMS\Core\Resource\Collection\LazyFolderCollection; +use TYPO3\CMS\Core\Resource\FileReference; +use TYPO3\CMS\Core\Resource\Folder; +use TYPO3\CMS\Core\Schema\TcaSchemaFactory; +use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase; + +final class RecordFieldTransformerTest extends FunctionalTestCase +{ + protected array $coreExtensionsToLoad = [ + 'workspaces', + ]; + + protected array $testExtensionsToLoad = [ + 'typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_relation_resolver', + ]; + + protected array $pathsToProvideInTestInstance = [ + 'typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/TestFolder/' => 'fileadmin/', + ]; + + #[Test] + public function canResolveFileReference(): void + { + $this->importCSVDataSet(__DIR__ . '/Fixtures/DataSet/file_reference.csv'); + $dummyRecord = $this->createTestRecordObject(['image' => 1]); + $fieldInformation = $this->get(TcaSchemaFactory::class)->get('tt_content')->getSubSchema('typo3tests_contentelementb')->getField('image'); + $subject = $this->get(RecordFieldTransformer::class); + $propertyClosure = $subject->transformField( + $fieldInformation, + $dummyRecord->getRawRecord(), + $this->get(Context::class) + ); + + self::assertInstanceOf(RecordPropertyClosure::class, $propertyClosure); + $result = $propertyClosure->instantiate(); + + self::assertInstanceOf(FileReference::class, $result); + self::assertEquals('/kasper-skarhoj1.jpg', $result->getIdentifier()); + self::assertIsArray($result->getProperties()); + + $resolvedRecord = $this->get(RecordFactory::class)->createResolvedRecordFromDatabaseRow('tt_content', $dummyRecord->toArray()); + self::assertInstanceOf(FileReference::class, $resolvedRecord['image']); + self::assertEquals('/kasper-skarhoj1.jpg', $resolvedRecord['image']->getIdentifier()); + } + + #[Test] + public function canHandleInvalidFileReference(): void + { + $this->importCSVDataSet(__DIR__ . '/Fixtures/DataSet/file_reference.csv'); + $dummyRecord = $this->createTestRecordObject(['uid' => 261, 'image' => 1]); + $fieldInformation = $this->get(TcaSchemaFactory::class)->get('tt_content')->getSubSchema('typo3tests_contentelementb')->getField('image'); + $subject = $this->get(RecordFieldTransformer::class); + $propertyClosure = $subject->transformField( + $fieldInformation, + $dummyRecord->getRawRecord(), + $this->get(Context::class) + ); + + self::assertInstanceOf(RecordPropertyClosure::class, $propertyClosure); + $result = $propertyClosure->instantiate(); + self::assertNull($result); + + $resolvedRecord = $this->get(RecordFactory::class)->createResolvedRecordFromDatabaseRow('tt_content', $dummyRecord->toArray()); + self::assertNull($resolvedRecord['image']); + } + + #[Test] + public function canResolveFileReferences(): void + { + $this->importCSVDataSet(__DIR__ . '/Fixtures/DataSet/file_references.csv'); + $dummyRecord = $this->createTestRecordObject(['media' => 2]); + $fieldInformation = $this->get(TcaSchemaFactory::class)->get('tt_content')->getField('media'); + $subject = $this->get(RecordFieldTransformer::class); + $result = $subject->transformField( + $fieldInformation, + $dummyRecord->getRawRecord(), + $this->get(Context::class) + ); + + foreach ($result as $fileReference) { + self::assertInstanceOf(FileReference::class, $fileReference); + self::assertEquals('/kasper-skarhoj1.jpg', $fileReference->getIdentifier()); + } + + self::assertCount(2, $result); + self::assertInstanceOf(LazyFileReferenceCollection::class, $result); + + $resolvedRecord = $this->get(RecordFactory::class)->createResolvedRecordFromDatabaseRow('tt_content', $dummyRecord->toArray()); + self::assertInstanceOf(LazyFileReferenceCollection::class, $resolvedRecord['media']); + self::assertInstanceOf(FileReference::class, $resolvedRecord['media'][0]); + self::assertEquals('/kasper-skarhoj1.jpg', $resolvedRecord['media'][0]->getIdentifier()); + } + + #[Test] + public function resolvesSingleFileReferenceWithoutMaxItems(): void + { + $this->importCSVDataSet(__DIR__ . '/Fixtures/DataSet/file_references.csv'); + $dummyRecord = $this->createTestRecordObject(['assets' => 1]); + $fieldInformation = $this->get(TcaSchemaFactory::class)->get('tt_content')->getField('assets'); + $subject = $this->get(RecordFieldTransformer::class); + $result = $subject->transformField( + $fieldInformation, + $dummyRecord->getRawRecord(), + $this->get(Context::class) + ); + + foreach ($result as $fileReference) { + self::assertInstanceOf(FileReference::class, $fileReference); + self::assertEquals('/kasper-skarhoj1.jpg', $fileReference->getIdentifier()); + } + + self::assertCount(1, $result); + self::assertInstanceOf(LazyFileReferenceCollection::class, $result); + + $resolvedRecord = $this->get(RecordFactory::class)->createResolvedRecordFromDatabaseRow('tt_content', $dummyRecord->toArray()); + self::assertCount(1, $resolvedRecord['assets']); + self::assertInstanceOf(LazyFileReferenceCollection::class, $resolvedRecord['assets']); + self::assertInstanceOf(FileReference::class, $resolvedRecord['assets'][0]); + self::assertEquals('/kasper-skarhoj1.jpg', $resolvedRecord['assets'][0]->getIdentifier()); + } + + #[Test] + public function canResolveFilesFromFolder(): void + { + $this->importCSVDataSet(__DIR__ . '/Fixtures/DataSet/folder_files.csv'); + $dummyRecord = $this->createTestRecordObject(['typo3tests_contentelementb_folder' => '1:/']); + $fieldInformation = $this->get(TcaSchemaFactory::class)->get('tt_content')->getField('typo3tests_contentelementb_folder'); + $subject = $this->get(RecordFieldTransformer::class); + $propertyClosure = $subject->transformField( + $fieldInformation, + $dummyRecord->getRawRecord(), + $this->get(Context::class) + ); + + self::assertInstanceOf(RecordPropertyClosure::class, $propertyClosure); + $result = $propertyClosure->instantiate(); + self::assertInstanceOf(Folder::class, $result); + self::assertEquals('/', $result->getIdentifier()); + self::assertEquals('1:/', $result->getCombinedIdentifier()); + self::assertCount(1, $result->getFiles()); + + $resolvedRecord = $this->get(RecordFactory::class)->createResolvedRecordFromDatabaseRow('tt_content', $dummyRecord->toArray()); + self::assertInstanceOf(Folder::class, $resolvedRecord['typo3tests_contentelementb_folder']); + self::assertEquals('/', $resolvedRecord['typo3tests_contentelementb_folder']->getIdentifier()); + self::assertEquals('1:/', $resolvedRecord['typo3tests_contentelementb_folder']->getCombinedIdentifier()); + self::assertCount(1, $resolvedRecord['typo3tests_contentelementb_folder']->getFiles()); + } + + #[Test] + public function canHandleInvalidFolder(): void + { + $this->importCSVDataSet(__DIR__ . '/Fixtures/DataSet/folder_files.csv'); + $dummyRecord = $this->createTestRecordObject(['typo3tests_contentelementb_folder' => '']); + $fieldInformation = $this->get(TcaSchemaFactory::class)->get('tt_content')->getField('typo3tests_contentelementb_folder'); + $subject = $this->get(RecordFieldTransformer::class); + $propertyClosure = $subject->transformField( + $fieldInformation, + $dummyRecord->getRawRecord(), + $this->get(Context::class) + ); + + self::assertInstanceOf(RecordPropertyClosure::class, $propertyClosure); + $result = $propertyClosure->instantiate(); + self::assertNull($result); + + $resolvedRecord = $this->get(RecordFactory::class)->createResolvedRecordFromDatabaseRow('tt_content', $dummyRecord->toArray()); + self::assertNull($resolvedRecord['typo3tests_contentelementb_folder']); + } + + #[Test] + public function canResolveFilesFromFolders(): void + { + $this->importCSVDataSet(__DIR__ . '/Fixtures/DataSet/folder_files.csv'); + $dummyRecord = $this->createTestRecordObject(['typo3tests_contentelementb_folder_recursive' => '1:/']); + $fieldInformation = $this->get(TcaSchemaFactory::class)->get('tt_content')->getField('typo3tests_contentelementb_folder_recursive'); + $subject = $this->get(RecordFieldTransformer::class); + $result = $subject->transformField( + $fieldInformation, + $dummyRecord->getRawRecord(), + $this->get(Context::class), + ); + + foreach ($result as $folder) { + self::assertInstanceOf(Folder::class, $folder); + self::assertEquals(1, $folder->getStorage()->getUid()); + self::assertCount(1, $folder->getFiles()); + } + + self::assertInstanceOf(LazyFolderCollection::class, $result); + self::assertCount(2, $result); + self::assertInstanceOf(Folder::class, $result[0]); + self::assertEquals('/sub/', $result[1]->getIdentifier()); + self::assertEquals('1:/sub/', $result[1]->getCombinedIdentifier()); + self::assertCount(1, $result[1]->getFiles()); + + $resolvedRecord = $this->get(RecordFactory::class)->createResolvedRecordFromDatabaseRow('tt_content', $dummyRecord->toArray()); + self::assertInstanceOf(LazyFolderCollection::class, $resolvedRecord['typo3tests_contentelementb_folder_recursive']); + self::assertCount(2, $resolvedRecord['typo3tests_contentelementb_folder_recursive']); + self::assertInstanceOf(Folder::class, $resolvedRecord['typo3tests_contentelementb_folder_recursive'][0]); + self::assertEquals('/sub/', $resolvedRecord['typo3tests_contentelementb_folder_recursive'][1]->getIdentifier()); + self::assertEquals('1:/sub/', $resolvedRecord['typo3tests_contentelementb_folder_recursive'][1]->getCombinedIdentifier()); + self::assertCount(1, $resolvedRecord['typo3tests_contentelementb_folder_recursive'][1]->getFiles()); + } + + #[Test] + public function canResolveCollections(): void + { + $this->importCSVDataSet(__DIR__ . '/Fixtures/DataSet/collections.csv'); + $dummyRecord = $this->createTestRecordObject(['typo3tests_contentelementb_collection' => 2]); + $fieldInformation = $this->get(TcaSchemaFactory::class)->get('tt_content')->getField('typo3tests_contentelementb_collection'); + $subject = $this->get(RecordFieldTransformer::class); + $result = $subject->transformField( + $fieldInformation, + $dummyRecord->getRawRecord(), + $this->get(Context::class) + ); + + self::assertCount(2, $result); + self::assertInstanceOf(LazyRecordCollection::class, $result); + self::assertSame('lorem foo bar', $result[0]['fieldA']); + self::assertSame('lorem foo bar 2', $result[1]['fieldA']); + + $resolvedRecord = $this->get(RecordFactory::class)->createResolvedRecordFromDatabaseRow('tt_content', $dummyRecord->toArray()); + self::assertInstanceOf(LazyRecordCollection::class, $resolvedRecord['typo3tests_contentelementb_collection']); + self::assertCount(2, $resolvedRecord['typo3tests_contentelementb_collection']); + self::assertSame('lorem foo bar', $resolvedRecord['typo3tests_contentelementb_collection'][0]['fieldA']); + self::assertSame('lorem foo bar 2', $resolvedRecord['typo3tests_contentelementb_collection'][1]['fieldA']); + } + + #[Test] + public function canResolveCollectionsRecursively(): void + { + $this->importCSVDataSet(__DIR__ . '/Fixtures/DataSet/collections_recursive.csv'); + $dummyRecord = $this->createTestRecordObject(['typo3tests_contentelementb_collection_recursive' => 2]); + $fieldInformation = $this->get(TcaSchemaFactory::class)->get('tt_content')->getField('typo3tests_contentelementb_collection_recursive'); + $subject = $this->get(RecordFieldTransformer::class); + $result = $subject->transformField( + $fieldInformation, + $dummyRecord->getRawRecord(), + $this->get(Context::class) + ); + + self::assertCount(2, $result); + self::assertInstanceOf(LazyRecordCollection::class, $result); + self::assertSame('lorem foo bar A', $result[0]['fieldA']); + self::assertSame('lorem foo bar A2', $result[1]['fieldA']); + self::assertCount(2, $result[0]['collection_inner']); + self::assertSame('lorem foo bar B', $result[0]['collection_inner'][0]['fieldB']); + self::assertSame('lorem foo bar B2', $result[0]['collection_inner'][1]['fieldB']); + + $resolvedRecord = $this->get(RecordFactory::class)->createResolvedRecordFromDatabaseRow('tt_content', $dummyRecord->toArray()); + self::assertInstanceOf(LazyRecordCollection::class, $resolvedRecord['typo3tests_contentelementb_collection_recursive']); + self::assertCount(2, $resolvedRecord['typo3tests_contentelementb_collection_recursive']); + self::assertSame('lorem foo bar A', $resolvedRecord['typo3tests_contentelementb_collection_recursive'][0]['fieldA']); + self::assertSame('lorem foo bar A2', $resolvedRecord['typo3tests_contentelementb_collection_recursive'][1]['fieldA']); + self::assertCount(2, $resolvedRecord['typo3tests_contentelementb_collection_recursive'][0]['collection_inner']); + self::assertSame('lorem foo bar B', $resolvedRecord['typo3tests_contentelementb_collection_recursive'][0]['collection_inner'][0]['fieldB']); + self::assertSame('lorem foo bar B2', $resolvedRecord['typo3tests_contentelementb_collection_recursive'][0]['collection_inner'][1]['fieldB']); + } + + #[Test] + public function canResolveCollectionsInWorkspaces(): void + { + $this->importCSVDataSet(__DIR__ . '/Fixtures/DataSet/collections.csv'); + $this->importCSVDataSet(__DIR__ . '/../Fixtures/be_users.csv'); + $this->setUpBackendUser(1); + $this->setWorkspaceId(1); + $dummyRecord = $this->createTestRecordObject([ + 'typo3tests_contentelementb_collection' => 2, + 't3ver_oid' => 260, + 't3ver_wsid' => 1, + '_ORIG_uid' => 261, + ]); + $fieldInformation = $this->get(TcaSchemaFactory::class)->get('tt_content')->getField('typo3tests_contentelementb_collection'); + $subject = $this->get(RecordFieldTransformer::class); + $result = $subject->transformField( + $fieldInformation, + $dummyRecord->getRawRecord(), + $this->get(Context::class) + ); + + self::assertCount(2, $result); + self::assertInstanceOf(LazyRecordCollection::class, $result); + self::assertSame('lorem foo bar WS', $result[0]['fieldA']); + self::assertSame('lorem foo bar 2 WS', $result[1]['fieldA']); + + $resolvedRecord = $this->get(RecordFactory::class)->createResolvedRecordFromDatabaseRow('tt_content', $dummyRecord->toArray()); + self::assertInstanceOf(LazyRecordCollection::class, $resolvedRecord['typo3tests_contentelementb_collection']); + self::assertCount(2, $resolvedRecord['typo3tests_contentelementb_collection']); + self::assertSame('lorem foo bar WS', $resolvedRecord['typo3tests_contentelementb_collection'][0]['fieldA']); + self::assertSame('lorem foo bar 2 WS', $resolvedRecord['typo3tests_contentelementb_collection'][1]['fieldA']); + } + + #[Test] + public function canResolveCategoriesManyToMany(): void + { + $this->importCSVDataSet(__DIR__ . '/Fixtures/DataSet/category_many_to_many.csv'); + $dummyRecord = $this->createTestRecordObject(); + $fieldInformation = $this->get(TcaSchemaFactory::class)->get('tt_content')->getField('typo3tests_contentelementb_categories_mm'); + $subject = $this->get(RecordFieldTransformer::class); + $result = $subject->transformField( + $fieldInformation, + $dummyRecord->getRawRecord(), + $this->get(Context::class) + ); + + self::assertCount(2, $result); + self::assertSame('Category 1', $result[0]['title']); + self::assertSame('Category 2', $result[1]['title']); + + $resolvedRecord = $this->get(RecordFactory::class)->createResolvedRecordFromDatabaseRow('tt_content', $dummyRecord->toArray()); + self::assertInstanceOf(LazyRecordCollection::class, $resolvedRecord['typo3tests_contentelementb_categories_mm']); + self::assertCount(2, $resolvedRecord['typo3tests_contentelementb_categories_mm']); + self::assertSame('Category 1', $resolvedRecord['typo3tests_contentelementb_categories_mm'][0]['title']); + self::assertSame('Category 2', $resolvedRecord['typo3tests_contentelementb_categories_mm'][1]['title']); + } + + #[Test] + public function canResolveCategoriesManyToManyInWorkspaces(): void + { + $this->importCSVDataSet(__DIR__ . '/Fixtures/DataSet/category_many_to_many.csv'); + $this->importCSVDataSet(__DIR__ . '/../Fixtures/be_users.csv'); + $this->setUpBackendUser(1); + $this->setWorkspaceId(1); + + $dummyRecord = $this->createTestRecordObject([ + 'typo3tests_contentelementb_categories_mm' => 2, + 'sys_language_uid' => 1, + 't3ver_oid' => 260, + 't3ver_wsid' => 1, + '_ORIG_uid' => 261, + ]); + $fieldInformation = $this->get(TcaSchemaFactory::class)->get('tt_content')->getField('typo3tests_contentelementb_categories_mm'); + $subject = $this->get(RecordFieldTransformer::class); + $result = $subject->transformField( + $fieldInformation, + $dummyRecord->getRawRecord(), + $this->get(Context::class) + ); + + self::assertCount(2, $result); + // @todo: this should be the other way around, but currently RelationResolver cannot handle different sorting in WS + self::assertSame('Category 2 ws', $result[1]['title']); + self::assertSame('Category 1 ws', $result[0]['title']); + } + + #[Test] + public function canResolveCategoriesManyToManyLocalizedOverlaysOff(): void + { + $this->importCSVDataSet(__DIR__ . '/Fixtures/DataSet/category_many_to_many_localized.csv'); + $this->importCSVDataSet(__DIR__ . '/../Fixtures/be_users.csv'); + $context = $this->get(Context::class); + $context->setAspect('language', new LanguageAspect(1, 1, LanguageAspect::OVERLAYS_OFF)); + $dummyRecord = $this->createTestRecordObject([ + 'typo3tests_contentelementb_categories_mm' => 2, + 'sys_language_uid' => 1, + 'l18n_parent' => 260, + '_LOCALIZED_UID' => 381, + ]); + $fieldInformation = $this->get(TcaSchemaFactory::class)->get('tt_content')->getField('typo3tests_contentelementb_categories_mm'); + $subject = $this->get(RecordFieldTransformer::class); + $result = $subject->transformField( + $fieldInformation, + $dummyRecord->getRawRecord(), + $context + ); + + self::assertCount(1, $result); + self::assertSame('Category 1 translated', $result[0]['title']); + + $resolvedRecord = $this->get(RecordFactory::class)->createResolvedRecordFromDatabaseRow('tt_content', array_replace($dummyRecord->toArray(), ['uid' => 381]), $context); + self::assertInstanceOf(LazyRecordCollection::class, $resolvedRecord['typo3tests_contentelementb_categories_mm']); + self::assertCount(1, $resolvedRecord['typo3tests_contentelementb_categories_mm']); + self::assertSame('Category 1 translated', $resolvedRecord['typo3tests_contentelementb_categories_mm'][0]['title']); + } + + #[Test] + public function canResolveCategoriesManyToManyLocalizedOverlaysOn(): void + { + $this->importCSVDataSet(__DIR__ . '/Fixtures/DataSet/category_many_to_many_localized.csv'); + $this->importCSVDataSet(__DIR__ . '/../Fixtures/be_users.csv'); + $context = $this->get(Context::class); + $context->setAspect('language', new LanguageAspect(1, 1, LanguageAspect::OVERLAYS_ON)); + $dummyRecord = $this->createTestRecordObject([ + 'typo3tests_contentelementb_categories_mm' => 2, + 'sys_language_uid' => 1, + 'l18n_parent' => 260, + '_LOCALIZED_UID' => 381, + ]); + $fieldInformation = $this->get(TcaSchemaFactory::class)->get('tt_content')->getField('typo3tests_contentelementb_categories_mm'); + $subject = $this->get(RecordFieldTransformer::class); + $result = $subject->transformField( + $fieldInformation, + $dummyRecord->getRawRecord(), + $context + ); + + self::assertCount(1, $result); + self::assertSame('Category 1 translated', $result[0]['title']); + + $resolvedRecord = $this->get(RecordFactory::class)->createResolvedRecordFromDatabaseRow('tt_content', array_replace($dummyRecord->toArray(), ['uid' => 381]), $context); + self::assertInstanceOf(LazyRecordCollection::class, $resolvedRecord['typo3tests_contentelementb_categories_mm']); + self::assertCount(1, $resolvedRecord['typo3tests_contentelementb_categories_mm']); + self::assertSame('Category 1 translated', $resolvedRecord['typo3tests_contentelementb_categories_mm'][0]['title']); + } + + #[Test] + public function canResolveCategoriesOneToOne(): void + { + $this->importCSVDataSet(__DIR__ . '/Fixtures/DataSet/category_one_to_one.csv'); + $dummyRecord = $this->createTestRecordObject([ + 'typo3tests_contentelementb_categories_11' => 2, + ]); + $fieldInformation = $this->get(TcaSchemaFactory::class)->get('tt_content')->getField('typo3tests_contentelementb_categories_11'); + $subject = $this->get(RecordFieldTransformer::class); + $propertyClosure = $subject->transformField( + $fieldInformation, + $dummyRecord->getRawRecord(), + $this->get(Context::class) + ); + + self::assertInstanceOf(RecordPropertyClosure::class, $propertyClosure); + $result = $propertyClosure->instantiate(); + self::assertInstanceOf(Record::class, $result); + self::assertSame(2, $result->getUid()); + self::assertSame('Category 1', $result['title']); + + $resolvedRecord = $this->get(RecordFactory::class)->createResolvedRecordFromDatabaseRow('tt_content', $dummyRecord->toArray()); + self::assertInstanceOf(Record::class, $resolvedRecord['typo3tests_contentelementb_categories_11']); + self::assertSame(2, $resolvedRecord['typo3tests_contentelementb_categories_11']['uid']); + self::assertSame('Category 1', $resolvedRecord['typo3tests_contentelementb_categories_11']['title']); + } + + #[Test] + public function canResolveCategoriesOneToMany(): void + { + $this->importCSVDataSet(__DIR__ . '/Fixtures/DataSet/category_one_to_many.csv'); + $dummyRecord = $this->createTestRecordObject([ + 'typo3tests_contentelementb_categories_1m' => '2,11', + ]); + $fieldInformation = $this->get(TcaSchemaFactory::class)->get('tt_content')->getField('typo3tests_contentelementb_categories_1m'); + $subject = $this->get(RecordFieldTransformer::class); + $result = $subject->transformField( + $fieldInformation, + $dummyRecord->getRawRecord(), + $this->get(Context::class) + ); + + self::assertCount(2, $result); + self::assertInstanceOf(LazyRecordCollection::class, $result); + self::assertSame('Category 1', $result[0]['title']); + self::assertSame('Category 2', $result[1]['title']); + + $resolvedRecord = $this->get(RecordFactory::class)->createResolvedRecordFromDatabaseRow('tt_content', $dummyRecord->toArray()); + self::assertCount(2, $resolvedRecord['typo3tests_contentelementb_categories_1m']); + self::assertInstanceOf(LazyRecordCollection::class, $resolvedRecord['typo3tests_contentelementb_categories_1m']); + self::assertSame('Category 1', $resolvedRecord['typo3tests_contentelementb_categories_1m'][0]['title']); + self::assertSame('Category 2', $resolvedRecord['typo3tests_contentelementb_categories_1m'][1]['title']); + } + + #[Test] + public function canResolveDbRelation(): void + { + $this->importCSVDataSet(__DIR__ . '/Fixtures/DataSet/db_relation.csv'); + $dummyRecord = $this->createTestRecordObject([ + 'typo3tests_contentelementb_pages_relation' => '1906', + ]); + $fieldInformation = $this->get(TcaSchemaFactory::class)->get('tt_content')->getSubSchema('typo3tests_contentelementb')->getField('typo3tests_contentelementb_pages_relation'); + $subject = $this->get(RecordFieldTransformer::class); + $propertyClosure = $subject->transformField( + $fieldInformation, + $dummyRecord->getRawRecord(), + $this->get(Context::class) + ); + + self::assertInstanceOf(RecordPropertyClosure::class, $propertyClosure); + $result = $propertyClosure->instantiate(); + self::assertInstanceOf(Record::class, $result); + self::assertSame(1906, $result->getUid()); + self::assertSame(1906, $result['uid']); + self::assertSame('Page 1', $result['title']); + + $resolvedRecord = $this->get(RecordFactory::class)->createResolvedRecordFromDatabaseRow('tt_content', $dummyRecord->toArray()); + $resolvedRelation = $resolvedRecord['typo3tests_contentelementb_pages_relation']; + self::assertInstanceOf(Record::class, $resolvedRelation); + self::assertSame(1906, $resolvedRelation->getUid()); + self::assertSame(1906, $resolvedRelation['uid']); + self::assertSame('Page 1', $resolvedRelation['title']); + } + + #[Test] + public function canResolveDbRelations(): void + { + $this->importCSVDataSet(__DIR__ . '/Fixtures/DataSet/db_relations.csv'); + $dummyRecord = $this->createTestRecordObject([ + 'typo3tests_contentelementb_pages_relations' => '1906,3389', + ]); + $fieldInformation = $this->get(TcaSchemaFactory::class)->get('tt_content')->getField('typo3tests_contentelementb_pages_relations'); + $subject = $this->get(RecordFieldTransformer::class); + $result = $subject->transformField( + $fieldInformation, + $dummyRecord->getRawRecord(), + $this->get(Context::class) + ); + + self::assertCount(2, $result); + self::assertInstanceOf(LazyRecordCollection::class, $result); + self::assertSame('Page 1', $result[0]['title']); + self::assertSame('Page 2', $result[1]['title']); + + $resolvedRecord = $this->get(RecordFactory::class)->createResolvedRecordFromDatabaseRow('tt_content', $dummyRecord->toArray()); + $resolvedRelation = $resolvedRecord['typo3tests_contentelementb_pages_relations']; + self::assertCount(2, $resolvedRelation); + self::assertInstanceOf(LazyRecordCollection::class, $resolvedRelation); + self::assertSame(1906, $resolvedRelation[0]->getUid()); + self::assertSame(1906, $resolvedRelation[0]['uid']); + self::assertSame('Page 1', $resolvedRelation[0]['title']); + self::assertSame('Page 2', $resolvedRelation[1]['title']); + } + + #[Test] + public function canResolveCircularRelation(): void + { + $this->importCSVDataSet(__DIR__ . '/Fixtures/DataSet/circular_relation.csv'); + $dummyRecord = $this->createTestRecordObject([ + 'typo3tests_contentelementb_circular_relation' => '260', + ]); + $fieldInformation = $this->get(TcaSchemaFactory::class)->get('tt_content')->getField('typo3tests_contentelementb_circular_relation'); + $subject = $this->get(RecordFieldTransformer::class); + $result = $subject->transformField( + $fieldInformation, + $dummyRecord->getRawRecord(), + $this->get(Context::class) + ); + + self::assertCount(1, $result); + self::assertInstanceOf(LazyRecordCollection::class, $result); + self::assertSame(260, $result[0]->getUid()); + + $resolvedRecord = $this->get(RecordFactory::class)->createResolvedRecordFromDatabaseRow('tt_content', $dummyRecord->toArray()); + $resolvedRelation = $resolvedRecord['typo3tests_contentelementb_circular_relation']; + self::assertCount(1, $resolvedRelation); + self::assertInstanceOf(LazyRecordCollection::class, $result); + self::assertSame(260, $resolvedRelation[0]->getUid()); + self::assertSame(260, $resolvedRelation[0]['uid']); + } + + #[Test] + public function canResolveDbRelationRecursive(): void + { + $this->importCSVDataSet(__DIR__ . '/Fixtures/DataSet/db_relation_recursive.csv'); + $dummyRecord = $this->createTestRecordObject([ + 'typo3tests_contentelementb_record_relation_recursive' => '1,2', + ]); + $fieldInformation = $this->get(TcaSchemaFactory::class)->get('tt_content')->getField('typo3tests_contentelementb_record_relation_recursive'); + $subject = $this->get(RecordFieldTransformer::class); + $result = $subject->transformField( + $fieldInformation, + $dummyRecord->getRawRecord(), + $this->get(Context::class) + ); + + self::assertInstanceOf(LazyRecordCollection::class, $result); + self::assertCount(2, $result); + self::assertSame('Record 1', $result[0]['title']); + self::assertSame('Record 2', $result[1]['title']); + self::assertCount(1, $result[0]['record_collection']); + self::assertCount(1, $result[1]['record_collection']); + self::assertSame('Collection 1', $result[0]['record_collection'][0]['text']); + self::assertSame('Collection 2', $result[1]['record_collection'][0]['text']); + + $resolvedRecord = $this->get(RecordFactory::class)->createResolvedRecordFromDatabaseRow('tt_content', $dummyRecord->toArray()); + $resolvedRelation = $resolvedRecord['typo3tests_contentelementb_record_relation_recursive']; + self::assertCount(2, $resolvedRelation); + self::assertInstanceOf(LazyRecordCollection::class, $resolvedRelation); + self::assertSame('Record 1', $resolvedRelation[0]['title']); + self::assertSame('Record 2', $resolvedRelation[1]['title']); + self::assertCount(1, $resolvedRelation[0]['record_collection']); + self::assertCount(1, $resolvedRelation[1]['record_collection']); + self::assertSame('Collection 1', $resolvedRelation[0]['record_collection'][0]['text']); + self::assertSame('Collection 2', $resolvedRelation[1]['record_collection'][0]['text']); + } + + #[Test] + public function canResolveDbRelationsInWorkspaces(): void + { + $this->importCSVDataSet(__DIR__ . '/Fixtures/DataSet/db_relations.csv'); + $this->importCSVDataSet(__DIR__ . '/../Fixtures/be_users.csv'); + $this->setUpBackendUser(1); + $this->setWorkspaceId(1); + $dummyRecord = $this->createTestRecordObject([ + 'uid' => 260, + 't3ver_oid' => 260, + 't3ver_wsid' => 1, + '_ORIG_uid' => 261, + 'typo3tests_contentelementb_pages_relations' => '1906,3389', + ]); + $fieldInformation = $this->get(TcaSchemaFactory::class)->get('tt_content')->getField('typo3tests_contentelementb_pages_relations'); + $subject = $this->get(RecordFieldTransformer::class); + $result = $subject->transformField( + $fieldInformation, + $dummyRecord->getRawRecord(), + $this->get(Context::class) + ); + + self::assertCount(2, $result); + self::assertInstanceOf(LazyRecordCollection::class, $result); + self::assertSame('Page 1 ws', $result[0]['title']); + self::assertSame('Page 2 ws', $result[1]['title']); + + $resolvedRecord = $this->get(RecordFactory::class)->createResolvedRecordFromDatabaseRow('tt_content', $dummyRecord->toArray()); + $resolvedRelation = $resolvedRecord['typo3tests_contentelementb_pages_relations']; + self::assertCount(2, $resolvedRelation); + self::assertInstanceOf(LazyRecordCollection::class, $resolvedRelation); + self::assertSame('Page 1 ws', $resolvedRelation[0]['title']); + self::assertSame('Page 2 ws', $resolvedRelation[1]['title']); + } + + #[Test] + public function canResolveMultipleDbRelations(): void + { + $this->importCSVDataSet(__DIR__ . '/Fixtures/DataSet/db_relation_multiple.csv'); + $dummyRecord = $this->createTestRecordObject([ + 'typo3tests_contentelementb_pages_content_relation' => 'pages_1,pages_2,tt_content_1,tt_content_2', + ]); + $fieldInformation = $this->get(TcaSchemaFactory::class)->get('tt_content')->getField('typo3tests_contentelementb_pages_content_relation'); + $subject = $this->get(RecordFieldTransformer::class); + $result = $subject->transformField( + $fieldInformation, + $dummyRecord->getRawRecord(), + $this->get(Context::class) + ); + self::assertCount(4, $result); + self::assertInstanceOf(LazyRecordCollection::class, $result); + self::assertSame('Page 1', $result[0]['title']); + self::assertSame('Page 2', $result[1]['title']); + self::assertSame('Content 1', $result[2]['header']); + self::assertSame('Content 2', $result[3]['header']); + + $resolvedRecord = $this->get(RecordFactory::class)->createResolvedRecordFromDatabaseRow('tt_content', $dummyRecord->toArray()); + $resolvedRelation = $resolvedRecord['typo3tests_contentelementb_pages_content_relation']; + self::assertCount(4, $resolvedRelation); + self::assertInstanceOf(LazyRecordCollection::class, $resolvedRelation); + self::assertSame('Page 1', $resolvedRelation[0]['title']); + self::assertSame('Page 2', $resolvedRelation[1]['title']); + self::assertSame('Content 1', $resolvedRelation[2]['header']); + self::assertSame('Content 2', $resolvedRelation[3]['header']); + } + + #[Test] + public function canResolveDbRelationsMM(): void + { + $this->importCSVDataSet(__DIR__ . '/Fixtures/DataSet/db_relation_mm.csv'); + $dummyRecord = $this->createTestRecordObject([ + 'uid' => 263, + 'typo3tests_contentelementb_pages_mm' => 2, + ]); + $fieldInformation = $this->get(TcaSchemaFactory::class)->get('tt_content')->getField('typo3tests_contentelementb_pages_mm'); + $subject = $this->get(RecordFieldTransformer::class); + $result = $subject->transformField( + $fieldInformation, + $dummyRecord->getRawRecord(), + $this->get(Context::class) + ); + + self::assertCount(2, $result); + self::assertInstanceOf(LazyRecordCollection::class, $result); + self::assertSame('Page 1', $result[0]['title']); + self::assertSame('Page 2', $result[1]['title']); + + $resolvedRecord = $this->get(RecordFactory::class)->createResolvedRecordFromDatabaseRow('tt_content', $dummyRecord->toArray()); + $resolvedRelation = $resolvedRecord['typo3tests_contentelementb_pages_mm']; + self::assertCount(2, $resolvedRelation); + self::assertInstanceOf(LazyRecordCollection::class, $resolvedRelation); + self::assertSame('Page 1', $resolvedRelation[0]['title']); + self::assertSame('Page 2', $resolvedRelation[1]['title']); + } + + public static function multipleItemsAsArrayConversionDataProvider(): \Generator + { + yield 'selectCheckboxFormat' => [ + 'fieldName' => 'typo3tests_contentelementb_select_checkbox', + 'input' => '1,2,3', + 'expected' => ['1', '2', '3'], + ]; + yield 'selectSingleBoxCommaList' => [ + 'fieldName' => 'typo3tests_contentelementb_select_single_box', + 'input' => '1,2,3', + 'expected' => ['1', '2', '3'], + ]; + yield 'selectMultipleSideBySideCommaList' => [ + 'fieldName' => 'typo3tests_contentelementb_select_multiple', + 'input' => '1,2,3', + 'expected' => ['1', '2', '3'], + ]; + yield 'selectMultipleSideBySideWithOneValue' => [ + 'fieldName' => 'typo3tests_contentelementb_select_multiple', + 'input' => '1', + 'expected' => ['1'], + ]; + yield 'selectMultipleSideBySideWithEmptyOneValue' => [ + 'fieldName' => 'typo3tests_contentelementb_select_multiple', + 'input' => '', + 'expected' => [], + ]; + yield 'canResolveJson' => [ + 'fieldName' => 'typo3tests_contentelementb_json', + 'input' => '{"foo": "bar"}', + 'expected' => ['foo' => 'bar'], + ]; + } + + #[Test] + #[DataProvider('multipleItemsAsArrayConversionDataProvider')] + public function multipleItemsAsArrayConversionConvertedToArray(string $fieldName, string|int $input, array $expected): void + { + $dummyRecord = $this->createTestRecordObject([ + $fieldName => $input, + ]); + $fieldInformation = $this->get(TcaSchemaFactory::class)->get('tt_content')->getField($fieldName); + $subject = $this->get(RecordFieldTransformer::class); + $result = $subject->transformField( + $fieldInformation, + $dummyRecord->getRawRecord(), + $this->get(Context::class) + ); + + self::assertSame($expected, $result); + + $resolvedRecord = $this->get(RecordFactory::class)->createResolvedRecordFromDatabaseRow('tt_content', $dummyRecord->toArray()); + self::assertSame($expected, $resolvedRecord[$fieldName]); + } + + public static function canConvertDateTimeDataProvider(): \Generator + { + yield 'canResolveDatetime' => [ + 'fieldName' => 'typo3tests_contentelementb_datetime', + 'input' => 30, + 'expected' => '1970-01-01T00:00:30+00:00', + ]; + yield 'canResolveDatetimeZero' => [ + 'fieldName' => 'typo3tests_contentelementb_datetime', + 'input' => 0, + 'expected' => null, + ]; + yield 'canResolveDatetimeNull' => [ + 'fieldName' => 'typo3tests_contentelementb_datetime_nullable', + 'input' => 30, + 'expected' => '1970-01-01T00:00:30+00:00', + ]; + yield 'canResolveDatetimeNullZero' => [ + 'fieldName' => 'typo3tests_contentelementb_datetime_nullable', + 'input' => 0, + 'expected' => '1970-01-01T00:00:00+00:00', + ]; + yield 'canResolveDatetimeNullNull' => [ + 'fieldName' => 'typo3tests_contentelementb_datetime_nullable', + 'input' => null, + 'expected' => null, + ]; + } + + #[Test] + #[DataProvider('canConvertDateTimeDataProvider')] + public function canConvertDateTime(string $fieldName, ?int $input, ?string $expected): void + { + $dummyRecord = $this->createTestRecordObject([ + $fieldName => $input, + ]); + $fieldInformation = $this->get(TcaSchemaFactory::class)->get('tt_content')->getField($fieldName); + $subject = $this->get(RecordFieldTransformer::class); + $result = $subject->transformField( + $fieldInformation, + $dummyRecord->getRawRecord(), + $this->get(Context::class) + ); + + self::assertSame($expected, $result?->format('c')); + + $resolvedRecord = $this->get(RecordFactory::class)->createResolvedRecordFromDatabaseRow('tt_content', $dummyRecord->toArray()); + self::assertSame($expected, $resolvedRecord[$fieldName]?->format('c')); + } + + #[Test] + public function canResolveSelectSingle(): void + { + $dummyRecord = $this->createTestRecordObject([ + 'typo3tests_contentelementb_select_single' => '1', + ]); + $fieldInformation = $this->get(TcaSchemaFactory::class)->get('tt_content')->getSubSchema('typo3tests_contentelementb')->getField('typo3tests_contentelementb_select_single'); + $subject = $this->get(RecordFieldTransformer::class); + $result = $subject->transformField( + $fieldInformation, + $dummyRecord->getRawRecord(), + $this->get(Context::class) + ); + + self::assertSame('1', $result); + + $resolvedRecord = $this->get(RecordFactory::class)->createResolvedRecordFromDatabaseRow('tt_content', $dummyRecord->toArray()); + self::assertSame('1', $resolvedRecord['typo3tests_contentelementb_select_single']); + } + + #[Test] + public function canResolveSelectRelationOneToOne(): void + { + $this->importCSVDataSet(__DIR__ . '/Fixtures/DataSet/select_one_to_one.csv'); + $dummyRecord = $this->createTestRecordObject([ + 'typo3tests_contentelementb_select_one_to_one' => '1', + ]); + $fieldInformation = $this->get(TcaSchemaFactory::class)->get('tt_content')->getField('typo3tests_contentelementb_select_one_to_one'); + $subject = $this->get(RecordFieldTransformer::class); + $propertyClosure = $subject->transformField( + $fieldInformation, + $dummyRecord->getRawRecord(), + $this->get(Context::class) + ); + + self::assertInstanceOf(RecordPropertyClosure::class, $propertyClosure); + $result = $propertyClosure->instantiate(); + self::assertInstanceOf(Record::class, $result); + self::assertSame('Record 1', $result['title']); + + $resolvedRecord = $this->get(RecordFactory::class)->createResolvedRecordFromDatabaseRow('tt_content', $dummyRecord->toArray()); + $resolvedRelation = $resolvedRecord['typo3tests_contentelementb_select_one_to_one']; + self::assertInstanceOf(Record::class, $resolvedRelation); + self::assertSame('Record 1', $resolvedRelation['title']); + } + + /** + * Special case where NO Collection is returned, since the field has relationship="oneToOne" + */ + #[Test] + public function canResolveSelectForeignTableSingle(): void + { + $this->importCSVDataSet(__DIR__ . '/Fixtures/DataSet/select_foreign.csv'); + $dummyRecord = $this->createTestRecordObject([ + 'typo3tests_contentelementb_select_foreign_native' => '1', + ]); + $fieldInformation = $this->get(TcaSchemaFactory::class)->get('tt_content')->getSubSchema('typo3tests_contentelementb')->getField('typo3tests_contentelementb_select_foreign_native'); + $subject = $this->get(RecordFieldTransformer::class); + $propertyClosure = $subject->transformField( + $fieldInformation, + $dummyRecord->getRawRecord(), + $this->get(Context::class) + ); + + self::assertInstanceOf(RecordPropertyClosure::class, $propertyClosure); + $result = $propertyClosure->instantiate(); + self::assertInstanceOf(Record::class, $result); + self::assertSame('Record 1', $result['title']); + + $resolvedRecord = $this->get(RecordFactory::class)->createResolvedRecordFromDatabaseRow('tt_content', $dummyRecord->toArray()); + $resolvedField = $resolvedRecord['typo3tests_contentelementb_select_foreign_native']; + self::assertInstanceOf(Record::class, $resolvedField); + self::assertSame('Record 1', $resolvedField['title']); + } + + /** + * Special case where null is returned since the relation is invalid and the field has relationship="oneToOne" + */ + #[Test] + public function resolveSelectForeignTableSingleToNullRecord(): void + { + $this->importCSVDataSet(__DIR__ . '/Fixtures/DataSet/select_foreign.csv'); + $dummyRecord = $this->createTestRecordObject([ + 'typo3tests_contentelementb_select_foreign_native' => '123', + ]); + $fieldInformation = $this->get(TcaSchemaFactory::class)->get('tt_content')->getSubSchema('typo3tests_contentelementb')->getField('typo3tests_contentelementb_select_foreign_native'); + $subject = $this->get(RecordFieldTransformer::class); + $propertyClosure = $subject->transformField( + $fieldInformation, + $dummyRecord->getRawRecord(), + $this->get(Context::class) + ); + + self::assertInstanceOf(RecordPropertyClosure::class, $propertyClosure); + $result = $propertyClosure->instantiate(); + self::assertNull($result); + + $resolvedRecord = $this->get(RecordFactory::class)->createResolvedRecordFromDatabaseRow('tt_content', $dummyRecord->toArray()); + self::assertNull($resolvedRecord['typo3tests_contentelementb_select_foreign_native']); + } + + /** + * Special case where a an empty Collection is returned, since the relation is invalid + */ + #[Test] + public function resolveSelectForeignTableToEmptyCollection(): void + { + $this->importCSVDataSet(__DIR__ . '/Fixtures/DataSet/select_foreign.csv'); + $dummyRecord = $this->createTestRecordObject([ + 'typo3tests_contentelementb_select_foreign' => '123', + ]); + $fieldInformation = $this->get(TcaSchemaFactory::class)->get('tt_content')->getField('typo3tests_contentelementb_select_foreign'); + $subject = $this->get(RecordFieldTransformer::class); + $result = $subject->transformField( + $fieldInformation, + $dummyRecord->getRawRecord(), + $this->get(Context::class) + ); + + self::assertInstanceOf(LazyRecordCollection::class, $result); + self::assertCount(0, $result); + + $resolvedRecord = $this->get(RecordFactory::class)->createResolvedRecordFromDatabaseRow('tt_content', $dummyRecord->toArray()); + $resolvedRelation = $resolvedRecord['typo3tests_contentelementb_select_foreign']; + self::assertInstanceOf(LazyRecordCollection::class, $resolvedRelation); + self::assertCount(0, $resolvedRelation); + } + + /** + * Special case where an empty Collection is returned, since the relation is invalid + */ + #[Test] + public function resolveSelectForeignTableMultipleToEmptyCollection(): void + { + $this->importCSVDataSet(__DIR__ . '/Fixtures/DataSet/foreign_table_select_multiple.csv'); + $dummyRecord = $this->createTestRecordObject([ + 'typo3tests_contentelementb_select_foreign_multiple' => '123', + ]); + $fieldInformation = $this->get(TcaSchemaFactory::class)->get('tt_content')->getField('typo3tests_contentelementb_select_foreign_multiple'); + $subject = $this->get(RecordFieldTransformer::class); + $result = $subject->transformField( + $fieldInformation, + $dummyRecord->getRawRecord(), + $this->get(Context::class) + ); + + self::assertInstanceOf(LazyRecordCollection::class, $result); + self::assertCount(0, $result); + + $resolvedRecord = $this->get(RecordFactory::class)->createResolvedRecordFromDatabaseRow('tt_content', $dummyRecord->toArray()); + $resolvedRelation = $resolvedRecord['typo3tests_contentelementb_select_foreign_multiple']; + self::assertInstanceOf(LazyRecordCollection::class, $resolvedRelation); + self::assertCount(0, $resolvedRelation); + } + + #[Test] + public function canResolveSelectForeignTableMultiple(): void + { + $this->importCSVDataSet(__DIR__ . '/Fixtures/DataSet/select_foreign.csv'); + $dummyRecord = $this->createTestRecordObject([ + 'typo3tests_contentelementb_select_foreign_multiple' => '1,2', + ]); + $fieldInformation = $this->get(TcaSchemaFactory::class)->get('tt_content')->getField('typo3tests_contentelementb_select_foreign_multiple'); + $subject = $this->get(RecordFieldTransformer::class); + $result = $subject->transformField( + $fieldInformation, + $dummyRecord->getRawRecord(), + $this->get(Context::class) + ); + + self::assertCount(2, $result); + self::assertSame('Record 1', $result[0]['title']); + self::assertSame('Record 2', $result[1]['title']); + + $resolvedRecord = $this->get(RecordFactory::class)->createResolvedRecordFromDatabaseRow('tt_content', $dummyRecord->toArray()); + $resolvedRelation = $resolvedRecord['typo3tests_contentelementb_select_foreign_multiple']; + self::assertInstanceOf(LazyRecordCollection::class, $resolvedRelation); + self::assertCount(2, $resolvedRelation); + self::assertSame('Record 1', $resolvedRelation[0]['title']); + self::assertSame('Record 2', $resolvedRelation[1]['title']); + } + + #[Test] + public function canResolveSelectForeignTableMultipleAndSame(): void + { + $this->importCSVDataSet(__DIR__ . '/Fixtures/DataSet/foreign_table_select_multiple.csv'); + $dummyRecord = $this->createTestRecordObject([ + 'typo3tests_contentelementb_select_foreign_multiple' => '1,1', + ]); + $fieldInformation = $this->get(TcaSchemaFactory::class)->get('tt_content')->getField('typo3tests_contentelementb_select_foreign_multiple'); + $subject = $this->get(RecordFieldTransformer::class); + $result = $subject->transformField( + $fieldInformation, + $dummyRecord->getRawRecord(), + $this->get(Context::class) + ); + + self::assertCount(2, $result); + self::assertSame('Record 1', $result[0]['title']); + self::assertSame('Collection 1', $result[0]['record_collection'][0]['text']); + self::assertSame('Record 1', $result[1]['title']); + self::assertSame('Collection 1', $result[1]['record_collection'][0]['text']); + self::assertSame($result[0], $result[1]); + + $resolvedRecord = $this->get(RecordFactory::class)->createResolvedRecordFromDatabaseRow('tt_content', $dummyRecord->toArray()); + $resolvedRelation = $resolvedRecord['typo3tests_contentelementb_select_foreign_multiple']; + self::assertInstanceOf(LazyRecordCollection::class, $resolvedRelation); + self::assertCount(2, $resolvedRelation); + self::assertSame('Record 1', $resolvedRelation[0]['title']); + self::assertSame('Collection 1', $resolvedRelation[0]['record_collection'][0]['text']); + self::assertSame('Record 1', $resolvedRelation[1]['title']); + self::assertSame('Collection 1', $resolvedRelation[1]['record_collection'][0]['text']); + self::assertSame($resolvedRelation[0], $resolvedRelation[1]); + } + + #[Test] + public function canResolveSelectForeignTableRecursive(): void + { + $this->importCSVDataSet(__DIR__ . '/Fixtures/DataSet/select_foreign_recursive.csv'); + $dummyRecord = $this->createTestRecordObject([ + 'typo3tests_contentelementb_select_foreign' => '1', + ]); + $fieldInformation = $this->get(TcaSchemaFactory::class)->get('tt_content')->getField('typo3tests_contentelementb_select_foreign'); + $subject = $this->get(RecordFieldTransformer::class); + $result = $subject->transformField( + $fieldInformation, + $dummyRecord->getRawRecord(), + $this->get(Context::class) + ); + + self::assertCount(1, $result); + self::assertInstanceOf(LazyRecordCollection::class, $result); + $result = $result[0]; + self::assertSame('Record 1', $result['title']); + self::assertCount(1, $result['record_collection']); + self::assertSame('Collection 1', $result['record_collection'][0]['text']); + + $resolvedRecord = $this->get(RecordFactory::class)->createResolvedRecordFromDatabaseRow('tt_content', $dummyRecord->toArray()); + $resolvedRelation = $resolvedRecord['typo3tests_contentelementb_select_foreign']; + self::assertInstanceOf(LazyRecordCollection::class, $resolvedRelation); + self::assertCount(1, $resolvedRelation); + self::assertSame('Record 1', $resolvedRelation[0]['title']); + self::assertCount(1, $resolvedRelation[0]['record_collection']); + self::assertSame('Collection 1', $resolvedRelation[0]['record_collection'][0]['text']); + } + + #[Test] + public function canResolveFlexForm(): void + { + $dummyRecord = $this->createTestRecordObject([ + 'typo3tests_contentelementb_flexfield' => '<?xml version="1.0" encoding="utf-8" standalone="yes" ?> +<T3FlexForms> + <data> + <sheet index="sDEF"> + <language index="lDEF"> + <field index="header"> + <value index="vDEF">Header in Flex</value> + </field> + <field index="textarea"> + <value index="vDEF">Text in Flex</value> + </field> + </language> + </sheet> + </data> +</T3FlexForms>', + ]); + $fieldInformation = $this->get(TcaSchemaFactory::class)->get('tt_content')->getField('typo3tests_contentelementb_flexfield'); + + $subject = $this->get(RecordFieldTransformer::class); + $result = $subject->transformField( + $fieldInformation, + $dummyRecord->getRawRecord(), + $this->get(Context::class) + ); + + self::assertIsArray($result); + self::assertSame('Header in Flex', $result['header']); + self::assertSame('Text in Flex', $result['textarea']); + + $resolvedRecord = $this->get(RecordFactory::class)->createResolvedRecordFromDatabaseRow('tt_content', $dummyRecord->toArray()); + $resolvedRelation = $resolvedRecord['typo3tests_contentelementb_flexfield']; + self::assertIsArray($resolvedRelation); + self::assertSame('Header in Flex', $resolvedRelation['header']); + self::assertSame('Text in Flex', $resolvedRelation['textarea']); + } + + #[Test] + public function canResolveFlexFormWithSheetsOtherThanDefault(): void + { + $dummyRecord = $this->createTestRecordObject([ + 'typo3tests_contentelementb_flexfield' => '<?xml version="1.0" encoding="utf-8" standalone="yes" ?> +<T3FlexForms> + <data> + <sheet index="sheet1"> + <language index="lDEF"> + <field index="header"> + <value index="vDEF">Header in Flex</value> + </field> + <field index="textarea"> + <value index="vDEF">Text in Flex</value> + </field> + </language> + </sheet> + <sheet index="sheet2"> + <language index="lDEF"> + <field index="link"> + <value index="vDEF">t3://page?uid=13</value> + </field> + <field index="number"> + <value index="vDEF">12</value> + </field> + </language> + </sheet> + </data> +</T3FlexForms>', + ]); + + $fieldInformation = $this->get(TcaSchemaFactory::class)->get('tt_content')->getField('typo3tests_contentelementb_flexfield'); + $subject = $this->get(RecordFieldTransformer::class); + $result = $subject->transformField( + $fieldInformation, + $dummyRecord->getRawRecord(), + $this->get(Context::class) + ); + + self::assertIsArray($result); + self::assertSame('Header in Flex', $result['header']); + self::assertSame('Text in Flex', $result['textarea']); + self::assertSame('t3://page?uid=13', $result['link']->url); + self::assertSame('12', $result['number']); + + $resolvedRecord = $this->get(RecordFactory::class)->createResolvedRecordFromDatabaseRow('tt_content', $dummyRecord->toArray()); + $resolvedRelation = $resolvedRecord['typo3tests_contentelementb_flexfield']; + self::assertIsArray($resolvedRelation); + self::assertSame('Header in Flex', $resolvedRelation['header']); + self::assertSame('Text in Flex', $resolvedRelation['textarea']); + self::assertSame('t3://page?uid=13', $resolvedRelation['link']->url); + self::assertSame('12', $resolvedRelation['number']); + } + + protected function setWorkspaceId(int $workspaceId): void + { + $GLOBALS['BE_USER']->workspace = $workspaceId; + GeneralUtility::makeInstance(Context::class)->setAspect('workspace', new WorkspaceAspect($workspaceId)); + } + + protected function getTestRecord(): array + { + return [ + 'uid' => 260, + 'pid' => 1, + 'sys_language_uid' => 0, + 'l18n_parent' => 0, + 'hidden' => 0, + 'starttime' => 0, + 'endtime' => 0, + 'fe_group' => '', + 'editlock' => 0, + 'rowDescription' => '', + 'CType' => 'typo3tests_contentelementb', + 'colPos' => 0, + 'image' => 0, + 'typo3tests_contentelementb_collection' => 0, + 'typo3tests_contentelementb_collection2' => 0, + 'typo3tests_contentelementb_collection_external' => 0, + 'typo3tests_contentelementb_collection_recursive' => 0, + 'typo3tests_contentelementb_categories_mm' => 0, + 'typo3tests_contentelementb_categories_11' => 0, + 'typo3tests_contentelementb_categories_1m' => 0, + 'typo3tests_contentelementb_pages_relation' => 0, + 'typo3tests_contentelementb_circular_relation' => 0, + 'typo3tests_contentelementb_record_relation_recursive' => 0, + 'typo3tests_contentelementb_pages_content_relation' => '', + 'typo3tests_contentelementb_pages_mm' => 0, + 'typo3tests_contentelementb_folder' => 0, + 'typo3tests_contentelementb_folder_recursive' => 0, + 'typo3tests_contentelementb_select_single' => '', + 'typo3tests_contentelementb_select_checkbox' => '', + 'typo3tests_contentelementb_select_single_box' => '', + 'typo3tests_contentelementb_select_multiple' => '', + 'typo3tests_contentelementb_select_foreign_multiple' => '', + 'typo3tests_contentelementb_flexfield' => '', + 'typo3tests_contentelementb_json' => '', + 'typo3tests_contentelementb_datetime' => 0, + 'typo3tests_contentelementb_datetime_nullable' => null, + 'typo3tests_contentelementb_select_foreign' => '', + ]; + } + + protected function createTestRecordObject(array $overriddenValues = []): Record + { + $dummyRecordData = $this->getTestRecord(); + $dummyRecordData = array_replace($dummyRecordData, $overriddenValues); + return $this->get(RecordFactory::class)->createFromDatabaseRow('tt_content', $dummyRecordData); + } +} diff --git a/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_classic_content/Configuration/TCA/test_content_carousel_item.php b/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_classic_content/Configuration/TCA/test_content_carousel_item.php index 22f691104fd58fa9c11911ceb7cc7c71d0b19434..4cae894c2edf1070d4291c89bbee6a784cff05d7 100644 --- a/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_classic_content/Configuration/TCA/test_content_carousel_item.php +++ b/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_classic_content/Configuration/TCA/test_content_carousel_item.php @@ -51,7 +51,7 @@ return [ 'label' => 'Image', 'config' => [ 'type' => 'file', - 'maxitems' => 1, + 'relationship' => 'manyToOne', ], ], ], diff --git a/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_relation_resolver/Configuration/TCA/Overrides/tt_content.php b/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_relation_resolver/Configuration/TCA/Overrides/tt_content.php new file mode 100644 index 0000000000000000000000000000000000000000..48a03e88c20a4f98be4199e6371d6e52be52a51d --- /dev/null +++ b/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_relation_resolver/Configuration/TCA/Overrides/tt_content.php @@ -0,0 +1,461 @@ +<?php + +$overrides = [ + 'palettes' => [ + 'typo3tests_contentelementb_palette' => [ + 'showitem' => 'typo3tests_contentelementb_select_single,typo3tests_contentelementb_select_one_to_one,typo3tests_contentelementb_select_checkbox,typo3tests_contentelementb_select_single_box,typo3tests_contentelementb_select_multiple,typo3tests_contentelementb_select_foreign,typo3tests_contentelementb_select_foreign_native,typo3tests_contentelementb_select_foreign_multiple', + ], + ], + 'columns' => [ + 'typo3tests_contentelementb_collection' => [ + 'config' => [ + 'type' => 'inline', + 'foreign_table' => 'typo3tests_contentelementb_collection', + 'foreign_field' => 'foreign_table_parent_uid', + ], + 'exclude' => true, + 'label' => 'collection', + ], + 'typo3tests_contentelementb_collection_recursive' => [ + 'config' => [ + 'type' => 'inline', + 'foreign_table' => 'typo3tests_contentelementb_collection_recursive', + 'foreign_field' => 'foreign_table_parent_uid', + ], + 'exclude' => true, + 'label' => 'collection_recursive', + ], + 'typo3tests_contentelementb_categories_mm' => [ + 'config' => [ + 'type' => 'category', + ], + 'exclude' => true, + 'label' => 'categories_mm', + ], + 'typo3tests_contentelementb_categories_11' => [ + 'config' => [ + 'type' => 'category', + 'relationship' => 'oneToOne', + ], + 'exclude' => true, + 'label' => 'categories_11', + ], + 'typo3tests_contentelementb_categories_1m' => [ + 'config' => [ + 'type' => 'category', + 'relationship' => 'oneToMany', + ], + 'exclude' => true, + 'label' => 'categories_1m', + ], + 'typo3tests_contentelementb_pages_relation' => [ + 'config' => [ + 'type' => 'group', + 'allowed' => 'pages', + ], + 'exclude' => true, + 'label' => 'pages_relation', + ], + 'typo3tests_contentelementb_pages_relations' => [ + 'config' => [ + 'type' => 'group', + 'allowed' => 'pages', + ], + 'exclude' => true, + 'label' => 'pages_relations', + ], + 'typo3tests_contentelementb_circular_relation' => [ + 'config' => [ + 'type' => 'group', + 'allowed' => 'tt_content', + ], + 'exclude' => true, + 'label' => 'circular_relation', + ], + 'typo3tests_contentelementb_record_relation_recursive' => [ + 'config' => [ + 'type' => 'group', + 'allowed' => 'test_record', + ], + 'exclude' => true, + 'label' => 'record_relation_recursive', + ], + 'typo3tests_contentelementb_pages_content_relation' => [ + 'config' => [ + 'type' => 'group', + 'allowed' => 'pages,tt_content', + ], + 'exclude' => true, + 'label' => 'pages_content_relation', + ], + 'typo3tests_contentelementb_pages_mm' => [ + 'config' => [ + 'type' => 'group', + 'MM' => 'block_pages_mm', + 'allowed' => 'pages', + ], + 'exclude' => true, + 'label' => 'pages_mm', + ], + 'typo3tests_contentelementb_folder' => [ + 'config' => [ + 'type' => 'folder', + 'relationship' => 'manyToOne', + ], + 'exclude' => true, + 'label' => 'folder', + ], + 'typo3tests_contentelementb_folder_recursive' => [ + 'config' => [ + 'type' => 'folder', + ], + 'exclude' => true, + 'label' => 'folder_recursive', + ], + 'typo3tests_contentelementb_select_checkbox' => [ + 'config' => [ + 'type' => 'select', + ], + 'exclude' => true, + 'label' => 'select_checkbox', + ], + 'typo3tests_contentelementb_select_single_box' => [ + 'config' => [ + 'type' => 'select', + ], + 'exclude' => true, + 'label' => 'select_single_box', + ], + 'typo3tests_contentelementb_select_single' => [ + 'config' => [ + 'type' => 'select', + 'renderType' => 'selectSingle', + ], + 'exclude' => true, + 'label' => 'select_single', + ], + 'typo3tests_contentelementb_select_one_to_one' => [ + 'config' => [ + 'type' => 'select', + 'relationship' => 'oneToOne', + 'foreign_table' => 'test_record', + ], + 'exclude' => true, + 'label' => 'select_one_to_one', + ], + 'typo3tests_contentelementb_select_multiple' => [ + 'config' => [ + 'type' => 'select', + ], + 'exclude' => true, + 'label' => 'select_multiple', + ], + 'typo3tests_contentelementb_select_foreign' => [ + 'config' => [ + 'type' => 'select', + 'foreign_table' => 'test_record', + ], + 'exclude' => true, + ], + 'typo3tests_contentelementb_select_foreign_native' => [ + 'config' => [ + 'type' => 'select', + 'foreign_table' => 'test_record', + ], + 'exclude' => true, + 'label' => 'select_foreign_native', + ], + 'typo3tests_contentelementb_select_foreign_multiple' => [ + 'config' => [ + 'type' => 'select', + 'foreign_table' => 'test_record', + ], + 'exclude' => true, + 'label' => 'select_foreign_multiple', + ], + 'typo3tests_contentelementb_flexfield' => [ + 'config' => [ + 'type' => 'flex', + 'ds' => [ + 'typo3tests_contentelementb' => '<T3FlexForms>' . "\n" + . ' <sheets type="array">' . "\n" + . ' <sDEF type="array">' . "\n" + . ' <ROOT type="array">' . "\n" + . ' <type>array</type>' . "\n" + . ' <el type="array">' . "\n" + . ' <field index="header" type="array">' . "\n" + . ' <label>header</label>' . "\n" + . ' <config>' . "\n" + . ' <type>input</type>' . "\n" + . ' </config>' . "\n" + . ' </field>' . "\n" + . ' <field index="textarea" type="array">' . "\n" + . ' <label>textarea</label>' . "\n" + . ' <config>' . "\n" + . ' <type>text</type>' . "\n" + . ' </config>' . "\n" + . ' </field>' . "\n" + . ' </el>' . "\n" + . ' </ROOT>' . "\n" + . ' </sDEF>' . "\n" + . ' <sheet2 type="array">' . "\n" + . ' <ROOT type="array">' . "\n" + . ' <type>array</type>' . "\n" + . ' <el>' . "\n" + . ' <link>' . "\n" + . ' <label>header</label>' . "\n" + . ' <config>' . "\n" + . ' <type>link</type>' . "\n" + . ' </config>' . "\n" + . ' </link>' . "\n" + . ' <number>' . "\n" + . ' <label>number</label>' . "\n" + . ' <config>' . "\n" + . ' <type>number</type>' . "\n" + . ' </config>' . "\n" + . ' </number>' . "\n" + . ' </el>' . "\n" + . ' </ROOT>' . "\n" + . ' </sheet2>' . "\n" + . ' </sheets>' . "\n" + . '</T3FlexForms>', + 'default' => '<T3DataStructure>' . "\n" + . ' <ROOT>' . "\n" + . ' <type>array</type>' . "\n" + . ' <el>' . "\n" + . ' <xmlTitle>' . "\n" + . ' <label>The Title:</label>' . "\n" + . ' <config>' . "\n" + . ' <type>input</type>' . "\n" + . ' <size>48</size>' . "\n" + . ' </config>' . "\n" + . ' </xmlTitle>' . "\n" + . ' </el>' . "\n" + . ' </ROOT>' . "\n" + . '</T3DataStructure>', + ], + 'ds_pointerField' => 'CType', + ], + 'exclude' => true, + 'label' => 'flexfield', + ], + 'typo3tests_contentelementb_json' => [ + 'config' => [ + 'type' => 'json', + ], + 'exclude' => true, + 'label' => 'json', + ], + 'typo3tests_contentelementb_datetime' => [ + 'config' => [ + 'type' => 'datetime', + ], + 'exclude' => true, + 'label' => 'datetime', + ], + 'typo3tests_contentelementb_datetime_nullable' => [ + 'config' => [ + 'type' => 'datetime', + 'nullable' => true, + ], + 'exclude' => true, + 'label' => 'datetime nullable', + ], + ], + 'types' => [ + 'typo3tests_contentelementb' => [ + 'showitem' => '--div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:general,--palette--;;general,image,media,assets,typo3tests_contentelementb_collection,typo3tests_contentelementb_collection_recursive,typo3tests_contentelementb_categories_mm,typo3tests_contentelementb_categories_11,typo3tests_contentelementb_categories_1m,typo3tests_contentelementb_pages_relation,typo3tests_contentelementb_circular_relation,typo3tests_contentelementb_record_relation_recursive,typo3tests_contentelementb_pages_content_relation,typo3tests_contentelementb_pages_relations,typo3tests_contentelementb_pages_mm,typo3tests_contentelementb_folder,typo3tests_contentelementb_folder_recursive,--palette--;;typo3tests_contentelementb_palette,typo3tests_contentelementb_flexfield,typo3tests_contentelementb_json,typo3tests_contentelementb_datetime,typo3tests_contentelementb_datetime_nullable,--div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:language,--palette--;;language,--div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:access,--palette--;;hidden,--palette--;;access,--div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:notes,rowDescription', + 'columnsOverrides' => [ + 'image' => [ + 'config' => [ + 'relationship' => 'manyToOne', + ], + ], + 'typo3tests_contentelementb_collection' => [ + 'label' => 'collection', + 'config' => [ + 'appearance' => [ + 'useSortable' => true, + ], + ], + ], + 'typo3tests_contentelementb_collection_recursive' => [ + 'label' => 'collection_recursive', + 'config' => [ + 'appearance' => [ + 'useSortable' => true, + ], + ], + ], + 'typo3tests_contentelementb_categories_mm' => [ + 'label' => 'categories_mm', + 'config' => [], + ], + 'typo3tests_contentelementb_categories_11' => [ + 'label' => 'categories_11', + 'config' => [], + ], + 'typo3tests_contentelementb_categories_1m' => [ + 'label' => 'categories_1m', + 'config' => [], + ], + 'typo3tests_contentelementb_pages_relation' => [ + 'label' => 'pages_relation', + 'config' => [ + 'relationship' => 'manyToOne', + ], + ], + 'typo3tests_contentelementb_pages_relations' => [ + 'label' => 'pages_relations', + 'config' => [], + ], + 'typo3tests_contentelementb_circular_relation' => [ + 'label' => 'circular_relation', + 'config' => [], + ], + 'typo3tests_contentelementb_record_relation_recursive' => [ + 'label' => 'record_relation_recursive', + 'config' => [], + ], + 'typo3tests_contentelementb_pages_content_relation' => [ + 'label' => 'pages_content_relation', + 'config' => [], + ], + 'typo3tests_contentelementb_pages_mm' => [ + 'label' => 'pages_mm', + 'config' => [], + ], + 'typo3tests_contentelementb_folder' => [ + 'label' => 'folder', + 'config' => [], + ], + 'typo3tests_contentelementb_folder_recursive' => [ + 'label' => 'folder_recursive', + 'config' => [], + ], + 'typo3tests_contentelementb_select_single' => [ + 'config' => [ + 'items' => [ + [ + 'label' => 'Foo 1', + 'value' => '1', + ], + [ + 'label' => 'Foo 2', + 'value' => '2', + ], + [ + 'label' => 'Foo 3', + 'value' => '3', + ], + ], + ], + ], + 'typo3tests_contentelementb_select_checkbox' => [ + 'label' => 'select_checkbox', + 'config' => [ + 'renderType' => 'selectCheckBox', + 'items' => [ + [ + 'label' => 'Foo 1', + 'value' => '1', + ], + [ + 'label' => 'Foo 2', + 'value' => '2', + ], + [ + 'label' => 'Foo 3', + 'value' => '3', + ], + ], + ], + ], + 'typo3tests_contentelementb_select_single_box' => [ + 'label' => 'select_single_box', + 'config' => [ + 'renderType' => 'selectSingleBox', + 'items' => [ + [ + 'label' => 'Foo 1', + 'value' => '1', + ], + [ + 'label' => 'Foo 2', + 'value' => '2', + ], + [ + 'label' => 'Foo 3', + 'value' => '3', + ], + ], + ], + ], + 'typo3tests_contentelementb_select_multiple' => [ + 'label' => 'select_multiple', + 'config' => [ + 'renderType' => 'selectMultipleSideBySide', + 'items' => [ + [ + 'label' => 'Foo 1', + 'value' => '1', + ], + [ + 'label' => 'Foo 2', + 'value' => '2', + ], + [ + 'label' => 'Foo 3', + 'value' => '3', + ], + ], + ], + ], + 'typo3tests_contentelementb_select_foreign' => [ + 'label' => 'select_foreign', + 'config' => [ + 'items' => [], + ], + ], + 'typo3tests_contentelementb_select_foreign_multiple' => [ + 'label' => 'select_foreign_multiple', + 'config' => [ + 'renderType' => 'selectMultipleSideBySide', + 'items' => [], + ], + ], + 'typo3tests_contentelementb_select_foreign_native' => [ + 'label' => 'select_foreign_native', + 'config' => [ + 'relationship' => 'manyToOne', + 'items' => [], + ], + ], + 'typo3tests_contentelementb_flexfield' => [ + 'label' => 'flexfield', + 'config' => [], + ], + 'typo3tests_contentelementb_json' => [ + 'label' => 'json', + 'config' => [], + ], + 'typo3tests_contentelementb_datetime' => [ + 'label' => 'json', + 'config' => [], + ], + 'typo3tests_contentelementb_datetime_nullable' => [ + 'label' => 'json', + 'config' => [], + ], + ], + ], + ], + 'ctrl' => [ + 'typeicon_classes' => [ + 'typo3tests_contentelementb' => 'tt_content-typo3tests_contentelementb-175ef6f', + ], + 'searchFields' => 'header,header_link,subheader,bodytext,pi_flexform,typo3tests_contentelementb_flexfield,typo3tests_contentelementb_json', + ], +]; + +$GLOBALS['TCA']['tt_content'] = array_replace_recursive($GLOBALS['TCA']['tt_content'], $overrides); diff --git a/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_relation_resolver/Configuration/TCA/collection_inner.php b/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_relation_resolver/Configuration/TCA/collection_inner.php new file mode 100644 index 0000000000000000000000000000000000000000..1346b7c4c64b9bf6c7c9b66ba2aab5bf2209a6bc --- /dev/null +++ b/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_relation_resolver/Configuration/TCA/collection_inner.php @@ -0,0 +1,64 @@ +<?php + +return [ + 'ctrl' => [ + 'title' => 'collection_inner', + 'label' => 'fieldB', + 'hideTable' => true, + 'enablecolumns' => [ + 'disabled' => 'hidden', + 'starttime' => 'starttime', + 'endtime' => 'endtime', + 'fe_group' => 'fe_group', + ], + 'editlock' => 'editlock', + 'delete' => 'deleted', + 'crdate' => 'crdate', + 'tstamp' => 'tstamp', + 'versioningWS' => true, + 'sortby' => 'sorting', + 'security' => [ + 'ignorePageTypeRestriction' => true, + ], + 'transOrigPointerField' => 'l10n_parent', + 'translationSource' => 'l10n_source', + 'transOrigDiffSourceField' => 'l10n_diffsource', + 'languageField' => 'sys_language_uid', + 'typeicon_classes' => [ + 'default' => 'collection_inner-1-116cf86', + ], + 'searchFields' => 'fieldB', + ], + 'palettes' => [ + 'language' => [ + 'showitem' => 'sys_language_uid,l10n_parent', + ], + 'hidden' => [ + 'label' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.palettes.visibility', + 'showitem' => 'hidden', + ], + 'access' => [ + 'label' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:palette.access', + 'showitem' => 'starttime;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:starttime_formlabel,endtime;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:endtime_formlabel,--linebreak--,fe_group;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:fe_group_formlabel,--linebreak--,editlock', + ], + ], + 'columns' => [ + 'foreign_table_parent_uid' => [ + 'config' => [ + 'type' => 'passthrough', + ], + ], + 'fieldB' => [ + 'label' => 'fieldB', + 'exclude' => true, + 'config' => [ + 'type' => 'input', + ], + ], + ], + 'types' => [ + 1 => [ + 'showitem' => '--div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:general,fieldB,--div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:language,--palette--;;language,--div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:access,--palette--;;hidden,--palette--;;access', + ], + ], +]; diff --git a/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_relation_resolver/Configuration/TCA/record_collection.php b/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_relation_resolver/Configuration/TCA/record_collection.php new file mode 100644 index 0000000000000000000000000000000000000000..a93669824eb521f2f338d707edba7280edc1f6ce --- /dev/null +++ b/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_relation_resolver/Configuration/TCA/record_collection.php @@ -0,0 +1,61 @@ +<?php + +return [ + 'ctrl' => [ + 'title' => 'record_collection', + 'label' => 'text', + 'hideTable' => true, + 'enablecolumns' => [ + 'disabled' => 'hidden', + 'starttime' => 'starttime', + 'endtime' => 'endtime', + 'fe_group' => 'fe_group', + ], + 'editlock' => 'editlock', + 'delete' => 'deleted', + 'crdate' => 'crdate', + 'tstamp' => 'tstamp', + 'versioningWS' => true, + 'sortby' => 'sorting', + 'transOrigPointerField' => 'l10n_parent', + 'translationSource' => 'l10n_source', + 'transOrigDiffSourceField' => 'l10n_diffsource', + 'languageField' => 'sys_language_uid', + 'typeicon_classes' => [ + 'default' => 'record_collection-1-cc2849f', + ], + 'searchFields' => 'text', + ], + 'palettes' => [ + 'language' => [ + 'showitem' => 'sys_language_uid,l10n_parent', + ], + 'hidden' => [ + 'label' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.palettes.visibility', + 'showitem' => 'hidden', + ], + 'access' => [ + 'label' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:palette.access', + 'showitem' => 'starttime;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:starttime_formlabel,endtime;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:endtime_formlabel,--linebreak--,fe_group;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:fe_group_formlabel,--linebreak--,editlock', + ], + ], + 'columns' => [ + 'foreign_table_parent_uid' => [ + 'config' => [ + 'type' => 'passthrough', + ], + ], + 'text' => [ + 'label' => 'text', + 'exclude' => true, + 'config' => [ + 'type' => 'input', + ], + ], + ], + 'types' => [ + 1 => [ + 'showitem' => '--div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:general,text,--div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:language,--palette--;;language,--div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:access,--palette--;;hidden,--palette--;;access', + ], + ], +]; diff --git a/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_relation_resolver/Configuration/TCA/test_record.php b/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_relation_resolver/Configuration/TCA/test_record.php new file mode 100644 index 0000000000000000000000000000000000000000..3d116835040bac0f71d0b9013447fc7cd67a62ff --- /dev/null +++ b/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_relation_resolver/Configuration/TCA/test_record.php @@ -0,0 +1,77 @@ +<?php + +return [ + 'ctrl' => [ + 'title' => 'typo3tests/test-record', + 'label' => 'title', + 'hideTable' => true, + 'enablecolumns' => [ + 'disabled' => 'hidden', + 'starttime' => 'starttime', + 'endtime' => 'endtime', + 'fe_group' => 'fe_group', + ], + 'origUid' => 't3_origuid', + 'editlock' => 'editlock', + 'delete' => 'deleted', + 'crdate' => 'crdate', + 'tstamp' => 'tstamp', + 'versioningWS' => true, + 'sortby' => 'sorting', + 'security' => [ + 'ignorePageTypeRestriction' => true, + ], + 'typeicon_classes' => [ + 'default' => 'test_record-typo3tests_testrecord-cc2849f', + ], + 'searchFields' => 'title', + ], + 'palettes' => [ + 'hidden' => [ + 'label' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.palettes.visibility', + 'showitem' => 'hidden', + ], + 'access' => [ + 'label' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:palette.access', + 'showitem' => 'starttime;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:starttime_formlabel,endtime;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:endtime_formlabel,--linebreak--,fe_group;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:fe_group_formlabel,--linebreak--,editlock', + ], + ], + 'columns' => [ + 'foreign_table_parent_uid' => [ + 'config' => [ + 'type' => 'passthrough', + ], + ], + 'tablenames' => [ + 'config' => [ + 'type' => 'passthrough', + ], + ], + 'fieldname' => [ + 'config' => [ + 'type' => 'passthrough', + ], + ], + 'title' => [ + 'label' => 'title', + 'exclude' => true, + 'config' => [ + 'type' => 'input', + ], + ], + 'record_collection' => [ + 'label' => 'record_collection', + 'exclude' => true, + 'config' => [ + 'type' => 'inline', + 'foreign_table' => 'record_collection', + 'foreign_field' => 'foreign_table_parent_uid', + ], + ], + ], + 'types' => [ + 1 => [ + 'showitem' => '--div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:general,title,record_collection,--div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:access,--palette--;;hidden,--palette--;;access', + ], + ], +]; diff --git a/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_relation_resolver/Configuration/TCA/typo3tests_contentelementb_collection.php b/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_relation_resolver/Configuration/TCA/typo3tests_contentelementb_collection.php new file mode 100644 index 0000000000000000000000000000000000000000..913df5530cb35fa890d8a018e0aa514196eaceb4 --- /dev/null +++ b/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_relation_resolver/Configuration/TCA/typo3tests_contentelementb_collection.php @@ -0,0 +1,64 @@ +<?php + +return [ + 'ctrl' => [ + 'title' => 'collection', + 'label' => 'fieldA', + 'hideTable' => true, + 'enablecolumns' => [ + 'disabled' => 'hidden', + 'starttime' => 'starttime', + 'endtime' => 'endtime', + 'fe_group' => 'fe_group', + ], + 'editlock' => 'editlock', + 'delete' => 'deleted', + 'crdate' => 'crdate', + 'tstamp' => 'tstamp', + 'versioningWS' => true, + 'sortby' => 'sorting', + 'security' => [ + 'ignorePageTypeRestriction' => true, + ], + 'transOrigPointerField' => 'l10n_parent', + 'translationSource' => 'l10n_source', + 'transOrigDiffSourceField' => 'l10n_diffsource', + 'languageField' => 'sys_language_uid', + 'typeicon_classes' => [ + 'default' => 'typo3tests_contentelementb_collection-1-116cf86', + ], + 'searchFields' => 'fieldA', + ], + 'palettes' => [ + 'language' => [ + 'showitem' => 'sys_language_uid,l10n_parent', + ], + 'hidden' => [ + 'label' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.palettes.visibility', + 'showitem' => 'hidden', + ], + 'access' => [ + 'label' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:palette.access', + 'showitem' => 'starttime;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:starttime_formlabel,endtime;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:endtime_formlabel,--linebreak--,fe_group;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:fe_group_formlabel,--linebreak--,editlock', + ], + ], + 'columns' => [ + 'foreign_table_parent_uid' => [ + 'config' => [ + 'type' => 'passthrough', + ], + ], + 'fieldA' => [ + 'label' => 'fieldA', + 'exclude' => true, + 'config' => [ + 'type' => 'input', + ], + ], + ], + 'types' => [ + 1 => [ + 'showitem' => '--div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:general,fieldA,--div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:language,--palette--;;language,--div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:access,--palette--;;hidden,--palette--;;access', + ], + ], +]; diff --git a/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_relation_resolver/Configuration/TCA/typo3tests_contentelementb_collection_recursive.php b/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_relation_resolver/Configuration/TCA/typo3tests_contentelementb_collection_recursive.php new file mode 100644 index 0000000000000000000000000000000000000000..8786b31bf6334867ab898850e8bd5c01906eedfa --- /dev/null +++ b/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_relation_resolver/Configuration/TCA/typo3tests_contentelementb_collection_recursive.php @@ -0,0 +1,73 @@ +<?php + +return [ + 'ctrl' => [ + 'title' => 'collection_recursive', + 'label' => 'fieldA', + 'hideTable' => true, + 'enablecolumns' => [ + 'disabled' => 'hidden', + 'starttime' => 'starttime', + 'endtime' => 'endtime', + 'fe_group' => 'fe_group', + ], + 'editlock' => 'editlock', + 'delete' => 'deleted', + 'crdate' => 'crdate', + 'tstamp' => 'tstamp', + 'versioningWS' => true, + 'sortby' => 'sorting', + 'security' => [ + 'ignorePageTypeRestriction' => true, + ], + 'transOrigPointerField' => 'l10n_parent', + 'translationSource' => 'l10n_source', + 'transOrigDiffSourceField' => 'l10n_diffsource', + 'languageField' => 'sys_language_uid', + 'typeicon_classes' => [ + 'default' => 'typo3tests_contentelementb_collection_recursive-1-116cf86', + ], + 'searchFields' => 'fieldA', + ], + 'palettes' => [ + 'language' => [ + 'showitem' => 'sys_language_uid,l10n_parent', + ], + 'hidden' => [ + 'label' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.palettes.visibility', + 'showitem' => 'hidden', + ], + 'access' => [ + 'label' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:palette.access', + 'showitem' => 'starttime;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:starttime_formlabel,endtime;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:endtime_formlabel,--linebreak--,fe_group;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:fe_group_formlabel,--linebreak--,editlock', + ], + ], + 'columns' => [ + 'foreign_table_parent_uid' => [ + 'config' => [ + 'type' => 'passthrough', + ], + ], + 'fieldA' => [ + 'label' => 'fieldA', + 'exclude' => true, + 'config' => [ + 'type' => 'input', + ], + ], + 'collection_inner' => [ + 'label' => 'collection_inner', + 'exclude' => true, + 'config' => [ + 'type' => 'inline', + 'foreign_table' => 'collection_inner', + 'foreign_field' => 'foreign_table_parent_uid', + ], + ], + ], + 'types' => [ + 1 => [ + 'showitem' => '--div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:general,fieldA,collection_inner,--div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:language,--palette--;;language,--div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:access,--palette--;;hidden,--palette--;;access', + ], + ], +]; diff --git a/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_relation_resolver/composer.json b/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_relation_resolver/composer.json new file mode 100644 index 0000000000000000000000000000000000000000..cda791ae4a3a4a43404ad6b7c5cc4f83d32bf13b --- /dev/null +++ b/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_relation_resolver/composer.json @@ -0,0 +1,14 @@ +{ + "name": "typo3tests/fixture-relation-resolver-test", + "type": "typo3-cms-extension", + "description": "FluidTemplateContentObject Test", + "license": "GPL-2.0-or-later", + "require": { + "typo3/cms-core": "13.3.*@dev" + }, + "extra": { + "typo3/cms": { + "extension-key": "test_relation_resolver" + } + } +} diff --git a/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_relation_resolver/ext_emconf.php b/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_relation_resolver/ext_emconf.php new file mode 100644 index 0000000000000000000000000000000000000000..638ba2ada49f75efda5685f35242bf9acbf8d852 --- /dev/null +++ b/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_relation_resolver/ext_emconf.php @@ -0,0 +1,18 @@ +<?php + +$EM_CONF[$_EXTKEY] = [ + 'title' => 'Relation Resolver Test', + 'description' => 'Relation Resolver Test', + 'category' => 'example', + 'version' => '13.3.0', + 'state' => 'beta', + 'author' => 'Nikita Hovratov', + 'author_company' => '', + 'constraints' => [ + 'depends' => [ + 'typo3' => '13.3.0', + ], + 'conflicts' => [], + 'suggests' => [], + ], +]; diff --git a/typo3/sysext/core/Tests/Unit/Configuration/Tca/TcaPreparationTest.php b/typo3/sysext/core/Tests/Unit/Configuration/Tca/TcaPreparationTest.php index 2e5383733995be82a22710d93f0683806f47c348..42099440a0b66bc7226dea923713f0da2efb80ee 100644 --- a/typo3/sysext/core/Tests/Unit/Configuration/Tca/TcaPreparationTest.php +++ b/typo3/sysext/core/Tests/Unit/Configuration/Tca/TcaPreparationTest.php @@ -757,10 +757,61 @@ final class TcaPreparationTest extends UnitTestCase self::assertEquals('jpg,png,gif', $subjectMethodReflection->invoke($subject, ['common-image-types,jpg,gif'])); } + public static function prepareSelectSingleAddsRelationshipDataProvider(): iterable + { + yield [ + [ + 'MM' => 'select_table_mm', + ], + 'manyToMany', + ]; + yield [ + [], + 'manyToOne', + ]; + } + + #[DataProvider('prepareSelectSingleAddsRelationshipDataProvider')] + #[Test] + public function prepareSelectSingleAddsRelationship(array $configuration, $expectedRelation): void + { + $subject = (new TcaPreparation())->prepare(['foo' => ['columns' => ['select' => ['config' => array_merge(['type' => 'select', 'renderType' => 'selectSingle', 'foreign_table' => 'tx_myextension_bar'], $configuration)]]]]); + self::assertEquals($expectedRelation, $subject['foo']['columns']['select']['config']['relationship']); + } + #[Test] - public function prepareSelectSingleAddsMaxItems(): void + public function prepareSelectSingleDoesNotOverwriteRelationship(): void + { + $subject = (new TcaPreparation())->prepare(['foo' => ['columns' => ['select' => ['config' => ['type' => 'select', 'renderType' => 'selectSingle', 'foreign_table' => 'tx_myextension_bar', 'relationship' => 'oneToOne']]]]]); + self::assertEquals('oneToOne', $subject['foo']['columns']['select']['config']['relationship']); + } + + #[Test] + public function prepareSelectSingleDoesNotAddRelationshipOnMissingForeignTable(): void { $subject = (new TcaPreparation())->prepare(['foo' => ['columns' => ['select' => ['config' => ['type' => 'select', 'renderType' => 'selectSingle']]]]]); - self::assertEquals(1, $subject['foo']['columns']['select']['config']['maxitems']); + self::assertNull($subject['foo']['columns']['select']['config']['relationship'] ?? null); + } + + public static function prepareRelationshipToOneAddsMaxItemsDataProvider(): iterable + { + yield ['select']; + yield ['inline']; + yield ['file']; + yield ['folder']; + yield ['group']; + yield ['input', 0]; + } + + #[DataProvider('prepareRelationshipToOneAddsMaxItemsDataProvider')] + #[Test] + public function prepareRelationshipToOneAddsMaxItems(string $type, int $maxitems = 1): void + { + $subject = (new TcaPreparation())->prepare(['foo' => ['columns' => ['relation' => ['config' => ['type' => $type, 'relationship' => 'oneToOne']]]]]); + self::assertEquals($maxitems, $subject['foo']['columns']['relation']['config']['maxitems'] ?? 0); + $subject = (new TcaPreparation())->prepare(['foo' => ['columns' => ['relation' => ['config' => ['type' => $type, 'relationship' => 'manyToOne']]]]]); + self::assertEquals($maxitems, $subject['foo']['columns']['relation']['config']['maxitems'] ?? 0); + $subject = (new TcaPreparation())->prepare(['foo' => ['columns' => ['relation' => ['config' => ['type' => $type, 'relationship' => 'manyToMany']]]]]); + self::assertEquals(0, $subject['foo']['columns']['relation']['config']['maxitems'] ?? 0); } } diff --git a/typo3/sysext/core/Tests/Unit/Domain/RecordFactoryTest.php b/typo3/sysext/core/Tests/Unit/Domain/RecordFactoryTest.php index b2be73e33df2c29a768344c89a779c0711ed5a9e..938ed168a9d6305cd9590a45f676b582bb021be9 100644 --- a/typo3/sysext/core/Tests/Unit/Domain/RecordFactoryTest.php +++ b/typo3/sysext/core/Tests/Unit/Domain/RecordFactoryTest.php @@ -19,6 +19,7 @@ namespace TYPO3\CMS\Core\Tests\Unit\Domain; use PHPUnit\Framework\Attributes\Test; use TYPO3\CMS\Core\Cache\Frontend\PhpFrontend; +use TYPO3\CMS\Core\DataHandling\RecordFieldTransformer; use TYPO3\CMS\Core\Domain\RecordFactory; use TYPO3\CMS\Core\Schema\FieldTypeFactory; use TYPO3\CMS\Core\Schema\RelationMapBuilder; @@ -40,7 +41,7 @@ final class RecordFactoryTest extends UnitTestCase $cacheMock ); $schemaFactory->load(['existing_schema' => ['ctrl' => [], 'columns' => []]]); - $subject = new RecordFactory($schemaFactory); + $subject = new RecordFactory($schemaFactory, $this->createMock(RecordFieldTransformer::class)); $subject->createFromDatabaseRow('foo', ['foo' => 1]); } @@ -62,7 +63,7 @@ final class RecordFactoryTest extends UnitTestCase 'types' => ['bar' => ['showitem' => 'type']], ], ]); - $subject = new RecordFactory($schemaFactory); + $subject = new RecordFactory($schemaFactory, $this->createMock(RecordFieldTransformer::class)); $recordObject = $subject->createFromDatabaseRow('foo', ['uid' => 1, 'pid' => 2, 'type' => 'bar']); self::assertEquals('bar', $recordObject->toArray()['type']); } @@ -85,7 +86,7 @@ final class RecordFactoryTest extends UnitTestCase 'types' => ['foo' => ['showitem' => 'foo']], ], ]); - $subject = new RecordFactory($schemaFactory); + $subject = new RecordFactory($schemaFactory, $this->createMock(RecordFieldTransformer::class)); $recordObject = $subject->createFromDatabaseRow('foo', ['uid' => 1, 'pid' => 2, 'type' => 'foo', 'foo' => 'fooValue', 'bar' => 'barValue']); self::assertFalse($recordObject->offsetExists('bar')); self::assertTrue($recordObject->offsetExists('foo')); diff --git a/typo3/sysext/core/Tests/Unit/Schema/RelationshipTypeTest.php b/typo3/sysext/core/Tests/Unit/Schema/RelationshipTypeTest.php index e60bad9e88c99260836dbad42af9eae271e56bc9..721cdef8c3a3d4741ccd1adf3606abbacbb91f63 100644 --- a/typo3/sysext/core/Tests/Unit/Schema/RelationshipTypeTest.php +++ b/typo3/sysext/core/Tests/Unit/Schema/RelationshipTypeTest.php @@ -34,6 +34,10 @@ final class RelationshipTypeTest extends UnitTestCase ['config' => []], RelationshipType::Undefined, ]; + yield 'detect select as static list - this is no relation => undefined' => [ + ['type' => 'select'], + RelationshipType::Undefined, + ]; yield 'use MM if given' => [ ['type' => 'text', 'config' => ['type' => 'text', 'MM' => 1]], RelationshipType::ManyToMany, @@ -46,10 +50,6 @@ final class RelationshipTypeTest extends UnitTestCase ['type' => 'group'], RelationshipType::List, ]; - yield 'detect select as static list' => [ - ['type' => 'select'], - RelationshipType::Static, - ]; yield 'detect select with MM' => [ ['type' => 'select', 'MM' => true], RelationshipType::ManyToMany, @@ -64,7 +64,15 @@ final class RelationshipTypeTest extends UnitTestCase ]; yield 'detect inline with foreign field' => [ ['type' => 'inline', 'foreign_table' => 'sys_file_reference', 'foreign_field' => 'uid_foreign'], - RelationshipType::ForeignField, + RelationshipType::OneToMany, + ]; + yield 'detect relationship set to "oneToOne"' => [ + ['type' => 'select', 'foreign_table' => 'sys_file_metadata', 'relationship' => 'oneToOne'], + RelationshipType::OneToOne, + ]; + yield 'detect relationship set to "manyToOne"' => [ + ['type' => 'group', 'allowed' => 'pages', 'relationship' => 'manyToOne'], + RelationshipType::ManyToOne, ]; yield 'subarray key overloads main level key' => [ ['type' => 'group', 'config' => ['type' => 'text']], diff --git a/typo3/sysext/extbase/Tests/Functional/Fixtures/Extensions/blog_example/Configuration/TCA/tx_blogexample_domain_model_post.php b/typo3/sysext/extbase/Tests/Functional/Fixtures/Extensions/blog_example/Configuration/TCA/tx_blogexample_domain_model_post.php index 364ef14b8f351bd3c5e8c8855e5f515984542831..d84884cc1e5f4f12d39bc3538dbe333bf319e637 100644 --- a/typo3/sysext/extbase/Tests/Functional/Fixtures/Extensions/blog_example/Configuration/TCA/tx_blogexample_domain_model_post.php +++ b/typo3/sysext/extbase/Tests/Functional/Fixtures/Extensions/blog_example/Configuration/TCA/tx_blogexample_domain_model_post.php @@ -101,7 +101,7 @@ return [ 'type' => 'group', 'allowed' => 'tx_blogexample_domain_model_person', 'foreign_table' => 'tx_blogexample_domain_model_person', - 'maxitems' => 1, + 'relationship' => 'manyToOne', 'fieldControl' => [ 'editPopup' => [ 'disabled' => false, @@ -195,7 +195,7 @@ return [ 'config' => [ 'type' => 'inline', // this will store the info uid in the additional_name field (CSV) 'foreign_table' => 'tx_blogexample_domain_model_info', - 'maxitems' => 1, + 'relationship' => 'manyToOne', 'default' => 0, ], ], @@ -206,7 +206,7 @@ return [ 'type' => 'inline', // this will store the post uid in the post field of the info table 'foreign_table' => 'tx_blogexample_domain_model_info', 'foreign_field' => 'post', - 'maxitems' => 1, + 'relationship' => 'manyToOne', 'default' => 0, ], ], diff --git a/typo3/sysext/extbase/Tests/Functional/Fixtures/Extensions/parent_child_translation/Configuration/TCA/tx_parentchildtranslation_domain_model_main.php b/typo3/sysext/extbase/Tests/Functional/Fixtures/Extensions/parent_child_translation/Configuration/TCA/tx_parentchildtranslation_domain_model_main.php index 356ba159a1ef039cc9957cc387d3c2b7d8703b6e..c3a777c8f8d23b6d1506acd0e0cca9a2b80fff07 100644 --- a/typo3/sysext/extbase/Tests/Functional/Fixtures/Extensions/parent_child_translation/Configuration/TCA/tx_parentchildtranslation_domain_model_main.php +++ b/typo3/sysext/extbase/Tests/Functional/Fixtures/Extensions/parent_child_translation/Configuration/TCA/tx_parentchildtranslation_domain_model_main.php @@ -54,7 +54,7 @@ return [ 'foreign_table' => 'tx_parentchildtranslation_domain_model_squeeze', 'foreign_table_where' => 'AND {#tx_parentchildtranslation_domain_model_squeeze}.{#sys_language_uid} IN (0,-1)', 'foreign_field' => 'parent', - 'maxitems' => 1, + 'relationship' => 'manyToOne', 'default' => 0, ], ], diff --git a/typo3/sysext/felogin/Configuration/TCA/Overrides/fe_groups.php b/typo3/sysext/felogin/Configuration/TCA/Overrides/fe_groups.php index 9c6d7b5ad08aa86617913991a37c0187030f0567..1cf6528465cc834ff5f96930aafd14ba81a88a0e 100644 --- a/typo3/sysext/felogin/Configuration/TCA/Overrides/fe_groups.php +++ b/typo3/sysext/felogin/Configuration/TCA/Overrides/fe_groups.php @@ -12,7 +12,7 @@ call_user_func(static function () { 'type' => 'group', 'allowed' => 'pages', 'size' => 1, - 'maxitems' => 1, + 'relationship' => 'manyToOne', ], ], ]; diff --git a/typo3/sysext/felogin/Configuration/TCA/Overrides/fe_users.php b/typo3/sysext/felogin/Configuration/TCA/Overrides/fe_users.php index 1d0aae5b765796c3e930f3030d779a16b8e46679..38e043632637ae113ac67c35a1bb2fb68f088496 100644 --- a/typo3/sysext/felogin/Configuration/TCA/Overrides/fe_users.php +++ b/typo3/sysext/felogin/Configuration/TCA/Overrides/fe_users.php @@ -12,7 +12,7 @@ call_user_func(static function () { 'type' => 'group', 'allowed' => 'pages', 'size' => 1, - 'maxitems' => 1, + 'relationship' => 'manyToOne', ], ], 'felogin_forgotHash' => [ diff --git a/typo3/sysext/frontend/Classes/Content/RecordCollector.php b/typo3/sysext/frontend/Classes/Content/RecordCollector.php index 2592e5292578319faa8575a5e74965f280912ff9..50aa6802211795c866fd6ee2af96f184c3e6b910 100644 --- a/typo3/sysext/frontend/Classes/Content/RecordCollector.php +++ b/typo3/sysext/frontend/Classes/Content/RecordCollector.php @@ -26,14 +26,16 @@ use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer; */ readonly class RecordCollector { - public function __construct(protected RecordFactory $recordFactory) {} + public function __construct( + protected RecordFactory $recordFactory, + protected RecordIdentityMap $recordIdentityMap + ) {} public function collect( string $table, array $select, ContentSlideMode $slideMode, - ContentObjectRenderer $contentObjectRenderer, - ?RecordIdentityMap $recordIdentityMap = null, + ContentObjectRenderer $contentObjectRenderer ): array { $slideCollectReverse = false; $collect = false; @@ -59,12 +61,12 @@ readonly class RecordCollector do { $recordsOnPid = $contentObjectRenderer->getRecords($table, $select); $recordsOnPid = array_map( - function ($record) use ($recordIdentityMap, $table) { - if ($recordIdentityMap !== null && $recordIdentityMap->hasIdentifier($table, (int)$record['uid'])) { - return $recordIdentityMap->findByIdentifier($table, (int)$record['uid']); + function ($record) use ($table) { + if ($this->recordIdentityMap->hasIdentifier($table, (int)$record['uid'])) { + return $this->recordIdentityMap->findByIdentifier($table, (int)$record['uid']); } - $obj = $this->recordFactory->createFromDatabaseRow($table, $record); - $recordIdentityMap?->add($obj); + $obj = $this->recordFactory->createResolvedRecordFromDatabaseRow($table, $record); + $this->recordIdentityMap->add($obj); return $obj; }, $recordsOnPid diff --git a/typo3/sysext/frontend/Classes/DataProcessing/PageContentFetchingProcessor.php b/typo3/sysext/frontend/Classes/DataProcessing/PageContentFetchingProcessor.php index a0cb540f1804eed6c517c1f07f003bf93fafab8c..b2b4b698e1ee172df135a371315f6b0aab0c544a 100644 --- a/typo3/sysext/frontend/Classes/DataProcessing/PageContentFetchingProcessor.php +++ b/typo3/sysext/frontend/Classes/DataProcessing/PageContentFetchingProcessor.php @@ -17,7 +17,6 @@ declare(strict_types=1); namespace TYPO3\CMS\Frontend\DataProcessing; -use TYPO3\CMS\Core\Domain\Persistence\RecordIdentityMap; use TYPO3\CMS\Core\Page\PageLayoutResolver; use TYPO3\CMS\Frontend\Content\ContentSlideMode; use TYPO3\CMS\Frontend\Content\RecordCollector; @@ -53,7 +52,6 @@ readonly class PageContentFetchingProcessor implements DataProcessorInterface public function __construct( protected RecordCollector $recordCollector, protected PageLayoutResolver $pageLayoutResolver, - protected RecordIdentityMap $recordIdentityMap, ) {} public function process( @@ -105,8 +103,7 @@ readonly class PageContentFetchingProcessor implements DataProcessorInterface 'orderBy' => 'colPos, sorting', ], ContentSlideMode::None, - $cObj, - $this->recordIdentityMap + $cObj ); // 1b. Sort the records into the contentArea they belong to foreach ($flatRecords as $recordToSort) { @@ -125,8 +122,7 @@ readonly class PageContentFetchingProcessor implements DataProcessorInterface 'orderBy' => 'sorting', ], ContentSlideMode::tryFrom($contentAreaData['slideMode'] ?? null), - $cObj, - $this->recordIdentityMap + $cObj ); $contentAreaData['records'] = $records; $contentAreaName = $contentAreaData['identifier']; diff --git a/typo3/sysext/frontend/Classes/DataProcessing/RecordTransformationProcessor.php b/typo3/sysext/frontend/Classes/DataProcessing/RecordTransformationProcessor.php index f43d8be9c6a5138cec1bd56311b4160bed28ea0d..ba9a5d60e64a2067785ad94735308dd545cad612 100644 --- a/typo3/sysext/frontend/Classes/DataProcessing/RecordTransformationProcessor.php +++ b/typo3/sysext/frontend/Classes/DataProcessing/RecordTransformationProcessor.php @@ -96,11 +96,11 @@ readonly class RecordTransformationProcessor implements DataProcessorInterface $output = []; if (array_is_list($input)) { foreach ($input as $record) { - $output[] = $this->recordFactory->createFromDatabaseRow($table, $record); + $output[] = $this->recordFactory->createResolvedRecordFromDatabaseRow($table, $record); } $defaultTargetVariableName = 'records'; } else { - $output = $this->recordFactory->createFromDatabaseRow($table, $input); + $output = $this->recordFactory->createResolvedRecordFromDatabaseRow($table, $input); $defaultTargetVariableName = 'record'; } $targetVariableName = $cObj->stdWrapValue('as', $processorConfiguration, $defaultTargetVariableName); diff --git a/typo3/sysext/frontend/Classes/Typolink/TypolinkParameter.php b/typo3/sysext/frontend/Classes/Typolink/TypolinkParameter.php index cbb69e4d1fe4e1f23b72efdaeca33969a07a9cdc..4790297968fc02fe493f5f379d70ecbe63ddec38 100644 --- a/typo3/sysext/frontend/Classes/Typolink/TypolinkParameter.php +++ b/typo3/sysext/frontend/Classes/Typolink/TypolinkParameter.php @@ -20,7 +20,7 @@ namespace TYPO3\CMS\Frontend\Typolink; /** * This class represents an object containing the resolved parameters of a typolink */ -readonly class TypolinkParameter implements \JsonSerializable +final readonly class TypolinkParameter implements \JsonSerializable { public function __construct( public string $url = '', diff --git a/typo3/sysext/frontend/Configuration/TCA/backend_layout.php b/typo3/sysext/frontend/Configuration/TCA/backend_layout.php index 6fbd3b1ba266500654ae07da08d340ac8d0d3735..6c7b5ab257455372df3db7aa4afc6b2b46613fae 100644 --- a/typo3/sysext/frontend/Configuration/TCA/backend_layout.php +++ b/typo3/sysext/frontend/Configuration/TCA/backend_layout.php @@ -45,7 +45,7 @@ return [ 'config' => [ 'type' => 'file', 'allowed' => 'common-image-types', - 'maxitems' => 1, + 'relationship' => 'manyToOne', 'appearance' => [ 'createNewRelationLinkTitle' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:images.addFileReference', ], diff --git a/typo3/sysext/frontend/Tests/Functional/DataProcessing/Fixtures/PageContentProcessor/Partials/SingleContent.html b/typo3/sysext/frontend/Tests/Functional/DataProcessing/Fixtures/PageContentProcessor/Partials/SingleContent.html index c4b93052867ffc4cea4d5cd2fe1763aa78552d11..d87365195e585b36a9a6be53b165b989de8eb98c 100644 --- a/typo3/sysext/frontend/Tests/Functional/DataProcessing/Fixtures/PageContentProcessor/Partials/SingleContent.html +++ b/typo3/sysext/frontend/Tests/Functional/DataProcessing/Fixtures/PageContentProcessor/Partials/SingleContent.html @@ -6,5 +6,8 @@ <f:case value="test_carousel"> <h3>{record.header}</h3> <p>Carousel Items will show up: {record.carousel_items}</p> + <f:for each="{record.carousel_items}" as="carouselItem"> + <h4>{carouselItem.header}</h4> + </f:for> </f:case> </f:switch> diff --git a/typo3/sysext/frontend/Tests/Functional/DataProcessing/Fixtures/RecordTransform/Partials/SingleContent.html b/typo3/sysext/frontend/Tests/Functional/DataProcessing/Fixtures/RecordTransform/Partials/SingleContent.html index c4b93052867ffc4cea4d5cd2fe1763aa78552d11..d87365195e585b36a9a6be53b165b989de8eb98c 100644 --- a/typo3/sysext/frontend/Tests/Functional/DataProcessing/Fixtures/RecordTransform/Partials/SingleContent.html +++ b/typo3/sysext/frontend/Tests/Functional/DataProcessing/Fixtures/RecordTransform/Partials/SingleContent.html @@ -6,5 +6,8 @@ <f:case value="test_carousel"> <h3>{record.header}</h3> <p>Carousel Items will show up: {record.carousel_items}</p> + <f:for each="{record.carousel_items}" as="carouselItem"> + <h4>{carouselItem.header}</h4> + </f:for> </f:case> </f:switch> diff --git a/typo3/sysext/frontend/Tests/Functional/DataProcessing/PageContentFetchingProcessorTest.php b/typo3/sysext/frontend/Tests/Functional/DataProcessing/PageContentFetchingProcessorTest.php index a57486f9c8bdce25c334f76abd438ac8baca0700..4bdc0e7fbbe94b9432999643a4fcfa93227179b8 100644 --- a/typo3/sysext/frontend/Tests/Functional/DataProcessing/PageContentFetchingProcessorTest.php +++ b/typo3/sysext/frontend/Tests/Functional/DataProcessing/PageContentFetchingProcessorTest.php @@ -81,6 +81,7 @@ final class PageContentFetchingProcessorTest extends FunctionalTestCase $body = (string)$response->getBody(); self::assertStringContainsString('Welcome to ACME guitars', $body); self::assertStringContainsString('Carousel Items will show up: 2', $body); + self::assertStringContainsString('Meet us at Guitar Brussels in 2035', $body); self::assertStringContainsString('Great to see you here', $body); self::assertStringContainsString('If you read this you are at the end.', $body); } diff --git a/typo3/sysext/frontend/Tests/Functional/DataProcessing/RecordTransformationProcessorTest.php b/typo3/sysext/frontend/Tests/Functional/DataProcessing/RecordTransformationProcessorTest.php index 1f3ac93adf97bddf25863bea5068829dd680f931..5ae16c2a70d5f2457e76314a4709ecafed3f81bd 100644 --- a/typo3/sysext/frontend/Tests/Functional/DataProcessing/RecordTransformationProcessorTest.php +++ b/typo3/sysext/frontend/Tests/Functional/DataProcessing/RecordTransformationProcessorTest.php @@ -58,7 +58,7 @@ final class RecordTransformationProcessorTest extends FunctionalTestCase $factory = DataHandlerFactory::fromYamlFile($scenarioFile); $writer = DataHandlerWriter::withBackendUser($backendUser); $writer->invokeFactory($factory); - static::failIfArrayIsNotEmpty($writer->getErrors()); + self::failIfArrayIsNotEmpty($writer->getErrors()); $connection = $this->get(ConnectionPool::class)->getConnectionForTable('pages'); $pageLayoutFileContents[] = file_get_contents(__DIR__ . '/Fixtures/PageLayouts/Default.tsconfig'); @@ -80,6 +80,8 @@ final class RecordTransformationProcessorTest extends FunctionalTestCase $response = $this->executeFrontendSubRequest((new InternalRequest('https://acme.com/'))->withPageId(1000)); $body = (string)$response->getBody(); self::assertStringContainsString('Welcome to ACME guitars', $body); + self::assertStringContainsString('Carousel Items will show up: 2', $body); + self::assertStringContainsString('Meet us at Guitar Brussels in 2035', $body); self::assertStringContainsString('Great to see you here', $body); self::assertStringContainsString('If you read this you are at the end.', $body); } diff --git a/typo3/sysext/indexed_search/Configuration/TCA/index_config.php b/typo3/sysext/indexed_search/Configuration/TCA/index_config.php index 8f5254742ec3c869a9f3f2a409c7af79822a7093..5d94678c5fc195938c2551f8799ff680c9a54b40 100644 --- a/typo3/sysext/indexed_search/Configuration/TCA/index_config.php +++ b/typo3/sysext/indexed_search/Configuration/TCA/index_config.php @@ -77,7 +77,7 @@ return [ 'type' => 'group', 'allowed' => 'pages', 'size' => 1, - 'maxitems' => 1, + 'relationship' => 'manyToOne', ], ], 'indexcfgs' => [ diff --git a/typo3/sysext/reactions/Configuration/TCA/Overrides/sys_reaction_create_record.php b/typo3/sysext/reactions/Configuration/TCA/Overrides/sys_reaction_create_record.php index c6a734a8d88c45cf5d9efff51fd39f8687fb3444..f23cd8cf6e41f3695b1a2fe0c11f2bd038b76e68 100644 --- a/typo3/sysext/reactions/Configuration/TCA/Overrides/sys_reaction_create_record.php +++ b/typo3/sysext/reactions/Configuration/TCA/Overrides/sys_reaction_create_record.php @@ -10,7 +10,7 @@ 'type' => 'group', 'allowed' => 'pages', 'size' => 1, - 'maxitems' => 1, + 'relationship' => 'manyToOne', ], ], 'fields' => [ diff --git a/typo3/sysext/reactions/Configuration/TCA/sys_reaction.php b/typo3/sysext/reactions/Configuration/TCA/sys_reaction.php index d5780e23bcd7ccddb8f371b43a110d3f476cbdc3..0c041492ac7ced8e0856e0afd1870d37f584d3d9 100644 --- a/typo3/sysext/reactions/Configuration/TCA/sys_reaction.php +++ b/typo3/sysext/reactions/Configuration/TCA/sys_reaction.php @@ -107,7 +107,7 @@ return [ 'type' => 'group', 'allowed' => 'be_users', 'size' => 1, - 'maxitems' => 1, + 'relationship' => 'manyToOne', ], ], // "table_name" is not referenced in this TCA but needs to be defined here to ensure extensions can diff --git a/typo3/sysext/styleguide/Classes/TcaDataGenerator/FieldGenerator/TypeInlineFalSelectSingle12Foreign.php b/typo3/sysext/styleguide/Classes/TcaDataGenerator/FieldGenerator/TypeInlineFalSelectSingle12Foreign.php index 15a868466eb13a41efb8867f0b71b62258292947..ece9ce5c694e695a1428cef4dd6cd37a0195802d 100644 --- a/typo3/sysext/styleguide/Classes/TcaDataGenerator/FieldGenerator/TypeInlineFalSelectSingle12Foreign.php +++ b/typo3/sysext/styleguide/Classes/TcaDataGenerator/FieldGenerator/TypeInlineFalSelectSingle12Foreign.php @@ -46,7 +46,7 @@ final class TypeInlineFalSelectSingle12Foreign extends AbstractFieldGenerator im 'label' => 'fal_1 selicon_field', 'config' => [ 'type' => 'file', - 'maxitems' => 1, + 'relationship' => 'manyToOne', ], ], ]; diff --git a/typo3/sysext/styleguide/Configuration/TCA/tx_styleguide_elements_folder.php b/typo3/sysext/styleguide/Configuration/TCA/tx_styleguide_elements_folder.php index f573991d39b19d49c78fe0e0ef9bda784e4c81f7..a74ed274522a610b6470663a132d693cfe6d6c91 100644 --- a/typo3/sysext/styleguide/Configuration/TCA/tx_styleguide_elements_folder.php +++ b/typo3/sysext/styleguide/Configuration/TCA/tx_styleguide_elements_folder.php @@ -39,11 +39,11 @@ return [ ], ], 'folder_3' => [ - 'label' => 'folder_3 maxitems=1', + 'label' => 'folder_3 relationship=manyToOne', 'description' => 'field description', 'config' => [ 'type' => 'folder', - 'maxitems' => 1, + 'relationship' => 'manyToOne', 'size' => 1, ], ], diff --git a/typo3/sysext/styleguide/Configuration/TCA/tx_styleguide_elements_group.php b/typo3/sysext/styleguide/Configuration/TCA/tx_styleguide_elements_group.php index 118ee7e4daaa3344ebb9dbe68cb8332024a59255..6105553f8b209ff501dfa481f16fdede0eb28b77 100644 --- a/typo3/sysext/styleguide/Configuration/TCA/tx_styleguide_elements_group.php +++ b/typo3/sysext/styleguide/Configuration/TCA/tx_styleguide_elements_group.php @@ -103,7 +103,7 @@ return [ 'type' => 'group', 'allowed' => 'tx_styleguide_staticdata', 'size' => 1, - 'maxitems' => 1, + 'relationship' => 'manyToOne', ], ], 'group_db_5' => [ @@ -127,7 +127,7 @@ return [ 'config' => [ 'type' => 'group', 'allowed' => 'pages', - 'maxitems' => 1, + 'relationship' => 'manyToOne', 'minitems' => 0, 'size' => 1, 'suggestOptions' => [ diff --git a/typo3/sysext/styleguide/Configuration/TCA/tx_styleguide_elements_imagemanipulation.php b/typo3/sysext/styleguide/Configuration/TCA/tx_styleguide_elements_imagemanipulation.php index a6899d804918b00170d811064706056ed2bf9076..9ce6a677bd50e9439205e967bd8c2614f0e7a2d1 100644 --- a/typo3/sysext/styleguide/Configuration/TCA/tx_styleguide_elements_imagemanipulation.php +++ b/typo3/sysext/styleguide/Configuration/TCA/tx_styleguide_elements_imagemanipulation.php @@ -28,7 +28,7 @@ return [ 'config' => [ 'type' => 'group', 'allowed' => 'sys_file', - 'maxitems' => 1, + 'relationship' => 'manyToOne', 'minitems' => 0, 'size' => 1, ], @@ -38,7 +38,7 @@ return [ 'config' => [ 'type' => 'group', 'allowed' => 'sys_file', - 'maxitems' => 1, + 'relationship' => 'manyToOne', 'minitems' => 0, 'size' => 1, ], @@ -48,7 +48,7 @@ return [ 'config' => [ 'type' => 'group', 'allowed' => 'sys_file', - 'maxitems' => 1, + 'relationship' => 'manyToOne', 'minitems' => 0, 'size' => 1, ], diff --git a/typo3/sysext/styleguide/Configuration/TCA/tx_styleguide_elements_select.php b/typo3/sysext/styleguide/Configuration/TCA/tx_styleguide_elements_select.php index 326ec9c7fce0befb3d13060f9f82d77618ae8714..faecbf74203b5c2f1f412026cf892fb3ed4faa48 100644 --- a/typo3/sysext/styleguide/Configuration/TCA/tx_styleguide_elements_select.php +++ b/typo3/sysext/styleguide/Configuration/TCA/tx_styleguide_elements_select.php @@ -386,11 +386,11 @@ return [ ], ], 'select_checkbox_2' => [ - 'label' => 'select_checkbox_2, maxitems=1', + 'label' => 'select_checkbox_2, relationship=manyToOne', 'config' => [ 'type' => 'select', 'renderType' => 'selectCheckBox', - 'maxitems' => 1, + 'relationship' => 'manyToOne', 'items' => [ ['label' => 'foo 1', 'value' => 1], ['label' => 'foo 2', 'value' => 2], @@ -609,11 +609,11 @@ return [ ], ], 'select_multiplesidebyside_9' => [ - 'label' => 'select_multiplesidebyside_9 maxitems=1', + 'label' => 'select_multiplesidebyside_9 relationship=manyToOne', 'config' => [ 'type' => 'select', 'renderType' => 'selectMultipleSideBySide', - 'maxitems' => 1, + 'relationship' => 'manyToOne', 'items' => [ ['label' => 'foo 1', 'value' => 1], ['label' => 'foo 2', 'value' => 2], diff --git a/typo3/sysext/styleguide/Configuration/TCA/tx_styleguide_elements_select_single_12_foreign.php b/typo3/sysext/styleguide/Configuration/TCA/tx_styleguide_elements_select_single_12_foreign.php index 7f364169742cfc20d67007d8db6595eff05469f9..7d25c080667853242cbbb78bd84a49246249cb0a 100644 --- a/typo3/sysext/styleguide/Configuration/TCA/tx_styleguide_elements_select_single_12_foreign.php +++ b/typo3/sysext/styleguide/Configuration/TCA/tx_styleguide_elements_select_single_12_foreign.php @@ -28,7 +28,7 @@ return [ 'config' => [ 'type' => 'file', 'allowed' => 'common-media-types', - 'maxitems' => 1, + 'relationship' => 'manyToOne', ], ], ], diff --git a/typo3/sysext/styleguide/Configuration/TCA/tx_styleguide_inline_11.php b/typo3/sysext/styleguide/Configuration/TCA/tx_styleguide_inline_11.php index 2951922fddf80f358a5289d1338ebe7cebd70abb..42ff03caa7aaf083b91563d29f2dcd72b1490bb9 100644 --- a/typo3/sysext/styleguide/Configuration/TCA/tx_styleguide_inline_11.php +++ b/typo3/sysext/styleguide/Configuration/TCA/tx_styleguide_inline_11.php @@ -33,7 +33,7 @@ return [ 'showAllLocalizationLink' => true, 'showPossibleLocalizationRecords' => true, ], - 'maxitems' => 1, + 'relationship' => 'manyToOne', ], ], ], diff --git a/typo3/sysext/styleguide/Configuration/TCA/tx_styleguide_inline_1nnol10n.php b/typo3/sysext/styleguide/Configuration/TCA/tx_styleguide_inline_1nnol10n.php index e436bfbe7c4aa3cff93beefb0481547effdda842..59e0bc05a39d4106ff0de21a366ebdd4eccb99c8 100644 --- a/typo3/sysext/styleguide/Configuration/TCA/tx_styleguide_inline_1nnol10n.php +++ b/typo3/sysext/styleguide/Configuration/TCA/tx_styleguide_inline_1nnol10n.php @@ -32,7 +32,7 @@ return [ 'foreign_field' => 'parentid', 'foreign_table_field' => 'parenttable', ], - 'maxitems' => 1, + 'relationship' => 'manyToOne', ], ], diff --git a/typo3/sysext/styleguide/Configuration/TCA/tx_styleguide_inline_mngroup_mm.php b/typo3/sysext/styleguide/Configuration/TCA/tx_styleguide_inline_mngroup_mm.php index 0a74d112bb858b74309679bd3729027754402d7d..9431a2da14eb054c65645350c49d451b38fab7ba 100644 --- a/typo3/sysext/styleguide/Configuration/TCA/tx_styleguide_inline_mngroup_mm.php +++ b/typo3/sysext/styleguide/Configuration/TCA/tx_styleguide_inline_mngroup_mm.php @@ -27,8 +27,7 @@ return [ 'config' => [ 'type' => 'group', 'size' => 1, - 'eval' => 'int', - 'maxitems' => 1, + 'relationship' => 'manyToOne', 'minitems' => 0, 'allowed' => 'tx_styleguide_inline_mngroup', 'hideSuggest' => true, @@ -44,8 +43,7 @@ return [ 'config' => [ 'type' => 'group', 'size' => 1, - 'eval' => 'int', - 'maxitems' => 1, + 'relationship' => 'manyToOne', 'minitems' => 0, 'allowed' => 'tx_styleguide_inline_mngroup_child', 'hideSuggest' => true, diff --git a/typo3/sysext/styleguide/Configuration/TCA/tx_styleguide_inline_mnsymmetricgroup_mm.php b/typo3/sysext/styleguide/Configuration/TCA/tx_styleguide_inline_mnsymmetricgroup_mm.php index 1b873d23d63e47b042cbfd4cc28a0d92a8ba8122..c3414d201f07a3f2fe87d2ecdb3c926913ab1524 100644 --- a/typo3/sysext/styleguide/Configuration/TCA/tx_styleguide_inline_mnsymmetricgroup_mm.php +++ b/typo3/sysext/styleguide/Configuration/TCA/tx_styleguide_inline_mnsymmetricgroup_mm.php @@ -28,7 +28,7 @@ return [ 'type' => 'group', 'allowed' => 'tx_styleguide_inline_mnsymmetricgroup', 'minitems' => 1, - 'maxitems' => 1, + 'relationship' => 'manyToOne', 'size' => 1, ], ], @@ -38,7 +38,7 @@ return [ 'type' => 'group', 'allowed' => 'tx_styleguide_inline_mnsymmetricgroup', 'minitems' => 1, - 'maxitems' => 1, + 'relationship' => 'manyToOne', 'size' => 1, ], ], diff --git a/typo3/sysext/styleguide/Configuration/TCA/tx_styleguide_inline_usecombinationgroup_mm.php b/typo3/sysext/styleguide/Configuration/TCA/tx_styleguide_inline_usecombinationgroup_mm.php index e1e52e5d663120aabbe2e77587431d40951fce92..ec2f7bc2f1ec89cb61672a4e52b816ec729363f1 100644 --- a/typo3/sysext/styleguide/Configuration/TCA/tx_styleguide_inline_usecombinationgroup_mm.php +++ b/typo3/sysext/styleguide/Configuration/TCA/tx_styleguide_inline_usecombinationgroup_mm.php @@ -37,7 +37,7 @@ return [ 'allowed' => 'tx_styleguide_inline_usecombinationgroup_child', 'size' => 1, 'minitems' => 1, - 'maxitems' => 1, + 'relationship' => 'manyToOne', ], ], ], diff --git a/typo3/sysext/styleguide/Configuration/TCA/tx_styleguide_required.php b/typo3/sysext/styleguide/Configuration/TCA/tx_styleguide_required.php index b1b747d4ffce3c82bb9670f8edfc49ac3a694f1b..b3d42da4346fb356776e42fdf8e7d8ac7d138ce4 100644 --- a/typo3/sysext/styleguide/Configuration/TCA/tx_styleguide_required.php +++ b/typo3/sysext/styleguide/Configuration/TCA/tx_styleguide_required.php @@ -157,13 +157,13 @@ return [ ], ], 'group_2' => [ - 'label' => 'group_2 db, minitems = 1, maxitems=1, size=1', + 'label' => 'group_2 db, minitems = 1, relationship=manyToOne, size=1', 'config' => [ 'type' => 'group', 'allowed' => 'tx_styleguide_staticdata', 'size' => 1, 'minitems' => 1, - 'maxitems' => 1, + 'relationship' => 'manyToOne', ], ], @@ -188,14 +188,14 @@ return [ ], 'inline_1' => [ - 'label' => 'inline_1 minitems=1, maxitems=1', + 'label' => 'inline_1 minitems=1, relationship=manyToOne', 'config' => [ 'type' => 'inline', 'foreign_table' => 'tx_styleguide_required_inline_1_child', 'foreign_field' => 'parentid', 'foreign_table_field' => 'parenttable', 'minitems' => 1, - 'maxitems' => 1, + 'relationship' => 'manyToOne', ], ], 'inline_2' => [ diff --git a/typo3/sysext/workspaces/Classes/Hook/DataHandlerHook.php b/typo3/sysext/workspaces/Classes/Hook/DataHandlerHook.php index 2addcdf4c78961a99f605413effb897d80d5bab7..41536f8050be31714be5d227d74d94535c24d240 100644 --- a/typo3/sysext/workspaces/Classes/Hook/DataHandlerHook.php +++ b/typo3/sysext/workspaces/Classes/Hook/DataHandlerHook.php @@ -453,7 +453,7 @@ class DataHandlerHook if ($fieldInformation->isType(TableColumnType::INLINE) && !$fieldInformation->isMovingChildrenEnabled()) { return; } - if ($fieldInformation->getRelationshipType() !== RelationshipType::ForeignField && $fieldInformation->getRelationshipType() !== RelationshipType::List) { + if (!$fieldInformation->getRelationshipType()->isSingularRelationship()) { return; } $configuration = $fieldInformation->getConfiguration(); @@ -892,7 +892,7 @@ class DataHandlerHook */ protected function version_swap_processFields($tableName, array $configuration, array $liveData, array $versionData, DataHandler $dataHandler) { - if (RelationshipType::fromTcaConfiguration($configuration) !== RelationshipType::ForeignField) { + if (RelationshipType::fromTcaConfiguration($configuration) !== RelationshipType::OneToMany) { return; } $foreignTable = $configuration['foreign_table'];