1276 lines
62 KiB
PHP
1276 lines
62 KiB
PHP
<?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\Database\Schema;
|
|
|
|
use Doctrine\DBAL\Platforms\SQLitePlatform as DoctrineSQLitePlatform;
|
|
use Doctrine\DBAL\Schema\Table;
|
|
use Doctrine\DBAL\Types\Types;
|
|
use TYPO3\CMS\Core\Database\ConnectionPool;
|
|
use TYPO3\CMS\Core\Database\Schema\Exception\DefaultTcaSchemaTablePositionException;
|
|
use TYPO3\CMS\Core\DataHandling\TableColumnType;
|
|
use TYPO3\CMS\Core\Schema\Capability\TcaSchemaCapability;
|
|
use TYPO3\CMS\Core\Schema\Field\CategoryFieldType;
|
|
use TYPO3\CMS\Core\Schema\Field\CheckboxFieldType;
|
|
use TYPO3\CMS\Core\Schema\Field\ColorFieldType;
|
|
use TYPO3\CMS\Core\Schema\Field\CountryFieldType;
|
|
use TYPO3\CMS\Core\Schema\Field\DateTimeFieldType;
|
|
use TYPO3\CMS\Core\Schema\Field\EmailFieldType;
|
|
use TYPO3\CMS\Core\Schema\Field\FileFieldType;
|
|
use TYPO3\CMS\Core\Schema\Field\FlexFormFieldType;
|
|
use TYPO3\CMS\Core\Schema\Field\FolderFieldType;
|
|
use TYPO3\CMS\Core\Schema\Field\GroupFieldType;
|
|
use TYPO3\CMS\Core\Schema\Field\ImageManipulationFieldType;
|
|
use TYPO3\CMS\Core\Schema\Field\InlineFieldType;
|
|
use TYPO3\CMS\Core\Schema\Field\InputFieldType;
|
|
use TYPO3\CMS\Core\Schema\Field\JsonFieldType;
|
|
use TYPO3\CMS\Core\Schema\Field\LanguageFieldType;
|
|
use TYPO3\CMS\Core\Schema\Field\LinkFieldType;
|
|
use TYPO3\CMS\Core\Schema\Field\NumberFieldType;
|
|
use TYPO3\CMS\Core\Schema\Field\PasswordFieldType;
|
|
use TYPO3\CMS\Core\Schema\Field\RadioFieldType;
|
|
use TYPO3\CMS\Core\Schema\Field\SelectRelationFieldType;
|
|
use TYPO3\CMS\Core\Schema\Field\SlugFieldType;
|
|
use TYPO3\CMS\Core\Schema\Field\StaticSelectFieldType;
|
|
use TYPO3\CMS\Core\Schema\Field\TextFieldType;
|
|
use TYPO3\CMS\Core\Schema\Field\UuidFieldType;
|
|
use TYPO3\CMS\Core\Schema\RelationshipType;
|
|
use TYPO3\CMS\Core\Schema\TcaSchema;
|
|
use TYPO3\CMS\Core\Schema\TcaSchemaFactory;
|
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
|
use TYPO3\CMS\Core\Utility\MathUtility;
|
|
|
|
/**
|
|
* This class is called by the SchemaMigrator after all extension's ext_tables.sql
|
|
* files have been parsed and processed to the doctrine Table/Column/Index objects.
|
|
*
|
|
* Method enrich() goes through all $GLOBALS['TCA'] tables and adds fields like
|
|
* 'uid', 'sorting', 'deleted' and friends if the feature is enabled in TCA and the
|
|
* field has not been defined in ext_tables.sql files.
|
|
*
|
|
* This allows extension developers to leave out the TYPO3 DB management fields
|
|
* and reduce ext_tables.sql of extensions down to the business fields.
|
|
*
|
|
* @internal
|
|
*/
|
|
readonly class DefaultTcaSchema
|
|
{
|
|
public function __construct(
|
|
private ConnectionPool $connectionPool,
|
|
private TcaSchemaFactory $tcaSchemaFactory,
|
|
) {}
|
|
|
|
/**
|
|
* Add fields to $tables array that has been created from ext_tables.sql files.
|
|
* This goes through all tables defined in TCA, looks for 'ctrl' features like
|
|
* "soft delete" ['ctrl']['delete'] and adds the field if it has not been
|
|
* defined in ext_tables.sql, yet.
|
|
*
|
|
* @param array<non-empty-string, Table> $tables
|
|
* @return array<non-empty-string, Table> Modified tables
|
|
*/
|
|
public function enrich(array $tables): array
|
|
{
|
|
// Sanity check to ensure all TCA tables are already defined in the incoming table list.
|
|
// This prevents misuse, calling code needs to ensure there is at least an empty
|
|
// table object (no columns) for all TCA tables.
|
|
$existingTableNames = array_keys($tables);
|
|
foreach ($this->tcaSchemaFactory->all() as $tableName => $schema) {
|
|
if (!in_array($tableName, $existingTableNames, true)) {
|
|
throw new \RuntimeException(
|
|
'Table name ' . $tableName . ' does not exist in incoming table list',
|
|
1696424993
|
|
);
|
|
}
|
|
}
|
|
|
|
$tables = $this->enrichSingleTableFieldsFromTcaCtrl($tables);
|
|
$tables = $this->enrichSingleTableFieldsFromTcaColumns($tables);
|
|
return $this->enrichMmTables($tables);
|
|
}
|
|
|
|
/**
|
|
* Add single fields like uid, sorting and similar, based on tables TCA 'ctrl' settings.
|
|
*
|
|
* @param array<non-empty-string, Table> $tables
|
|
* @return array<non-empty-string, Table>
|
|
*/
|
|
protected function enrichSingleTableFieldsFromTcaCtrl(array $tables): array
|
|
{
|
|
foreach ($this->tcaSchemaFactory->all() as $tableName => $schema) {
|
|
if (!$this->isColumnDefinedForTable($tables, $tableName, 'uid')) {
|
|
$tables[$tableName]->addColumn(
|
|
$this->quote('uid'),
|
|
Types::INTEGER,
|
|
[
|
|
'notnull' => true,
|
|
'unsigned' => true,
|
|
'autoincrement' => true,
|
|
]
|
|
);
|
|
$tables[$tableName]->setPrimaryKey(['uid']);
|
|
}
|
|
|
|
// pid column and prepare parent key if pid is not defined
|
|
$pidColumnAdded = false;
|
|
if (!$this->isColumnDefinedForTable($tables, $tableName, 'pid')) {
|
|
$options = [
|
|
'default' => 0,
|
|
'notnull' => true,
|
|
'unsigned' => true,
|
|
];
|
|
$tables[$tableName]->addColumn($this->quote('pid'), Types::INTEGER, $options);
|
|
$pidColumnAdded = true;
|
|
}
|
|
|
|
// tstamp column
|
|
// not converted to bigint because already unsigned and date before 1970 not needed
|
|
if ($schema->hasCapability(TcaSchemaCapability::UpdatedAt)
|
|
&& !$this->isColumnDefinedForTable($tables, $tableName, $schema->getCapability(TcaSchemaCapability::UpdatedAt)->getFieldName())
|
|
) {
|
|
$tables[$tableName]->addColumn(
|
|
$this->quote($schema->getCapability(TcaSchemaCapability::UpdatedAt)->getFieldName()),
|
|
Types::INTEGER,
|
|
[
|
|
'default' => 0,
|
|
'notnull' => true,
|
|
'unsigned' => true,
|
|
]
|
|
);
|
|
}
|
|
|
|
// crdate column
|
|
if ($schema->hasCapability(TcaSchemaCapability::CreatedAt)
|
|
&& !$this->isColumnDefinedForTable($tables, $tableName, $schema->getCapability(TcaSchemaCapability::CreatedAt)->getFieldName())
|
|
) {
|
|
$tables[$tableName]->addColumn(
|
|
$this->quote($schema->getCapability(TcaSchemaCapability::CreatedAt)->getFieldName()),
|
|
Types::INTEGER,
|
|
[
|
|
'default' => 0,
|
|
'notnull' => true,
|
|
'unsigned' => true,
|
|
]
|
|
);
|
|
}
|
|
|
|
// deleted column - soft delete
|
|
if ($schema->hasCapability(TcaSchemaCapability::SoftDelete)
|
|
&& !$this->isColumnDefinedForTable($tables, $tableName, $schema->getCapability(TcaSchemaCapability::SoftDelete)->getFieldName())
|
|
) {
|
|
$tables[$tableName]->addColumn(
|
|
$this->quote($schema->getCapability(TcaSchemaCapability::SoftDelete)->getFieldName()),
|
|
Types::SMALLINT,
|
|
[
|
|
'default' => 0,
|
|
'notnull' => true,
|
|
'unsigned' => true,
|
|
]
|
|
);
|
|
}
|
|
|
|
// disabled column
|
|
if ($schema->hasCapability(TcaSchemaCapability::RestrictionDisabledField)
|
|
&& !$this->isColumnDefinedForTable($tables, $tableName, $schema->getCapability(TcaSchemaCapability::RestrictionDisabledField)->getFieldName())
|
|
) {
|
|
$tables[$tableName]->addColumn(
|
|
$this->quote($schema->getCapability(TcaSchemaCapability::RestrictionDisabledField)->getFieldName()),
|
|
Types::SMALLINT,
|
|
[
|
|
'default' => 0,
|
|
'notnull' => true,
|
|
'unsigned' => true,
|
|
]
|
|
);
|
|
}
|
|
|
|
// starttime column
|
|
// not converted to bigint because already unsigned and date before 1970 not needed
|
|
if ($schema->hasCapability(TcaSchemaCapability::RestrictionStartTime)
|
|
&& !$this->isColumnDefinedForTable($tables, $tableName, $schema->getCapability(TcaSchemaCapability::RestrictionStartTime)->getFieldName())
|
|
) {
|
|
$tables[$tableName]->addColumn(
|
|
$this->quote($schema->getCapability(TcaSchemaCapability::RestrictionStartTime)->getFieldName()),
|
|
Types::INTEGER,
|
|
[
|
|
'default' => 0,
|
|
'notnull' => true,
|
|
'unsigned' => true,
|
|
]
|
|
);
|
|
}
|
|
|
|
// endtime column
|
|
// not converted to bigint because already unsigned and date before 1970 not needed
|
|
if ($schema->hasCapability(TcaSchemaCapability::RestrictionEndTime)
|
|
&& !$this->isColumnDefinedForTable($tables, $tableName, $schema->getCapability(TcaSchemaCapability::RestrictionEndTime)->getFieldName())
|
|
) {
|
|
$tables[$tableName]->addColumn(
|
|
$this->quote($schema->getCapability(TcaSchemaCapability::RestrictionEndTime)->getFieldName()),
|
|
Types::INTEGER,
|
|
[
|
|
'default' => 0,
|
|
'notnull' => true,
|
|
'unsigned' => true,
|
|
]
|
|
);
|
|
}
|
|
|
|
// fe_group column
|
|
if ($schema->hasCapability(TcaSchemaCapability::RestrictionUserGroup)
|
|
&& !$this->isColumnDefinedForTable($tables, $tableName, $schema->getCapability(TcaSchemaCapability::RestrictionUserGroup)->getFieldName())
|
|
) {
|
|
$tables[$tableName]->addColumn(
|
|
$this->quote($schema->getCapability(TcaSchemaCapability::RestrictionUserGroup)->getFieldName()),
|
|
Types::STRING,
|
|
[
|
|
'default' => '0',
|
|
'notnull' => true,
|
|
'length' => 255,
|
|
]
|
|
);
|
|
}
|
|
|
|
// sorting column
|
|
if ($schema->hasCapability(TcaSchemaCapability::SortByField)
|
|
&& !$this->isColumnDefinedForTable($tables, $tableName, $schema->getCapability(TcaSchemaCapability::SortByField)->getFieldName())
|
|
) {
|
|
$tables[$tableName]->addColumn(
|
|
$this->quote($schema->getCapability(TcaSchemaCapability::SortByField)->getFieldName()),
|
|
Types::INTEGER,
|
|
[
|
|
'default' => 0,
|
|
'notnull' => true,
|
|
'unsigned' => false,
|
|
]
|
|
);
|
|
}
|
|
|
|
// index on pid column and maybe others - only if pid has not been defined via ext_tables.sql before
|
|
if ($pidColumnAdded && !$this->isIndexDefinedForTable($tables, $tableName, 'parent')) {
|
|
$parentIndexFields = ['pid'];
|
|
if ($schema->hasCapability(TcaSchemaCapability::SoftDelete)) {
|
|
$parentIndexFields[] = $schema->getCapability(TcaSchemaCapability::SoftDelete)->getFieldName();
|
|
}
|
|
if ($schema->hasCapability(TcaSchemaCapability::RestrictionDisabledField)) {
|
|
$parentIndexFields[] = $schema->getCapability(TcaSchemaCapability::RestrictionDisabledField)->getFieldName();
|
|
}
|
|
$tables[$tableName]->addIndex($parentIndexFields, 'parent');
|
|
}
|
|
|
|
// description column
|
|
if ($schema->hasCapability(TcaSchemaCapability::InternalDescription)
|
|
&& !$this->isColumnDefinedForTable($tables, $tableName, $schema->getCapability(TcaSchemaCapability::InternalDescription)->getFieldName())
|
|
) {
|
|
$tables[$tableName]->addColumn(
|
|
$this->quote($schema->getCapability(TcaSchemaCapability::InternalDescription)->getFieldName()),
|
|
Types::TEXT,
|
|
[
|
|
'notnull' => false,
|
|
'length' => 65535,
|
|
]
|
|
);
|
|
}
|
|
|
|
// editlock column
|
|
if ($schema->hasCapability(TcaSchemaCapability::EditLock)
|
|
&& !$this->isColumnDefinedForTable($tables, $tableName, $schema->getCapability(TcaSchemaCapability::EditLock)->getFieldName())
|
|
) {
|
|
$tables[$tableName]->addColumn(
|
|
$this->quote($schema->getCapability(TcaSchemaCapability::EditLock)->getFieldName()),
|
|
Types::SMALLINT,
|
|
[
|
|
'default' => 0,
|
|
'notnull' => true,
|
|
'unsigned' => true,
|
|
]
|
|
);
|
|
}
|
|
|
|
// sys_language_uid column
|
|
$languageColumnAdded = false;
|
|
if ($schema->isLanguageAware()
|
|
&& !$this->isColumnDefinedForTable($tables, $tableName, $schema->getCapability(TcaSchemaCapability::Language)->getLanguageField()->getName())
|
|
) {
|
|
$tables[$tableName]->addColumn(
|
|
$this->quote($schema->getCapability(TcaSchemaCapability::Language)->getLanguageField()->getName()),
|
|
Types::INTEGER,
|
|
[
|
|
'default' => 0,
|
|
'notnull' => true,
|
|
'unsigned' => false,
|
|
]
|
|
);
|
|
$languageColumnAdded = true;
|
|
}
|
|
|
|
// language_tag column
|
|
if ($schema->isLanguageAware()
|
|
&& !$this->isColumnDefinedForTable($tables, $tableName, 'language_tag')
|
|
) {
|
|
$tables[$tableName]->addColumn(
|
|
$this->quote('language_tag'),
|
|
Types::STRING,
|
|
[
|
|
'default' => '',
|
|
'notnull' => true,
|
|
'length' => 35,
|
|
]
|
|
);
|
|
}
|
|
|
|
// l10n_parent column
|
|
$translationOriginPointerColumnAdded = false;
|
|
if ($schema->isLanguageAware()
|
|
&& !$this->isColumnDefinedForTable($tables, $tableName, $schema->getCapability(TcaSchemaCapability::Language)->getTranslationOriginPointerField()->getName())
|
|
) {
|
|
$tables[$tableName]->addColumn(
|
|
$this->quote($schema->getCapability(TcaSchemaCapability::Language)->getTranslationOriginPointerField()->getName()),
|
|
Types::INTEGER,
|
|
[
|
|
'default' => 0,
|
|
'notnull' => true,
|
|
'unsigned' => true,
|
|
]
|
|
);
|
|
$translationOriginPointerColumnAdded = true;
|
|
}
|
|
|
|
// Add index for sys_language_uid and l10n_parent
|
|
if ($languageColumnAdded
|
|
&& $translationOriginPointerColumnAdded
|
|
&& !$this->isIndexDefinedForTable($tables, $tableName, 'language_identifier')
|
|
) {
|
|
$tables[$tableName]->addIndex([
|
|
(string)$schema->getCapability(TcaSchemaCapability::Language)->getTranslationOriginPointerField()->getName(),
|
|
(string)$schema->getCapability(TcaSchemaCapability::Language)->getLanguageField()->getName(),
|
|
], 'language_identifier');
|
|
}
|
|
|
|
// l10n_source column
|
|
if ($schema->isLanguageAware()
|
|
&& $schema->getCapability(TcaSchemaCapability::Language)->hasTranslationSourceField()
|
|
&& !$this->isColumnDefinedForTable($tables, $tableName, $schema->getCapability(TcaSchemaCapability::Language)->getTranslationSourceField()->getName())
|
|
) {
|
|
$tables[$tableName]->addColumn(
|
|
$this->quote($schema->getCapability(TcaSchemaCapability::Language)->getTranslationSourceField()->getName()),
|
|
Types::INTEGER,
|
|
[
|
|
'default' => 0,
|
|
'notnull' => true,
|
|
'unsigned' => true,
|
|
]
|
|
);
|
|
$tables[$tableName]->addIndex([$schema->getCapability(TcaSchemaCapability::Language)->getTranslationSourceField()->getName()], 'translation_source');
|
|
}
|
|
|
|
// l10n_state column, this is not defined in TCA, but always added if the table is language-aware
|
|
if ($schema->isLanguageAware()
|
|
&& !$this->isColumnDefinedForTable($tables, $tableName, 'l10n_state')
|
|
) {
|
|
$tables[$tableName]->addColumn(
|
|
$this->quote('l10n_state'),
|
|
Types::TEXT,
|
|
[
|
|
'notnull' => false,
|
|
'length' => 65535,
|
|
]
|
|
);
|
|
}
|
|
|
|
// t3_origuid column
|
|
if ($schema->hasCapability(TcaSchemaCapability::AncestorReferenceField)
|
|
&& !$this->isColumnDefinedForTable($tables, $tableName, $schema->getCapability(TcaSchemaCapability::AncestorReferenceField)->getFieldName())
|
|
) {
|
|
$tables[$tableName]->addColumn(
|
|
$this->quote($schema->getCapability(TcaSchemaCapability::AncestorReferenceField)->getFieldName()),
|
|
Types::INTEGER,
|
|
[
|
|
'default' => 0,
|
|
'notnull' => true,
|
|
'unsigned' => true,
|
|
]
|
|
);
|
|
}
|
|
|
|
// l18n_diffsource column
|
|
if ($schema->isLanguageAware() && $schema->getCapability(TcaSchemaCapability::Language)->hasDiffSourceField()
|
|
&& !$this->isColumnDefinedForTable($tables, $tableName, $schema->getCapability(TcaSchemaCapability::Language)->getDiffSourceField()->getName())
|
|
) {
|
|
$tables[$tableName]->addColumn(
|
|
$this->quote($schema->getCapability(TcaSchemaCapability::Language)->getDiffSourceField()->getName()),
|
|
Types::BLOB,
|
|
[
|
|
// mediumblob (16MB) on mysql
|
|
'length' => 16777215,
|
|
'notnull' => false,
|
|
]
|
|
);
|
|
}
|
|
|
|
// workspaces t3ver_oid column
|
|
if ($schema->isWorkspaceAware()
|
|
&& !$this->isColumnDefinedForTable($tables, $tableName, 't3ver_oid')
|
|
) {
|
|
$tables[$tableName]->addColumn(
|
|
$this->quote('t3ver_oid'),
|
|
Types::INTEGER,
|
|
[
|
|
'default' => 0,
|
|
'notnull' => true,
|
|
'unsigned' => true,
|
|
]
|
|
);
|
|
}
|
|
|
|
// workspaces t3ver_wsid column
|
|
if ($schema->isWorkspaceAware()
|
|
&& !$this->isColumnDefinedForTable($tables, $tableName, 't3ver_wsid')
|
|
) {
|
|
$tables[$tableName]->addColumn(
|
|
$this->quote('t3ver_wsid'),
|
|
Types::INTEGER,
|
|
[
|
|
'default' => 0,
|
|
'notnull' => true,
|
|
'unsigned' => true,
|
|
]
|
|
);
|
|
}
|
|
|
|
// workspaces t3ver_state column
|
|
if ($schema->isWorkspaceAware()
|
|
&& !$this->isColumnDefinedForTable($tables, $tableName, 't3ver_state')
|
|
) {
|
|
$tables[$tableName]->addColumn(
|
|
$this->quote('t3ver_state'),
|
|
Types::SMALLINT,
|
|
[
|
|
'default' => 0,
|
|
'notnull' => true,
|
|
'unsigned' => false,
|
|
]
|
|
);
|
|
}
|
|
|
|
// workspaces t3ver_stage column
|
|
if ($schema->isWorkspaceAware()
|
|
&& !$this->isColumnDefinedForTable($tables, $tableName, 't3ver_stage')
|
|
) {
|
|
$tables[$tableName]->addColumn(
|
|
$this->quote('t3ver_stage'),
|
|
Types::INTEGER,
|
|
[
|
|
'default' => 0,
|
|
'notnull' => true,
|
|
'unsigned' => false,
|
|
]
|
|
);
|
|
}
|
|
|
|
// workspaces index on t3ver_oid and t3ver_wsid fields
|
|
if ($schema->isWorkspaceAware()
|
|
&& !$this->isIndexDefinedForTable($tables, $tableName, 't3ver_oid')
|
|
) {
|
|
$tables[$tableName]->addIndex(['t3ver_oid', 't3ver_wsid'], 't3ver_oid');
|
|
}
|
|
}
|
|
|
|
return $tables;
|
|
}
|
|
|
|
/**
|
|
* Add single fields based on tables TCA 'columns'.
|
|
*
|
|
* @param array<non-empty-string, Table> $tables
|
|
* @return array<non-empty-string, Table>
|
|
*/
|
|
protected function enrichSingleTableFieldsFromTcaColumns(array $tables): array
|
|
{
|
|
foreach ($this->tcaSchemaFactory->all() as $tableName => $schema) {
|
|
/** @var TcaSchema $schema */
|
|
// In the following, columns for TCA fields with a dedicated TCA type are
|
|
// added. In the unlikely case that no columns exist, we can skip the table.
|
|
if ($schema->getFields()->count() === 0) {
|
|
continue;
|
|
}
|
|
$tableConnectionPlatform = $this->connectionPool->getConnectionForTable($tableName)->getDatabasePlatform();
|
|
|
|
foreach ($schema->getFields() as $fieldName => $fieldType) {
|
|
if ($this->isColumnDefinedForTable($tables, $tableName, $fieldName)) {
|
|
continue;
|
|
}
|
|
$fieldTypeConfiguration = $fieldType->getConfiguration();
|
|
switch (true) {
|
|
case $fieldType instanceof CategoryFieldType:
|
|
if ($fieldType->getRelationshipType() === RelationshipType::OneToMany) {
|
|
$tables[$tableName]->addColumn(
|
|
$this->quote($fieldName),
|
|
Types::TEXT,
|
|
[
|
|
'notnull' => false,
|
|
]
|
|
);
|
|
} else {
|
|
$tables[$tableName]->addColumn(
|
|
$this->quote($fieldName),
|
|
Types::INTEGER,
|
|
[
|
|
'default' => 0,
|
|
'notnull' => true,
|
|
'unsigned' => true,
|
|
]
|
|
);
|
|
}
|
|
break;
|
|
|
|
case $fieldType instanceof DateTimeFieldType:
|
|
$dbType = $fieldType->getPersistenceType() ?? '';
|
|
// Add datetime fields for all tables, defining datetime columns (TCA type=datetime), except
|
|
// those columns, which had already been added due to definition in "ctrl", e.g. "starttime".
|
|
if ($dbType) {
|
|
$tables[$tableName]->addColumn(
|
|
$this->quote($fieldName),
|
|
$dbType,
|
|
[
|
|
'notnull' => !$fieldType->isNullable(),
|
|
]
|
|
);
|
|
} else {
|
|
// int unsigned: from 1970 to 2106.
|
|
// int signed: from 1901 to 2038.
|
|
// bigint unsigned/signed: from whenever to whenever
|
|
//
|
|
// Anything like crdate,tstamp,starttime,endtime is good with
|
|
// "int unsigned" and can survive the 2038 apocalypse (until 2106).
|
|
//
|
|
// However, anything that has birthdates or dates
|
|
// from the past (sys_file_metadata.content_creation_date) was saved
|
|
// as a SIGNED INT. It allowed birthdays of people older than 1970,
|
|
// but with the downside that it ends in 2038.
|
|
//
|
|
// This is now changed to utilize BIGINT everywhere, even when smaller
|
|
// date ranges are requested. To reduce complexity, we specifically
|
|
// do not evaluate "range.upper/lower" fields and use a unified type here.
|
|
$tables[$tableName]->addColumn(
|
|
$this->quote($fieldName),
|
|
Types::BIGINT,
|
|
[
|
|
'default' => $fieldType->isNullable() ? null : 0,
|
|
'notnull' => !$fieldType->isNullable(),
|
|
'unsigned' => false,
|
|
]
|
|
);
|
|
}
|
|
break;
|
|
|
|
case $fieldType instanceof SlugFieldType:
|
|
$tables[$tableName]->addColumn(
|
|
$this->quote($fieldName),
|
|
Types::TEXT,
|
|
[
|
|
'length' => 65535,
|
|
'notnull' => false,
|
|
]
|
|
);
|
|
break;
|
|
|
|
case $fieldType instanceof JsonFieldType:
|
|
$tables[$tableName]->addColumn(
|
|
$this->quote($fieldName),
|
|
Types::JSON,
|
|
[
|
|
'notnull' => false,
|
|
]
|
|
);
|
|
break;
|
|
|
|
case $fieldType instanceof UuidFieldType:
|
|
$tables[$tableName]->addColumn(
|
|
$this->quote($fieldName),
|
|
Types::GUID,
|
|
[
|
|
'notnull' => true,
|
|
]
|
|
);
|
|
break;
|
|
|
|
case $fieldType instanceof FileFieldType:
|
|
$tables[$tableName]->addColumn(
|
|
$this->quote($fieldName),
|
|
Types::INTEGER,
|
|
[
|
|
'default' => 0,
|
|
'notnull' => true,
|
|
'unsigned' => true,
|
|
]
|
|
);
|
|
break;
|
|
|
|
case $fieldType instanceof FolderFieldType:
|
|
case $fieldType instanceof ImageManipulationFieldType:
|
|
case $fieldType instanceof FlexFormFieldType:
|
|
case $fieldType instanceof TextFieldType:
|
|
$tables[$tableName]->addColumn(
|
|
$this->quote($fieldName),
|
|
Types::TEXT,
|
|
[
|
|
'notnull' => false,
|
|
]
|
|
);
|
|
break;
|
|
|
|
case $fieldType instanceof EmailFieldType:
|
|
$tables[$tableName]->addColumn(
|
|
$this->quote($fieldName),
|
|
Types::STRING,
|
|
[
|
|
'length' => 255,
|
|
'default' => ($fieldType->isNullable() ? null : ''),
|
|
'notnull' => !$fieldType->isNullable(),
|
|
]
|
|
);
|
|
break;
|
|
|
|
case $fieldType instanceof CheckboxFieldType:
|
|
$tables[$tableName]->addColumn(
|
|
$this->quote($fieldName),
|
|
Types::SMALLINT,
|
|
[
|
|
// Even though CheckboxFieldType::getDefaultValue() returns null, the DB stores "0"
|
|
// as this was like that before, and might have complications, so should be analyzed separately
|
|
'default' => $fieldType->getDefaultValue() ?? 0,
|
|
'notnull' => true,
|
|
'unsigned' => true,
|
|
]
|
|
);
|
|
break;
|
|
|
|
case $fieldType instanceof LanguageFieldType:
|
|
$tables[$tableName]->addColumn(
|
|
$this->quote($fieldName),
|
|
Types::INTEGER,
|
|
[
|
|
'default' => 0,
|
|
'notnull' => true,
|
|
'unsigned' => false,
|
|
]
|
|
);
|
|
break;
|
|
|
|
case $fieldType instanceof GroupFieldType:
|
|
if ($fieldType->getRelationshipType() === RelationshipType::ManyToMany) {
|
|
$tables[$tableName]->addColumn(
|
|
$this->quote($fieldName),
|
|
Types::INTEGER,
|
|
[
|
|
'default' => 0,
|
|
'notnull' => true,
|
|
'unsigned' => true,
|
|
]
|
|
);
|
|
} else {
|
|
$tables[$tableName]->addColumn(
|
|
$this->quote($fieldName),
|
|
Types::TEXT,
|
|
[
|
|
'notnull' => false,
|
|
]
|
|
);
|
|
}
|
|
break;
|
|
|
|
case $fieldType instanceof PasswordFieldType:
|
|
$tables[$tableName]->addColumn(
|
|
$this->quote($fieldName),
|
|
Types::STRING,
|
|
[
|
|
'default' => ($fieldType->isNullable() ? null : ''),
|
|
'notnull' => !$fieldType->isNullable(),
|
|
]
|
|
);
|
|
break;
|
|
|
|
case $fieldType instanceof ColorFieldType:
|
|
$tables[$tableName]->addColumn(
|
|
$this->quote($fieldName),
|
|
Types::STRING,
|
|
[
|
|
'length' => $fieldType->supportsOpacity() ? 9 : 7,
|
|
'default' => ($fieldType->isNullable() ? null : ''),
|
|
'notnull' => !$fieldType->isNullable(),
|
|
]
|
|
);
|
|
break;
|
|
|
|
case $fieldType instanceof RadioFieldType:
|
|
$hasItemsProcFunc = ($fieldTypeConfiguration['itemsProcFunc'] ?? '') !== ''
|
|
|| ($fieldTypeConfiguration['itemsProcessors'] ?? []) !== [];
|
|
$items = $fieldTypeConfiguration['items'] ?? [];
|
|
// With itemsProcFunc we can't be sure, which values are persisted. Use type string.
|
|
if ($hasItemsProcFunc) {
|
|
$tables[$tableName]->addColumn(
|
|
$this->quote($fieldName),
|
|
Types::STRING,
|
|
[
|
|
'length' => 255,
|
|
'default' => '',
|
|
'notnull' => true,
|
|
]
|
|
);
|
|
break;
|
|
}
|
|
// If no items are configured, use type string to be safe for values added directly.
|
|
if ($items === []) {
|
|
$tables[$tableName]->addColumn(
|
|
$this->quote($fieldName),
|
|
Types::STRING,
|
|
[
|
|
'length' => 255,
|
|
'default' => '',
|
|
'notnull' => true,
|
|
]
|
|
);
|
|
break;
|
|
}
|
|
// If only one value is NOT an integer use type string.
|
|
foreach ($items as $item) {
|
|
if (!MathUtility::canBeInterpretedAsInteger($item['value'])) {
|
|
$tables[$tableName]->addColumn(
|
|
$this->quote($fieldName),
|
|
Types::STRING,
|
|
[
|
|
'length' => 255,
|
|
'default' => '',
|
|
'notnull' => true,
|
|
]
|
|
);
|
|
// continue with next $tableDefinition['columns']
|
|
// see: DefaultTcaSchemaTest->enrichAddsRadioStringVerifyThatCorrectLoopIsContinued()
|
|
break 2;
|
|
}
|
|
}
|
|
// Use integer type.
|
|
$allValues = array_map(fn(array $item): int => (int)$item['value'], $items);
|
|
$minValue = min($allValues);
|
|
$maxValue = max($allValues);
|
|
// Try to safe some bytes - can be reconsidered to simply use Types::INTEGER.
|
|
$integerType = ($minValue >= -32768 && $maxValue < 32768)
|
|
? Types::SMALLINT
|
|
: Types::INTEGER;
|
|
$tables[$tableName]->addColumn(
|
|
$this->quote($fieldName),
|
|
$integerType,
|
|
[
|
|
'default' => 0,
|
|
'notnull' => true,
|
|
]
|
|
);
|
|
break;
|
|
|
|
case $fieldType instanceof LinkFieldType:
|
|
$tables[$tableName]->addColumn(
|
|
$this->quote($fieldName),
|
|
Types::TEXT,
|
|
[
|
|
'length' => 65535,
|
|
'default' => $fieldType->isNullable() ? null : '',
|
|
'notnull' => !$fieldType->isNullable(),
|
|
]
|
|
);
|
|
break;
|
|
|
|
case $fieldType instanceof InputFieldType:
|
|
$length = (int)($fieldTypeConfiguration['max'] ?? 255);
|
|
if ($length > 255) {
|
|
$tables[$tableName]->addColumn(
|
|
$this->quote($fieldName),
|
|
Types::TEXT,
|
|
[
|
|
'length' => 65535,
|
|
'default' => $fieldType->isNullable() ? null : '',
|
|
'notnull' => !$fieldType->isNullable(),
|
|
]
|
|
);
|
|
break;
|
|
}
|
|
$tables[$tableName]->addColumn(
|
|
$this->quote($fieldName),
|
|
Types::STRING,
|
|
[
|
|
'length' => $length,
|
|
'default' => $fieldType->isNullable() ? null : '',
|
|
'notnull' => !$fieldType->isNullable(),
|
|
]
|
|
);
|
|
break;
|
|
|
|
case $fieldType instanceof InlineFieldType:
|
|
// Must be MM or foreign_field
|
|
if (in_array($fieldType->getRelationshipType(), [RelationshipType::OneToOne, RelationshipType::ManyToMany, RelationshipType::OneToMany], true)
|
|
|| ($fieldType->getRelationshipType() === RelationshipType::ManyToOne && ($fieldTypeConfiguration['foreign_field'] ?? '') !== '')
|
|
) {
|
|
// Parent "count" field
|
|
$tables[$tableName]->addColumn(
|
|
$this->quote($fieldName),
|
|
Types::INTEGER,
|
|
[
|
|
'default' => 0,
|
|
'notnull' => true,
|
|
'unsigned' => true,
|
|
]
|
|
);
|
|
} else {
|
|
// Inline "csv"
|
|
$tables[$tableName]->addColumn(
|
|
$this->quote($fieldName),
|
|
Types::STRING,
|
|
[
|
|
'default' => '',
|
|
'notnull' => true,
|
|
'length' => 255,
|
|
]
|
|
);
|
|
}
|
|
if (($fieldTypeConfiguration['foreign_field'] ?? '') !== '') {
|
|
// Add definition for "foreign_field" (contains parent uid) in the child table if it is not defined
|
|
// in child TCA, or if it is "just" a "passthrough" field, and not manually configured in ext_tables.sql
|
|
$childTable = $fieldTypeConfiguration['foreign_table'];
|
|
if (!(($tables[$childTable] ?? null) instanceof Table)) {
|
|
throw new DefaultTcaSchemaTablePositionException('Table ' . $childTable . ' not found in schema list', 1527854474);
|
|
}
|
|
$childTableForeignFieldName = $fieldTypeConfiguration['foreign_field'];
|
|
if ($this->tcaSchemaFactory->has($childTable)) {
|
|
$childSchema = $this->tcaSchemaFactory->get($childTable);
|
|
if ((!$childSchema->hasField($childTableForeignFieldName) || $childSchema->getField($childTableForeignFieldName)->isType(TableColumnType::PASSTHROUGH))
|
|
&& !$this->isColumnDefinedForTable($tables, $childTable, $childTableForeignFieldName)
|
|
) {
|
|
$tables[$childTable]->addColumn(
|
|
$this->quote($childTableForeignFieldName),
|
|
Types::INTEGER,
|
|
[
|
|
'default' => 0,
|
|
'notnull' => true,
|
|
'unsigned' => true,
|
|
]
|
|
);
|
|
}
|
|
// Add definition for "foreign_table_field" (contains name of parent table) in the child table if it is not
|
|
// defined in child TCA or if it is "just" a "passthrough" field, and not manually configured in ext_tables.sql
|
|
$childTableForeignTableFieldName = $fieldTypeConfiguration['foreign_table_field'] ?? '';
|
|
if ($childTableForeignTableFieldName !== ''
|
|
&& (!$childSchema->hasField($childTableForeignTableFieldName) || $childSchema->getField($childTableForeignTableFieldName)->isType(TableColumnType::PASSTHROUGH))
|
|
&& !$this->isColumnDefinedForTable($tables, $childTable, $childTableForeignTableFieldName)
|
|
) {
|
|
$tables[$childTable]->addColumn(
|
|
$this->quote($childTableForeignTableFieldName),
|
|
Types::STRING,
|
|
[
|
|
'default' => '',
|
|
'notnull' => true,
|
|
'length' => 255,
|
|
]
|
|
);
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
|
|
case $fieldType instanceof NumberFieldType:
|
|
$type = $fieldType->getFormat() === 'decimal' ? Types::DECIMAL : Types::INTEGER;
|
|
$lowerRange = $fieldTypeConfiguration['range']['lower'] ?? -1;
|
|
// Integer type for all database platforms.
|
|
if ($type === Types::INTEGER) {
|
|
$tables[$tableName]->addColumn(
|
|
$this->quote($fieldName),
|
|
Types::INTEGER,
|
|
[
|
|
'default' => $fieldType->isNullable() === true ? null : 0,
|
|
'notnull' => !$fieldType->isNullable(),
|
|
'unsigned' => $lowerRange >= 0,
|
|
]
|
|
);
|
|
break;
|
|
}
|
|
// SQLite internally defines NUMERIC() fields as real, and therefore as floating numbers. pdo_sqlite
|
|
// then returns PHP float which can lead to rounding issues. See https://bugs.php.net/bug.php?id=81397
|
|
// for more details. We create a 'string' field on SQLite as workaround.
|
|
// @todo: Database schema should be created with MySQL in mind and not mixed. Transforming to the
|
|
// concrete database platform is handled in the database compare area. Sadly, this is not
|
|
// possible right now but upcoming preparation towards doctrine/dbal 4 makes it possible to
|
|
// move this "hack" to a different place.
|
|
if ($tableConnectionPlatform instanceof DoctrineSQLitePlatform) {
|
|
$tables[$tableName]->addColumn(
|
|
$this->quote($fieldName),
|
|
Types::STRING,
|
|
[
|
|
'default' => $fieldType->isNullable() === true ? null : '0.00',
|
|
'notnull' => !$fieldType->isNullable(),
|
|
'length' => 255,
|
|
]
|
|
);
|
|
break;
|
|
}
|
|
// Decimal for all supported platforms except SQLite
|
|
$tables[$tableName]->addColumn(
|
|
$this->quote($fieldName),
|
|
Types::DECIMAL,
|
|
[
|
|
'default' => $fieldType->isNullable() === true ? null : 0.00,
|
|
'notnull' => !$fieldType->isNullable(),
|
|
'unsigned' => $lowerRange >= 0,
|
|
'precision' => 10,
|
|
'scale' => 2,
|
|
]
|
|
);
|
|
break;
|
|
|
|
case $fieldType instanceof SelectRelationFieldType || $fieldType instanceof StaticSelectFieldType:
|
|
if (($fieldTypeConfiguration['MM'] ?? '') !== '') {
|
|
// MM relation, this is a "parent count" field. Have an int.
|
|
$tables[$tableName]->addColumn(
|
|
$this->quote($fieldName),
|
|
Types::INTEGER,
|
|
[
|
|
'notnull' => true,
|
|
'default' => 0,
|
|
'unsigned' => true,
|
|
]
|
|
);
|
|
break;
|
|
}
|
|
$dbFieldLength = (int)($fieldTypeConfiguration['dbFieldLength'] ?? 0);
|
|
// If itemsProcFunc is not set, check the item values
|
|
if (
|
|
($fieldTypeConfiguration['itemsProcFunc'] ?? '') === ''
|
|
|| ($fieldTypeConfiguration['itemsProcessors'] ?? []) !== []
|
|
) {
|
|
$items = $fieldTypeConfiguration['items'] ?? [];
|
|
$itemsContainsOnlyIntegers = true;
|
|
$itemsContainNull = false;
|
|
foreach ($items as $item) {
|
|
// Null values are valid for integer columns (stored as database NULL)
|
|
if ($item['value'] === null) {
|
|
$itemsContainNull = true;
|
|
continue;
|
|
}
|
|
if (!MathUtility::canBeInterpretedAsInteger($item['value'])) {
|
|
$itemsContainsOnlyIntegers = false;
|
|
break;
|
|
}
|
|
}
|
|
$itemsAreAllPositive = true;
|
|
foreach ($items as $item) {
|
|
// Skip null values for positive check (null is neither positive nor negative)
|
|
if ($item['value'] === null) {
|
|
continue;
|
|
}
|
|
if ($item['value'] < 0) {
|
|
$itemsAreAllPositive = false;
|
|
break;
|
|
}
|
|
}
|
|
// @todo: The dependency to renderType is unfortunate here. It's only purpose is to potentially have int fields
|
|
// instead of string when this is a 'single' relation / value. However, renderType should usually not
|
|
// influence DB layer at all. Maybe 'selectSingle' should be changed to an own 'type' instead to make
|
|
// this more explicit. Maybe DataHandler could benefit from this as well?
|
|
if (($fieldTypeConfiguration['renderType'] ?? '') === 'selectSingle' || ($fieldTypeConfiguration['maxitems'] ?? 0) === 1) {
|
|
// With 'selectSingle' or with 'maxitems = 1', only a single value can be selected.
|
|
if (
|
|
!is_array($fieldTypeConfiguration['fileFolderConfig'] ?? false)
|
|
&& ($items !== [] || ($fieldTypeConfiguration['foreign_table'] ?? '') !== '')
|
|
&& $itemsContainsOnlyIntegers === true
|
|
) {
|
|
// If the item list is empty, or if it contains only int values, an int field is enough.
|
|
// Also, the config must not be a 'fileFolderConfig' field which takes string values.
|
|
// When items contain a null value, allow NULL in the database column.
|
|
$defaultValue = $fieldType->getDefaultValue();
|
|
$tables[$tableName]->addColumn(
|
|
$this->quote($fieldName),
|
|
Types::INTEGER,
|
|
[
|
|
'notnull' => !$itemsContainNull,
|
|
'default' => $itemsContainNull && $defaultValue === null ? null : (int)($defaultValue ?? 0),
|
|
'unsigned' => $itemsAreAllPositive,
|
|
]
|
|
);
|
|
break;
|
|
}
|
|
// If int is no option, have a string field.
|
|
// When items contain a null value, allow NULL in the database column.
|
|
$defaultValue = $fieldType->getDefaultValue();
|
|
$tables[$tableName]->addColumn(
|
|
$this->quote($fieldName),
|
|
Types::STRING,
|
|
[
|
|
'notnull' => !$itemsContainNull,
|
|
'default' => $itemsContainNull && $defaultValue === null ? null : (string)($defaultValue ?? ''),
|
|
'length' => $dbFieldLength > 0 ? $dbFieldLength : 255,
|
|
]
|
|
);
|
|
break;
|
|
}
|
|
if ($itemsContainsOnlyIntegers) {
|
|
// Multiple values can be selected and will be stored comma separated. When manual item values are
|
|
// all integers, or if there is a foreign_table, we end up with a comma separated list of integers.
|
|
// Using string / varchar 255 here should be long enough to store plenty of values, and can be
|
|
// changed by setting 'dbFieldLength'.
|
|
$tables[$tableName]->addColumn(
|
|
$this->quote($fieldName),
|
|
Types::STRING,
|
|
[
|
|
// @todo: nullable = true is not a good default here. This stems from the fact that this
|
|
// if triggers a lot of TEXT->VARCHAR() field changes during upgrade, where TEXT
|
|
// is always nullable, but varchar() is not. As such, we for now declare this
|
|
// nullable, but could have a look at it later again when a value upgrade
|
|
// for such cases is in place that updates existing null fields to empty string.
|
|
'notnull' => false,
|
|
'default' => (string)($fieldType->getDefaultValue() ?? ''),
|
|
'length' => $dbFieldLength > 0 ? $dbFieldLength : 255,
|
|
]
|
|
);
|
|
break;
|
|
}
|
|
}
|
|
if ($dbFieldLength > 0) {
|
|
// If nothing else matches, but there is a dbFieldLength set, have varchar with that length.
|
|
$tables[$tableName]->addColumn(
|
|
$this->quote($fieldName),
|
|
Types::STRING,
|
|
[
|
|
'notnull' => true,
|
|
'default' => (string)($fieldType->getDefaultValue() ?? ''),
|
|
'length' => $dbFieldLength,
|
|
]
|
|
);
|
|
} else {
|
|
// Final fallback creates a (nullable) text field.
|
|
$tables[$tableName]->addColumn(
|
|
$this->quote($fieldName),
|
|
Types::TEXT,
|
|
[
|
|
'notnull' => false,
|
|
]
|
|
);
|
|
}
|
|
break;
|
|
case $fieldType instanceof CountryFieldType:
|
|
$tables[$tableName]->addColumn(
|
|
$this->quote($fieldName),
|
|
Types::STRING,
|
|
[
|
|
'length' => 16, // Even though ISO2 is stored by default, custom additional items may need some (limited) storage
|
|
'notnull' => false,
|
|
]
|
|
);
|
|
break;
|
|
|
|
}
|
|
}
|
|
}
|
|
|
|
return $tables;
|
|
}
|
|
|
|
/**
|
|
* Find table fields that configure a "true" MM relation and define the
|
|
* according mm table schema for them. True MM tables are intermediate tables
|
|
* that have NO TCA itself. Those are indicated by type=select and type=group
|
|
* and type=inline fields with MM property.
|
|
*
|
|
* @param array<non-empty-string, Table> $tables
|
|
* @return array<non-empty-string, Table>
|
|
*/
|
|
protected function enrichMmTables(array $tables): array
|
|
{
|
|
foreach ($this->tcaSchemaFactory->all() as $schema) {
|
|
foreach ($schema->getFields() as $field) {
|
|
// Broken TCA or not of expected type, or no MM, or foreign side
|
|
if (!$field->isType(TableColumnType::SELECT, TableColumnType::GROUP, TableColumnType::INLINE, TableColumnType::CATEGORY)) {
|
|
continue;
|
|
}
|
|
$fieldConfiguration = $field->getConfiguration();
|
|
if (!is_string($fieldConfiguration['MM'] ?? false)
|
|
// Consider this mm only if looking at it from the local side
|
|
|| ($fieldConfiguration['MM_opposite_field'] ?? false)
|
|
) {
|
|
continue;
|
|
}
|
|
$mmTableName = $fieldConfiguration['MM'];
|
|
if (!array_key_exists($mmTableName, $tables)) {
|
|
// If the mm table is defined, work with it. Else add at and.
|
|
$tables[$mmTableName] = GeneralUtility::makeInstance(
|
|
Table::class,
|
|
$mmTableName
|
|
);
|
|
}
|
|
|
|
// Add 'uid' field with primary key if multiple is set: 'multiple' allows using a left or right
|
|
// side more than once in a relation which would lead to duplicate primary key entries. To
|
|
// avoid this, we add a uid column and make it primary key instead.
|
|
$needsUid = (bool)($fieldConfiguration['multiple'] ?? false);
|
|
if ($needsUid && !$this->isColumnDefinedForTable($tables, $mmTableName, 'uid')) {
|
|
$tables[$mmTableName]->addColumn(
|
|
$this->quote('uid'),
|
|
Types::INTEGER,
|
|
[
|
|
'notnull' => true,
|
|
'unsigned' => true,
|
|
'autoincrement' => true,
|
|
]
|
|
);
|
|
$tables[$mmTableName]->setPrimaryKey(['uid']);
|
|
}
|
|
|
|
if (!$this->isColumnDefinedForTable($tables, $mmTableName, 'uid_local')) {
|
|
$tables[$mmTableName]->addColumn(
|
|
$this->quote('uid_local'),
|
|
Types::INTEGER,
|
|
[
|
|
'default' => 0,
|
|
'notnull' => true,
|
|
'unsigned' => true,
|
|
]
|
|
);
|
|
}
|
|
// Without "multiple", the primary key set below starts with "uid_local" and already
|
|
// indexes it, so a dedicated single-column index would be a redundant prefix. With
|
|
// "multiple" the primary key is the "uid" field instead, so a "uid_local" index is
|
|
// kept to serve relation lookups by the local side.
|
|
if ($needsUid && !$this->isIndexDefinedForTable($tables, $mmTableName, 'uid_local')) {
|
|
$tables[$mmTableName]->addIndex(['uid_local'], 'uid_local');
|
|
}
|
|
|
|
if (!$this->isColumnDefinedForTable($tables, $mmTableName, 'uid_foreign')) {
|
|
$tables[$mmTableName]->addColumn(
|
|
$this->quote('uid_foreign'),
|
|
Types::INTEGER,
|
|
[
|
|
'default' => 0,
|
|
'notnull' => true,
|
|
'unsigned' => true,
|
|
]
|
|
);
|
|
}
|
|
if (!$this->isIndexDefinedForTable($tables, $mmTableName, 'uid_foreign')) {
|
|
$tables[$mmTableName]->addIndex(['uid_foreign'], 'uid_foreign');
|
|
}
|
|
|
|
if (!$this->isColumnDefinedForTable($tables, $mmTableName, 'sorting')) {
|
|
$tables[$mmTableName]->addColumn(
|
|
$this->quote('sorting'),
|
|
Types::INTEGER,
|
|
[
|
|
'default' => 0,
|
|
'notnull' => true,
|
|
'unsigned' => true,
|
|
]
|
|
);
|
|
}
|
|
if (!$this->isColumnDefinedForTable($tables, $mmTableName, 'sorting_foreign')) {
|
|
$tables[$mmTableName]->addColumn(
|
|
$this->quote('sorting_foreign'),
|
|
Types::INTEGER,
|
|
[
|
|
'default' => 0,
|
|
'notnull' => true,
|
|
'unsigned' => true,
|
|
]
|
|
);
|
|
}
|
|
|
|
$hasTablenamesFieldname = false;
|
|
if ( // Local side of MM with MM_oppositeUsage forces tablenames and fieldname
|
|
!empty($fieldConfiguration['MM_oppositeUsage'])
|
|
|| (
|
|
// MM group with allowed more than one table forces tablenames and fieldname
|
|
$field->isType(TableColumnType::GROUP) && !empty($fieldConfiguration['allowed'])
|
|
&& (
|
|
count(GeneralUtility::trimExplode(',', $fieldConfiguration['allowed'])) > 1
|
|
|| $fieldConfiguration['allowed'] === '*'
|
|
)
|
|
)
|
|
) {
|
|
$hasTablenamesFieldname = true;
|
|
// This local table can be the target of multiple foreign tables and table fields. The mm table
|
|
// thus needs two further fields to specify which foreign/table field combination links is used.
|
|
// Those are stored in two additional fields called "tablenames" and "fieldname".
|
|
if (!$this->isColumnDefinedForTable($tables, $mmTableName, 'tablenames')) {
|
|
$tables[$mmTableName]->addColumn(
|
|
$this->quote('tablenames'),
|
|
Types::STRING,
|
|
[
|
|
'default' => '',
|
|
'length' => 64,
|
|
'notnull' => true,
|
|
]
|
|
);
|
|
}
|
|
if (!$this->isColumnDefinedForTable($tables, $mmTableName, 'fieldname')) {
|
|
$tables[$mmTableName]->addColumn(
|
|
$this->quote('fieldname'),
|
|
Types::STRING,
|
|
[
|
|
'default' => '',
|
|
'length' => 64,
|
|
'notnull' => true,
|
|
]
|
|
);
|
|
}
|
|
}
|
|
|
|
// Primary key handling: If there is a uid field, PK has been added above already.
|
|
// Otherwise, the PK combination is either "uid_local, uid_foreign", or
|
|
// "uid_local, uid_foreign, tablenames, fieldname" if this is a multi-foreign setup.
|
|
if (!$needsUid && $tables[$mmTableName]->getPrimaryKey() === null && $hasTablenamesFieldname) {
|
|
$tables[$mmTableName]->setPrimaryKey(['uid_local', 'uid_foreign', 'tablenames', 'fieldname']);
|
|
} elseif (!$needsUid && $tables[$mmTableName]->getPrimaryKey() === null) {
|
|
$tables[$mmTableName]->setPrimaryKey(['uid_local', 'uid_foreign']);
|
|
}
|
|
}
|
|
}
|
|
return $tables;
|
|
}
|
|
|
|
/**
|
|
* True if a column with a given name is defined within the incoming
|
|
* array of Table's.
|
|
*
|
|
* @param array<non-empty-string, Table> $tables
|
|
*/
|
|
protected function isColumnDefinedForTable(array $tables, string $tableName, string $fieldName): bool
|
|
{
|
|
return ($tables[$tableName] ?? null)?->hasColumn($fieldName) ?? false;
|
|
}
|
|
|
|
/**
|
|
* True if an index with a given name is defined within the incoming
|
|
* array of Table's.
|
|
*
|
|
* @param array<non-empty-string, Table> $tables
|
|
*/
|
|
protected function isIndexDefinedForTable(array $tables, string $tableName, string $indexName): bool
|
|
{
|
|
return ($tables[$tableName] ?? null)?->hasIndex($indexName) ?? false;
|
|
}
|
|
|
|
protected function quote(string $identifier): string
|
|
{
|
|
return '`' . $identifier . '`';
|
|
}
|
|
}
|