1<?php 2 3declare(strict_types=1); 4 5/* 6 * This file is part of the TYPO3 CMS project. 7 * 8 * It is free software; you can redistribute it and/or modify it under 9 * the terms of the GNU General Public License, either version 2 10 * of the License, or any later version. 11 * 12 * For the full copyright and license information, please read the 13 * LICENSE.txt file that was distributed with this source code. 14 * 15 * The TYPO3 project - inspiring people to share! 16 */ 17 18namespace TYPO3\CMS\Core\Database\Schema\Parser; 19 20use Doctrine\DBAL\Platforms\AbstractPlatform; 21use Doctrine\DBAL\Platforms\MySqlPlatform; 22use Doctrine\DBAL\Schema\Column; 23use Doctrine\DBAL\Schema\Index; 24use Doctrine\DBAL\Schema\Table; 25use Doctrine\DBAL\Types\Type; 26use Doctrine\DBAL\Types\Types; 27use TYPO3\CMS\Core\Database\ConnectionPool; 28use TYPO3\CMS\Core\Database\Schema\Parser\AST\CreateColumnDefinitionItem; 29use TYPO3\CMS\Core\Database\Schema\Parser\AST\CreateForeignKeyDefinitionItem; 30use TYPO3\CMS\Core\Database\Schema\Parser\AST\CreateIndexDefinitionItem; 31use TYPO3\CMS\Core\Database\Schema\Parser\AST\CreateTableStatement; 32use TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType\AbstractDataType; 33use TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType\BigIntDataType; 34use TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType\BinaryDataType; 35use TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType\BlobDataType; 36use TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType\CharDataType; 37use TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType\DateDataType; 38use TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType\DateTimeDataType; 39use TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType\DecimalDataType; 40use TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType\DoubleDataType; 41use TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType\EnumDataType; 42use TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType\FloatDataType; 43use TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType\IntegerDataType; 44use TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType\JsonDataType; 45use TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType\LongBlobDataType; 46use TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType\LongTextDataType; 47use TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType\MediumBlobDataType; 48use TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType\MediumIntDataType; 49use TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType\MediumTextDataType; 50use TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType\NumericDataType; 51use TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType\RealDataType; 52use TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType\SetDataType; 53use TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType\SmallIntDataType; 54use TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType\TextDataType; 55use TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType\TimeDataType; 56use TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType\TimestampDataType; 57use TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType\TinyBlobDataType; 58use TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType\TinyIntDataType; 59use TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType\TinyTextDataType; 60use TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType\VarBinaryDataType; 61use TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType\VarCharDataType; 62use TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType\YearDataType; 63use TYPO3\CMS\Core\Database\Schema\Parser\AST\IndexColumnName; 64use TYPO3\CMS\Core\Database\Schema\Parser\AST\ReferenceDefinition; 65use TYPO3\CMS\Core\Database\Schema\Types\EnumType; 66use TYPO3\CMS\Core\Database\Schema\Types\SetType; 67use TYPO3\CMS\Core\Utility\GeneralUtility; 68 69/** 70 * Converts a CreateTableStatement syntax node into a Doctrine Table 71 * object that represents the table defined in the original SQL statement. 72 */ 73class TableBuilder 74{ 75 /** 76 * @var Table 77 */ 78 protected $table; 79 80 /** 81 * @var AbstractPlatform 82 */ 83 protected $platform; 84 85 /** 86 * TableBuilder constructor. 87 * 88 * @param \Doctrine\DBAL\Platforms\AbstractPlatform $platform 89 * @throws \InvalidArgumentException 90 * @throws \Doctrine\DBAL\DBALException 91 */ 92 public function __construct(AbstractPlatform $platform = null) 93 { 94 // Register custom data types as no connection might have 95 // been established yet so the types would not be available 96 // when building tables/columns. 97 $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class); 98 99 foreach ($connectionPool->getCustomDoctrineTypes() as $type => $className) { 100 if (!Type::hasType($type)) { 101 Type::addType($type, $className); 102 } 103 } 104 $this->platform = $platform ?: GeneralUtility::makeInstance(MySqlPlatform::class); 105 } 106 107 /** 108 * Create a Doctrine Table object based on the parsed MySQL SQL command. 109 * 110 * @param \TYPO3\CMS\Core\Database\Schema\Parser\AST\CreateTableStatement $tableStatement 111 * @return \Doctrine\DBAL\Schema\Table 112 * @throws \Doctrine\DBAL\Schema\SchemaException 113 * @throws \RuntimeException 114 * @throws \InvalidArgumentException 115 */ 116 public function create(CreateTableStatement $tableStatement): Table 117 { 118 $this->table = GeneralUtility::makeInstance( 119 Table::class, 120 $tableStatement->tableName->getQuotedName(), 121 [], 122 [], 123 [], 124 0, 125 $this->buildTableOptions($tableStatement->tableOptions) 126 ); 127 128 foreach ($tableStatement->createDefinition->items as $item) { 129 switch (get_class($item)) { 130 case CreateColumnDefinitionItem::class: 131 $this->addColumn($item); 132 break; 133 case CreateIndexDefinitionItem::class: 134 $this->addIndex($item); 135 break; 136 case CreateForeignKeyDefinitionItem::class: 137 $this->addForeignKey($item); 138 break; 139 default: 140 throw new \RuntimeException( 141 'Unknown item definition of type "' . get_class($item) . '" encountered.', 142 1472044085 143 ); 144 } 145 } 146 147 return $this->table; 148 } 149 150 /** 151 * @param \TYPO3\CMS\Core\Database\Schema\Parser\AST\CreateColumnDefinitionItem $item 152 * @return \Doctrine\DBAL\Schema\Column 153 * @throws \Doctrine\DBAL\Schema\SchemaException 154 * @throws \RuntimeException 155 */ 156 protected function addColumn(CreateColumnDefinitionItem $item): Column 157 { 158 $column = $this->table->addColumn( 159 $item->columnName->getQuotedName(), 160 $this->getDoctrineColumnTypeName($item->dataType) 161 ); 162 163 $column->setNotnull(!$item->allowNull); 164 $column->setAutoincrement((bool)$item->autoIncrement); 165 $column->setComment($item->comment); 166 167 // Set default value (unless it's an auto increment column) 168 if ($item->hasDefaultValue && !$column->getAutoincrement()) { 169 $column->setDefault($item->defaultValue); 170 } 171 172 if ($item->dataType->getLength()) { 173 $column->setLength($item->dataType->getLength()); 174 } 175 176 if ($item->dataType->getPrecision() >= 0) { 177 $column->setPrecision($item->dataType->getPrecision()); 178 } 179 180 if ($item->dataType->getScale() >= 0) { 181 $column->setScale($item->dataType->getScale()); 182 } 183 184 if ($item->dataType->isUnsigned()) { 185 $column->setUnsigned(true); 186 } 187 188 // Select CHAR/VARCHAR or BINARY/VARBINARY 189 if ($item->dataType->isFixed()) { 190 $column->setFixed(true); 191 } 192 193 if ($item->dataType instanceof EnumDataType 194 || $item->dataType instanceof SetDataType 195 ) { 196 $column->setPlatformOption('unquotedValues', $item->dataType->getValues()); 197 } 198 199 if ($item->index) { 200 $this->table->addIndex([$item->columnName->getQuotedName()]); 201 } 202 203 if ($item->unique) { 204 $this->table->addUniqueIndex([$item->columnName->getQuotedName()]); 205 } 206 207 if ($item->primary) { 208 $this->table->setPrimaryKey([$item->columnName->getQuotedName()]); 209 } 210 211 if ($item->reference !== null) { 212 $this->addForeignKeyConstraint( 213 [$item->columnName->getQuotedName()], 214 $item->reference 215 ); 216 } 217 218 return $column; 219 } 220 221 /** 222 * @param \TYPO3\CMS\Core\Database\Schema\Parser\AST\CreateIndexDefinitionItem $item 223 * @return \Doctrine\DBAL\Schema\Index 224 * @throws \Doctrine\DBAL\Schema\SchemaException 225 * @throws \InvalidArgumentException 226 */ 227 protected function addIndex(CreateIndexDefinitionItem $item): Index 228 { 229 $indexName = $item->indexName->getQuotedName(); 230 231 $columnNames = array_map( 232 function (IndexColumnName $columnName) { 233 if ($columnName->length) { 234 return $columnName->columnName->getQuotedName() . '(' . $columnName->length . ')'; 235 } 236 return $columnName->columnName->getQuotedName(); 237 }, 238 $item->columnNames 239 ); 240 241 if ($item->isPrimary) { 242 $this->table->setPrimaryKey($columnNames); 243 $index = $this->table->getPrimaryKey(); 244 } else { 245 $index = GeneralUtility::makeInstance( 246 Index::class, 247 $indexName, 248 $columnNames, 249 $item->isUnique, 250 $item->isPrimary 251 ); 252 253 if ($item->isFulltext) { 254 $index->addFlag('fulltext'); 255 } elseif ($item->isSpatial) { 256 $index->addFlag('spatial'); 257 } 258 259 $this->table = GeneralUtility::makeInstance( 260 Table::class, 261 $this->table->getQuotedName($this->platform), 262 $this->table->getColumns(), 263 array_merge($this->table->getIndexes(), [strtolower($indexName) => $index]), 264 $this->table->getForeignKeys(), 265 0, 266 $this->table->getOptions() 267 ); 268 } 269 270 return $index; 271 } 272 273 /** 274 * Prepare an explicit foreign key definition item to be added to the table being built. 275 * 276 * @param \TYPO3\CMS\Core\Database\Schema\Parser\AST\CreateForeignKeyDefinitionItem $item 277 */ 278 protected function addForeignKey(CreateForeignKeyDefinitionItem $item) 279 { 280 $indexName = $item->indexName->getQuotedName() ?: null; 281 $localColumnNames = array_map( 282 function (IndexColumnName $columnName) { 283 return $columnName->columnName->getQuotedName(); 284 }, 285 $item->columnNames 286 ); 287 $this->addForeignKeyConstraint($localColumnNames, $item->reference, $indexName); 288 } 289 290 /** 291 * Add a foreign key constraint to the table being built. 292 * 293 * @param string[] $localColumnNames 294 * @param \TYPO3\CMS\Core\Database\Schema\Parser\AST\ReferenceDefinition $referenceDefinition 295 * @param string $indexName 296 */ 297 protected function addForeignKeyConstraint( 298 array $localColumnNames, 299 ReferenceDefinition $referenceDefinition, 300 string $indexName = null 301 ) { 302 $foreignTableName = $referenceDefinition->tableName->getQuotedName(); 303 $foreignColumnNames = array_map( 304 function (IndexColumnName $columnName) { 305 return $columnName->columnName->getQuotedName(); 306 }, 307 $referenceDefinition->columnNames 308 ); 309 310 $options = [ 311 'onDelete' => $referenceDefinition->onDelete, 312 'onUpdate' => $referenceDefinition->onUpdate, 313 ]; 314 315 $this->table->addForeignKeyConstraint( 316 $foreignTableName, 317 $localColumnNames, 318 $foreignColumnNames, 319 $options, 320 $indexName 321 ); 322 } 323 324 /** 325 * @param \TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType\AbstractDataType $dataType 326 * @return string 327 * @throws \RuntimeException 328 */ 329 protected function getDoctrineColumnTypeName(AbstractDataType $dataType): string 330 { 331 $doctrineType = null; 332 switch (get_class($dataType)) { 333 case TinyIntDataType::class: 334 // TINYINT is MySQL specific and mapped to a standard SMALLINT 335 case SmallIntDataType::class: 336 $doctrineType = Types::SMALLINT; 337 break; 338 case MediumIntDataType::class: 339 // MEDIUMINT is MySQL specific and mapped to a standard INT 340 case IntegerDataType::class: 341 $doctrineType = Types::INTEGER; 342 break; 343 case BigIntDataType::class: 344 $doctrineType = Types::BIGINT; 345 break; 346 case BinaryDataType::class: 347 case VarBinaryDataType::class: 348 // CHAR/VARCHAR is determined by "fixed" column property 349 $doctrineType = Types::BINARY; 350 break; 351 case TinyBlobDataType::class: 352 case MediumBlobDataType::class: 353 case BlobDataType::class: 354 case LongBlobDataType::class: 355 // Actual field type is determined by field length 356 $doctrineType = Types::BLOB; 357 break; 358 case DateDataType::class: 359 $doctrineType = Types::DATE_MUTABLE; 360 break; 361 case TimestampDataType::class: 362 case DateTimeDataType::class: 363 // TIMESTAMP or DATETIME are determined by "version" column property 364 $doctrineType = Types::DATETIME_MUTABLE; 365 break; 366 case NumericDataType::class: 367 case DecimalDataType::class: 368 $doctrineType = Types::DECIMAL; 369 break; 370 case RealDataType::class: 371 case FloatDataType::class: 372 case DoubleDataType::class: 373 $doctrineType = Types::FLOAT; 374 break; 375 case TimeDataType::class: 376 $doctrineType = Types::TIME_MUTABLE; 377 break; 378 case TinyTextDataType::class: 379 case MediumTextDataType::class: 380 case TextDataType::class: 381 case LongTextDataType::class: 382 $doctrineType = Types::TEXT; 383 break; 384 case CharDataType::class: 385 case VarCharDataType::class: 386 $doctrineType = Types::STRING; 387 break; 388 case EnumDataType::class: 389 $doctrineType = EnumType::TYPE; 390 break; 391 case SetDataType::class: 392 $doctrineType = SetType::TYPE; 393 break; 394 case JsonDataType::class: 395 // JSON is not supported in Doctrine 2.5, mapping to the more generic TEXT type 396 $doctrineType = Types::TEXT; 397 break; 398 case YearDataType::class: 399 // The YEAR data type is MySQL specific and offers little to no benefit. 400 // The two-digit year logic implemented in this data type (1-69 mapped to 401 // 2001-2069, 70-99 mapped to 1970-1999) can be easily implemented in the 402 // application and for all other accounts it's an integer with a valid 403 // range of 1901 to 2155. 404 // Using a SMALLINT covers the value range and ensures database compatibility. 405 $doctrineType = Types::SMALLINT; 406 break; 407 default: 408 throw new \RuntimeException( 409 'Unsupported data type: ' . get_class($dataType) . '!', 410 1472046376 411 ); 412 } 413 414 return $doctrineType; 415 } 416 417 /** 418 * Build the table specific options as far as they are supported by Doctrine. 419 * 420 * @param array $tableOptions 421 * @return array 422 */ 423 protected function buildTableOptions(array $tableOptions): array 424 { 425 $options = []; 426 427 if (!empty($tableOptions['engine'])) { 428 $options['engine'] = (string)$tableOptions['engine']; 429 } 430 if (!empty($tableOptions['character_set'])) { 431 $options['charset'] = (string)$tableOptions['character_set']; 432 } 433 if (!empty($tableOptions['collation'])) { 434 $options['collate'] = (string)$tableOptions['collation']; 435 } 436 if (!empty($tableOptions['auto_increment'])) { 437 $options['auto_increment'] = (string)$tableOptions['auto_increment']; 438 } 439 if (!empty($tableOptions['comment'])) { 440 $options['comment'] = (string)$tableOptions['comment']; 441 } 442 if (!empty($tableOptions['row_format'])) { 443 $options['row_format'] = (string)$tableOptions['row_format']; 444 } 445 446 return $options; 447 } 448} 449