TYPO3 v15 dev-main snapshot ()

This commit is contained in:
2026-08-10 22:31:15 +02:00
commit 6830982e7d
295 changed files with 31995 additions and 0 deletions
@@ -0,0 +1,210 @@
<?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\Extbase\Persistence\Generic\Mapper;
use TYPO3\CMS\Core\DataHandling\TableColumnType;
use TYPO3\CMS\Core\Schema\Field\CountryFieldType;
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\Field\StaticSelectFieldType;
use TYPO3\CMS\Core\Schema\RelationshipType;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Persistence\Generic\Mapper\ColumnMap\Relation;
use TYPO3\CMS\Extbase\Reflection\ClassSchema\Exception\NoSuchPropertyException;
use TYPO3\CMS\Extbase\Reflection\ReflectionService;
/**
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
readonly class ColumnMapFactory
{
public function __construct(
private ReflectionService $reflectionService,
) {}
public function create(FieldTypeInterface $field, string $propertyName, string $className): ColumnMap
{
$propertyType = null;
$propertyCollectionValueType = null;
try {
$property = $this->reflectionService->getClassSchema($className)->getProperty($propertyName);
$nonProxyPropertyTypes = $property->getFilteredTypes([$property, 'filterLazyLoadingProxyAndLazyObjectStorage']);
$primaryType = $nonProxyPropertyTypes[0] ?? null;
$propertyType = $primaryType?->getClassName() ?? $primaryType?->getBuiltinType() ?? null;
if ($primaryType?->isCollection() && $primaryType->getCollectionValueTypes() !== []) {
$primaryCollectionValueType = $primaryType->getCollectionValueTypes()[0];
$propertyCollectionValueType = $primaryCollectionValueType->getClassName() ?? $primaryCollectionValueType->getBuiltinType();
}
} catch (NoSuchPropertyException) {
// $type and $propertyCollectionValueType kept null
}
// @todo: The relation related handling below smells fishy at various places. Some TCA
// details are ignored, some are at least opinionated, some are wrong. The combination
// of fetching details from TCA *and* the model class makes everything quite complex.
// This should be consolidated.
// Also, the mixture of extbase internal "Relation", core TableColumnType, plus
// core TcaSchema details is complex and should be simplified to what we really need.
// Last, ColumnMap is not fully used throughout extbase, various details tend to
// still access TCA details directly.
// In the end, we may be better off removing extbase "Relation" altogether and
// add TcaSchema $columnConfiguration to ColumnMap to sort out TCA details at
// the few places where needed directly? This would be more in-line with DataHandler
// as well and raises fewer state questions in consumers, which reduces complexity.
$columnConfiguration = $field->getConfiguration();
$columnName = $field->getName();
$tableColumnType = TableColumnType::tryFrom($field->getType());
$childTableName = null;
if ($field->isType(TableColumnType::GROUP)) {
// TCA type="group" has no TCA property "foreign_table" and can only deal with single-table
// relations in extbase (no support for union types). That means `allowed` should only
// contain ONE table entry, as Extbase can only evaluate the first one, if multiple
// are defined.
$allowed = GeneralUtility::trimExplode(',', $columnConfiguration['allowed'] ?? '', true);
$childTableName = $allowed[0] ?? $columnConfiguration['foreign_table'] ?? null;
} elseif ($field instanceof RelationalFieldTypeInterface) {
$childTableName = $columnConfiguration['foreign_table'] ?? null;
}
if ($field instanceof DateTimeFieldType) {
// TCA type="datetime" considers "dbtype" and is done.
return new ColumnMap(
columnName: $columnName,
type: $tableColumnType,
dateTimeFormat: $field->getFormat(),
dateTimeStorageFormat: $field->getPersistenceType(),
isNullable: $field->isNullable(),
);
}
if (($field instanceof RelationalFieldTypeInterface) && $field->getRelationshipType() === RelationshipType::ManyToMany) {
if (!isset($columnConfiguration['MM'])) {
throw new \LogicException(
'TCA schema of column ' . $columnName . ' is "ManytoMany", but TCA config has no MM property set',
1733560101
);
}
return new ColumnMap(
columnName: $columnName,
type: $tableColumnType,
typeOfRelation: Relation::HAS_AND_BELONGS_TO_MANY,
childTableName: $childTableName,
relationTableName: $columnConfiguration['MM'],
relationTableMatchFields: is_array($columnConfiguration['MM_match_fields'] ?? false) ? $columnConfiguration['MM_match_fields'] : [],
parentKeyFieldName: !empty($columnConfiguration['MM_opposite_field']) ? 'uid_foreign' : 'uid_local',
childKeyFieldName: !empty($columnConfiguration['MM_opposite_field']) ? 'uid_local' : 'uid_foreign',
childSortByFieldName: !empty($columnConfiguration['MM_opposite_field']) ? 'sorting_foreign' : 'sorting',
isNullable: $field->isNullable(),
);
}
if ($propertyCollectionValueType !== null) {
// The field might not be a RelationFieldType, e.g. for TCA type "passthrough" or type "select"
// without items. However, the model defines a relation and therefore overrules the TCA schema lookup.
// This also overrules any "maxitems" or "renderType" configuration!
return new ColumnMap(
columnName: $columnName,
type: $tableColumnType,
typeOfRelation: Relation::HAS_MANY,
childTableName: $childTableName,
relationTableMatchFields: is_array($columnConfiguration['foreign_match_fields'] ?? false) ? $columnConfiguration['foreign_match_fields'] : [],
parentKeyFieldName: $columnConfiguration['foreign_field'] ?? null,
parentTableFieldName: $columnConfiguration['foreign_table_field'] ?? null,
childSortByFieldName: $columnConfiguration['foreign_sortby'] ?? null,
childTableDefaultSortings: $columnConfiguration['foreign_default_sortby'] ?? null,
isNullable: $field->isNullable(),
);
}
if ($propertyType !== null && strpbrk($propertyType, '_\\') !== false) {
// @todo: Check this. Seems to be a check for Tx_Foo_Bar style class names?!
return new ColumnMap(
columnName: $columnName,
type: $tableColumnType,
typeOfRelation: Relation::HAS_ONE,
childTableName: $childTableName,
relationTableMatchFields: is_array($columnConfiguration['foreign_match_fields'] ?? false) ? $columnConfiguration['foreign_match_fields'] : [],
parentKeyFieldName: $columnConfiguration['foreign_field'] ?? null,
parentTableFieldName: $columnConfiguration['foreign_table_field'] ?? null,
childSortByFieldName: $columnConfiguration['foreign_sortby'] ?? null,
isNullable: $field->isNullable(),
);
}
if ($field instanceof FolderFieldType) {
// Folder is a special case which always has a relation to one or many "folders".
// In case "maxitems" is set to > 1 and relationship is not explicitly set to "*toOne"
// it's HAS_MANY, in all other cases it's HAS_ONE. It can never belong to many.
// @todo: Get rid of the "maxitems" and rely purely on the evaluated relationship type
// @todo: TCA type="folder" has no TCA property "relationship"!
$relation = Relation::HAS_ONE;
if (!in_array((string)($columnConfiguration['relationship'] ?? ''), ['oneToOne', 'manyToOne'], true)
&& (!isset($columnConfiguration['maxitems']) || $columnConfiguration['maxitems'] > 1)
) {
$relation = Relation::HAS_MANY;
}
return new ColumnMap(
columnName: $columnName,
type: $tableColumnType,
typeOfRelation: $relation,
isNullable: $field->isNullable(),
);
}
if ($field instanceof CountryFieldType) {
$relation = Relation::HAS_ONE;
return new ColumnMap(
columnName: $columnName,
type: $tableColumnType,
typeOfRelation: $relation,
);
}
if (
(
$field instanceof RelationalFieldTypeInterface
&& $field->getRelationshipType()->hasMany()
&& (
!$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))
)
)
|| (
$field instanceof StaticSelectFieldType
&& (int)($columnConfiguration['maxitems'] ?? 0) > 1 // @todo: Get rid of the "maxitems" and rely purely on the relationship type
)
) {
return new ColumnMap(
columnName: $columnName,
type: $tableColumnType,
typeOfRelation: Relation::HAS_MANY,
isNullable: $field->isNullable(),
);
}
return new ColumnMap(
columnName: $columnName,
type: $tableColumnType,
isNullable: $field->isNullable(),
);
}
}