Completed
Push — master ( 7c1fce...baf93a )
by
unknown
18:15
created

BackendLayoutView   F

Complexity

Total Complexity 65

Size/Duplication

Total Lines 474
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 172
dl 0
loc 474
rs 3.2
c 0
b 0
f 0
wmc 65

21 Methods

Rating   Name   Duplication   Size   Complexity  
B getColPosListItemsParsed() 0 19 7
A addItems() 0 20 6
A setDataProviderCollection() 0 3 1
A getDataProviderCollection() 0 3 1
B getSelectedCombinedIdentifier() 0 32 9
A determinePageId() 0 32 4
A getPage() 0 20 1
A colPosListItemProcFunc() 0 6 2
A getIdentifiersToBeExcluded() 0 13 2
B parseStructure() 0 33 6
A addColPosListLayoutItems() 0 7 3
A getRootLine() 0 3 1
A __construct() 0 3 1
A getDefaultColumnLayout() 0 3 1
A createDataProviderContext() 0 3 1
A getSelectedBackendLayout() 0 7 2
A initializeDataProviderCollection() 0 13 3
A getLanguageService() 0 3 1
A getColumnName() 0 9 2
A getBackendLayoutForPage() 0 20 5
B addBackendLayoutItems() 0 31 6

How to fix   Complexity   

Complex Class

Complex classes like BackendLayoutView often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use BackendLayoutView, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/*
4
 * This file is part of the TYPO3 CMS project.
5
 *
6
 * It is free software; you can redistribute it and/or modify it under
7
 * the terms of the GNU General Public License, either version 2
8
 * of the License, or any later version.
9
 *
10
 * For the full copyright and license information, please read the
11
 * LICENSE.txt file that was distributed with this source code.
12
 *
13
 * The TYPO3 project - inspiring people to share!
14
 */
15
16
namespace TYPO3\CMS\Backend\View;
17
18
use TYPO3\CMS\Backend\Configuration\TypoScript\ConditionMatching\ConditionMatcher;
19
use TYPO3\CMS\Backend\Utility\BackendUtility;
20
use TYPO3\CMS\Backend\View\BackendLayout\BackendLayout;
21
use TYPO3\CMS\Backend\View\BackendLayout\DataProviderCollection;
22
use TYPO3\CMS\Backend\View\BackendLayout\DataProviderContext;
23
use TYPO3\CMS\Backend\View\BackendLayout\DefaultDataProvider;
24
use TYPO3\CMS\Core\Database\ConnectionPool;
25
use TYPO3\CMS\Core\Localization\LanguageService;
26
use TYPO3\CMS\Core\SingletonInterface;
27
use TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser;
28
use TYPO3\CMS\Core\Utility\ArrayUtility;
29
use TYPO3\CMS\Core\Utility\GeneralUtility;
30
31
/**
32
 * Backend layout for CMS
33
 * @internal This class is a TYPO3 Backend implementation and is not considered part of the Public TYPO3 API.
34
 */
35
class BackendLayoutView implements SingletonInterface
36
{
37
    /**
38
     * @var DataProviderCollection
39
     */
40
    protected $dataProviderCollection;
41
42
    /**
43
     * @var array
44
     */
45
    protected $selectedCombinedIdentifier = [];
46
47
    /**
48
     * @var array
49
     */
50
    protected $selectedBackendLayout = [];
51
52
    /**
53
     * Creates this object and initializes data providers.
54
     */
55
    public function __construct()
56
    {
57
        $this->initializeDataProviderCollection();
58
    }
59
60
    /**
61
     * Initializes data providers
62
     */
63
    protected function initializeDataProviderCollection()
64
    {
65
        $dataProviderCollection = GeneralUtility::makeInstance(DataProviderCollection::class);
66
        $dataProviderCollection->add('default', DefaultDataProvider::class);
67
68
        if (!empty($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['BackendLayoutDataProvider'])) {
69
            $dataProviders = (array)$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['BackendLayoutDataProvider'];
70
            foreach ($dataProviders as $identifier => $className) {
71
                $dataProviderCollection->add($identifier, $className);
72
            }
73
        }
74
75
        $this->setDataProviderCollection($dataProviderCollection);
76
    }
77
78
    /**
79
     * @param DataProviderCollection $dataProviderCollection
80
     */
81
    public function setDataProviderCollection(DataProviderCollection $dataProviderCollection)
82
    {
83
        $this->dataProviderCollection = $dataProviderCollection;
84
    }
85
86
    /**
87
     * @return DataProviderCollection
88
     */
89
    public function getDataProviderCollection()
90
    {
91
        return $this->dataProviderCollection;
92
    }
93
94
    /**
95
     * Gets backend layout items to be shown in the forms engine.
96
     * This method is called as "itemsProcFunc" with the accordant context
97
     * for pages.backend_layout and pages.backend_layout_next_level.
98
     *
99
     * @param array $parameters
100
     */
101
    public function addBackendLayoutItems(array $parameters)
102
    {
103
        $pageId = $this->determinePageId($parameters['table'], $parameters['row']) ?: 0;
104
        $pageTsConfig = (array)BackendUtility::getPagesTSconfig($pageId);
105
        $identifiersToBeExcluded = $this->getIdentifiersToBeExcluded($pageTsConfig);
106
107
        $dataProviderContext = $this->createDataProviderContext()
108
            ->setPageId($pageId)
109
            ->setData($parameters['row'])
110
            ->setTableName($parameters['table'])
111
            ->setFieldName($parameters['field'])
112
            ->setPageTsConfig($pageTsConfig);
113
114
        $backendLayoutCollections = $this->getDataProviderCollection()->getBackendLayoutCollections($dataProviderContext);
115
        foreach ($backendLayoutCollections as $backendLayoutCollection) {
116
            $combinedIdentifierPrefix = '';
117
            if ($backendLayoutCollection->getIdentifier() !== 'default') {
118
                $combinedIdentifierPrefix = $backendLayoutCollection->getIdentifier() . '__';
119
            }
120
121
            foreach ($backendLayoutCollection->getAll() as $backendLayout) {
122
                $combinedIdentifier = $combinedIdentifierPrefix . $backendLayout->getIdentifier();
123
124
                if (in_array($combinedIdentifier, $identifiersToBeExcluded, true)) {
125
                    continue;
126
                }
127
128
                $parameters['items'][] = [
129
                    $this->getLanguageService()->sL($backendLayout->getTitle()),
130
                    $combinedIdentifier,
131
                    $backendLayout->getIconPath(),
132
                ];
133
            }
134
        }
135
    }
136
137
    /**
138
     * Determines the page id for a given record of a database table.
139
     *
140
     * @param string $tableName
141
     * @param array $data
142
     * @return int|false Returns page id or false on error
143
     */
144
    protected function determinePageId($tableName, array $data)
145
    {
146
        if (strpos($data['uid'], 'NEW') === 0) {
147
            // negative uid_pid values of content elements indicate that the element
148
            // has been inserted after an existing element so there is no pid to get
149
            // the backendLayout for and we have to get that first
150
            if ($data['pid'] < 0) {
151
                $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
152
                    ->getQueryBuilderForTable($tableName);
153
                $queryBuilder->getRestrictions()
154
                    ->removeAll();
155
                $pageId = $queryBuilder
156
                    ->select('pid')
157
                    ->from($tableName)
158
                    ->where(
159
                        $queryBuilder->expr()->eq(
160
                            'uid',
161
                            $queryBuilder->createNamedParameter(abs($data['pid']), \PDO::PARAM_INT)
162
                        )
163
                    )
164
                    ->execute()
165
                    ->fetchColumn();
166
            } else {
167
                $pageId = $data['pid'];
168
            }
169
        } elseif ($tableName === 'pages') {
170
            $pageId = $data['uid'];
171
        } else {
172
            $pageId = $data['pid'];
173
        }
174
175
        return $pageId;
176
    }
177
178
    /**
179
     * Returns the backend layout which should be used for this page.
180
     *
181
     * @param int $pageId
182
     * @return bool|string Identifier of the backend layout to be used, or FALSE if none
183
     */
184
    public function getSelectedCombinedIdentifier($pageId)
185
    {
186
        if (!isset($this->selectedCombinedIdentifier[$pageId])) {
187
            $page = $this->getPage($pageId);
188
            $this->selectedCombinedIdentifier[$pageId] = (string)$page['backend_layout'];
189
190
            if ($this->selectedCombinedIdentifier[$pageId] === '-1') {
191
                // If it is set to "none" - don't use any
192
                $this->selectedCombinedIdentifier[$pageId] = false;
193
            } elseif ($this->selectedCombinedIdentifier[$pageId] === '' || $this->selectedCombinedIdentifier[$pageId] === '0') {
194
                // If it not set check the root-line for a layout on next level and use this
195
                // (root-line starts with current page and has page "0" at the end)
196
                $rootLine = $this->getRootLine($pageId);
197
                // Remove first and last element (current and root page)
198
                array_shift($rootLine);
199
                array_pop($rootLine);
200
                foreach ($rootLine as $rootLinePage) {
201
                    $this->selectedCombinedIdentifier[$pageId] = (string)$rootLinePage['backend_layout_next_level'];
202
                    if ($this->selectedCombinedIdentifier[$pageId] === '-1') {
203
                        // If layout for "next level" is set to "none" - don't use any and stop searching
204
                        $this->selectedCombinedIdentifier[$pageId] = false;
205
                        break;
206
                    }
207
                    if ($this->selectedCombinedIdentifier[$pageId] !== '' && $this->selectedCombinedIdentifier[$pageId] !== '0') {
208
                        // Stop searching if a layout for "next level" is set
209
                        break;
210
                    }
211
                }
212
            }
213
        }
214
        // If it is set to a positive value use this
215
        return $this->selectedCombinedIdentifier[$pageId];
216
    }
217
218
    /**
219
     * Gets backend layout identifiers to be excluded
220
     *
221
     * @param array $pageTSconfig
222
     * @return array
223
     */
224
    protected function getIdentifiersToBeExcluded(array $pageTSconfig)
225
    {
226
        $identifiersToBeExcluded = [];
227
228
        if (ArrayUtility::isValidPath($pageTSconfig, 'options./backendLayout./exclude')) {
229
            $identifiersToBeExcluded = GeneralUtility::trimExplode(
230
                ',',
231
                ArrayUtility::getValueByPath($pageTSconfig, 'options./backendLayout./exclude'),
232
                true
233
            );
234
        }
235
236
        return $identifiersToBeExcluded;
237
    }
238
239
    /**
240
     * Gets colPos items to be shown in the forms engine.
241
     * This method is called as "itemsProcFunc" with the accordant context
242
     * for tt_content.colPos.
243
     *
244
     * @param array $parameters
245
     */
246
    public function colPosListItemProcFunc(array $parameters)
247
    {
248
        $pageId = $this->determinePageId($parameters['table'], $parameters['row']);
249
250
        if ($pageId !== false) {
251
            $parameters['items'] = $this->addColPosListLayoutItems($pageId, $parameters['items']);
252
        }
253
    }
254
255
    /**
256
     * Adds items to a colpos list
257
     *
258
     * @param int $pageId
259
     * @param array $items
260
     * @return array
261
     */
262
    protected function addColPosListLayoutItems($pageId, $items)
263
    {
264
        $layout = $this->getSelectedBackendLayout($pageId);
265
        if ($layout && $layout['__items']) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $layout of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
266
            $items = $layout['__items'];
267
        }
268
        return $items;
269
    }
270
271
    /**
272
     * Gets the list of available columns for a given page id
273
     *
274
     * @param int $id
275
     * @return array $tcaItems
276
     */
277
    public function getColPosListItemsParsed($id)
278
    {
279
        $tsConfig = BackendUtility::getPagesTSconfig($id)['TCEFORM.']['tt_content.']['colPos.'] ?? [];
280
        $tcaConfig = $GLOBALS['TCA']['tt_content']['columns']['colPos']['config'];
281
        $tcaItems = $tcaConfig['items'];
282
        $tcaItems = $this->addItems($tcaItems, $tsConfig['addItems.']);
283
        if (isset($tcaConfig['itemsProcFunc']) && $tcaConfig['itemsProcFunc']) {
284
            $tcaItems = $this->addColPosListLayoutItems($id, $tcaItems);
285
        }
286
        if (!empty($tsConfig['removeItems'])) {
287
            foreach (GeneralUtility::trimExplode(',', $tsConfig['removeItems'], true) as $removeId) {
288
                foreach ($tcaItems as $key => $item) {
289
                    if ($item[1] == $removeId) {
290
                        unset($tcaItems[$key]);
291
                    }
292
                }
293
            }
294
        }
295
        return $tcaItems;
296
    }
297
298
    /**
299
     * Merges items into an item-array, optionally with an icon
300
     * example:
301
     * TCEFORM.pages.doktype.addItems.13 = My Label
302
     * TCEFORM.pages.doktype.addItems.13.icon = EXT:t3skin/icons/gfx/i/pages.gif
303
     *
304
     * @param array $items The existing item array
305
     * @param array $iArray An array of items to add. NOTICE: The keys are mapped to values, and the values and mapped to be labels. No possibility of adding an icon.
306
     * @return array The updated $item array
307
     * @internal
308
     */
309
    protected function addItems($items, $iArray)
310
    {
311
        $languageService = static::getLanguageService();
0 ignored issues
show
Bug Best Practice introduced by
The method TYPO3\CMS\Backend\View\B...w::getLanguageService() is not static, but was called statically. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

311
        /** @scrutinizer ignore-call */ 
312
        $languageService = static::getLanguageService();
Loading history...
312
        if (is_array($iArray)) {
0 ignored issues
show
introduced by
The condition is_array($iArray) is always true.
Loading history...
313
            foreach ($iArray as $value => $label) {
314
                // if the label is an array (that means it is a subelement
315
                // like "34.icon = mylabel.png", skip it (see its usage below)
316
                if (is_array($label)) {
317
                    continue;
318
                }
319
                // check if the value "34 = mylabel" also has a "34.icon = myimage.png"
320
                if (isset($iArray[$value . '.']) && $iArray[$value . '.']['icon']) {
321
                    $icon = $iArray[$value . '.']['icon'];
322
                } else {
323
                    $icon = '';
324
                }
325
                $items[] = [$languageService->sL($label), $value, $icon];
326
            }
327
        }
328
        return $items;
329
    }
330
331
    /**
332
     * Gets the selected backend layout structure as an array
333
     *
334
     * @param int $pageId
335
     * @return array|null $backendLayout
336
     */
337
    public function getSelectedBackendLayout($pageId)
338
    {
339
        $layout = $this->getBackendLayoutForPage((int)$pageId);
340
        if ($layout instanceof BackendLayout) {
0 ignored issues
show
introduced by
$layout is always a sub-type of TYPO3\CMS\Backend\View\BackendLayout\BackendLayout.
Loading history...
341
            return $layout->getStructure();
342
        }
343
        return null;
344
    }
345
346
    /**
347
     * Get the BackendLayout object and parse the structure based on the UserTSconfig
348
     * @param int $pageId
349
     * @return BackendLayout
350
     */
351
    public function getBackendLayoutForPage(int $pageId): ?BackendLayout
352
    {
353
        if (isset($this->selectedBackendLayout[$pageId])) {
354
            return $this->selectedBackendLayout[$pageId];
355
        }
356
        $selectedCombinedIdentifier = $this->getSelectedCombinedIdentifier($pageId);
357
        // If no backend layout is selected, use default
358
        if (empty($selectedCombinedIdentifier)) {
359
            $selectedCombinedIdentifier = 'default';
360
        }
361
        $backendLayout = $this->getDataProviderCollection()->getBackendLayout($selectedCombinedIdentifier, $pageId);
362
        // If backend layout is not found available anymore, use default
363
        if ($backendLayout === null) {
364
            $backendLayout = $this->getDataProviderCollection()->getBackendLayout('default', $pageId);
365
        }
366
367
        if ($backendLayout instanceof BackendLayout) {
368
            $this->selectedBackendLayout[$pageId] = $backendLayout;
369
        }
370
        return $backendLayout;
371
    }
372
373
    /**
374
     * @param BackendLayout $backendLayout
375
     * @return array
376
     * @internal
377
     */
378
    public function parseStructure(BackendLayout $backendLayout): array
379
    {
380
        $parser = GeneralUtility::makeInstance(TypoScriptParser::class);
381
        $conditionMatcher = GeneralUtility::makeInstance(ConditionMatcher::class);
382
        $parser->parse(TypoScriptParser::checkIncludeLines($backendLayout->getConfiguration()), $conditionMatcher);
383
384
        $backendLayoutData = [];
385
        $backendLayoutData['config'] = $backendLayout->getConfiguration();
386
        $backendLayoutData['__config'] = $parser->setup;
387
        $backendLayoutData['__items'] = [];
388
        $backendLayoutData['__colPosList'] = [];
389
        $backendLayoutData['usedColumns'] = [];
390
391
        // create items and colPosList
392
        if (!empty($backendLayoutData['__config']['backend_layout.']['rows.'])) {
393
            foreach ($backendLayoutData['__config']['backend_layout.']['rows.'] as $row) {
394
                if (!empty($row['columns.'])) {
395
                    foreach ($row['columns.'] as $column) {
396
                        if (!isset($column['colPos'])) {
397
                            continue;
398
                        }
399
                        $backendLayoutData['__items'][] = [
400
                            $this->getColumnName($column),
401
                            $column['colPos'],
402
                            null
403
                        ];
404
                        $backendLayoutData['__colPosList'][] = $column['colPos'];
405
                        $backendLayoutData['usedColumns'][(int)$column['colPos']] = $column['name'];
406
                    }
407
                }
408
            }
409
        }
410
        return $backendLayoutData;
411
    }
412
413
    /**
414
     * Get default columns layout
415
     *
416
     * @return string Default four column layout
417
     * @static
418
     */
419
    public static function getDefaultColumnLayout()
420
    {
421
        return '
422
		backend_layout {
423
			colCount = 1
424
			rowCount = 1
425
			rows {
426
				1 {
427
					columns {
428
						1 {
429
							name = LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:colPos.I.1
430
							colPos = 0
431
						}
432
					}
433
				}
434
			}
435
		}
436
		';
437
    }
438
439
    /**
440
     * Gets a page record.
441
     *
442
     * @param int $pageId
443
     * @return array|null
444
     */
445
    protected function getPage($pageId)
446
    {
447
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
448
            ->getQueryBuilderForTable('pages');
449
        $queryBuilder->getRestrictions()
450
            ->removeAll();
451
        $page = $queryBuilder
452
            ->select('uid', 'pid', 'backend_layout')
453
            ->from('pages')
454
            ->where(
455
                $queryBuilder->expr()->eq(
456
                    'uid',
457
                    $queryBuilder->createNamedParameter($pageId, \PDO::PARAM_INT)
458
                )
459
            )
460
            ->execute()
461
            ->fetch();
462
        BackendUtility::workspaceOL('pages', $page);
463
464
        return $page;
465
    }
466
467
    /**
468
     * Gets the page root-line.
469
     *
470
     * @param int $pageId
471
     * @return array
472
     */
473
    protected function getRootLine($pageId)
474
    {
475
        return BackendUtility::BEgetRootLine($pageId, '', true);
476
    }
477
478
    /**
479
     * @return DataProviderContext
480
     */
481
    protected function createDataProviderContext()
482
    {
483
        return GeneralUtility::makeInstance(DataProviderContext::class);
484
    }
485
486
    /**
487
     * @return LanguageService
488
     */
489
    protected function getLanguageService()
490
    {
491
        return $GLOBALS['LANG'];
492
    }
493
494
    /**
495
     * Get column name from colPos item structure
496
     *
497
     * @param array $column
498
     * @return string
499
     */
500
    protected function getColumnName($column)
501
    {
502
        $columnName = $column['name'];
503
504
        if (GeneralUtility::isFirstPartOfStr($columnName, 'LLL:')) {
505
            $columnName = $this->getLanguageService()->sL($columnName);
506
        }
507
508
        return $columnName;
509
    }
510
}
511