Passed
Push — master ( 03f78e...954a3f )
by
unknown
12:06
created

TcaMigration::migrate()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 18
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 14
dl 0
loc 18
rs 9.7998
c 0
b 0
f 0
cc 1
nc 1
nop 1
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
18
namespace TYPO3\CMS\Core\Migrations;
19
20
/**
21
 * Migrate TCA from old to new syntax. Used in bootstrap and Flex Form Data Structures.
22
 *
23
 * @internal Class and API may change any time.
24
 */
25
class TcaMigration
26
{
27
    /**
28
     * Accumulate migration messages
29
     *
30
     * @var array
31
     */
32
    protected $messages = [];
33
34
    /**
35
     * Run some general TCA validations, then migrate old TCA to new TCA.
36
     *
37
     * This class is typically called within bootstrap with empty caches after all TCA
38
     * files from extensions have been loaded. The migration is then applied and
39
     * the migrated result is cached.
40
     * For flex form TCA, this class is called dynamically if opening a record in the backend.
41
     *
42
     * See unit tests for details.
43
     *
44
     * @param array $tca
45
     * @return array
46
     */
47
    public function migrate(array $tca): array
48
    {
49
        $this->validateTcaType($tca);
50
51
        $tca = $this->migrateColumnsConfig($tca);
52
        $tca = $this->migrateLocalizeChildrenAtParentLocalization($tca);
53
        $tca = $this->migratePagesLanguageOverlayRemoval($tca);
54
        $tca = $this->removeSelIconFieldPath($tca);
55
        $tca = $this->removeSetToDefaultOnCopy($tca);
56
        $tca = $this->sanitizeControlSectionIntegrity($tca);
57
        $tca = $this->removeEnableMultiSelectFilterTextfieldConfiguration($tca);
58
        $tca = $this->removeExcludeFieldForTransOrigPointerField($tca);
59
        $tca = $this->removeShowRecordFieldListField($tca);
60
        $tca = $this->removeWorkspacePlaceholderShadowColumnsConfiguration($tca);
61
        $tca = $this->migrateLanguageFieldToTcaTypeLanguage($tca);
62
        $tca = $this->migrateSpecialLanguagesToTcaTypeLanguage($tca);
63
64
        return $tca;
65
    }
66
67
    /**
68
     * Get messages of migrated fields. Can be used for deprecation messages after migrate() was called.
69
     *
70
     * @return array Migration messages
71
     */
72
    public function getMessages(): array
73
    {
74
        return $this->messages;
75
    }
76
77
    /**
78
     * Check for required TCA configuration
79
     *
80
     * @param array $tca Incoming TCA
81
     */
82
    protected function validateTcaType(array $tca)
83
    {
84
        foreach ($tca as $table => $tableDefinition) {
85
            if (!isset($tableDefinition['columns']) || !is_array($tableDefinition['columns'])) {
86
                continue;
87
            }
88
            foreach ($tableDefinition['columns'] as $fieldName => $fieldConfig) {
89
                if (isset($fieldConfig['config']) && is_array($fieldConfig['config']) && empty($fieldConfig['config']['type'])) {
90
                    throw new \UnexpectedValueException(
91
                        'Missing "type" in TCA of field "[\'' . $table . '\'][\'' . $fieldName . '\'][\'config\']".',
92
                        1482394401
93
                    );
94
                }
95
            }
96
        }
97
    }
98
99
    /**
100
     * Find columns fields that don't have a 'config' section at all, add
101
     * ['config']['type'] = 'none'; for those to enforce config
102
     *
103
     * @param array $tca Incoming TCA
104
     * @return array
105
     */
106
    protected function migrateColumnsConfig(array $tca): array
107
    {
108
        foreach ($tca as $table => &$tableDefinition) {
109
            if (!isset($tableDefinition['columns']) || !is_array($tableDefinition['columns'])) {
110
                continue;
111
            }
112
            foreach ($tableDefinition['columns'] as $fieldName => &$fieldConfig) {
113
                if ((!isset($fieldConfig['config']) || !is_array($fieldConfig['config'])) && !isset($fieldConfig['type'])) {
114
                    $fieldConfig['config'] = [
115
                        'type' => 'none',
116
                    ];
117
                    $this->messages[] = 'TCA table "' . $table . '" columns field "' . $fieldName . '"'
118
                        . ' had no mandatory "config" section. This has been added with default type "none":'
119
                        . ' TCA "' . $table . '[\'columns\'][\'' . $fieldName . '\'][\'config\'][\'type\'] = \'none\'"';
120
                }
121
            }
122
        }
123
        return $tca;
124
    }
125
126
    /**
127
     * Option $TCA[$table]['columns'][$columnName]['config']['behaviour']['localizeChildrenAtParentLocalization']
128
     * is always on, so this option can be removed.
129
     *
130
     * @param array $tca
131
     * @return array the modified TCA structure
132
     */
133
    protected function migrateLocalizeChildrenAtParentLocalization(array $tca): array
134
    {
135
        foreach ($tca as $table => &$tableDefinition) {
136
            if (!isset($tableDefinition['columns']) || !is_array($tableDefinition['columns'])) {
137
                continue;
138
            }
139
            foreach ($tableDefinition['columns'] as $fieldName => &$fieldConfig) {
140
                if (($fieldConfig['config']['type'] ?? null) !== 'inline') {
141
                    continue;
142
                }
143
144
                $localizeChildrenAtParentLocalization = ($fieldConfig['config']['behaviour']['localizeChildrenAtParentLocalization'] ?? null);
145
                if ($localizeChildrenAtParentLocalization === null) {
146
                    continue;
147
                }
148
149
                if ($localizeChildrenAtParentLocalization) {
150
                    $this->messages[] = 'The TCA setting \'localizeChildrenAtParentLocalization\' is deprecated '
151
                        . ' and should be removed from TCA for ' . $table . '[\'columns\']'
152
                        . '[\'' . $fieldName . '\'][\'config\'][\'behaviour\'][\'localizeChildrenAtParentLocalization\']';
153
                } else {
154
                    $this->messages[] = 'The TCA setting \'localizeChildrenAtParentLocalization\' is deprecated '
155
                        . ', as this functionality is always enabled. The option should be removed from TCA for '
156
                        . $table . '[\'columns\'][\'' . $fieldName . '\'][\'config\'][\'behaviour\']'
157
                        . '[\'localizeChildrenAtParentLocalization\']';
158
                }
159
                unset($fieldConfig['config']['behaviour']['localizeChildrenAtParentLocalization']);
160
            }
161
        }
162
        return $tca;
163
    }
164
165
    /**
166
     * Removes $TCA['pages_language_overlay'] if defined.
167
     *
168
     * @param array $tca
169
     * @return array the modified TCA structure
170
     */
171
    protected function migratePagesLanguageOverlayRemoval(array $tca)
172
    {
173
        if (isset($tca['pages_language_overlay'])) {
174
            $this->messages[] = 'The TCA table \'pages_language_overlay\' is'
175
                . ' not used anymore and has been removed automatically in'
176
                . ' order to avoid negative side-effects.';
177
            unset($tca['pages_language_overlay']);
178
        }
179
        return $tca;
180
    }
181
182
    /**
183
     * Removes configuration removeEnableMultiSelectFilterTextfield
184
     *
185
     * @param array $tca
186
     * @return array the modified TCA structure
187
     */
188
    protected function removeEnableMultiSelectFilterTextfieldConfiguration(array $tca): array
189
    {
190
        foreach ($tca as $table => &$tableDefinition) {
191
            if (!isset($tableDefinition['columns']) || !is_array($tableDefinition['columns'])) {
192
                continue;
193
            }
194
            foreach ($tableDefinition['columns'] as $fieldName => &$fieldConfig) {
195
                if (!isset($fieldConfig['config']['enableMultiSelectFilterTextfield'])) {
196
                    continue;
197
                }
198
199
                $this->messages[] = 'The TCA setting \'enableMultiSelectFilterTextfield\' is deprecated '
200
                    . ' and should be removed from TCA for ' . $table . '[\'columns\']'
201
                    . '[\'' . $fieldName . '\'][\'config\'][\'enableMultiSelectFilterTextfield\']';
202
                unset($fieldConfig['config']['enableMultiSelectFilterTextfield']);
203
            }
204
        }
205
        return $tca;
206
    }
207
208
    /**
209
     * Removes $TCA[$mytable][ctrl][selicon_field_path]
210
     *
211
     * @param array $tca
212
     * @return array the modified TCA structure
213
     */
214
    protected function removeSelIconFieldPath(array $tca): array
215
    {
216
        foreach ($tca as $table => &$configuration) {
217
            if (isset($configuration['ctrl']['selicon_field_path'])) {
218
                $this->messages[] = 'The TCA table \'' . $table . '\' defines '
219
                    . '[ctrl][selicon_field_path] which should be removed from TCA, '
220
                    . 'as it is not in use anymore.';
221
                unset($configuration['ctrl']['selicon_field_path']);
222
            }
223
        }
224
        return $tca;
225
    }
226
227
    /**
228
     * Removes $TCA[$mytable][ctrl][setToDefaultOnCopy]
229
     *
230
     * @param array $tca
231
     * @return array the modified TCA structure
232
     */
233
    protected function removeSetToDefaultOnCopy(array $tca): array
234
    {
235
        foreach ($tca as $table => &$configuration) {
236
            if (isset($configuration['ctrl']['setToDefaultOnCopy'])) {
237
                $this->messages[] = 'The TCA table \'' . $table . '\' defines '
238
                    . '[ctrl][setToDefaultOnCopy] which should be removed from TCA, '
239
                    . 'as it is not in use anymore.';
240
                unset($configuration['ctrl']['setToDefaultOnCopy']);
241
            }
242
        }
243
        return $tca;
244
    }
245
246
    /**
247
     * Ensures that system internal columns that are required for data integrity
248
     * (e.g. localize or copy a record) are available in case they have been defined
249
     * in $GLOBALS['TCA'][<table-name>]['ctrl'].
250
     *
251
     * The list of references to usages below is not necessarily complete.
252
     *
253
     * @param array $tca
254
     * @return array
255
     *
256
     * @see \TYPO3\CMS\Core\DataHandling\DataHandler::fillInFieldArray()
257
     */
258
    protected function sanitizeControlSectionIntegrity(array $tca): array
259
    {
260
        $defaultControlSectionColumnConfig = [
261
            'type' => 'passthrough',
262
            'default' => 0,
263
        ];
264
        $controlSectionNames = [
265
            'origUid' => $defaultControlSectionColumnConfig,
266
            'languageField' => [
267
                'type' => 'language'
268
            ],
269
            'transOrigPointerField' => $defaultControlSectionColumnConfig,
270
            'translationSource' => $defaultControlSectionColumnConfig,
271
        ];
272
273
        foreach ($tca as $tableName => &$configuration) {
274
            foreach ($controlSectionNames as $controlSectionName => $controlSectionColumnConfig) {
275
                $columnName = $configuration['ctrl'][$controlSectionName] ?? null;
276
                if (empty($columnName) || !empty($configuration['columns'][$columnName])) {
277
                    continue;
278
                }
279
                $configuration['columns'][$columnName] = [
280
                    'config' => $controlSectionColumnConfig
281
                ];
282
            }
283
        }
284
        return $tca;
285
    }
286
287
    /**
288
     * Removes $TCA[$mytable][columns][_transOrigPointerField_][exclude] if defined
289
     *
290
     * @param array $tca
291
     *
292
     * @return array
293
     */
294
    protected function removeExcludeFieldForTransOrigPointerField(array $tca): array
295
    {
296
        foreach ($tca as $table => &$configuration) {
297
            if (isset($configuration['ctrl']['transOrigPointerField'],
298
                $configuration['columns'][$configuration['ctrl']['transOrigPointerField']]['exclude'])
299
            ) {
300
                $this->messages[] = 'The \'' . $table . '\' TCA tables transOrigPointerField '
301
                    . '\'' . $configuration['ctrl']['transOrigPointerField'] . '\' is defined '
302
                    . ' as excluded field which is no longer needed and should therefore be removed. ';
303
                unset($configuration['columns'][$configuration['ctrl']['transOrigPointerField']]['exclude']);
304
            }
305
        }
306
307
        return $tca;
308
    }
309
310
    /**
311
     * Removes $TCA[$mytable]['interface']['showRecordFieldList'] and also $TCA[$mytable]['interface']
312
     * if `showRecordFieldList` was the only key in the array.
313
     *
314
     * @param array $tca
315
     * @return array
316
     */
317
    protected function removeShowRecordFieldListField(array $tca): array
318
    {
319
        foreach ($tca as $table => &$configuration) {
320
            if (!isset($configuration['interface']['showRecordFieldList'])) {
321
                continue;
322
            }
323
            $this->messages[] = 'The \'' . $table . '\' TCA configuration \'showRecordFieldList\''
324
                . ' inside the section \'interface\' is not evaluated anymore and should therefore be removed.';
325
            unset($configuration['interface']['showRecordFieldList']);
326
            if ($configuration['interface'] === []) {
327
                unset($configuration['interface']);
328
            }
329
        }
330
331
        return $tca;
332
    }
333
334
    /**
335
     * Removes $TCA[$mytable][ctrl][shadowColumnsForMovePlaceholders]
336
     * and $TCA[$mytable][ctrl][shadowColumnsForNewPlaceholders]
337
     *
338
     * @param array $tca
339
     * @return array the modified TCA structure
340
     */
341
    protected function removeWorkspacePlaceholderShadowColumnsConfiguration(array $tca): array
342
    {
343
        foreach ($tca as $table => &$configuration) {
344
            if (isset($configuration['ctrl']['shadowColumnsForNewPlaceholders'])) {
345
                $this->messages[] = 'The TCA table \'' . $table . '\' defines '
346
                    . '[ctrl][shadowColumnsForNewPlaceholders] which should be removed from TCA, '
347
                    . 'as it is not in use anymore.';
348
                unset($configuration['ctrl']['shadowColumnsForNewPlaceholders']);
349
            }
350
            if (isset($configuration['ctrl']['shadowColumnsForMovePlaceholders'])) {
351
                $this->messages[] = 'The TCA table \'' . $table . '\' defines '
352
                    . '[ctrl][shadowColumnsForMovePlaceholders] which should be removed from TCA, '
353
                    . 'as it is not in use anymore.';
354
                unset($configuration['ctrl']['shadowColumnsForMovePlaceholders']);
355
            }
356
        }
357
        return $tca;
358
    }
359
360
    /**
361
     * Replaces $TCA[$mytable][columns][$TCA[$mytable][ctrl][languageField]][config] with
362
     * $TCA[$mytable][columns][$TCA[$mytable][ctrl][languageField]][config][type] = 'language'
363
     *
364
     * @param array $tca
365
     * @return array
366
     */
367
    protected function migrateLanguageFieldToTcaTypeLanguage(array $tca): array
368
    {
369
        foreach ($tca as $table => &$configuration) {
370
            if (isset($configuration['ctrl']['languageField'], $configuration['columns'][$configuration['ctrl']['languageField']])
371
                && ($configuration['columns'][$configuration['ctrl']['languageField']]['config']['type'] ?? '') !== 'language'
372
            ) {
373
                $this->messages[] = 'The TCA field \'' . $configuration['ctrl']['languageField'] . '\' '
374
                    . 'of table \'' . $table . '\' is defined as the \'languageField\' and should '
375
                    . 'therefore use the TCA type \'language\' instead of TCA type \'select\' with '
376
                    . '\'foreign_table=sys_language\' or \'special=languages\'.';
377
                $configuration['columns'][$configuration['ctrl']['languageField']]['config'] = [
378
                    'type' => 'language'
379
                ];
380
            }
381
        }
382
383
        return $tca;
384
    }
385
386
    /**
387
     * Replaces $TCA[$mytable][columns][field][config][special] = 'languages' with
388
     * $TCA[$mytable][columns][field][config][type] = 'language'
389
     *
390
     * @param array $tca
391
     * @return array
392
     */
393
    protected function migrateSpecialLanguagesToTcaTypeLanguage(array $tca): array
394
    {
395
        foreach ($tca as $table => &$tableDefinition) {
396
            if (!isset($tableDefinition['columns']) || !is_array($tableDefinition['columns'])) {
397
                continue;
398
            }
399
            foreach ($tableDefinition['columns'] as $fieldName => &$fieldConfig) {
400
                if ((string)($fieldConfig['config']['type'] ?? '') !== 'select'
401
                    || (string)($fieldConfig['config']['special'] ?? '') !== 'languages'
402
                ) {
403
                    continue;
404
                }
405
                $this->messages[] = 'The TCA field \'' . $fieldName . '\' of table \'' . $table . '\' is '
406
                    . 'defined as type \'select\' with the \'special=languages\' option. This is not '
407
                    . 'evaluated anymore and should be replaced by the TCA type \'language\'.';
408
                $fieldConfig['config'] = [
409
                    'type' => 'language'
410
                ];
411
            }
412
        }
413
414
        return $tca;
415
    }
416
}
417