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