From 6c7b876c7a7f20b525768a0346d741aff3dffea4 Mon Sep 17 00:00:00 2001 From: Oliver Bartsch <bo@cedev.de> Date: Wed, 28 Aug 2024 13:43:50 +0200 Subject: [PATCH] [TASK] Streamline extbase data / column mapping MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The extbase data and column mapping is now properly using the TCA schema API. While streamlining the code some divergences regarding relation handling have been observed (e.g. use of "maxitems" and "renderType" options). Those will be tackled separately, since streamlining this might be breaking. Resolves: #104808 Releases: main Change-Id: I5aedf761372a4c14da76619a85f931a411b1dcb9 Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/85811 Tested-by: Stefan Bürk <stefan@buerk.tech> Reviewed-by: Oliver Bartsch <bo@cedev.de> Reviewed-by: Stefan Bürk <stefan@buerk.tech> Tested-by: core-ci <typo3@b13.com> Tested-by: Oliver Bartsch <bo@cedev.de> Reviewed-by: Benni Mack <benni@typo3.org> Tested-by: Benni Mack <benni@typo3.org> --- .../DataHandling/RecordFieldTransformer.php | 4 +- .../Schema/Capability/FieldCapability.php | 5 + .../SystemInternalFieldCapability.php | 5 + .../Schema/Field/DateTimeFieldType.php | 4 +- .../core/Classes/Schema/RelationshipType.php | 7 +- .../Generic/Mapper/ColumnMapFactory.php | 177 +++++++----------- .../Generic/Mapper/DataMapFactory.php | 82 ++++---- .../Classes/Domain/Model/Example.php | 33 ++++ ...tx_testdatamapper_domain_model_example.php | 18 ++ .../Generic/Mapper/ColumnMapFactoryTest.php | 33 ++-- .../Generic/Mapper/DataMapperTest.php | 12 +- .../Generic/Mapper/DataMapperTest.php | 43 ++--- 12 files changed, 210 insertions(+), 213 deletions(-) diff --git a/typo3/sysext/core/Classes/DataHandling/RecordFieldTransformer.php b/typo3/sysext/core/Classes/DataHandling/RecordFieldTransformer.php index 607e595acb3c..a3d4e05cce84 100644 --- a/typo3/sysext/core/Classes/DataHandling/RecordFieldTransformer.php +++ b/typo3/sysext/core/Classes/DataHandling/RecordFieldTransformer.php @@ -88,7 +88,7 @@ readonly class RecordFieldTransformer // type=file needs to be handled before RelationalFieldTypeInterface if ($fieldInformation instanceof FileFieldType) { - if ($fieldInformation->getRelationshipType()->isToOne()) { + if ($fieldInformation->getRelationshipType()->hasOne()) { return new RecordPropertyClosure( function () use ($rawRecord, $fieldInformation, $context): ?FileReference { $fileReference = $this->relationResolver->resolveFileReferences($rawRecord, $fieldInformation, $context)[0] ?? null; @@ -108,7 +108,7 @@ readonly class RecordFieldTransformer /** @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()) { + if ($fieldInformation->getRelationshipType()->hasOne()) { return new RecordPropertyClosure( function () use ($rawRecord, $fieldInformation, $context, $recordFactory): ?RecordInterface { $recordData = $this->relationResolver->resolve($rawRecord, $fieldInformation, $context)[0] ?? null; diff --git a/typo3/sysext/core/Classes/Schema/Capability/FieldCapability.php b/typo3/sysext/core/Classes/Schema/Capability/FieldCapability.php index 21c8e355724e..61bdfa363421 100644 --- a/typo3/sysext/core/Classes/Schema/Capability/FieldCapability.php +++ b/typo3/sysext/core/Classes/Schema/Capability/FieldCapability.php @@ -44,4 +44,9 @@ final readonly class FieldCapability implements SchemaCapabilityInterface { return $this->field; } + + public function __toString(): string + { + return $this->getFieldName(); + } } diff --git a/typo3/sysext/core/Classes/Schema/Capability/SystemInternalFieldCapability.php b/typo3/sysext/core/Classes/Schema/Capability/SystemInternalFieldCapability.php index 6feada11c52c..fdfca48d3ae1 100644 --- a/typo3/sysext/core/Classes/Schema/Capability/SystemInternalFieldCapability.php +++ b/typo3/sysext/core/Classes/Schema/Capability/SystemInternalFieldCapability.php @@ -36,4 +36,9 @@ final readonly class SystemInternalFieldCapability implements SchemaCapabilityIn { return $this->fieldName; } + + public function __toString(): string + { + return $this->getFieldName(); + } } diff --git a/typo3/sysext/core/Classes/Schema/Field/DateTimeFieldType.php b/typo3/sysext/core/Classes/Schema/Field/DateTimeFieldType.php index e20639f638f5..18e1824b83c1 100644 --- a/typo3/sysext/core/Classes/Schema/Field/DateTimeFieldType.php +++ b/typo3/sysext/core/Classes/Schema/Field/DateTimeFieldType.php @@ -17,6 +17,8 @@ declare(strict_types=1); namespace TYPO3\CMS\Core\Schema\Field; +use TYPO3\CMS\Core\Database\Query\QueryHelper; + /** * @internal This is an experimental implementation and might change until TYPO3 v13 LTS */ @@ -34,7 +36,7 @@ final readonly class DateTimeFieldType extends AbstractFieldType implements Fiel public function getPersistenceType(): ?string { - return $this->configuration['dbtype'] ?? null; + return in_array($this->configuration['dbType'] ?? null, QueryHelper::getDateTimeTypes(), true) ? $this->configuration['dbType'] : null; } public function isNullable(): bool diff --git a/typo3/sysext/core/Classes/Schema/RelationshipType.php b/typo3/sysext/core/Classes/Schema/RelationshipType.php index 541112fe4273..7bc2ce621cc9 100644 --- a/typo3/sysext/core/Classes/Schema/RelationshipType.php +++ b/typo3/sysext/core/Classes/Schema/RelationshipType.php @@ -72,11 +72,16 @@ enum RelationshipType: string return self::Undefined; } - public function isToOne(): bool + public function hasOne(): bool { return in_array($this, [self::OneToOne, self::ManyToOne], true); } + public function hasMany(): bool + { + return in_array($this, [self::ManyToMany, self::OneToMany, self::List], true); + } + public function isSingularRelationship(): bool { return in_array($this, [self::OneToOne, self::ManyToOne, self::OneToMany, self::List], true); diff --git a/typo3/sysext/extbase/Classes/Persistence/Generic/Mapper/ColumnMapFactory.php b/typo3/sysext/extbase/Classes/Persistence/Generic/Mapper/ColumnMapFactory.php index 9814a5feef75..f435e084234b 100644 --- a/typo3/sysext/extbase/Classes/Persistence/Generic/Mapper/ColumnMapFactory.php +++ b/typo3/sysext/extbase/Classes/Persistence/Generic/Mapper/ColumnMapFactory.php @@ -17,10 +17,13 @@ declare(strict_types=1); namespace TYPO3\CMS\Extbase\Persistence\Generic\Mapper; -use TYPO3\CMS\Core\Database\Query\QueryHelper; use TYPO3\CMS\Core\DataHandling\TableColumnType; +use TYPO3\CMS\Core\Schema\Field\DateTimeFieldType; +use TYPO3\CMS\Core\Schema\Field\FieldTypeInterface; +use TYPO3\CMS\Core\Schema\Field\FolderFieldType; +use TYPO3\CMS\Core\Schema\Field\RelationalFieldTypeInterface; +use TYPO3\CMS\Core\Schema\RelationshipType; use TYPO3\CMS\Core\Utility\GeneralUtility; -use TYPO3\CMS\Extbase\Persistence\Generic\Exception\UnsupportedRelationException; use TYPO3\CMS\Extbase\Persistence\Generic\Mapper\ColumnMap\Relation; use TYPO3\CMS\Extbase\Reflection\ClassSchema\Exception\NoPropertyTypesException; use TYPO3\CMS\Extbase\Reflection\ClassSchema\Exception\NoSuchPropertyException; @@ -29,15 +32,15 @@ use TYPO3\CMS\Extbase\Reflection\ReflectionService; /** * @internal only to be used within Extbase, not part of TYPO3 Core API. */ -class ColumnMapFactory +readonly class ColumnMapFactory { public function __construct( - private readonly ReflectionService $reflectionService + private ReflectionService $reflectionService ) {} - public function create(string $columnName, array $columnDefinition, string $propertyName, string $className): ColumnMap + public function create(FieldTypeInterface $field, string $propertyName, string $className): ColumnMap { - $columnMap = GeneralUtility::makeInstance(ColumnMap::class, $columnName); + $columnMap = GeneralUtility::makeInstance(ColumnMap::class, $field->getName()); try { $property = $this->reflectionService->getClassSchema($className)->getProperty($propertyName); $nonProxyPropertyTypes = $property->getFilteredTypes([$property, 'filterLazyLoadingProxyAndLazyObjectStorage']); @@ -59,105 +62,70 @@ class ColumnMapFactory } catch (NoSuchPropertyException|NoPropertyTypesException $e) { [$type, $elementType] = [null, null]; } - $columnMap = $this->setType($columnMap, $columnDefinition['config']); - $columnMap = $this->setRelations($columnMap, $columnDefinition['config'], $type, $elementType); - return $this->setDateTimeStorageFormat($columnMap, $columnDefinition['config']); - } - - /** - * Set the table column type - */ - protected function setType(ColumnMap $columnMap, array $columnConfiguration): ColumnMap - { - // todo: this method should only be called with proper arguments which means that the TCA integrity check should - // todo: take place outside this method. - - $tableColumnType = $columnConfiguration['type'] ?? null; - $columnMap->setType(TableColumnType::tryFrom($tableColumnType) ?? TableColumnType::INPUT); + // @todo Why type "input" - shouldn't we better throw an exception here? + $columnMap->setType(TableColumnType::tryFrom($field->getType()) ?? TableColumnType::INPUT); + if ($field instanceof DateTimeFieldType) { + $columnMap->setDateTimeStorageFormat($field->getPersistenceType()); + } + $columnMap = $this->setRelations($columnMap, $field, $type, $elementType); return $columnMap; } /** * This method tries to determine the type of relation to other tables and sets it based on * the $TCA column configuration - * - * @param ColumnMap $columnMap The column map - * @param array|null $columnConfiguration The column configuration from $TCA */ - protected function setRelations(ColumnMap $columnMap, ?array $columnConfiguration, ?string $type, ?string $elementType): ColumnMap + protected function setRelations(ColumnMap $columnMap, FieldTypeInterface $field, ?string $type, ?string $elementType): ColumnMap { - if (!isset($columnConfiguration)) { + $columnConfiguration = $field->getConfiguration(); + if ($field instanceof FolderFieldType + && !in_array((string)($columnConfiguration['relationship'] ?? ''), ['oneToOne', 'manyToOne'], true) + && (!isset($columnConfiguration['maxitems']) || $columnConfiguration['maxitems'] > 1) + ) { + $columnMap->setTypeOfRelation(Relation::HAS_MANY); + return $columnMap; + } + + if ($field instanceof RelationalFieldTypeInterface === false) { return $columnMap; } - if (isset($columnConfiguration['MM'])) { - return $this->setManyToManyRelation($columnMap, $columnConfiguration); + if ($field->getRelationshipType() === RelationshipType::ManyToMany) { + return $this->setManyToManyRelation($columnMap, $field); } if ($elementType !== null) { - return $this->setOneToManyRelation($columnMap, $columnConfiguration); + return $this->setOneToManyRelation($columnMap, $field); } if ($type !== null && strpbrk($type, '_\\') !== false) { // @todo: check the strpbrk function call. Seems to be a check for Tx_Foo_Bar style class names - return $this->setOneToOneRelation($columnMap, $columnConfiguration); + return $this->setOneToOneRelation($columnMap, $field); } - if ( - isset($columnConfiguration['type'], $columnConfiguration['renderType']) - && $columnConfiguration['type'] === 'select' + $columnConfiguration = $field->getConfiguration(); + // @todo we should get rid of the "maxitems" and "renderType" cases here and rely purely on + // the evaluated relationship type -> to be consistent with all non extbase components. + if ($field->getRelationshipType()->hasMany() && ( - $columnConfiguration['renderType'] !== 'selectSingle' - || (isset($columnConfiguration['maxitems']) && $columnConfiguration['maxitems'] > 1) + !$field->isType(TableColumnType::GROUP, TableColumnType::SELECT) + || ($field->isType(TableColumnType::GROUP) && (!isset($columnConfiguration['maxitems']) || $columnConfiguration['maxitems'] > 1)) + || ($field->isType(TableColumnType::SELECT) && (($columnConfiguration['renderType'] ?? '') !== 'selectSingle' || (int)($columnConfiguration['maxitems'] ?? 0) > 1)) ) ) { $columnMap->setTypeOfRelation(Relation::HAS_MANY); return $columnMap; } - if ( - isset($columnConfiguration['type']) && ($columnConfiguration['type'] === 'group' || $columnConfiguration['type'] === 'folder') - && (!isset($columnConfiguration['maxitems']) || $columnConfiguration['maxitems'] > 1) - ) { - $columnMap->setTypeOfRelation(Relation::HAS_MANY); - return $columnMap; - } - - return $columnMap; - } - - /** - * Sets datetime storage format based on $TCA column configuration. - * - * @param ColumnMap $columnMap The column map - * @param array|null $columnConfiguration The column configuration from $TCA - */ - protected function setDateTimeStorageFormat(ColumnMap $columnMap, ?array $columnConfiguration = null): ColumnMap - { - // todo: this method should only be called with proper arguments which means that the TCA integrity check should - // todo: take place outside this method. - - if ($columnMap->getType() === TableColumnType::DATETIME - && in_array($columnConfiguration['dbType'] ?? '', QueryHelper::getDateTimeTypes(), true) - ) { - $columnMap->setDateTimeStorageFormat($columnConfiguration['dbType']); - } - return $columnMap; } /** - * This method sets the configuration for a 1:1 relation based on - * the $TCA column configuration - * - * @param ColumnMap $columnMap The column map - * @param array|null $columnConfiguration The column configuration from $TCA + * This method sets the configuration for a 1:1 relation based on the $TCA column configuration */ - protected function setOneToOneRelation(ColumnMap $columnMap, ?array $columnConfiguration = null): ColumnMap + protected function setOneToOneRelation(ColumnMap $columnMap, FieldTypeInterface $field): ColumnMap { - // todo: this method should only be called with proper arguments which means that the TCA integrity check should - // todo: take place outside this method. - + $columnConfiguration = $field->getConfiguration(); $columnMap->setTypeOfRelation(Relation::HAS_ONE); // check if foreign_table is set, which usually won't be the case for type "group" fields if (!empty($columnConfiguration['foreign_table'])) { @@ -177,16 +145,11 @@ class ColumnMapFactory * This method sets the configuration for a 1:n relation based on * the $TCA column configuration * - * @param ColumnMap $columnMap The column map - * @param array|null $columnConfiguration The column configuration from $TCA - * * @internal */ - public function setOneToManyRelation(ColumnMap $columnMap, ?array $columnConfiguration = null): ColumnMap + public function setOneToManyRelation(ColumnMap $columnMap, FieldTypeInterface $field): ColumnMap { - // todo: this method should only be called with proper arguments which means that the TCA integrity check should - // todo: take place outside this method. - + $columnConfiguration = $field->getConfiguration(); $columnMap->setTypeOfRelation(Relation::HAS_MANY); // check if foreign_table is set, which usually won't be the case for type "group" fields if (!empty($columnConfiguration['foreign_table'])) { @@ -204,44 +167,30 @@ class ColumnMapFactory } /** - * This method sets the configuration for a m:n relation based on - * the $TCA column configuration - * - * @param ColumnMap $columnMap The column map - * @param array|null $columnConfiguration The column configuration from $TCA - * @throws \TYPO3\CMS\Extbase\Persistence\Generic\Exception\UnsupportedRelationException + * This method sets the configuration for a m:n relation based on the $TCA column configuration */ - protected function setManyToManyRelation(ColumnMap $columnMap, ?array $columnConfiguration = null): ColumnMap + protected function setManyToManyRelation(ColumnMap $columnMap, FieldTypeInterface $field): ColumnMap { - // todo: this method should only be called with proper arguments which means that the TCA integrity check should - // todo: take place outside this method. - - if (isset($columnConfiguration['MM'])) { - $columnMap->setTypeOfRelation(Relation::HAS_AND_BELONGS_TO_MANY); - // check if foreign_table is set, which usually won't be the case for type "group" fields - if (!empty($columnConfiguration['foreign_table'])) { - $columnMap->setChildTableName($columnConfiguration['foreign_table']); - } - // todo: don't update column map if value(s) isn't/aren't set. - $columnMap->setRelationTableName($columnConfiguration['MM']); - if (isset($columnConfiguration['MM_match_fields']) && is_array($columnConfiguration['MM_match_fields'])) { - $columnMap->setRelationTableMatchFields($columnConfiguration['MM_match_fields']); - } - // todo: don't update column map if value(s) isn't/aren't set. - if (!empty($columnConfiguration['MM_opposite_field'])) { - $columnMap->setParentKeyFieldName('uid_foreign'); - $columnMap->setChildKeyFieldName('uid_local'); - $columnMap->setChildSortByFieldName('sorting_foreign'); - } else { - $columnMap->setParentKeyFieldName('uid_local'); - $columnMap->setChildKeyFieldName('uid_foreign'); - $columnMap->setChildSortByFieldName('sorting'); - } + $columnConfiguration = $field->getConfiguration(); + $columnMap->setTypeOfRelation(Relation::HAS_AND_BELONGS_TO_MANY); + // check if foreign_table is set, which usually won't be the case for type "group" fields + if ($columnConfiguration['foreign_table'] ?? false) { + $columnMap->setChildTableName($columnConfiguration['foreign_table']); + } + // todo: don't update column map if value(s) isn't/aren't set. + $columnMap->setRelationTableName($columnConfiguration['MM']); + if (isset($columnConfiguration['MM_match_fields']) && is_array($columnConfiguration['MM_match_fields'])) { + $columnMap->setRelationTableMatchFields($columnConfiguration['MM_match_fields']); + } + // todo: don't update column map if value(s) isn't/aren't set. + if (!empty($columnConfiguration['MM_opposite_field'])) { + $columnMap->setParentKeyFieldName('uid_foreign'); + $columnMap->setChildKeyFieldName('uid_local'); + $columnMap->setChildSortByFieldName('sorting_foreign'); } else { - // todo: this else part is actually superfluous because \TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMapFactory::setRelations - // todo: only calls this method if $columnConfiguration['MM'] is set. - - throw new UnsupportedRelationException('The given information to build a many-to-many-relation was not sufficient. Check your TCA definitions. mm-relations with IRRE must have at least a defined "MM" or "foreign_selector".', 1268817963); + $columnMap->setParentKeyFieldName('uid_local'); + $columnMap->setChildKeyFieldName('uid_foreign'); + $columnMap->setChildSortByFieldName('sorting'); } return $columnMap; } diff --git a/typo3/sysext/extbase/Classes/Persistence/Generic/Mapper/DataMapFactory.php b/typo3/sysext/extbase/Classes/Persistence/Generic/Mapper/DataMapFactory.php index 69b858d85b3f..4578e017b5cd 100644 --- a/typo3/sysext/extbase/Classes/Persistence/Generic/Mapper/DataMapFactory.php +++ b/typo3/sysext/extbase/Classes/Persistence/Generic/Mapper/DataMapFactory.php @@ -19,6 +19,8 @@ namespace TYPO3\CMS\Extbase\Persistence\Generic\Mapper; use TYPO3\CMS\Core\Cache\CacheManager; use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface; +use TYPO3\CMS\Core\Schema\Capability\TcaSchemaCapability; +use TYPO3\CMS\Core\Schema\TcaSchemaFactory; use TYPO3\CMS\Core\SingletonInterface; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface; @@ -66,6 +68,8 @@ class DataMapFactory implements SingletonInterface private ColumnMapFactory $columnMapFactory; + private TcaSchemaFactory $tcaSchemaFactory; + protected string $baseCacheIdentifier; public function __construct( @@ -74,6 +78,7 @@ class DataMapFactory implements SingletonInterface CacheManager $cacheManager, ClassesConfiguration $classesConfiguration, ColumnMapFactory $columnMapFactory, + TcaSchemaFactory $tcaSchemaFactory, string $baseCacheIdentifier ) { $this->reflectionService = $reflectionService; @@ -82,6 +87,7 @@ class DataMapFactory implements SingletonInterface $this->dataMapCache = $this->cacheManager->getCache('extbase'); $this->classesConfiguration = $classesConfiguration; $this->columnMapFactory = $columnMapFactory; + $this->tcaSchemaFactory = $tcaSchemaFactory; $this->baseCacheIdentifier = $baseCacheIdentifier; } @@ -147,13 +153,12 @@ class DataMapFactory implements SingletonInterface $dataMap = GeneralUtility::makeInstance(DataMap::class, $className, $tableName, $recordType, $subclasses); $dataMap = $this->addMetaDataColumnNames($dataMap, $tableName); - foreach ($this->getColumnsDefinition($tableName) as $columnName => $columnDefinition) { + foreach ($this->tcaSchemaFactory->get($tableName)->getFields() as $columnName => $columnDefinition) { $propertyName = $fieldNameToPropertyNameMapping[$columnName] ?? GeneralUtility::underscoredToLowerCamelCase($columnName); $dataMap->addColumnMap( $propertyName, $this->columnMapFactory->create( - $columnName, $columnDefinition, $propertyName, $className @@ -183,62 +188,55 @@ class DataMapFactory implements SingletonInterface return $tableName; } - /** - * Returns the TCA columns array of the specified table - * - * @param string $tableName An optional table name to fetch the columns definition from - * @return array The TCA columns definition - */ - protected function getColumnsDefinition(string $tableName): array - { - return is_array($GLOBALS['TCA'][$tableName]['columns'] ?? null) ? $GLOBALS['TCA'][$tableName]['columns'] : []; - } - protected function addMetaDataColumnNames(DataMap $dataMap, string $tableName): DataMap { - $controlSection = $GLOBALS['TCA'][$tableName]['ctrl'] ?? null; - if ($controlSection === null) { + if (!$this->tcaSchemaFactory->has($tableName)) { return $dataMap; } + $schema = $this->tcaSchemaFactory->get($tableName); $dataMap->setPageIdColumnName('pid'); - if (isset($controlSection['tstamp'])) { - $dataMap->setModificationDateColumnName($controlSection['tstamp']); + if ($schema->hasCapability(TcaSchemaCapability::UpdatedAt)) { + $dataMap->setModificationDateColumnName((string)$schema->getCapability(TcaSchemaCapability::UpdatedAt)); } - if (isset($controlSection['crdate'])) { - $dataMap->setCreationDateColumnName($controlSection['crdate']); + if ($schema->hasCapability(TcaSchemaCapability::CreatedAt)) { + $dataMap->setCreationDateColumnName((string)$schema->getCapability(TcaSchemaCapability::CreatedAt)); } - if (isset($controlSection['delete'])) { - $dataMap->setDeletedFlagColumnName($controlSection['delete']); + if ($schema->hasCapability(TcaSchemaCapability::SoftDelete)) { + $dataMap->setDeletedFlagColumnName((string)$schema->getCapability(TcaSchemaCapability::SoftDelete)); } - if (isset($controlSection['languageField'])) { - $dataMap->setLanguageIdColumnName($controlSection['languageField']); - } - if (isset($controlSection['transOrigPointerField'])) { - $dataMap->setTranslationOriginColumnName($controlSection['transOrigPointerField']); + if ($schema->hasCapability(TcaSchemaCapability::Language)) { + $languageCapability = $schema->getCapability(TcaSchemaCapability::Language); + $dataMap->setLanguageIdColumnName($languageCapability->getLanguageField()->getName()); + $dataMap->setTranslationOriginColumnName($languageCapability->getTranslationOriginPointerField()->getName()); + if ($languageCapability->hasDiffSourceField()) { + $dataMap->setTranslationOriginDiffSourceName($languageCapability->getDiffSourceField()->getName()); + } } - if (isset($controlSection['transOrigDiffSourceField'])) { - $dataMap->setTranslationOriginDiffSourceName($controlSection['transOrigDiffSourceField']); + if ($schema->getSubSchemaDivisorField() !== null) { + $dataMap->setRecordTypeColumnName($schema->getSubSchemaDivisorField()->getName()); } - if (isset($controlSection['type'])) { - $dataMap->setRecordTypeColumnName($controlSection['type']); + if ($schema->hasCapability(TcaSchemaCapability::RestrictionRootLevel)) { + // @todo Evaluate if this is correct. We currently have to use canExistOnPages() to keep previous + // behaviour, which is (bool)$rootlevel, so treating "-1" and "1" as TURE, and only 0 als FALSE. + $dataMap->setRootLevel($schema->getCapability(TcaSchemaCapability::RestrictionRootLevel)->canExistOnPages()); } - if (isset($controlSection['rootLevel'])) { - $dataMap->setRootLevel($controlSection['rootLevel']); + if (isset($schema->getRawConfiguration()['is_static'])) { + $dataMap->setIsStatic($schema->getRawConfiguration()['is_static']); } - if (isset($controlSection['is_static'])) { - $dataMap->setIsStatic($controlSection['is_static']); + if ($schema->hasCapability(TcaSchemaCapability::RestrictionDisabledField)) { + $dataMap->setDisabledFlagColumnName((string)$schema->getCapability(TcaSchemaCapability::RestrictionDisabledField)); } - if (isset($controlSection['enablecolumns']['disabled'])) { - $dataMap->setDisabledFlagColumnName($controlSection['enablecolumns']['disabled']); + if ($schema->hasCapability(TcaSchemaCapability::RestrictionStartTime)) { + $dataMap->setDisabledFlagColumnName((string)$schema->getCapability(TcaSchemaCapability::RestrictionStartTime)); } - if (isset($controlSection['enablecolumns']['starttime'])) { - $dataMap->setStartTimeColumnName($controlSection['enablecolumns']['starttime']); + if ($schema->hasCapability(TcaSchemaCapability::RestrictionEndTime)) { + $dataMap->setDisabledFlagColumnName((string)$schema->getCapability(TcaSchemaCapability::RestrictionEndTime)); } - if (isset($controlSection['enablecolumns']['endtime'])) { - $dataMap->setEndTimeColumnName($controlSection['enablecolumns']['endtime']); + if ($schema->hasCapability(TcaSchemaCapability::RestrictionUserGroup)) { + $dataMap->setDisabledFlagColumnName((string)$schema->getCapability(TcaSchemaCapability::RestrictionUserGroup)); } - if (isset($controlSection['enablecolumns']['fe_group'])) { - $dataMap->setFrontEndUserGroupColumnName($controlSection['enablecolumns']['fe_group']); + if ($schema->hasCapability(TcaSchemaCapability::RestrictionDisabledField)) { + $dataMap->setDisabledFlagColumnName((string)$schema->getCapability(TcaSchemaCapability::RestrictionDisabledField)); } return $dataMap; } diff --git a/typo3/sysext/extbase/Tests/Functional/Fixtures/Extensions/test_data_mapper/Classes/Domain/Model/Example.php b/typo3/sysext/extbase/Tests/Functional/Fixtures/Extensions/test_data_mapper/Classes/Domain/Model/Example.php index 79bfec7b923a..2af05a5f2911 100644 --- a/typo3/sysext/extbase/Tests/Functional/Fixtures/Extensions/test_data_mapper/Classes/Domain/Model/Example.php +++ b/typo3/sysext/extbase/Tests/Functional/Fixtures/Extensions/test_data_mapper/Classes/Domain/Model/Example.php @@ -29,6 +29,9 @@ class Example extends AbstractEntity protected ?\DateTime $uninitializedDateTimeProperty; protected \DateTime $uninitializedMandatoryDateTimeProperty; protected ?\DateTime $initializedDateTimeProperty = null; + protected ?\DateTime $initializedDateTimePropertyDate = null; + protected ?\DateTime $initializedDateTimePropertyDatetime = null; + protected ?\DateTime $initializedDateTimePropertyTime = null; protected ?CustomDateTime $customDateTime = null; public $unknownType; public Enum\StringBackedEnum $stringBackedEnum; @@ -116,6 +119,36 @@ class Example extends AbstractEntity $this->initializedDateTimeProperty = $initializedDateTimeProperty; } + public function getInitializedDateTimePropertyDate(): ?\DateTime + { + return $this->initializedDateTimePropertyDate; + } + + public function setInitializedDateTimePropertyDate(?\DateTime $initializedDateTimePropertyDate): void + { + $this->initializedDateTimePropertyDate = $initializedDateTimePropertyDate; + } + + public function getInitializedDateTimePropertyDatetime(): ?\DateTime + { + return $this->initializedDateTimePropertyDatetime; + } + + public function setInitializedDateTimePropertyDatetime(?\DateTime $initializedDateTimePropertyDatetime): void + { + $this->initializedDateTimePropertyDatetime = $initializedDateTimePropertyDatetime; + } + + public function getInitializedDateTimePropertyTime(): ?\DateTime + { + return $this->initializedDateTimePropertyTime; + } + + public function setInitializedDateTimePropertyTime(?\DateTime $initializedDateTimePropertyTime): void + { + $this->initializedDateTimePropertyTime = $initializedDateTimePropertyTime; + } + public function getCustomDateTime(): ?CustomDateTime { return $this->customDateTime; diff --git a/typo3/sysext/extbase/Tests/Functional/Fixtures/Extensions/test_data_mapper/Configuration/TCA/tx_testdatamapper_domain_model_example.php b/typo3/sysext/extbase/Tests/Functional/Fixtures/Extensions/test_data_mapper/Configuration/TCA/tx_testdatamapper_domain_model_example.php index 68adf7331f07..86d5faa70035 100644 --- a/typo3/sysext/extbase/Tests/Functional/Fixtures/Extensions/test_data_mapper/Configuration/TCA/tx_testdatamapper_domain_model_example.php +++ b/typo3/sysext/extbase/Tests/Functional/Fixtures/Extensions/test_data_mapper/Configuration/TCA/tx_testdatamapper_domain_model_example.php @@ -43,6 +43,24 @@ return [ 'type' => 'datetime', ], ], + 'initialized_date_time_property_date' => [ + 'config' => [ + 'type' => 'datetime', + 'dbType' => 'date', + ], + ], + 'initialized_date_time_property_datetime' => [ + 'config' => [ + 'type' => 'datetime', + 'dbType' => 'datetime', + ], + ], + 'initialized_date_time_property_time' => [ + 'config' => [ + 'type' => 'datetime', + 'dbType' => 'time', + ], + ], 'custom_date_time' => [ 'config' => [ 'type' => 'datetime', diff --git a/typo3/sysext/extbase/Tests/Functional/Persistence/Generic/Mapper/ColumnMapFactoryTest.php b/typo3/sysext/extbase/Tests/Functional/Persistence/Generic/Mapper/ColumnMapFactoryTest.php index d849d6da1fa0..7083e0859a7c 100644 --- a/typo3/sysext/extbase/Tests/Functional/Persistence/Generic/Mapper/ColumnMapFactoryTest.php +++ b/typo3/sysext/extbase/Tests/Functional/Persistence/Generic/Mapper/ColumnMapFactoryTest.php @@ -20,6 +20,8 @@ namespace TYPO3\CMS\Extbase\Tests\Functional\Persistence\Generic\Mapper; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Test; use TYPO3\CMS\Core\DataHandling\TableColumnType; +use TYPO3\CMS\Core\Schema\FieldTypeFactory; +use TYPO3\CMS\Core\Schema\RelationMap; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Extbase\Persistence\Generic\Mapper\ColumnMap; use TYPO3\CMS\Extbase\Persistence\Generic\Mapper\ColumnMap\Relation; @@ -32,11 +34,13 @@ final class ColumnMapFactoryTest extends FunctionalTestCase protected bool $initializeDatabase = false; protected ColumnMapFactory $columnMapFactory; + protected FieldTypeFactory $fieldTypeFactory; protected function setUp(): void { parent::setUp(); $this->columnMapFactory = $this->get(ColumnMapFactory::class); + $this->fieldTypeFactory = $this->get(FieldTypeFactory::class); } public static function createWithGroupTypeDataProvider(): \Generator @@ -97,7 +101,7 @@ final class ColumnMapFactoryTest extends FunctionalTestCase { self::assertEquals( $expectedColumnMap, - $this->columnMapFactory->create($columnName, $columnConfiguration, $propertyName, ColumnMapFactoryEntityFixture::class) + $this->columnMapFactory->create($this->fieldTypeFactory->createFieldType($columnName, $columnConfiguration, 'virtual', new RelationMap()), $propertyName, ColumnMapFactoryEntityFixture::class) ); } @@ -219,7 +223,7 @@ final class ColumnMapFactoryTest extends FunctionalTestCase { self::assertEquals( $expectedColumnMap, - $this->columnMapFactory->create($columnName, $columnConfiguration, $propertyName, ColumnMapFactoryEntityFixture::class) + $this->columnMapFactory->create($this->fieldTypeFactory->createFieldType($columnName, $columnConfiguration, 'virtual', new RelationMap()), $propertyName, ColumnMapFactoryEntityFixture::class) ); } @@ -248,7 +252,7 @@ final class ColumnMapFactoryTest extends FunctionalTestCase { self::assertEquals( $expectedColumnMap, - $this->columnMapFactory->create($columnName, $columnConfiguration, $propertyName, ColumnMapFactoryEntityFixture::class) + $this->columnMapFactory->create($this->fieldTypeFactory->createFieldType($columnName, $columnConfiguration, 'virtual', new RelationMap()), $propertyName, ColumnMapFactoryEntityFixture::class) ); } @@ -284,7 +288,7 @@ final class ColumnMapFactoryTest extends FunctionalTestCase { self::assertEquals( $expectedColumnMap, - $this->columnMapFactory->create($columnName, $columnConfiguration, $propertyName, ColumnMapFactoryEntityFixture::class) + $this->columnMapFactory->create($this->fieldTypeFactory->createFieldType($columnName, $columnConfiguration, 'virtual', new RelationMap()), $propertyName, ColumnMapFactoryEntityFixture::class) ); } @@ -304,8 +308,7 @@ final class ColumnMapFactoryTest extends FunctionalTestCase ], ]; - $columnMap = $this->columnMapFactory->create($columnName, $columnConfiguration, $propertyName, ColumnMapFactoryEntityFixture::class); - + $columnMap = $this->columnMapFactory->create($this->fieldTypeFactory->createFieldType($columnName, $columnConfiguration, 'virtual', new RelationMap()), $propertyName, ColumnMapFactoryEntityFixture::class); self::assertSame( [ 'fieldname' => 'foo_model', @@ -330,8 +333,7 @@ final class ColumnMapFactoryTest extends FunctionalTestCase ], ]; - $columnMap = $this->columnMapFactory->create($columnName, $columnConfiguration, $propertyName, ColumnMapFactoryEntityFixture::class); - + $columnMap = $this->columnMapFactory->create($this->fieldTypeFactory->createFieldType($columnName, $columnConfiguration, 'virtual', new RelationMap()), $propertyName, ColumnMapFactoryEntityFixture::class); self::assertSame( [ 'fieldname' => 'foo_model', @@ -364,7 +366,8 @@ final class ColumnMapFactoryTest extends FunctionalTestCase ], ]; - self::assertEquals($expectedColumnMap, $this->columnMapFactory->create($columnName, $columnConfiguration, $propertyName, ColumnMapFactoryEntityFixture::class)); + $columnMap = $this->columnMapFactory->create($this->fieldTypeFactory->createFieldType($columnName, $columnConfiguration, 'virtual', new RelationMap()), $propertyName, ColumnMapFactoryEntityFixture::class); + self::assertEquals($expectedColumnMap, $columnMap); } #[Test] @@ -390,7 +393,8 @@ final class ColumnMapFactoryTest extends FunctionalTestCase ], ]; - self::assertEquals($expectedColumnMap, $this->columnMapFactory->create($columnName, $columnConfiguration, $propertyName, ColumnMapFactoryEntityFixture::class)); + $columnMap = $this->columnMapFactory->create($this->fieldTypeFactory->createFieldType($columnName, $columnConfiguration, 'virtual', new RelationMap()), $propertyName, ColumnMapFactoryEntityFixture::class); + self::assertEquals($expectedColumnMap, $columnMap); } #[Test] @@ -416,7 +420,8 @@ final class ColumnMapFactoryTest extends FunctionalTestCase ], ]; - self::assertEquals($expectedColumnMap, $this->columnMapFactory->create($columnName, $columnConfiguration, $propertyName, ColumnMapFactoryEntityFixture::class)); + $columnMap = $this->columnMapFactory->create($this->fieldTypeFactory->createFieldType($columnName, $columnConfiguration, 'virtual', new RelationMap()), $propertyName, ColumnMapFactoryEntityFixture::class); + self::assertEquals($expectedColumnMap, $columnMap); } public static function columnMapIsInitializedWithFieldEvaluationsForDateTimeFieldsDataProvider(): array @@ -442,8 +447,7 @@ final class ColumnMapFactoryTest extends FunctionalTestCase ], ]; - $columnMap = $this->columnMapFactory->create($columnName, $columnConfiguration, $propertyName, ColumnMapFactoryEntityFixture::class); - + $columnMap = $this->columnMapFactory->create($this->fieldTypeFactory->createFieldType($columnName, $columnConfiguration, 'virtual', new RelationMap()), $propertyName, ColumnMapFactoryEntityFixture::class); self::assertSame($expectedValue, $columnMap->getDateTimeStorageFormat()); } @@ -490,8 +494,7 @@ final class ColumnMapFactoryTest extends FunctionalTestCase ], ]; - $columnMap = $this->columnMapFactory->create($columnName, $columnConfiguration, $propertyName, ColumnMapFactoryEntityFixture::class); - + $columnMap = $this->columnMapFactory->create($this->fieldTypeFactory->createFieldType($columnName, $columnConfiguration, 'virtual', new RelationMap()), $propertyName, ColumnMapFactoryEntityFixture::class); self::assertSame($expectedType, $columnMap->getType()); } } diff --git a/typo3/sysext/extbase/Tests/Functional/Persistence/Generic/Mapper/DataMapperTest.php b/typo3/sysext/extbase/Tests/Functional/Persistence/Generic/Mapper/DataMapperTest.php index d2c9b0477e78..a6ea93436daa 100644 --- a/typo3/sysext/extbase/Tests/Functional/Persistence/Generic/Mapper/DataMapperTest.php +++ b/typo3/sysext/extbase/Tests/Functional/Persistence/Generic/Mapper/DataMapperTest.php @@ -367,17 +367,17 @@ final class DataMapperTest extends FunctionalTestCase #[Test] public function mapDateTimeHandlesDifferentFieldEvaluations(string|int|null $value, ?string $storageFormat, ?string $expectedValue): void { - $GLOBALS['TCA']['tx_testdatamapper_domain_model_example']['columns']['initialized_date_time_property']['config']['dbType'] = $storageFormat; $rows = [ [ 'uid' => 123, - 'initialized_date_time_property' => $value, + 'initialized_date_time_property' . ($storageFormat !== null ? '_' . $storageFormat : '') => $value, ], ]; $dataMapper = $this->get(DataMapper::class); $mappedObjectsArray = $dataMapper->map(Example::class, $rows); - self::assertSame($expectedValue, $mappedObjectsArray[0]->getInitializedDateTimeProperty()?->format('c')); + $getter = 'getInitializedDateTimeProperty' . ($storageFormat !== null ? ucfirst($storageFormat) : ''); + self::assertSame($expectedValue, $mappedObjectsArray[0]->{$getter}()?->format('c')); // Flush DataMapFactory cache on each run. $cacheManager = $this->get(CacheManager::class); @@ -404,18 +404,18 @@ final class DataMapperTest extends FunctionalTestCase date_default_timezone_set('America/Chicago'); $usedTimeZone = date_default_timezone_get(); - $GLOBALS['TCA']['tx_testdatamapper_domain_model_example']['columns']['initialized_date_time_property']['config']['dbType'] = $storageFormat; $rows = [ [ 'uid' => 123, - 'initialized_date_time_property' => $value, + 'initialized_date_time_property' . ($storageFormat !== null ? '_' . $storageFormat : '') => $value, ], ]; $dataMapper = $this->get(DataMapper::class); $mappedObjectsArray = $dataMapper->map(Example::class, $rows); + $getter = 'getInitializedDateTimeProperty' . ($storageFormat !== null ? ucfirst($storageFormat) : ''); $expectedValue = $expectedValue !== null ? new \DateTime($expectedValue, new \DateTimeZone($usedTimeZone)) : $expectedValue; - self::assertEquals($expectedValue, $mappedObjectsArray[0]->getInitializedDateTimeProperty()); + self::assertEquals($expectedValue, $mappedObjectsArray[0]->{$getter}()); // Flush DataMapFactory cache on each run. $cacheManager = $this->get(CacheManager::class); diff --git a/typo3/sysext/extbase/Tests/Unit/Persistence/Generic/Mapper/DataMapperTest.php b/typo3/sysext/extbase/Tests/Unit/Persistence/Generic/Mapper/DataMapperTest.php index 58e530646455..eeb59ec5529c 100644 --- a/typo3/sysext/extbase/Tests/Unit/Persistence/Generic/Mapper/DataMapperTest.php +++ b/typo3/sysext/extbase/Tests/Unit/Persistence/Generic/Mapper/DataMapperTest.php @@ -21,6 +21,8 @@ use Doctrine\Instantiator\InstantiatorInterface; use PHPUnit\Framework\Attributes\Test; use Psr\EventDispatcher\EventDispatcherInterface; use TYPO3\CMS\Core\Cache\CacheManager; +use TYPO3\CMS\Core\Schema\Field\SelectRelationFieldType; +use TYPO3\CMS\Core\Schema\TcaSchemaFactory; use TYPO3\CMS\Extbase\Configuration\ConfigurationManager; use TYPO3\CMS\Extbase\Persistence\ClassesConfiguration; use TYPO3\CMS\Extbase\Persistence\Generic\Mapper\ColumnMap; @@ -58,6 +60,7 @@ final class DataMapperTest extends UnitTestCase $this->createMock(CacheManager::class), $this->createMock(ClassesConfiguration::class), $this->columnMapFactory, + $this->createMock(TcaSchemaFactory::class), 'foo' ); @@ -78,9 +81,7 @@ final class DataMapperTest extends UnitTestCase // Arrange $this->columnMapFactory->setOneToManyRelation( $this->columnMap, - [ - 'foreign_table' => 'tx_myextension_bar', - ] + new SelectRelationFieldType('foo', ['foreign_table' => 'tx_myextension_bar'], []) ); // Act @@ -96,10 +97,7 @@ final class DataMapperTest extends UnitTestCase // Arrange $this->columnMapFactory->setOneToManyRelation( $this->columnMap, - [ - 'foreign_table' => 'tx_myextension_bar', - 'foreign_default_sortby' => '', - ] + new SelectRelationFieldType('foo', ['foreign_table' => 'tx_myextension_bar', 'foreign_default_sortby' => ''], []) ); // Act @@ -115,10 +113,7 @@ final class DataMapperTest extends UnitTestCase // Arrange $this->columnMapFactory->setOneToManyRelation( $this->columnMap, - [ - 'foreign_table' => 'tx_myextension_bar', - 'foreign_default_sortby' => 'pid invalid', - ] + new SelectRelationFieldType('foo', ['foreign_table' => 'tx_myextension_bar', 'foreign_default_sortby' => 'pid invalid'], []) ); // Act @@ -137,10 +132,7 @@ final class DataMapperTest extends UnitTestCase // Arrange $this->columnMapFactory->setOneToManyRelation( $this->columnMap, - [ - 'foreign_table' => 'tx_myextension_bar', - 'foreign_sortby' => 'uid', - ] + new SelectRelationFieldType('foo', ['foreign_table' => 'tx_myextension_bar', 'foreign_sortby' => 'uid'], []) ); // Act @@ -159,11 +151,7 @@ final class DataMapperTest extends UnitTestCase // Arrange $this->columnMapFactory->setOneToManyRelation( $this->columnMap, - [ - 'foreign_table' => 'tx_myextension_bar', - 'foreign_sortby' => 'uid', - 'foreign_default_sortby' => 'pid', - ] + new SelectRelationFieldType('foo', ['foreign_table' => 'tx_myextension_bar', 'foreign_sortby' => 'uid', 'foreign_default_sortby' => 'pid'], []) ); // Act @@ -182,10 +170,7 @@ final class DataMapperTest extends UnitTestCase // Arrange $this->columnMapFactory->setOneToManyRelation( $this->columnMap, - [ - 'foreign_table' => 'tx_myextension_bar', - 'foreign_default_sortby' => 'pid', - ] + new SelectRelationFieldType('foo', ['foreign_table' => 'tx_myextension_bar', 'foreign_default_sortby' => 'pid'], []) ); // Act @@ -204,10 +189,7 @@ final class DataMapperTest extends UnitTestCase // Arrange $this->columnMapFactory->setOneToManyRelation( $this->columnMap, - [ - 'foreign_table' => 'tx_myextension_bar', - 'foreign_default_sortby' => 'pid desc', - ] + new SelectRelationFieldType('foo', ['foreign_table' => 'tx_myextension_bar', 'foreign_default_sortby' => 'pid desc'], []) ); // Act @@ -226,10 +208,7 @@ final class DataMapperTest extends UnitTestCase // Arrange $this->columnMapFactory->setOneToManyRelation( $this->columnMap, - [ - 'foreign_table' => 'tx_myextension_bar', - 'foreign_default_sortby' => 'pid desc, title, uid asc', - ] + new SelectRelationFieldType('foo', ['foreign_table' => 'tx_myextension_bar', 'foreign_default_sortby' => 'pid desc, title, uid asc'], []) ); // Act -- GitLab