[TASK] Update doctrine/dbal to ~2.8.0
[Packages/TYPO3.CMS.git] / typo3 / sysext / core / Classes / Database / Schema / DefaultTcaSchema.php
1 <?php
2 declare(strict_types = 1);
3
4 namespace TYPO3\CMS\Core\Database\Schema;
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\SqlitePlatform;
20 use Doctrine\DBAL\Schema\Table;
21 use TYPO3\CMS\Core\Database\ConnectionPool;
22 use TYPO3\CMS\Core\Utility\GeneralUtility;
23
24 /**
25 * This class is called by the SchemaMigrator after all extension's ext_tables.sql
26 * files have been parsed and processed to the doctrine Table/Column/Index objects.
27 *
28 * Method enrich() goes through all $GLOBALS['TCA'] tables and adds fields like
29 * 'uid', 'sorting', 'deleted' and friends if the feature is enabled in TCA and the
30 * field has not been defined in ext_tables.sql files.
31 *
32 * This allows extension developers to leave out the TYPO3 DB management fields
33 * and reduce ext_tables.sql of extensions down to the business fields.
34 *
35 * @internal
36 */
37 class DefaultTcaSchema
38 {
39 /**
40 * Add fields to $tables array that has been created from ext_tables.sql files.
41 * This goes through all tables defined in TCA, looks for 'ctrl' features like
42 * "soft delete" ['ctrl']['delete'] and adds the field if it has not been
43 * defined in ext_tables.sql, yet.
44 *
45 * Note the incoming $tables array must be created from all ext_tables.sql files
46 * of all loaded extensions, and TCA must be up-to-date.
47 *
48 * @param Table[] $tables
49 * @return Table[]
50 */
51 public function enrich(array $tables): array
52 {
53 foreach ($GLOBALS['TCA'] as $tableName => $tableDefinition) {
54 $isTableDefined = $this->isTableDefined($tables, $tableName);
55 $tablePosition = null;
56 if ($isTableDefined) {
57 // If the table is given in existing $tables list, add all fields to the first
58 // position of that table - in case it is in there multiple times which happens
59 // if extensions add single fields to tables that have been defined in
60 // other ext_tables.sql, too.
61 $tablePosition = $this->getTableFirstPosition($tables, $tableName);
62 } else {
63 // Else create this table and add it to table list
64 $table = GeneralUtility::makeInstance(Table::class, $tableName);
65 $tables[] = $table;
66 $tableKeys = array_keys($tables);
67 $tablePosition = end($tableKeys);
68 }
69
70 // uid column and primary key if uid is not defined
71 if (!$this->isColumnDefinedForTable($tables, $tableName, 'uid')) {
72 $tables[$tablePosition]->addColumn(
73 'uid',
74 'integer',
75 [
76 'notnull' => true,
77 'unsigned' => true,
78 'autoincrement' => true,
79 ]
80 );
81 // SQLite does not need primary key, only needs autoincrement on integer fields
82 if (!$this->tableRunsOnSqlite($tableName)) {
83 $tables[$tablePosition]->setPrimaryKey(['uid']);
84 }
85 }
86
87 // pid column and prepare parent key if pid is not defined
88 $pidColumnAdded = false;
89 if (!$this->isColumnDefinedForTable($tables, $tableName, 'pid')) {
90 $options = [
91 'default' => 0,
92 'notnull' => true,
93 'unsigned' => false,
94 ];
95 if (empty($tableDefinition['ctrl']['versioningWS'])) {
96 // We need negative pid's (-1) if table is workspace aware
97 $options['unsigned'] = true;
98 }
99 $tables[$tablePosition]->addColumn('pid', 'integer', $options);
100 $pidColumnAdded = true;
101 }
102
103 // tstamp column
104 if (!empty($tableDefinition['ctrl']['tstamp'])
105 && !$this->isColumnDefinedForTable($tables, $tableName, $tableDefinition['ctrl']['tstamp'])
106 ) {
107 $tables[$tablePosition]->addColumn(
108 $this->quote($tableDefinition['ctrl']['tstamp']),
109 'integer',
110 [
111 'default' => 0,
112 'notnull' => true,
113 'unsigned' => true,
114 ]
115 );
116 }
117
118 // crdate column
119 if (!empty($tableDefinition['ctrl']['crdate'])
120 && !$this->isColumnDefinedForTable($tables, $tableName, $tableDefinition['ctrl']['crdate'])
121 ) {
122 $tables[$tablePosition]->addColumn(
123 $this->quote($tableDefinition['ctrl']['crdate']),
124 'integer',
125 [
126 'default' => 0,
127 'notnull' => true,
128 'unsigned' => true,
129 ]
130 );
131 }
132
133 // cruser_id column
134 if (!empty($tableDefinition['ctrl']['cruser_id'])
135 && !$this->isColumnDefinedForTable($tables, $tableName, $tableDefinition['ctrl']['cruser_id'])
136 ) {
137 $tables[$tablePosition]->addColumn(
138 $this->quote($tableDefinition['ctrl']['cruser_id']),
139 'integer',
140 [
141 'default' => 0,
142 'notnull' => true,
143 'unsigned' => true,
144 ]
145 );
146 }
147
148 // deleted column - soft delete
149 if (!empty($tableDefinition['ctrl']['delete'])
150 && !$this->isColumnDefinedForTable($tables, $tableName, $tableDefinition['ctrl']['delete'])
151 ) {
152 $tables[$tablePosition]->addColumn(
153 $this->quote($tableDefinition['ctrl']['delete']),
154 'smallint',
155 [
156 'default' => 0,
157 'notnull' => true,
158 'unsigned' => true,
159 ]
160 );
161 }
162
163 // disabled column
164 if (!empty($tableDefinition['ctrl']['enablecolumns']['disabled'])
165 && !$this->isColumnDefinedForTable($tables, $tableName, $tableDefinition['ctrl']['enablecolumns']['disabled'])
166 ) {
167 $tables[$tablePosition]->addColumn(
168 $this->quote($tableDefinition['ctrl']['enablecolumns']['disabled']),
169 'smallint',
170 [
171 'default' => 0,
172 'notnull' => true,
173 'unsigned' => true,
174 ]
175 );
176 }
177
178 // starttime column
179 if (!empty($tableDefinition['ctrl']['enablecolumns']['starttime'])
180 && !$this->isColumnDefinedForTable($tables, $tableName, $tableDefinition['ctrl']['enablecolumns']['starttime'])
181 ) {
182 $tables[$tablePosition]->addColumn(
183 $this->quote($tableDefinition['ctrl']['enablecolumns']['starttime']),
184 'integer',
185 [
186 'default' => 0,
187 'notnull' => true,
188 'unsigned' => true,
189 ]
190 );
191 }
192
193 // endtime column
194 if (!empty($tableDefinition['ctrl']['enablecolumns']['endtime'])
195 && !$this->isColumnDefinedForTable($tables, $tableName, $tableDefinition['ctrl']['enablecolumns']['endtime'])
196 ) {
197 $tables[$tablePosition]->addColumn(
198 $this->quote($tableDefinition['ctrl']['enablecolumns']['endtime']),
199 'integer',
200 [
201 'default' => 0,
202 'notnull' => true,
203 'unsigned' => true,
204 ]
205 );
206 }
207
208 // fe_group column
209 if (!empty($tableDefinition['ctrl']['enablecolumns']['fe_group'])
210 && !$this->isColumnDefinedForTable($tables, $tableName, $tableDefinition['ctrl']['enablecolumns']['fe_group'])
211 ) {
212 $tables[$tablePosition]->addColumn(
213 $this->quote($tableDefinition['ctrl']['enablecolumns']['fe_group']),
214 'string',
215 [
216 'default' => '0',
217 'notnull' => true,
218 'length' => 255,
219 ]
220 );
221 }
222
223 // sorting column
224 if (!empty($tableDefinition['ctrl']['sortby'])
225 && !$this->isColumnDefinedForTable($tables, $tableName, $tableDefinition['ctrl']['sortby'])
226 ) {
227 $tables[$tablePosition]->addColumn(
228 $this->quote($tableDefinition['ctrl']['sortby']),
229 'integer',
230 [
231 'default' => 0,
232 'notnull' => true,
233 'unsigned' => false,
234 ]
235 );
236 }
237
238 // index on pid column and maybe others - only if pid has not been defined via ext_tables.sql before
239 if ($pidColumnAdded && !$this->isIndexDefinedForTable($tables, $tableName, 'parent')) {
240 $parentIndexFields = ['pid'];
241 if (!empty($tableDefinition['ctrl']['delete'])) {
242 $parentIndexFields[] = (string)$tableDefinition['ctrl']['delete'];
243 }
244 if (!empty($tableDefinition['ctrl']['enablecolumns']['disabled'])) {
245 $parentIndexFields[] = (string)$tableDefinition['ctrl']['enablecolumns']['disabled'];
246 }
247 $tables[$tablePosition]->addIndex($parentIndexFields, 'parent');
248 }
249
250 // description column
251 if (!empty($tableDefinition['ctrl']['descriptionColumn'])
252 && !$this->isColumnDefinedForTable($tables, $tableName, $tableDefinition['ctrl']['descriptionColumn'])
253 ) {
254 $tables[$tablePosition]->addColumn(
255 $this->quote($tableDefinition['ctrl']['descriptionColumn']),
256 'text',
257 [
258 'notnull' => false,
259 'length' => 65535,
260 ]
261 );
262 }
263
264 // editlock column
265 if (!empty($tableDefinition['ctrl']['editlock'])
266 && !$this->isColumnDefinedForTable($tables, $tableName, $tableDefinition['ctrl']['editlock'])
267 ) {
268 $tables[$tablePosition]->addColumn(
269 $this->quote($tableDefinition['ctrl']['editlock']),
270 'smallint',
271 [
272 'default' => 0,
273 'notnull' => true,
274 'unsigned' => true,
275 ]
276 );
277 }
278
279 // sys_language_uid column
280 if (!empty($tableDefinition['ctrl']['languageField'])
281 && !$this->isColumnDefinedForTable($tables, $tableName, $tableDefinition['ctrl']['languageField'])
282 ) {
283 $tables[$tablePosition]->addColumn(
284 $this->quote((string)$tableDefinition['ctrl']['languageField']),
285 'integer',
286 [
287 'default' => 0,
288 'notnull' => true,
289 'unsigned' => false,
290 ]
291 );
292 }
293
294 // l10n_parent column
295 if (!empty($tableDefinition['ctrl']['languageField'])
296 && !empty($tableDefinition['ctrl']['transOrigPointerField'])
297 && !$this->isColumnDefinedForTable($tables, $tableName, $tableDefinition['ctrl']['transOrigPointerField'])
298 ) {
299 $tables[$tablePosition]->addColumn(
300 $this->quote((string)$tableDefinition['ctrl']['transOrigPointerField']),
301 'integer',
302 [
303 'default' => 0,
304 'notnull' => true,
305 'unsigned' => true,
306 ]
307 );
308 }
309
310 // l10n_source column
311 if (!empty($tableDefinition['ctrl']['languageField'])
312 && !empty($tableDefinition['ctrl']['translationSource'])
313 && !$this->isColumnDefinedForTable($tables, $tableName, $tableDefinition['ctrl']['translationSource'])
314 ) {
315 $tables[$tablePosition]->addColumn(
316 $this->quote((string)$tableDefinition['ctrl']['translationSource']),
317 'integer',
318 [
319 'default' => 0,
320 'notnull' => true,
321 'unsigned' => true,
322 ]
323 );
324 }
325
326 // l10n_state column
327 if (!empty($tableDefinition['ctrl']['languageField'])
328 && !empty($tableDefinition['ctrl']['transOrigPointerField'])
329 && !$this->isColumnDefinedForTable($tables, $tableName, 'l10n_state')
330 ) {
331 $tables[$tablePosition]->addColumn(
332 $this->quote('l10n_state'),
333 'text',
334 [
335 'notnull' => false,
336 'length' => 65535,
337 ]
338 );
339 }
340
341 // t3_origuid column
342 if (!empty($tableDefinition['ctrl']['origUid'])
343 && !$this->isColumnDefinedForTable($tables, $tableName, $tableDefinition['ctrl']['origUid'])
344 ) {
345 $tables[$tablePosition]->addColumn(
346 $this->quote($tableDefinition['ctrl']['origUid']),
347 'integer',
348 [
349 'default' => 0,
350 'notnull' => true,
351 'unsigned' => true,
352 ]
353 );
354 }
355
356 // l18n_diffsource column
357 if (!empty($tableDefinition['ctrl']['transOrigDiffSourceField'])
358 && !$this->isColumnDefinedForTable($tables, $tableName, $tableDefinition['ctrl']['transOrigDiffSourceField'])
359 ) {
360 $tables[$tablePosition]->addColumn(
361 $this->quote($tableDefinition['ctrl']['transOrigDiffSourceField']),
362 'blob',
363 [
364 // mediumblob (16MB) on mysql
365 'length' => 16777215,
366 'notnull' => false,
367 ]
368 );
369 }
370
371 // workspaces t3ver_oid column
372 if (!empty($tableDefinition['ctrl']['versioningWS'])
373 && (bool)$tableDefinition['ctrl']['versioningWS'] === true
374 && !$this->isColumnDefinedForTable($tables, $tableName, 't3ver_oid')
375 ) {
376 $tables[$tablePosition]->addColumn(
377 $this->quote('t3ver_oid'),
378 'integer',
379 [
380 'default' => 0,
381 'notnull' => true,
382 'unsigned' => true,
383 ]
384 );
385 }
386
387 // workspaces t3ver_id column
388 if (!empty($tableDefinition['ctrl']['versioningWS'])
389 && (bool)$tableDefinition['ctrl']['versioningWS'] === true
390 && !$this->isColumnDefinedForTable($tables, $tableName, 't3ver_id')
391 ) {
392 $tables[$tablePosition]->addColumn(
393 $this->quote('t3ver_id'),
394 'integer',
395 [
396 'default' => 0,
397 'notnull' => true,
398 'unsigned' => true,
399 ]
400 );
401 }
402
403 // workspaces t3ver_wsid column
404 if (!empty($tableDefinition['ctrl']['versioningWS'])
405 && (bool)$tableDefinition['ctrl']['versioningWS'] === true
406 && !$this->isColumnDefinedForTable($tables, $tableName, 't3ver_wsid')
407 ) {
408 $tables[$tablePosition]->addColumn(
409 $this->quote('t3ver_wsid'),
410 'integer',
411 [
412 'default' => 0,
413 'notnull' => true,
414 'unsigned' => true,
415 ]
416 );
417 }
418
419 // workspaces t3ver_state column
420 if (!empty($tableDefinition['ctrl']['versioningWS'])
421 && (bool)$tableDefinition['ctrl']['versioningWS'] === true
422 && !$this->isColumnDefinedForTable($tables, $tableName, 't3ver_state')
423 ) {
424 $tables[$tablePosition]->addColumn(
425 $this->quote('t3ver_state'),
426 'smallint',
427 [
428 'default' => 0,
429 'notnull' => true,
430 'unsigned' => false,
431 ]
432 );
433 }
434
435 // workspaces t3ver_stage column
436 if (!empty($tableDefinition['ctrl']['versioningWS'])
437 && (bool)$tableDefinition['ctrl']['versioningWS'] === true
438 && !$this->isColumnDefinedForTable($tables, $tableName, 't3ver_stage')
439 ) {
440 $tables[$tablePosition]->addColumn(
441 $this->quote('t3ver_stage'),
442 'integer',
443 [
444 'default' => 0,
445 'notnull' => true,
446 'unsigned' => false,
447 ]
448 );
449 }
450
451 // workspaces t3ver_count column
452 if (!empty($tableDefinition['ctrl']['versioningWS'])
453 && (bool)$tableDefinition['ctrl']['versioningWS'] === true
454 && !$this->isColumnDefinedForTable($tables, $tableName, 't3ver_count')
455 ) {
456 $tables[$tablePosition]->addColumn(
457 $this->quote('t3ver_count'),
458 'integer',
459 [
460 'default' => 0,
461 'notnull' => true,
462 'unsigned' => true,
463 ]
464 );
465 }
466
467 // workspaces t3ver_tstamp column
468 if (!empty($tableDefinition['ctrl']['versioningWS'])
469 && (bool)$tableDefinition['ctrl']['versioningWS'] === true
470 && !$this->isColumnDefinedForTable($tables, $tableName, 't3ver_tstamp')
471 ) {
472 $tables[$tablePosition]->addColumn(
473 $this->quote('t3ver_tstamp'),
474 'integer',
475 [
476 'default' => 0,
477 'notnull' => true,
478 'unsigned' => true,
479 ]
480 );
481 }
482
483 // workspaces t3ver_move_id column
484 if (!empty($tableDefinition['ctrl']['versioningWS'])
485 && (bool)$tableDefinition['ctrl']['versioningWS'] === true
486 && !$this->isColumnDefinedForTable($tables, $tableName, 't3ver_move_id')
487 ) {
488 $tables[$tablePosition]->addColumn(
489 $this->quote('t3ver_move_id'),
490 'integer',
491 [
492 'default' => 0,
493 'notnull' => true,
494 'unsigned' => true,
495 ]
496 );
497 }
498
499 // workspaces index on t3ver_oid and t3ver_wsid fields
500 if (!empty($tableDefinition['ctrl']['versioningWS'])
501 && (bool)$tableDefinition['ctrl']['versioningWS'] === true
502 && !$this->isIndexDefinedForTable($tables, $tableName, 't3ver_oid')
503 ) {
504 $tables[$tablePosition]->addIndex(['t3ver_oid', 't3ver_wsid'], 't3ver_oid');
505 }
506 }
507
508 return $tables;
509 }
510
511 /**
512 * If the enrich() method adds fields, they should be added in the beginning of a table.
513 *
514 * @param string $tableName
515 * @return string[]
516 */
517 public function getPrioritizedFieldNames(string $tableName): array
518 {
519 if (!isset($GLOBALS['TCA'][$tableName]['ctrl'])) {
520 return [];
521 }
522
523 $prioritizedFieldNames = [
524 'uid',
525 'pid'
526 ];
527
528 $tableDefinition = $GLOBALS['TCA'][$tableName]['ctrl'];
529
530 if (!empty($tableDefinition['crdate'])) {
531 $prioritizedFieldNames[] = $tableDefinition['crdate'];
532 }
533 if (!empty($tableDefinition['tstamp'])) {
534 $prioritizedFieldNames[] = $tableDefinition['tstamp'];
535 }
536 if (!empty($tableDefinition['cruser_id'])) {
537 $prioritizedFieldNames[] = $tableDefinition['cruser_id'];
538 }
539 if (!empty($tableDefinition['delete'])) {
540 $prioritizedFieldNames[] = $tableDefinition['delete'];
541 }
542 if (!empty($tableDefinition['enablecolumns']['disabled'])) {
543 $prioritizedFieldNames[] = $tableDefinition['enablecolumns']['disabled'];
544 }
545 if (!empty($tableDefinition['enablecolumns']['starttime'])) {
546 $prioritizedFieldNames[] = $tableDefinition['enablecolumns']['starttime'];
547 }
548 if (!empty($tableDefinition['enablecolumns']['endtime'])) {
549 $prioritizedFieldNames[] = $tableDefinition['enablecolumns']['endtime'];
550 }
551 if (!empty($tableDefinition['enablecolumns']['fe_group'])) {
552 $prioritizedFieldNames[] = $tableDefinition['enablecolumns']['fe_group'];
553 }
554 if (!empty($tableDefinition['languageField'])) {
555 $prioritizedFieldNames[] = $tableDefinition['languageField'];
556 if (!empty($tableDefinition['transOrigPointerField'])) {
557 $prioritizedFieldNames[] = $tableDefinition['transOrigPointerField'];
558 $prioritizedFieldNames[] = 'l10n_state';
559 }
560 if (!empty($tableDefinition['translationSource'])) {
561 $prioritizedFieldNames[] = $tableDefinition['translationSource'];
562 }
563 if (!empty($tableDefinition['transOrigDiffSourceField'])) {
564 $prioritizedFieldNames[] = $tableDefinition['transOrigDiffSourceField'];
565 }
566 }
567 if (!empty($tableDefinition['sortby'])) {
568 $prioritizedFieldNames[] = $tableDefinition['sortby'];
569 }
570 if (!empty($tableDefinition['descriptionColumn'])) {
571 $prioritizedFieldNames[] = $tableDefinition['descriptionColumn'];
572 }
573 if (!empty($tableDefinition['editlock'])) {
574 $prioritizedFieldNames[] = $tableDefinition['editlock'];
575 }
576 if (!empty($tableDefinition['origUid'])) {
577 $prioritizedFieldNames[] = $tableDefinition['origUid'];
578 }
579 if (!empty($tableDefinition['versioningWS'])) {
580 $prioritizedFieldNames[] = 't3ver_wsid';
581 $prioritizedFieldNames[] = 't3ver_oid';
582 $prioritizedFieldNames[] = 't3ver_state';
583 $prioritizedFieldNames[] = 't3ver_stage';
584 $prioritizedFieldNames[] = 't3ver_id';
585 $prioritizedFieldNames[] = 't3ver_move_id';
586 $prioritizedFieldNames[] = 't3ver_count';
587 $prioritizedFieldNames[] = 't3ver_tstamp';
588 }
589
590 return $prioritizedFieldNames;
591 }
592
593 /**
594 * True if table with given table name is defined within incoming $tables array
595 *
596 * @param Table[] $tables
597 * @param string $tableName
598 * @return bool
599 */
600 protected function isTableDefined(array $tables, string $tableName): bool
601 {
602 foreach ($tables as $table) {
603 if ($table->getName() === $tableName) {
604 return true;
605 }
606 }
607 return false;
608 }
609
610 /**
611 * True if a column with a given name is defined within the incoming
612 * array of Table's.
613 *
614 * @param Table[] $tables
615 * @param string $tableName
616 * @param string $fieldName
617 * @return bool
618 */
619 protected function isColumnDefinedForTable(array $tables, string $tableName, string $fieldName): bool
620 {
621 foreach ($tables as $table) {
622 if ($table->getName() !== $tableName) {
623 continue;
624 }
625 $columns = $table->getColumns();
626 foreach ($columns as $column) {
627 if ($column->getName() === $fieldName) {
628 return true;
629 }
630 }
631 }
632 return false;
633 }
634
635 /**
636 * True if an index with a given name is defined within the incoming
637 * array of Table's.
638 *
639 * @param Table[] $tables
640 * @param string $tableName
641 * @param string $indexName
642 * @return bool
643 */
644 protected function isIndexDefinedForTable(array $tables, string $tableName, string $indexName): bool
645 {
646 foreach ($tables as $table) {
647 if ($table->getName() !== $tableName) {
648 continue;
649 }
650 $indexes = $table->getIndexes();
651 foreach ($indexes as $index) {
652 if ($index->getName() === $indexName) {
653 return true;
654 }
655 }
656 }
657 return false;
658 }
659
660 /**
661 * The incoming $tables array can contain Table objects for the same table
662 * multiple times. This can happen if an extension has the main CREATE TABLE
663 * statement in its ext_tables.sql and another extension adds or changes further
664 * fields in an own CREATE TABLE statement.
665 *
666 * @todo It would be better if the incoming $tables structure would be cleaned
667 * @todo to contain a table only once before this class is entered.
668 *
669 * @param Table[] $tables
670 * @param string $tableName
671 * @return int
672 * @throws \RuntimeException
673 */
674 protected function getTableFirstPosition(array $tables, string $tableName): int
675 {
676 foreach ($tables as $position => $table) {
677 if ($table->getName() === $tableName) {
678 return (int)$position;
679 }
680 }
681 throw new \RuntimeException('Table ' . $tableName . ' not found in schema list', 1527854474);
682 }
683
684 /**
685 * @param string $identifier
686 * @return string
687 */
688 protected function quote(string $identifier): string
689 {
690 return '`' . $identifier . '`';
691 }
692
693 /**
694 * SQLite does not need primary key, only needs autoincrement on integer fields
695 * See https://github.com/doctrine/dbal/pull/3141
696 * @param string $tableName
697 * @return bool
698 */
699 protected function tableRunsOnSqlite(string $tableName): bool
700 {
701 $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($tableName);
702 return $connection->getDatabasePlatform() instanceof SqlitePlatform;
703 }
704 }