diff --git a/typo3/sysext/core/Classes/Domain/RawRecord.php b/typo3/sysext/core/Classes/Domain/RawRecord.php index 3f3287b5e61a27eddc256b288275e12c219277ab..c5b9ed02cdf67d124a9417cbce24c2ff79f63a8e 100644 --- a/typo3/sysext/core/Classes/Domain/RawRecord.php +++ b/typo3/sysext/core/Classes/Domain/RawRecord.php @@ -54,7 +54,7 @@ readonly class RawRecord implements RecordInterface public function getRecordType(): ?string { if (str_contains($this->type, '.')) { - return GeneralUtility::revExplode('.', $this->type, 2)[1]; + return GeneralUtility::trimExplode('.', $this->type, true)[1] ?? null; } return null; } @@ -62,7 +62,7 @@ readonly class RawRecord implements RecordInterface public function getMainType(): string { if (str_contains($this->type, '.')) { - return explode('.', $this->type)[0]; + return explode('.', $this->type)[0] ?? ''; } return $this->type; } diff --git a/typo3/sysext/core/Classes/Domain/RecordFactory.php b/typo3/sysext/core/Classes/Domain/RecordFactory.php index 554c034fde88b38d3f4892e17795dcaecdd6d90a..b3b49aca2e72c46fa393f69989afde0dd531009c 100644 --- a/typo3/sysext/core/Classes/Domain/RecordFactory.php +++ b/typo3/sysext/core/Classes/Domain/RecordFactory.php @@ -76,6 +76,13 @@ readonly class RecordFactory $subSchema = null; if ($schema->hasSubSchema($rawRecord->getRecordType() ?? '')) { $subSchema = $schema->getSubSchema($rawRecord->getRecordType()); + // @todo Support of "subtypes" will most likely be deprecated in upcoming versions + if ($subSchema->getSubTypeDivisorField() !== null + && $rawRecord->has($subSchema->getSubTypeDivisorField()->getName()) + && isset($subSchema->getSubSchemata()[$rawRecord->get($subSchema->getSubTypeDivisorField()->getName())]) + ) { + $subSchema = $subSchema->getSubSchema($rawRecord->get($subSchema->getSubTypeDivisorField()->getName())); + } } // Only use the fields that are defined in the schema @@ -103,6 +110,13 @@ readonly class RecordFactory $subSchema = null; if ($schema->hasSubSchema($rawRecord->getRecordType() ?? '')) { $subSchema = $schema->getSubSchema($rawRecord->getRecordType()); + // @todo Support of "subtypes" will most likely be deprecated in upcoming versions + if ($subSchema->getSubTypeDivisorField() !== null + && $rawRecord->has($subSchema->getSubTypeDivisorField()->getName()) + && isset($subSchema->getSubSchemata()[$rawRecord->get($subSchema->getSubTypeDivisorField()->getName())]) + ) { + $subSchema = $subSchema->getSubSchema($rawRecord->get($subSchema->getSubTypeDivisorField()->getName())); + } } // Only use the fields that are defined in the schema diff --git a/typo3/sysext/core/Classes/Schema/TcaSchema.php b/typo3/sysext/core/Classes/Schema/TcaSchema.php index e30ae41f93da26843771e94a28b2d0dad25b0f65..8fa1207c6679e89bd56992d9929915b9ad995668 100644 --- a/typo3/sysext/core/Classes/Schema/TcaSchema.php +++ b/typo3/sysext/core/Classes/Schema/TcaSchema.php @@ -227,6 +227,17 @@ readonly class TcaSchema implements SchemaInterface return null; } + /** + * @internal "subtype" is not considered as API of TcaSchema since this feature will most likely be deprecated in upcoming versions + */ + public function getSubTypeDivisorField(): ?FieldTypeInterface + { + if (isset($this->schemaConfiguration['subtype_value_field']) && isset($this->fields[$this->schemaConfiguration['subtype_value_field']])) { + return $this->fields[$this->schemaConfiguration['subtype_value_field']]; + } + return null; + } + /** * @return PassiveRelation[] */ diff --git a/typo3/sysext/core/Classes/Schema/TcaSchemaFactory.php b/typo3/sysext/core/Classes/Schema/TcaSchemaFactory.php index 42e5b0b9423de997bc80fbc54e734e4aeafb6762..fcb91254383e0357a138f5f2f13c35d2b01934a7 100644 --- a/typo3/sysext/core/Classes/Schema/TcaSchemaFactory.php +++ b/typo3/sysext/core/Classes/Schema/TcaSchemaFactory.php @@ -211,13 +211,55 @@ class TcaSchemaFactory $subSchemaFields[$fieldName] = $field; } - $subSchema = new TcaSchema( + + // @todo Support of "subtypes" will most likely be deprecated in upcoming versions + $subTypeSchemata = []; + if (isset($subSchemaDefinition['subtype_value_field']) + && ($subTypeDivisorField = $subSchemaFields[$subSchemaDefinition['subtype_value_field']] ?? null) !== null + ) { + // Add all the sub schema fields first. Afterwards extend this list based + // on "subtypes_addlist" and reduce list based on "subtypes_excludelist". + $subTypeFields = $subSchemaFields; + $subTypes = array_filter(array_map(static fn(array $item) => $item['value'] ?? '', $subTypeDivisorField->getConfiguration()['items'] ?? [])); + foreach ($subTypes as $subType) { + // Add fields based on "subtypes_addlist" configuration + if ($subSchemaDefinition['subtypes_addlist'][$subType] ?? false) { + $subTypeAddFields = GeneralUtility::trimExplode(',', (string)$subSchemaDefinition['subtypes_addlist'][$subType], true); + foreach ($subTypeAddFields as $fieldName) { + // Fetch the field from either the sub schema (taking columnsOverrides into account) or + // fall back to the field based on the default configuration. In case field does not + // exists, don't add it since it's not possible to add fields via subtypes, which have + // not been defined beforehand. + $field = $subSchemaFields[$fieldName] ?? $allFields[$fieldName] ?? null; + if ($field === null) { + continue; + } + $subTypeFields[$fieldName] = $field; + } + } + // Remove fields based on "subtypes_excludelist" configuration + if ($subSchemaDefinition['subtypes_excludelist'][$subType] ?? false) { + $subTypeExcludeFields = GeneralUtility::trimExplode(',', (string)$subSchemaDefinition['subtypes_excludelist'][$subType], true); + foreach ($subTypeExcludeFields as $fieldName) { + unset($subTypeFields[$fieldName]); + } + } + + $subTypeSchemata[$subType] = new TcaSchema( + $schemaName . '.' . $subSchemaName . '.' . $subType, + new FieldCollection($subTypeFields), + array_replace_recursive($schemaConfiguration, $subSchemaDefinition) + ); + } + } + + $subSchemata[$subSchemaName] = new TcaSchema( $schemaName . '.' . $subSchemaName, new FieldCollection($subSchemaFields), // Merge parts from the "types" section into the ctrl section of the main schema - array_replace_recursive($schemaConfiguration, $subSchemaDefinition) + array_replace_recursive($schemaConfiguration, $subSchemaDefinition), + $subTypeSchemata !== [] ? new SchemaCollection($subTypeSchemata) : null ); - $subSchemata[$subSchemaName] = $subSchema; } } $schema = new TcaSchema( diff --git a/typo3/sysext/core/Tests/Unit/Domain/RawRecordTest.php b/typo3/sysext/core/Tests/Unit/Domain/RawRecordTest.php new file mode 100644 index 0000000000000000000000000000000000000000..0571563dbbbdfc4dad8ec77525771b6522f4aa50 --- /dev/null +++ b/typo3/sysext/core/Tests/Unit/Domain/RawRecordTest.php @@ -0,0 +1,66 @@ +<?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\Unit\Domain; + +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Test; +use TYPO3\CMS\Core\Domain\RawRecord; +use TYPO3\CMS\Core\Domain\Record\ComputedProperties; +use TYPO3\TestingFramework\Core\Unit\UnitTestCase; + +final class RawRecordTest extends UnitTestCase +{ + public static function getTypeReturnsExpectedValueDataProvider(): iterable + { + yield 'full type' => [ + 'tt_content', + 'tt_content', + 'tt_content', + null, + ]; + yield 'record type' => [ + 'tt_content.list', + 'tt_content.list', + 'tt_content', + 'list', + ]; + yield 'sub type is ignored' => [ + 'tt_content.list.tx_blog_pi1', + 'tt_content.list.tx_blog_pi1', + 'tt_content', + 'list', + ]; + yield 'invalid config' => [ + 'tt_content....', + 'tt_content....', + 'tt_content', + null, + ]; + } + + #[DataProvider('getTypeReturnsExpectedValueDataProvider')] + #[Test] + public function getTypeReturnsExpectedParts(string $type, string $fullType, string $mainType, ?string $recordType): void + { + $record = new RawRecord(123, 456, [], $this->createMock(ComputedProperties::class), $type); + + self::assertSame($fullType, $record->getFullType()); + self::assertSame($mainType, $record->getMainType()); + self::assertSame($recordType, $record->getRecordType()); + } +} diff --git a/typo3/sysext/core/Tests/Unit/Schema/TcaSchemaFactoryTest.php b/typo3/sysext/core/Tests/Unit/Schema/TcaSchemaFactoryTest.php index 69c61191f4bbfbb2b4eddfc3d128f2c9d186cba0..a19d56731eb4af5dd94705c5e6be33d40c76bcd7 100644 --- a/typo3/sysext/core/Tests/Unit/Schema/TcaSchemaFactoryTest.php +++ b/typo3/sysext/core/Tests/Unit/Schema/TcaSchemaFactoryTest.php @@ -20,6 +20,7 @@ namespace TYPO3\CMS\Core\Tests\Unit\Schema; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Test; use TYPO3\CMS\Core\Cache\Frontend\PhpFrontend; +use TYPO3\CMS\Core\Schema\Field\FieldTypeInterface; use TYPO3\CMS\Core\Schema\FieldTypeFactory; use TYPO3\CMS\Core\Schema\RelationMapBuilder; use TYPO3\CMS\Core\Schema\TcaSchemaFactory; @@ -388,7 +389,7 @@ final class TcaSchemaFactoryTest extends UnitTestCase } #[Test] - public function subtypesInfoIsMergedWithMainSchemaInformation(): void + public function recordTypesInfoIsMergedWithMainSchemaInformation(): void { $cacheMock = $this->createMock(PhpFrontend::class); $cacheMock->method('has')->with(self::isType('string'))->willReturn(false); @@ -424,4 +425,103 @@ final class TcaSchemaFactoryTest extends UnitTestCase self::assertSame('typeSpecificRenderer', $subSchema->getRawConfiguration()['previewRenderer']); } + + public static function subtypesConfigurationIsAppliedToSubSchemaDataProvider(): iterable + { + yield 'No changes in subtype' => [ + '', + '', + ['type', 'list_type', 'foo', 'bar'], + ]; + yield 'Add fields' => [ + 'baz', + '', + ['type', 'list_type', 'foo', 'bar', 'baz'], + ]; + yield 'Remove fields' => [ + '', + 'foo', + ['type', 'list_type', 'bar'], + ]; + yield 'Add and remove fields' => [ + 'baz', + 'foo', + ['type', 'list_type', 'bar', 'baz'], + ]; + yield 'Unknown field is not added' => [ + 'unknown', + '', + ['type', 'list_type', 'foo', 'bar'], + ]; + } + + #[DataProvider('subtypesConfigurationIsAppliedToSubSchemaDataProvider')] + #[Test] + public function subtypesConfigurationIsAppliedToSubSchema(string $addList, string $excludeList, array $fields): void + { + $cacheMock = $this->createMock(PhpFrontend::class); + $cacheMock->method('has')->with(self::isType('string'))->willReturn(false); + $subject = new TcaSchemaFactory( + new RelationMapBuilder(), + new FieldTypeFactory(), + '', + $cacheMock + ); + $subject->load([ + 'myTable' => [ + 'ctrl' => [ + 'type' => 'type', + ], + 'columns' => [ + 'type' => [ + 'config' => [ + 'type' => 'select', + 'items' => [ + ['label' => 'list', 'value' => 'list'], + ], + ], + ], + 'list_type' => [ + 'config' => [ + 'type' => 'select', + 'items' => [ + ['label' => 'Blog', 'value' => 'tx_blog_pi1'], + ], + ], + ], + 'foo' => [ + 'config' => [ + 'type' => 'input', + ], + ], + 'bar' => [ + 'config' => [ + 'type' => 'input', + ], + ], + 'baz' => [ + 'config' => [ + 'type' => 'input', + ], + ], + ], + 'types' => [ + 'list' => [ + 'showitem' => 'type,list_type,foo,bar', + 'subtype_value_field' => 'list_type', + 'subtypes_addlist' => [ + 'tx_blog_pi1' => $addList, + ], + 'subtypes_excludelist' => [ + 'tx_blog_pi1' => $excludeList, + ], + ], + ], + ], + ]); + + $schema = $subject->get('myTable.list'); + self::assertTrue($schema->hasSubSchema('tx_blog_pi1')); + self::assertSame($fields, array_values(array_map(static fn(FieldTypeInterface $field) => $field->getName(), iterator_to_array($schema->getSubSchema('tx_blog_pi1')->getFields()->getIterator())))); + } }