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(), ); } }