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