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