Skip to content

Commit

Permalink
[WIP][FEATURE] Content Type: FileType
Browse files Browse the repository at this point in the history
The table sys_file_reference of TYPO3 is special. It has a type field,
which references a value of another table "sys_file". As these values
are fixed, it is not possible to extend it with custom types. This
means the only possibility is to override the types "showitem" section.

Some adjustments on various places are needed to make this work:
- FileField is treated as if it has no typeField
- An exception is made to override the types section
- Append the hidden palette filePalette

todo: Think about a clever syntax for local overrides with
overrideChildTca.

Fixes: #76
  • Loading branch information
nhovratov committed Dec 4, 2024
1 parent 0b23feb commit d146927
Show file tree
Hide file tree
Showing 9 changed files with 112 additions and 26 deletions.
2 changes: 1 addition & 1 deletion Classes/Command/ListContentBlocksCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,8 @@ protected function getAvailableContentBlocks(): array
$list = [];
foreach ($this->contentBlockRegistry->getAll() as $loadedContentBlock) {
$table = match ($loadedContentBlock->getContentType()) {
ContentType::CONTENT_ELEMENT, ContentType::PAGE_TYPE => $loadedContentBlock->getContentType()->getTable(),
ContentType::RECORD_TYPE => $loadedContentBlock->getYaml()['table'],
default => $loadedContentBlock->getContentType()->getTable(),
};
$typeName = $loadedContentBlock->getYaml()['typeName'];
$list[] = [
Expand Down
9 changes: 7 additions & 2 deletions Classes/Definition/ContentType/ContentType.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,15 @@ enum ContentType
{
case CONTENT_ELEMENT;
case PAGE_TYPE;
case FILE_TYPE;
case RECORD_TYPE;

public function getTable(): ?string
{
return match ($this) {
self::CONTENT_ELEMENT => 'tt_content',
self::PAGE_TYPE => 'pages',
self::FILE_TYPE => 'sys_file_reference',
self::RECORD_TYPE => null,
};
}
Expand All @@ -40,7 +42,7 @@ public function getTypeField(): ?string
return match ($this) {
self::CONTENT_ELEMENT => 'CType',
self::PAGE_TYPE => 'doktype',
self::RECORD_TYPE => null,
self::FILE_TYPE, self::RECORD_TYPE => null,
};
}

Expand All @@ -49,6 +51,7 @@ public static function getByTable(string $table): self
return match ($table) {
'tt_content' => self::CONTENT_ELEMENT,
'pages' => self::PAGE_TYPE,
'sys_file_reference' => self::FILE_TYPE,
default => self::RECORD_TYPE,
};
}
Expand All @@ -58,6 +61,7 @@ public function getHumanReadable(): string
return match ($this) {
self::CONTENT_ELEMENT => 'Content Element',
self::PAGE_TYPE => 'Page Type',
self::FILE_TYPE => 'File Type',
self::RECORD_TYPE => 'Record Type',
};
}
Expand All @@ -66,7 +70,7 @@ public function getDefaultGroup(): ?string
{
return match ($this) {
self::CONTENT_ELEMENT, self::PAGE_TYPE => 'default',
self::RECORD_TYPE => null,
self::FILE_TYPE, self::RECORD_TYPE => null,
};
}

Expand All @@ -75,6 +79,7 @@ public function getShortName(): string
return match ($this) {
self::CONTENT_ELEMENT => 'content-element',
self::PAGE_TYPE => 'page-type',
self::FILE_TYPE => 'file-type',
self::RECORD_TYPE => 'record-type',
};
}
Expand Down
4 changes: 3 additions & 1 deletion Classes/Definition/Factory/ContentTypeFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,9 @@ public function create(array $typeDefinition, string $table): ContentTypeInterfa
return match ($contentType) {
ContentType::CONTENT_ELEMENT => ContentElementDefinition::createFromArray($typeDefinition, $table),
ContentType::PAGE_TYPE => PageTypeDefinition::createFromArray($typeDefinition, $table),
ContentType::RECORD_TYPE => RecordTypeDefinition::createFromArray($typeDefinition, $table)
// @todo It's not ideal that FileType reuses RecordTypeDefinition.
// @todo It actually only needs showItems, overrideColumns and typeName. Create new interface?
ContentType::FILE_TYPE, ContentType::RECORD_TYPE => RecordTypeDefinition::createFromArray($typeDefinition, $table)
};
}
}
5 changes: 4 additions & 1 deletion Classes/Definition/Factory/Processing/ProcessingInput.php
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,12 @@ private function getTypeFieldNative(SimpleTcaSchemaFactory $simpleTcaSchemaFacto

private function resolveTypeName(): string|int
{
if (array_key_exists('typeName', $this->yaml)) {
return $this->yaml['typeName'];
}
if ($this->typeField === null) {
return '1';
}
return $this->yaml['typeName'];
// @todo typeName is missing, throw exception here?

Check failure on line 91 in Classes/Definition/Factory/Processing/ProcessingInput.php

View workflow job for this annotation

GitHub Actions / PHPStan TYPO3 v13 PHP 8.2

Method TYPO3\CMS\ContentBlocks\Definition\Factory\Processing\ProcessingInput::resolveTypeName() should return int|string but return statement is missing.

Check failure on line 91 in Classes/Definition/Factory/Processing/ProcessingInput.php

View workflow job for this annotation

GitHub Actions / PHPStan TYPO3 v13 PHP 8.2

Method TYPO3\CMS\ContentBlocks\Definition\Factory\Processing\ProcessingInput::resolveTypeName() should return int|string but return statement is missing.
}
}
60 changes: 47 additions & 13 deletions Classes/Generator/TcaGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
use TYPO3\CMS\ContentBlocks\Definition\ContentType\ContentType;
use TYPO3\CMS\ContentBlocks\Definition\ContentType\ContentTypeInterface;
use TYPO3\CMS\ContentBlocks\Definition\ContentType\PageTypeDefinition;
use TYPO3\CMS\ContentBlocks\Definition\ContentType\RecordTypeDefinition;
use TYPO3\CMS\ContentBlocks\Definition\PaletteDefinition;
use TYPO3\CMS\ContentBlocks\Definition\TableDefinition;
use TYPO3\CMS\ContentBlocks\Definition\TableDefinitionCollection;
Expand Down Expand Up @@ -151,7 +150,7 @@ protected function generateTableTca(TableDefinition $tableDefinition, array $bas
}
$fieldType->setDataStructure($dataStructure);
}
if ($tableDefinition->hasTypeField()) {
if ($tableDefinition->hasTypeField() || $tableDefinition->getContentType() === ContentType::FILE_TYPE) {
$tca['columns'][$column->getUniqueIdentifier()] = $this->getColumnTcaForTableWithTypeField($tableDefinition, $column, $baseTca);
// Ensure label exists for the standard column definition. This is used e.g. in the List module.
if (!$column->useExistingField()) {
Expand Down Expand Up @@ -295,20 +294,23 @@ protected function fillTypeFieldSelectItems(): void
protected function processTypeDefinition(ContentTypeInterface $typeDefinition, TableDefinition $tableDefinition): array
{
$columnsOverrides = $this->getColumnsOverrides($typeDefinition, $tableDefinition);
$tca = match ($typeDefinition::class) {
ContentElementDefinition::class => $this->processContentElement($typeDefinition, $columnsOverrides),
PageTypeDefinition::class => $this->processPageType($typeDefinition, $columnsOverrides),
RecordTypeDefinition::class => $this->processRecordType($typeDefinition, $columnsOverrides, $tableDefinition),
default => throw new \InvalidArgumentException(
'Unsupported type definition: ' . $typeDefinition::class,
1714050376
),
$tca = match ($tableDefinition->getContentType()) {
ContentType::CONTENT_ELEMENT => $this->processContentElement($typeDefinition, $columnsOverrides),
ContentType::PAGE_TYPE => $this->processPageType($typeDefinition, $columnsOverrides),
ContentType::FILE_TYPE => $this->processFileType($typeDefinition, $columnsOverrides),
ContentType::RECORD_TYPE => $this->processRecordType($typeDefinition, $columnsOverrides, $tableDefinition),
};
return $tca;
}

protected function processContentElement(ContentElementDefinition $typeDefinition, array $columnsOverrides): array
protected function processContentElement(ContentTypeInterface $typeDefinition, array $columnsOverrides): array
{
if (!$typeDefinition instanceof ContentElementDefinition) {
throw new \InvalidArgumentException(
'Expected ContentElementDefinition, got ' . get_class($typeDefinition),
1733344806
);
}
$typeDefinitionArray = [
'previewRenderer' => PreviewRenderer::class,
'showitem' => $this->getContentElementStandardShowItem($typeDefinition),
Expand All @@ -322,7 +324,7 @@ protected function processContentElement(ContentElementDefinition $typeDefinitio
return $typeDefinitionArray;
}

protected function processPageType(PageTypeDefinition $typeDefinition, array $columnsOverrides): array
protected function processPageType(ContentTypeInterface $typeDefinition, array $columnsOverrides): array
{
$typeDefinitionArray = [
'showitem' => $this->getPageTypeStandardShowItem($typeDefinition),
Expand All @@ -333,7 +335,18 @@ protected function processPageType(PageTypeDefinition $typeDefinition, array $co
return $typeDefinitionArray;
}

protected function processRecordType(RecordTypeDefinition $typeDefinition, array $columnsOverrides, TableDefinition $tableDefinition): array
protected function processFileType(ContentTypeInterface $typeDefinition, array $columnsOverrides): array
{
$typeDefinitionArray = [
'showitem' => $this->getFileTypeStandardShowItem($typeDefinition),
];
if ($columnsOverrides !== []) {
$typeDefinitionArray['columnsOverrides'] = $columnsOverrides;
}
return $typeDefinitionArray;
}

protected function processRecordType(ContentTypeInterface $typeDefinition, array $columnsOverrides, TableDefinition $tableDefinition): array
{
$typeDefinitionArray = [
'showitem' => $this->getRecordTypeStandardShowItem($typeDefinition, $tableDefinition),
Expand Down Expand Up @@ -751,6 +764,27 @@ protected function getRecordTypeStandardShowItem(ContentTypeInterface $typeDefin
return $showItem;
}

protected function getFileTypeStandardShowItem(ContentTypeInterface $typeDefinition): string
{
$showItemArray = $typeDefinition->getShowItems();
$firstItemIsTab = ($showItemArray[0] ?? null) instanceof TabDefinition;
$generalTab = '--div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:general';
if ($firstItemIsTab) {
$tabDefinition = array_shift($showItemArray);
$generalTab = $this->processShowItem([$tabDefinition]);
}
$showItem = $this->processShowItem($showItemArray);
$parts = [];
$parts[] = $generalTab;
if ($showItem !== '') {
$parts[] = $showItem;
}
// Add hidden palette with system fields and uid_local.
$parts[] = '--palette--;;filePalette';
$showItem = implode(',', $parts);
return $showItem;
}

protected function buildSystemFields(SystemFieldPalettesInterface $capability): array
{
$parts = [];
Expand Down
21 changes: 14 additions & 7 deletions Classes/Loader/ContentBlockLoader.php
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,10 @@ public function loadUncached(): ContentBlockRegistry
if (is_dir($recordTypesFolder)) {
$loadedContentBlocks[] = $this->loadContentBlocksInExtension($recordTypesFolder, $extensionKey, ContentType::RECORD_TYPE);
}
$fileTypesFolder = $package->getPackagePath() . ContentBlockPathUtility::getRelativeFileTypesPath();
if (is_dir($fileTypesFolder)) {
$loadedContentBlocks[] = $this->loadContentBlocksInExtension($fileTypesFolder, $extensionKey, ContentType::FILE_TYPE);
}
}
$loadedContentBlocks = array_merge([], ...$loadedContentBlocks);
$sortByPriority = fn(LoadedContentBlock $a, LoadedContentBlock $b): int => (int)($b->getYaml()['priority'] ?? 0) <=> (int)($a->getYaml()['priority'] ?? 0);
Expand Down Expand Up @@ -146,7 +150,7 @@ protected function loadContentBlocksInExtension(string $path, string $extensionK
$absoluteContentBlockPath = $splFileInfo->getPathname();
$contentBlockFolderName = $splFileInfo->getRelativePathname();
$contentBlockExtPath = ContentBlockPathUtility::getContentBlockExtPath($extensionKey, $contentBlockFolderName, $contentType);
$configYaml = $this->parseConfigYaml($absoluteContentBlockPath, $contentBlockExtPath, $contentType);
$configYaml = $this->parseConfigYaml($absoluteContentBlockPath, $contentType);
if ($configYaml === null) {
continue;
}
Expand All @@ -162,7 +166,7 @@ protected function loadContentBlocksInExtension(string $path, string $extensionK
return $result;
}

protected function parseConfigYaml(string $absoluteContentBlockPath, string $contentBlockExtPath, ContentType $contentType): ?array
protected function parseConfigYaml(string $absoluteContentBlockPath, ContentType $contentType): ?array
{
$contentBlockDefinitionFileName = ContentBlockPathUtility::getContentBlockDefinitionFileName();
$yamlPath = $absoluteContentBlockPath . '/' . $contentBlockDefinitionFileName;
Expand Down Expand Up @@ -212,14 +216,17 @@ protected function loadSingleContentBlock(
if (!file_exists($absolutePath)) {
throw new \RuntimeException('Content Block "' . $name . '" could not be found in "' . $absolutePath . '".', 1678699637);
}
// Override table and typeField for Content Elements and Page Types.
if ($contentType === ContentType::CONTENT_ELEMENT || $contentType === ContentType::PAGE_TYPE) {
// Hard override table.
if ($contentType->getTable() !== null) {
$yaml['table'] = $contentType->getTable();
}
// Hard override type field.
if ($contentType->getTypeField() !== null) {
$yaml['typeField'] = $contentType->getTypeField();
}
// Create typeName
$typeName = $yaml['typeName'] ?? UniqueIdentifierCreator::createContentTypeIdentifier($name);
$yaml['typeName'] ??= $typeName;
// Create typeName, if not set.
$yaml['typeName'] ??= UniqueIdentifierCreator::createContentTypeIdentifier($name);
$typeName = $yaml['typeName'];
if (!array_key_exists('table', $yaml)) {
throw new \RuntimeException('Content Block "' . $name . '" does not define required "table".', 1731412650);
}
Expand Down
3 changes: 2 additions & 1 deletion Classes/Service/Icon/ContentTypeIconResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,8 @@ public static function getDefaultContentTypeIcon(ContentType $contentType): stri
$iconPath = match ($contentType) {
ContentType::CONTENT_ELEMENT => 'EXT:content_blocks/Resources/Public/Icons/DefaultContentElementIcon.svg',
ContentType::PAGE_TYPE => 'EXT:content_blocks/Resources/Public/Icons/DefaultPageTypeIcon.svg',
ContentType::RECORD_TYPE => 'EXT:content_blocks/Resources/Public/Icons/DefaultRecordTypeIcon.svg',
// @todo FileType doesn't need an icon, but this is right now required for ContentTypeInterface.
ContentType::FILE_TYPE, ContentType::RECORD_TYPE => 'EXT:content_blocks/Resources/Public/Icons/DefaultRecordTypeIcon.svg',
};
return $iconPath;
}
Expand Down
11 changes: 11 additions & 0 deletions Classes/Utility/ContentBlockPathUtility.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public static function getContentBlockExtPath(string $extensionKey, string $cont
$contentTypeFolder = match ($contentType) {
ContentType::CONTENT_ELEMENT => self::getRelativeContentElementsPath(),
ContentType::PAGE_TYPE => self::getRelativePageTypesPath(),
ContentType::FILE_TYPE => self::getRelativeFileTypesPath(),
ContentType::RECORD_TYPE => self::getRelativeRecordTypesPath(),
};
return 'EXT:' . $extensionKey . '/' . $contentTypeFolder . '/' . $contentBlockFolderName;
Expand Down Expand Up @@ -105,6 +106,11 @@ public static function getRelativeRecordTypesPath(): string
return self::getSubDirectoryName() . '/' . self::getRecordTypesFolder();
}

public static function getRelativeFileTypesPath(): string
{
return self::getSubDirectoryName() . '/' . self::getFileTypesFolder();
}

public static function getSubDirectoryName(): string
{
return 'ContentBlocks';
Expand All @@ -125,6 +131,11 @@ public static function getRecordTypesFolder(): string
return 'RecordTypes';
}

public static function getFileTypesFolder(): string
{
return 'FileTypes';
}

public static function getAssetsFolder(): string
{
return 'assets';
Expand Down
23 changes: 23 additions & 0 deletions ContentBlocks/FileTypes/my-file-reference/config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
name: nikita/my-file-reference
typeName: 2
fields:
- identifier: basic
type: Palette
label: LLL:EXT:core/Resources/Private/Language/locallang_tca.xlf:sys_file_reference.imageoverlayPalette
fields:
- identifier: alternative
useExistingField: true
- identifier: description
useExistingField: true
- type: Linebreak
- identifier: link
useExistingField: true
- identifier: title
useExistingField: true
- type: Linebreak
- identifier: custom_field
type: Text
label: My custom Field
- type: Linebreak
- identifier: crop
useExistingField: true

0 comments on commit d146927

Please sign in to comment.