‪TYPO3CMS  ‪main
DefaultTcaSchema.php
Go to the documentation of this file.
1 <?php
2 
3 declare(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 
19 
20 use Doctrine\DBAL\Platforms\SqlitePlatform as DoctrineSQLitePlatform;
21 use Doctrine\DBAL\Schema\Table;
22 use Doctrine\DBAL\Types\Types;
28 
43 {
53  public function ‪enrich(array $tables): array
54  {
55  // Sanity check to ensure all TCA tables are already defined in incoming table list.
56  // This prevents a misuse, calling code needs to ensure there is at least an empty
57  // table object (no columns) for all TCA tables.
58  $tableNamesFromTca = array_keys(‪$GLOBALS['TCA']);
59  $existingTableNames = [];
60  foreach ($tables as $table) {
61  $existingTableNames[] = $table->getName();
62  }
63  foreach ($tableNamesFromTca as $tableName) {
64  if (!in_array($tableName, $existingTableNames, true)) {
65  throw new \RuntimeException(
66  'Table name ' . $tableName . ' does not exist in incoming table list',
67  1696424993
68  );
69  }
70  }
71 
72  $tables = $this->‪enrichSingleTableFieldsFromTcaCtrl($tables);
73  $tables = $this->‪enrichSingleTableFieldsFromTcaColumns($tables);
74  return $this->‪enrichMmTables($tables);
75  }
76 
80  protected function ‪enrichSingleTableFieldsFromTcaCtrl($tables)
81  {
82  foreach (‪$GLOBALS['TCA'] as $tableName => $tableDefinition) {
83  // If the table is given in existing $tables list, add all fields to the first
84  // position of that table - in case it is in there multiple times which happens
85  // if extensions add single fields to tables that have been defined in
86  // other ext_tables.sql, too.
87  $tablePosition = $this->‪getTableFirstPosition($tables, $tableName);
88 
89  // uid column and primary key if uid is not defined
90  if (!$this->‪isColumnDefinedForTable($tables, $tableName, 'uid')) {
91  $tables[$tablePosition]->addColumn(
92  $this->‪quote('uid'),
93  Types::INTEGER,
94  [
95  'notnull' => true,
96  'unsigned' => true,
97  'autoincrement' => true,
98  ]
99  );
100  $tables[$tablePosition]->setPrimaryKey(['uid']);
101  }
102 
103  // pid column and prepare parent key if pid is not defined
104  $pidColumnAdded = false;
105  if (!$this->‪isColumnDefinedForTable($tables, $tableName, 'pid')) {
106  $options = [
107  'default' => 0,
108  'notnull' => true,
109  'unsigned' => true,
110  ];
111  $tables[$tablePosition]->addColumn($this->‪quote('pid'), Types::INTEGER, $options);
112  $pidColumnAdded = true;
113  }
114 
115  // tstamp column
116  // not converted to bigint because already unsigned and date before 1970 not needed
117  if (!empty($tableDefinition['ctrl']['tstamp'])
118  && !$this->‪isColumnDefinedForTable($tables, $tableName, $tableDefinition['ctrl']['tstamp'])
119  ) {
120  $tables[$tablePosition]->addColumn(
121  $this->‪quote($tableDefinition['ctrl']['tstamp']),
122  Types::INTEGER,
123  [
124  'default' => 0,
125  'notnull' => true,
126  'unsigned' => true,
127  ]
128  );
129  }
130 
131  // crdate column
132  if (!empty($tableDefinition['ctrl']['crdate'])
133  && !$this->‪isColumnDefinedForTable($tables, $tableName, $tableDefinition['ctrl']['crdate'])
134  ) {
135  $tables[$tablePosition]->addColumn(
136  $this->‪quote($tableDefinition['ctrl']['crdate']),
137  Types::INTEGER,
138  [
139  'default' => 0,
140  'notnull' => true,
141  'unsigned' => true,
142  ]
143  );
144  }
145 
146  // deleted column - soft delete
147  if (!empty($tableDefinition['ctrl']['delete'])
148  && !$this->‪isColumnDefinedForTable($tables, $tableName, $tableDefinition['ctrl']['delete'])
149  ) {
150  $tables[$tablePosition]->addColumn(
151  $this->‪quote($tableDefinition['ctrl']['delete']),
152  Types::SMALLINT,
153  [
154  'default' => 0,
155  'notnull' => true,
156  'unsigned' => true,
157  ]
158  );
159  }
160 
161  // disabled column
162  if (!empty($tableDefinition['ctrl']['enablecolumns']['disabled'])
163  && !$this->‪isColumnDefinedForTable($tables, $tableName, $tableDefinition['ctrl']['enablecolumns']['disabled'])
164  ) {
165  $tables[$tablePosition]->addColumn(
166  $this->‪quote($tableDefinition['ctrl']['enablecolumns']['disabled']),
167  Types::SMALLINT,
168  [
169  'default' => 0,
170  'notnull' => true,
171  'unsigned' => true,
172  ]
173  );
174  }
175 
176  // starttime column
177  // not converted to bigint because already unsigned and date before 1970 not needed
178  if (!empty($tableDefinition['ctrl']['enablecolumns']['starttime'])
179  && !$this->‪isColumnDefinedForTable($tables, $tableName, $tableDefinition['ctrl']['enablecolumns']['starttime'])
180  ) {
181  $tables[$tablePosition]->addColumn(
182  $this->‪quote($tableDefinition['ctrl']['enablecolumns']['starttime']),
183  Types::INTEGER,
184  [
185  'default' => 0,
186  'notnull' => true,
187  'unsigned' => true,
188  ]
189  );
190  }
191 
192  // endtime column
193  // not converted to bigint because already unsigned and date before 1970 not needed
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  Types::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  Types::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  Types::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  Types::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  Types::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  Types::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  Types::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  Types::INTEGER,
318  [
319  'default' => 0,
320  'notnull' => true,
321  'unsigned' => true,
322  ]
323  );
324  $tables[$tablePosition]->addIndex([$tableDefinition['ctrl']['translationSource']], 'translation_source');
325  }
326 
327  // l10n_state column
328  if (!empty($tableDefinition['ctrl']['languageField'])
329  && !empty($tableDefinition['ctrl']['transOrigPointerField'])
330  && !$this->‪isColumnDefinedForTable($tables, $tableName, 'l10n_state')
331  ) {
332  $tables[$tablePosition]->addColumn(
333  $this->‪quote('l10n_state'),
334  Types::TEXT,
335  [
336  'notnull' => false,
337  'length' => 65535,
338  ]
339  );
340  }
341 
342  // t3_origuid column
343  if (!empty($tableDefinition['ctrl']['origUid'])
344  && !$this->‪isColumnDefinedForTable($tables, $tableName, $tableDefinition['ctrl']['origUid'])
345  ) {
346  $tables[$tablePosition]->addColumn(
347  $this->‪quote($tableDefinition['ctrl']['origUid']),
348  Types::INTEGER,
349  [
350  'default' => 0,
351  'notnull' => true,
352  'unsigned' => true,
353  ]
354  );
355  }
356 
357  // l18n_diffsource column
358  if (!empty($tableDefinition['ctrl']['transOrigDiffSourceField'])
359  && !$this->‪isColumnDefinedForTable($tables, $tableName, $tableDefinition['ctrl']['transOrigDiffSourceField'])
360  ) {
361  $tables[$tablePosition]->addColumn(
362  $this->‪quote($tableDefinition['ctrl']['transOrigDiffSourceField']),
363  Types::BLOB,
364  [
365  // mediumblob (16MB) on mysql
366  'length' => 16777215,
367  'notnull' => false,
368  ]
369  );
370  }
371 
372  // workspaces t3ver_oid column
373  if (!empty($tableDefinition['ctrl']['versioningWS'])
374  && (bool)$tableDefinition['ctrl']['versioningWS'] === true
375  && !$this->‪isColumnDefinedForTable($tables, $tableName, 't3ver_oid')
376  ) {
377  $tables[$tablePosition]->addColumn(
378  $this->‪quote('t3ver_oid'),
379  Types::INTEGER,
380  [
381  'default' => 0,
382  'notnull' => true,
383  'unsigned' => true,
384  ]
385  );
386  }
387 
388  // workspaces t3ver_wsid column
389  if (!empty($tableDefinition['ctrl']['versioningWS'])
390  && (bool)$tableDefinition['ctrl']['versioningWS'] === true
391  && !$this->‪isColumnDefinedForTable($tables, $tableName, 't3ver_wsid')
392  ) {
393  $tables[$tablePosition]->addColumn(
394  $this->‪quote('t3ver_wsid'),
395  Types::INTEGER,
396  [
397  'default' => 0,
398  'notnull' => true,
399  'unsigned' => true,
400  ]
401  );
402  }
403 
404  // workspaces t3ver_state column
405  if (!empty($tableDefinition['ctrl']['versioningWS'])
406  && (bool)$tableDefinition['ctrl']['versioningWS'] === true
407  && !$this->‪isColumnDefinedForTable($tables, $tableName, 't3ver_state')
408  ) {
409  $tables[$tablePosition]->addColumn(
410  $this->‪quote('t3ver_state'),
411  Types::SMALLINT,
412  [
413  'default' => 0,
414  'notnull' => true,
415  'unsigned' => false,
416  ]
417  );
418  }
419 
420  // workspaces t3ver_stage column
421  if (!empty($tableDefinition['ctrl']['versioningWS'])
422  && (bool)$tableDefinition['ctrl']['versioningWS'] === true
423  && !$this->‪isColumnDefinedForTable($tables, $tableName, 't3ver_stage')
424  ) {
425  $tables[$tablePosition]->addColumn(
426  $this->‪quote('t3ver_stage'),
427  Types::INTEGER,
428  [
429  'default' => 0,
430  'notnull' => true,
431  'unsigned' => false,
432  ]
433  );
434  }
435 
436  // workspaces index on t3ver_oid and t3ver_wsid fields
437  if (!empty($tableDefinition['ctrl']['versioningWS'])
438  && (bool)$tableDefinition['ctrl']['versioningWS'] === true
439  && !$this->‪isIndexDefinedForTable($tables, $tableName, 't3ver_oid')
440  ) {
441  $tables[$tablePosition]->addIndex(['t3ver_oid', 't3ver_wsid'], 't3ver_oid');
442  }
443  }
444 
445  return $tables;
446  }
447 
451  protected function ‪enrichSingleTableFieldsFromTcaColumns($tables)
452  {
453  foreach (‪$GLOBALS['TCA'] as $tableName => $tableDefinition) {
454  // If the table is given in existing $tables list, add all fields to the first
455  // position of that table - in case it is supplied multiple times which happens
456  // if extensions add single fields to tables that have been defined in
457  // other ext_tables.sql, too.
458  $tablePosition = $this->‪getTableFirstPosition($tables, $tableName);
459 
460  // In the following, columns for TCA fields with a dedicated TCA type are
461  // added. In the unlikely case that no columns exist, we can skip the table.
462  if (!isset($tableDefinition['columns']) || !is_array($tableDefinition['columns'])) {
463  continue;
464  }
465 
466  // Add category fields for all tables, defining category columns (TCA type=category)
467  foreach ($tableDefinition['columns'] as $fieldName => $fieldConfig) {
468  if ((string)($fieldConfig['config']['type'] ?? '') !== 'category'
469  || $this->‪isColumnDefinedForTable($tables, $tableName, $fieldName)
470  ) {
471  continue;
472  }
473 
474  if (($fieldConfig['config']['relationship'] ?? '') === 'oneToMany') {
475  $tables[$tablePosition]->addColumn(
476  $this->‪quote($fieldName),
477  Types::TEXT,
478  [
479  'notnull' => false,
480  ]
481  );
482  } else {
483  $tables[$tablePosition]->addColumn(
484  $this->‪quote($fieldName),
485  Types::INTEGER,
486  [
487  'default' => 0,
488  'notnull' => true,
489  'unsigned' => true,
490  ]
491  );
492  }
493  }
494 
495  // Add datetime fields for all tables, defining datetime columns (TCA type=datetime), except
496  // those columns, which had already been added due to definition in "ctrl", e.g. "starttime".
497  foreach ($tableDefinition['columns'] as $fieldName => $fieldConfig) {
498  if ((string)($fieldConfig['config']['type'] ?? '') !== 'datetime'
499  || $this->‪isColumnDefinedForTable($tables, $tableName, $fieldName)
500  ) {
501  continue;
502  }
503 
504  if (in_array($fieldConfig['config']['dbType'] ?? '', ‪QueryHelper::getDateTimeTypes(), true)) {
505  $tables[$tablePosition]->addColumn(
506  $this->‪quote($fieldName),
507  $fieldConfig['config']['dbType'],
508  [
509  'notnull' => false,
510  ]
511  );
512  } else {
513  // int unsigned: from 1970 to 2106.
514  // int signed: from 1901 to 2038.
515  // bigint unsigned/signed: from whenever to whenever
516  //
517  // Anything like crdate,tstamp,starttime,endtime is good with
518  // "int unsigned" and can survive the 2038 apocalypse (until 2106).
519  //
520  // However, anything that has birthdates or dates
521  // from the past (sys_file_metadata.content_creation_date) was saved
522  // as a SIGNED INT. It allowed birthdays of people older than 1970,
523  // but with the downside that it ends in 2038.
524  //
525  // This is now changed to utilize BIGINT everywhere, even when smaller
526  // date ranges are requested. To reduce complexity, we specifically
527  // do not evaluate "range.upper/lower" fields and use a unified type here.
528  $tables[$tablePosition]->addColumn(
529  $this->‪quote($fieldName),
530  Types::BIGINT,
531  [
532  'default' => 0,
533  'notnull' => !($fieldConfig['config']['nullable'] ?? false),
534  'unsigned' => false,
535  ]
536  );
537  }
538  }
539 
540  // Add slug fields for all tables, defining slug columns (TCA type=slug)
541  foreach ($tableDefinition['columns'] as $fieldName => $fieldConfig) {
542  if ((string)($fieldConfig['config']['type'] ?? '') !== 'slug'
543  || $this->‪isColumnDefinedForTable($tables, $tableName, $fieldName)
544  ) {
545  continue;
546  }
547  $tables[$tablePosition]->addColumn(
548  $this->‪quote($fieldName),
549  Types::STRING,
550  [
551  'length' => 2048,
552  'notnull' => false,
553  ]
554  );
555  }
556 
557  // Add json fields for all tables, defining json columns (TCA type=json)
558  foreach ($tableDefinition['columns'] as $fieldName => $fieldConfig) {
559  if ((string)($fieldConfig['config']['type'] ?? '') !== 'json'
560  || $this->‪isColumnDefinedForTable($tables, $tableName, $fieldName)
561  ) {
562  continue;
563  }
564  $tables[$tablePosition]->addColumn(
565  $this->‪quote($fieldName),
566  Types::JSON,
567  [
568  'notnull' => false,
569  ]
570  );
571  }
572 
573  // Add uuid fields for all tables, defining uuid columns (TCA type=uuid)
574  foreach ($tableDefinition['columns'] as $fieldName => $fieldConfig) {
575  if ((string)($fieldConfig['config']['type'] ?? '') !== 'uuid'
576  || $this->‪isColumnDefinedForTable($tables, $tableName, $fieldName)
577  ) {
578  continue;
579  }
580  $tables[$tablePosition]->addColumn(
581  $this->‪quote($fieldName),
582  Types::STRING,
583  [
584  'length' => 36,
585  'default' => '',
586  'notnull' => true,
587  ]
588  );
589  }
590 
591  // Add file fields for all tables, defining file columns (TCA type=file)
592  foreach ($tableDefinition['columns'] as $fieldName => $fieldConfig) {
593  if ((string)($fieldConfig['config']['type'] ?? '') !== 'file'
594  || $this->‪isColumnDefinedForTable($tables, $tableName, $fieldName)
595  ) {
596  continue;
597  }
598  $tables[$tablePosition]->addColumn(
599  $this->‪quote($fieldName),
600  Types::INTEGER,
601  [
602  'default' => 0,
603  'notnull' => true,
604  'unsigned' => true,
605  ]
606  );
607  }
608 
609  // Add folder fields for all tables, defining file columns (TCA type=folder)
610  foreach ($tableDefinition['columns'] as $fieldName => $fieldConfig) {
611  if ((string)($fieldConfig['config']['type'] ?? '') !== 'folder'
612  || $this->‪isColumnDefinedForTable($tables, $tableName, $fieldName)
613  ) {
614  continue;
615  }
616  $tables[$tablePosition]->addColumn(
617  $this->‪quote($fieldName),
618  Types::TEXT,
619  [
620  'notnull' => false,
621  ]
622  );
623  }
624 
625  // Add email fields for all tables, defining email columns (TCA type=email)
626  foreach ($tableDefinition['columns'] as $fieldName => $fieldConfig) {
627  if ((string)($fieldConfig['config']['type'] ?? '') !== 'email'
628  || $this->‪isColumnDefinedForTable($tables, $tableName, $fieldName)
629  ) {
630  continue;
631  }
632  $isNullable = (bool)($fieldConfig['config']['nullable'] ?? false);
633  $tables[$tablePosition]->addColumn(
634  $this->‪quote($fieldName),
635  Types::STRING,
636  [
637  'length' => 255,
638  'default' => ($isNullable ? null : ''),
639  'notnull' => !$isNullable,
640  ]
641  );
642  }
643 
644  // Add check fields for all tables, defining check columns (TCA type=check)
645  foreach ($tableDefinition['columns'] as $fieldName => $fieldConfig) {
646  if ((string)($fieldConfig['config']['type'] ?? '') !== 'check'
647  || $this->‪isColumnDefinedForTable($tables, $tableName, $fieldName)
648  ) {
649  continue;
650  }
651  $tables[$tablePosition]->addColumn(
652  $this->‪quote($fieldName),
653  Types::SMALLINT,
654  [
655  'default' => 0,
656  'notnull' => true,
657  'unsigned' => true,
658  ]
659  );
660  }
661 
662  // Add file fields for all tables, defining crop columns (TCA type=imageManipulation)
663  foreach ($tableDefinition['columns'] as $fieldName => $fieldConfig) {
664  if ((string)($fieldConfig['config']['type'] ?? '') !== 'imageManipulation'
665  || $this->‪isColumnDefinedForTable($tables, $tableName, $fieldName)
666  ) {
667  continue;
668  }
669  $tables[$tablePosition]->addColumn(
670  $this->‪quote($fieldName),
671  Types::TEXT,
672  [
673  'notnull' => false,
674  ]
675  );
676  }
677 
678  // Add fields for all tables, defining language columns (TCA type=language)
679  foreach ($tableDefinition['columns'] as $fieldName => $fieldConfig) {
680  if ((string)($fieldConfig['config']['type'] ?? '') !== 'language'
681  || $this->‪isColumnDefinedForTable($tables, $tableName, $fieldName)
682  ) {
683  continue;
684  }
685  $tables[$tablePosition]->addColumn(
686  $this->‪quote($fieldName),
687  Types::INTEGER,
688  [
689  'default' => 0,
690  'notnull' => true,
691  'unsigned' => false,
692  ]
693  );
694  }
695 
696  // Add fields for all tables, defining group columns (TCA type=group)
697  foreach ($tableDefinition['columns'] as $fieldName => $fieldConfig) {
698  if ((string)($fieldConfig['config']['type'] ?? '') !== 'group'
699  || $this->‪isColumnDefinedForTable($tables, $tableName, $fieldName)
700  ) {
701  continue;
702  }
703  if (isset($fieldConfig['config']['MM'])) {
704  $tables[$tablePosition]->addColumn(
705  $this->‪quote($fieldName),
706  Types::INTEGER,
707  [
708  'default' => 0,
709  'notnull' => true,
710  'unsigned' => true,
711  ]
712  );
713  } else {
714  $tables[$tablePosition]->addColumn(
715  $this->‪quote($fieldName),
716  Types::TEXT,
717  [
718  'notnull' => false,
719  ]
720  );
721  }
722  }
723 
724  // Add fields for all tables, defining flex columns (TCA type=flex)
725  foreach ($tableDefinition['columns'] as $fieldName => $fieldConfig) {
726  if ((string)($fieldConfig['config']['type'] ?? '') !== 'flex'
727  || $this->‪isColumnDefinedForTable($tables, $tableName, $fieldName)
728  ) {
729  continue;
730  }
731  $tables[$tablePosition]->addColumn(
732  $this->‪quote($fieldName),
733  Types::TEXT,
734  [
735  'notnull' => false,
736  ]
737  );
738  }
739 
740  // Add fields for all tables, defining text columns (TCA type=text)
741  foreach ($tableDefinition['columns'] as $fieldName => $fieldConfig) {
742  if ((string)($fieldConfig['config']['type'] ?? '') !== 'text'
743  || $this->‪isColumnDefinedForTable($tables, $tableName, $fieldName)
744  ) {
745  continue;
746  }
747  $tables[$tablePosition]->addColumn(
748  $this->‪quote($fieldName),
749  Types::TEXT,
750  [
751  'notnull' => false,
752  ]
753  );
754  }
755 
756  // Add fields for all tables, defining password columns (TCA type=password)
757  foreach ($tableDefinition['columns'] as $fieldName => $fieldConfig) {
758  if ((string)($fieldConfig['config']['type'] ?? '') !== 'password'
759  || $this->‪isColumnDefinedForTable($tables, $tableName, $fieldName)
760  ) {
761  continue;
762  }
763  if ($fieldConfig['config']['nullable'] ?? false) {
764  $tables[$tablePosition]->addColumn(
765  $this->‪quote($fieldName),
766  Types::STRING,
767  [
768  'default' => null,
769  'notnull' => false,
770  ]
771  );
772  } else {
773  $tables[$tablePosition]->addColumn(
774  $this->‪quote($fieldName),
775  Types::STRING,
776  [
777  'default' => '',
778  'notnull' => true,
779  ]
780  );
781  }
782  }
783 
784  // Add fields for all tables, defining color columns (TCA type=color)
785  foreach ($tableDefinition['columns'] as $fieldName => $fieldConfig) {
786  if ((string)($fieldConfig['config']['type'] ?? '') !== 'color'
787  || $this->‪isColumnDefinedForTable($tables, $tableName, $fieldName)
788  ) {
789  continue;
790  }
791  if ($fieldConfig['config']['nullable'] ?? false) {
792  $tables[$tablePosition]->addColumn(
793  $this->‪quote($fieldName),
794  Types::STRING,
795  [
796  'length' => 7,
797  'default' => null,
798  'notnull' => false,
799  ]
800  );
801  } else {
802  $tables[$tablePosition]->addColumn(
803  $this->‪quote($fieldName),
804  Types::STRING,
805  [
806  'length' => 7,
807  'default' => '',
808  'notnull' => true,
809  ]
810  );
811  }
812  }
813 
814  // Add fields for all tables, defining radio columns (TCA type=radio)
815  foreach ($tableDefinition['columns'] as $fieldName => $fieldConfig) {
816  if ((string)($fieldConfig['config']['type'] ?? '') !== 'radio'
817  || $this->‪isColumnDefinedForTable($tables, $tableName, $fieldName)
818  ) {
819  continue;
820  }
821  $hasItemsProcFunc = ($fieldConfig['config']['itemsProcFunc'] ?? '') !== '';
822  $items = $fieldConfig['config']['items'] ?? [];
823 
824  // With itemsProcFunc we can't be sure, which values are persisted. Use type string.
825  if ($hasItemsProcFunc) {
826  $tables[$tablePosition]->addColumn(
827  $this->‪quote($fieldName),
828  Types::STRING,
829  [
830  'length' => 255,
831  'default' => '',
832  'notnull' => true,
833  ]
834  );
835  continue;
836  }
837 
838  // If no items are configured, use type string to be safe for values added directly.
839  if ($items === []) {
840  $tables[$tablePosition]->addColumn(
841  $this->‪quote($fieldName),
842  Types::STRING,
843  [
844  'length' => 255,
845  'default' => '',
846  'notnull' => true,
847  ]
848  );
849  continue;
850  }
851 
852  // If only one value is NOT an integer use type string.
853  foreach ($items as $item) {
854  if (!‪MathUtility::canBeInterpretedAsInteger($item['value'])) {
855  $tables[$tablePosition]->addColumn(
856  $this->‪quote($fieldName),
857  Types::STRING,
858  [
859  'length' => 255,
860  'default' => '',
861  'notnull' => true,
862  ]
863  );
864  // continue with next $tableDefinition['columns']
865  // see: DefaultTcaSchemaTest->enrichAddsRadioStringVerifyThatCorrectLoopIsContinued()
866  continue 2;
867  }
868  }
869 
870  // Use integer type.
871  $allValues = array_map(fn(array $item): int => (int)$item['value'], $items);
872  $minValue = min($allValues);
873  $maxValue = max($allValues);
874  // Try to safe some bytes - can be reconsidered to simply use Types::INTEGER.
875  $integerType = ($minValue >= -32768 && $maxValue < 32768)
876  ? Types::SMALLINT
877  : Types::INTEGER;
878  $tables[$tablePosition]->addColumn(
879  $this->‪quote($fieldName),
880  $integerType,
881  [
882  'default' => 0,
883  'notnull' => true,
884  ]
885  );
886 
887  // Keep the house clean.
888  unset($items, $allValues, $minValue, $maxValue, $integerType);
889  }
890 
891  // Add fields for all tables, defining link columns (TCA type=link)
892  foreach ($tableDefinition['columns'] as $fieldName => $fieldConfig) {
893  if ((string)($fieldConfig['config']['type'] ?? '') !== 'link'
894  || $this->‪isColumnDefinedForTable($tables, $tableName, $fieldName)
895  ) {
896  continue;
897  }
898  $nullable = $fieldConfig['config']['nullable'] ?? false;
899  $tables[$tablePosition]->addColumn(
900  $this->‪quote($fieldName),
901  Types::STRING,
902  [
903  'length' => 2048,
904  'default' => $nullable ? null : '',
905  'notnull' => !$nullable,
906  ]
907  );
908  }
909 
910  // Add fields for all tables, defining inline columns (TCA type=inline)
911  foreach ($tableDefinition['columns'] as $fieldName => $fieldConfig) {
912  if ((string)($fieldConfig['config']['type'] ?? '') !== 'inline'
913  || $this->‪isColumnDefinedForTable($tables, $tableName, $fieldName)
914  ) {
915  continue;
916  }
917  if (($fieldConfig['config']['MM'] ?? '') !== '' || ($fieldConfig['config']['foreign_field'] ?? '') !== '') {
918  // Parent "count" field
919  $tables[$tablePosition]->addColumn(
920  $this->‪quote($fieldName),
921  Types::INTEGER,
922  [
923  'default' => 0,
924  'notnull' => true,
925  'unsigned' => true,
926  ]
927  );
928  } else {
929  // Inline "csv"
930  $tables[$tablePosition]->addColumn(
931  $this->‪quote($fieldName),
932  Types::STRING,
933  [
934  'default' => '',
935  'notnull' => true,
936  'length' => 255,
937  ]
938  );
939  }
940  if (($fieldConfig['config']['foreign_field'] ?? '') !== '') {
941  // Add definition for "foreign_field" (contains parent uid) in the child table if it is not defined
942  // in child TCA or if it is "just" a "passthrough" field, and not manually configured in ext_tables.sql
943  $childTable = $fieldConfig['config']['foreign_table'];
944  $childTablePosition = $this->‪getTableFirstPosition($tables, $childTable);
945  $childTableForeignFieldName = $fieldConfig['config']['foreign_field'];
946  $childTableForeignFieldConfig = ‪$GLOBALS['TCA'][$childTable]['columns'][$childTableForeignFieldName] ?? [];
947  if (($childTableForeignFieldConfig === [] || ($childTableForeignFieldConfig['config']['type'] ?? '') === 'passthrough')
948  && !$this->‪isColumnDefinedForTable($tables, $childTable, $childTableForeignFieldName)
949  ) {
950  $tables[$childTablePosition]->addColumn(
951  $this->‪quote($childTableForeignFieldName),
952  Types::INTEGER,
953  [
954  'default' => 0,
955  'notnull' => true,
956  'unsigned' => true,
957  ]
958  );
959  }
960  // Add definition for "foreign_table_field" (contains name of parent table) in the child table if it is not
961  // defined in child TCA or if it is "just" a "passthrough" field, and not manually configured in ext_tables.sql
962  $childTableForeignTableFieldName = $fieldConfig['config']['foreign_table_field'] ?? '';
963  $childTableForeignTableFieldConfig = ‪$GLOBALS['TCA'][$childTable]['columns'][$childTableForeignTableFieldName] ?? [];
964  if ($childTableForeignTableFieldName !== ''
965  && ($childTableForeignTableFieldConfig === [] || ($childTableForeignTableFieldConfig['config']['type'] ?? '') === 'passthrough')
966  && !$this->‪isColumnDefinedForTable($tables, $childTable, $childTableForeignTableFieldName)
967  ) {
968  $tables[$childTablePosition]->addColumn(
969  $this->‪quote($childTableForeignTableFieldName),
970  Types::STRING,
971  [
972  'default' => '',
973  'notnull' => true,
974  'length' => 255,
975  ]
976  );
977  }
978  }
979  }
980 
981  // Add fields for all tables, defining number columns (TCA type=number)
982  $tableConnectionPlatform = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($tableName)->getDatabasePlatform();
983  foreach ($tableDefinition['columns'] as $fieldName => $fieldConfig) {
984  if ((string)($fieldConfig['config']['type'] ?? '') !== 'number'
985  || $this->‪isColumnDefinedForTable($tables, $tableName, $fieldName)
986  ) {
987  continue;
988  }
989  $type = ($fieldConfig['config']['format'] ?? '') === 'decimal' ? Types::DECIMAL : Types::INTEGER;
990  $nullable = $fieldConfig['config']['nullable'] ?? false;
991  $lowerRange = $fieldConfig['config']['range']['lower'] ?? -1;
992  // Integer type for all database platforms.
993  if ($type === Types::INTEGER) {
994  $tables[$tablePosition]->addColumn(
995  $this->‪quote($fieldName),
996  Types::INTEGER,
997  [
998  'default' => $nullable === true ? null : 0,
999  'notnull' => !$nullable,
1000  'unsigned' => $lowerRange >= 0,
1001  ]
1002  );
1003  continue;
1004  }
1005  // SQLite internally defines NUMERIC() fields as real, and therefore as floating numbers. pdo_sqlite
1006  // then returns PHP float which can lead to rounding issues. See https://bugs.php.net/bug.php?id=81397
1007  // for more details. We create a 'string' field on SQLite as workaround.
1008  // @todo Database schema should be created with MySQL in mind and not mixed. Transforming to the
1009  // concrete database platform is handled in the database compare area. Sadly, this is not
1010  // possible right now but upcoming preparation towards doctrine/dbal 4 makes it possible to
1011  // move this "hack" to a different place.
1012  if ($tableConnectionPlatform instanceof DoctrineSQLitePlatform) {
1013  $tables[$tablePosition]->addColumn(
1014  $this->‪quote($fieldName),
1015  Types::STRING,
1016  [
1017  'default' => $nullable === true ? null : '0.00',
1018  'notnull' => !$nullable,
1019  'length' => 255,
1020  ]
1021  );
1022  continue;
1023  }
1024  // Decimal for all supported platforms except SQLite
1025  $tables[$tablePosition]->addColumn(
1026  $this->‪quote($fieldName),
1027  Types::DECIMAL,
1028  [
1029  'default' => $nullable === true ? null : 0.00,
1030  'notnull' => !$nullable,
1031  'unsigned' => $lowerRange >= 0,
1032  'precision' => 10,
1033  'scale' => 2,
1034  ]
1035  );
1036  }
1037  // Cleanup
1038  unset($tableConnectionPlatform, $type, $nullable, $lowerRange);
1039  }
1040 
1041  return $tables;
1042  }
1043 
1050  protected function ‪enrichMmTables($tables): array
1051  {
1052  foreach (‪$GLOBALS['TCA'] as $tableName => $tableDefinition) {
1053  if (!is_array($tableDefinition['columns'] ?? false)) {
1054  // TCA definition in general is broken if there are no specified columns. Skip to be sure here.
1055  continue;
1056  }
1057  foreach ($tableDefinition['columns'] as $tcaColumn) {
1058  if (
1059  !is_array($tcaColumn['config'] ?? false)
1060  || !is_string($tcaColumn['config']['type'] ?? false)
1061  || !in_array($tcaColumn['config']['type'], ['select', 'group', 'inline', 'category'], true)
1062  || !is_string($tcaColumn['config']['MM'] ?? false)
1063  // Consider this mm only if looking at it from the local side
1064  || ($tcaColumn['config']['MM_opposite_field'] ?? false)
1065  ) {
1066  // Broken TCA or not of expected type, or no MM, or foreign side
1067  continue;
1068  }
1069  $mmTableName = $tcaColumn['config']['MM'];
1070  try {
1071  // If the mm table is defined, work with it. Else add at and.
1072  $tablePosition = $this->‪getTableFirstPosition($tables, $mmTableName);
1074  $tablePosition = array_key_last($tables) + 1;
1075  $tables[$tablePosition] = GeneralUtility::makeInstance(
1076  Table::class,
1077  $mmTableName
1078  );
1079  }
1080 
1081  // Add 'uid' field with primary key if multiple is set: 'multiple' allows using a left or right
1082  // side more than once in a relation which would lead to duplicate primary key entries. To
1083  // avoid this, we add a uid column and make it primary key instead.
1084  $needsUid = (bool)($tcaColumn['config']['multiple'] ?? false);
1085  if ($needsUid && !$this->‪isColumnDefinedForTable($tables, $mmTableName, 'uid')) {
1086  $tables[$tablePosition]->addColumn(
1087  $this->‪quote('uid'),
1088  Types::INTEGER,
1089  [
1090  'notnull' => true,
1091  'unsigned' => true,
1092  'autoincrement' => true,
1093  ]
1094  );
1095  $tables[$tablePosition]->setPrimaryKey(['uid']);
1096  }
1097 
1098  if (!$this->‪isColumnDefinedForTable($tables, $mmTableName, 'uid_local')) {
1099  $tables[$tablePosition]->addColumn(
1100  $this->‪quote('uid_local'),
1101  Types::INTEGER,
1102  [
1103  'default' => 0,
1104  'notnull' => true,
1105  'unsigned' => true,
1106  ]
1107  );
1108  }
1109  if (!$this->‪isIndexDefinedForTable($tables, $mmTableName, 'uid_local')) {
1110  $tables[$tablePosition]->addIndex(['uid_local'], 'uid_local');
1111  }
1112 
1113  if (!$this->‪isColumnDefinedForTable($tables, $mmTableName, 'uid_foreign')) {
1114  $tables[$tablePosition]->addColumn(
1115  $this->‪quote('uid_foreign'),
1116  Types::INTEGER,
1117  [
1118  'default' => 0,
1119  'notnull' => true,
1120  'unsigned' => true,
1121  ]
1122  );
1123  }
1124  if (!$this->‪isIndexDefinedForTable($tables, $mmTableName, 'uid_foreign')) {
1125  $tables[$tablePosition]->addIndex(['uid_foreign'], 'uid_foreign');
1126  }
1127 
1128  if (!$this->‪isColumnDefinedForTable($tables, $mmTableName, 'sorting')) {
1129  $tables[$tablePosition]->addColumn(
1130  $this->‪quote('sorting'),
1131  Types::INTEGER,
1132  [
1133  'default' => 0,
1134  'notnull' => true,
1135  'unsigned' => true,
1136  ]
1137  );
1138  }
1139  if (!$this->‪isColumnDefinedForTable($tables, $mmTableName, 'sorting_foreign')) {
1140  $tables[$tablePosition]->addColumn(
1141  $this->‪quote('sorting_foreign'),
1142  Types::INTEGER,
1143  [
1144  'default' => 0,
1145  'notnull' => true,
1146  'unsigned' => true,
1147  ]
1148  );
1149  }
1150 
1151  if (!empty($tcaColumn['config']['MM_oppositeUsage'])) {
1152  // This local table can be the target of multiple foreign tables and table fields. The mm table
1153  // thus needs two further fields to specify which foreign/table field combination links is used.
1154  // Those are stored in two additional fields called "tablenames" and "fieldname".
1155  if (!$this->‪isColumnDefinedForTable($tables, $mmTableName, 'tablenames')) {
1156  $tables[$tablePosition]->addColumn(
1157  $this->‪quote('tablenames'),
1158  Types::STRING,
1159  [
1160  'default' => '',
1161  'length' => 64,
1162  'notnull' => true,
1163  ]
1164  );
1165  }
1166  if (!$this->‪isColumnDefinedForTable($tables, $mmTableName, 'fieldname')) {
1167  $tables[$tablePosition]->addColumn(
1168  $this->‪quote('fieldname'),
1169  Types::STRING,
1170  [
1171  'default' => '',
1172  'length' => 64,
1173  'notnull' => true,
1174  ]
1175  );
1176  }
1177  }
1178 
1179  // Primary key handling: If there is a uid field, PK has been added above already.
1180  // Otherwise, the PK combination is either "uid_local, uid_foreign", or
1181  // "uid_local, uid_foreign, tablenames, fieldname" if this is a multi-foreign setup.
1182  if (!$needsUid && $tables[$tablePosition]->getPrimaryKey() === null && !empty($tcaColumn['config']['MM_oppositeUsage'])) {
1183  $tables[$tablePosition]->setPrimaryKey(['uid_local', 'uid_foreign', 'tablenames', 'fieldname']);
1184  } elseif (!$needsUid && $tables[$tablePosition]->getPrimaryKey() === null) {
1185  $tables[$tablePosition]->setPrimaryKey(['uid_local', 'uid_foreign']);
1186  }
1187  }
1188  }
1189  return $tables;
1190  }
1191 
1198  protected function ‪isColumnDefinedForTable(array $tables, string $tableName, string $fieldName): bool
1199  {
1200  foreach ($tables as $table) {
1201  if ($table->getName() !== $tableName) {
1202  continue;
1203  }
1204  $columns = $table->getColumns();
1205  foreach ($columns as $column) {
1206  if ($column->getName() === $fieldName) {
1207  return true;
1208  }
1209  }
1210  }
1211  return false;
1212  }
1213 
1220  protected function ‪isIndexDefinedForTable(array $tables, string $tableName, string $indexName): bool
1221  {
1222  foreach ($tables as $table) {
1223  if ($table->getName() !== $tableName) {
1224  continue;
1225  }
1226  $indexes = $table->getIndexes();
1227  foreach ($indexes as $index) {
1228  if ($index->getName() === $indexName) {
1229  return true;
1230  }
1231  }
1232  }
1233  return false;
1234  }
1235 
1248  protected function ‪getTableFirstPosition(array $tables, string $tableName): int
1249  {
1250  foreach ($tables as $position => $table) {
1251  if ($table->getName() === $tableName) {
1252  return (int)$position;
1253  }
1254  }
1255  throw new ‪DefaultTcaSchemaTablePositionException('Table ' . $tableName . ' not found in schema list', 1527854474);
1256  }
1257 
1258  protected function ‪quote(string ‪$identifier): string
1259  {
1260  return '`' . ‪$identifier . '`';
1261  }
1262 }
‪TYPO3\CMS\Core\Database\Schema\DefaultTcaSchema\getTableFirstPosition
‪getTableFirstPosition(array $tables, string $tableName)
Definition: DefaultTcaSchema.php:1248
‪TYPO3\CMS\Core\Database\Schema\DefaultTcaSchema\isIndexDefinedForTable
‪isIndexDefinedForTable(array $tables, string $tableName, string $indexName)
Definition: DefaultTcaSchema.php:1220
‪TYPO3\CMS\Core\Database\Schema\DefaultTcaSchema\quote
‪quote(string $identifier)
Definition: DefaultTcaSchema.php:1258
‪TYPO3\CMS\Core\Database\Schema\DefaultTcaSchema\isColumnDefinedForTable
‪isColumnDefinedForTable(array $tables, string $tableName, string $fieldName)
Definition: DefaultTcaSchema.php:1198
‪TYPO3\CMS\Core\Database\Schema
Definition: Comparator.php:18
‪TYPO3\CMS\Core\Database\Schema\DefaultTcaSchema
Definition: DefaultTcaSchema.php:43
‪TYPO3\CMS\Core\Utility\MathUtility\canBeInterpretedAsInteger
‪static bool canBeInterpretedAsInteger(mixed $var)
Definition: MathUtility.php:69
‪TYPO3\CMS\Core\Database\Query\QueryHelper
Definition: QueryHelper.php:32
‪TYPO3\CMS\Core\Database\Query\QueryHelper\getDateTimeTypes
‪static array getDateTimeTypes()
Definition: QueryHelper.php:211
‪TYPO3\CMS\Core\Database\Schema\DefaultTcaSchema\enrichSingleTableFieldsFromTcaCtrl
‪enrichSingleTableFieldsFromTcaCtrl($tables)
Definition: DefaultTcaSchema.php:80
‪TYPO3\CMS\Core\Database\Schema\DefaultTcaSchema\enrichMmTables
‪enrichMmTables($tables)
Definition: DefaultTcaSchema.php:1050
‪$GLOBALS
‪$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['adminpanel']['modules']
Definition: ext_localconf.php:25
‪TYPO3\CMS\Core\Utility\MathUtility
Definition: MathUtility.php:24
‪TYPO3\CMS\Core\Database\ConnectionPool
Definition: ConnectionPool.php:48
‪TYPO3\CMS\Core\Database\Schema\DefaultTcaSchema\enrichSingleTableFieldsFromTcaColumns
‪enrichSingleTableFieldsFromTcaColumns($tables)
Definition: DefaultTcaSchema.php:451
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:51
‪TYPO3\CMS\Core\Database\Schema\DefaultTcaSchema\enrich
‪Table[] enrich(array $tables)
Definition: DefaultTcaSchema.php:53
‪TYPO3\CMS\Webhooks\Message\$identifier
‪identifier readonly string $identifier
Definition: FileAddedMessage.php:37
‪TYPO3\CMS\Core\Database\Schema\Exception\DefaultTcaSchemaTablePositionException
Definition: DefaultTcaSchemaTablePositionException.php:27