Passed
Push — master ( cf10c6...acb8ba )
by
unknown
20:02
created

removeShowRemovedLocalizationRecords()   B

Complexity

Conditions 7
Paths 3

Size

Total Lines 20
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

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