[!!!][FEATURE] Streamline Fluid Styled Content and CSS Styled Content
[Packages/TYPO3.CMS.git] / typo3 / sysext / core / Classes / Category / CategoryRegistry.php
1 <?php
2 namespace TYPO3\CMS\Core\Category;
3
4 /*
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
17 use TYPO3\CMS\Core\SingletonInterface;
18 use TYPO3\CMS\Core\Utility\ArrayUtility;
19 use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
20 use TYPO3\CMS\Core\Utility\GeneralUtility;
21 use TYPO3\CMS\Lang\LanguageService;
22
23 /**
24 * Class to register category configurations.
25 */
26 class CategoryRegistry implements SingletonInterface
27 {
28 /**
29 * @var array
30 */
31 protected $registry = [];
32
33 /**
34 * @var array
35 */
36 protected $extensions = [];
37
38 /**
39 * @var array
40 */
41 protected $addedCategoryTabs = [];
42
43 /**
44 * @var string
45 */
46 protected $template = '';
47
48 /**
49 * Returns a class instance
50 *
51 * @return CategoryRegistry
52 */
53 public static function getInstance()
54 {
55 return GeneralUtility::makeInstance(__CLASS__);
56 }
57
58 /**
59 * Creates this object.
60 */
61 public function __construct()
62 {
63 $this->template = str_repeat(PHP_EOL, 3) . 'CREATE TABLE %s (' . PHP_EOL
64 . ' %s int(11) DEFAULT \'0\' NOT NULL' . PHP_EOL . ');' . str_repeat(PHP_EOL, 3);
65 }
66
67 /**
68 * Adds a new category configuration to this registry.
69 * TCA changes are directly applied
70 *
71 * @param string $extensionKey Extension key to be used
72 * @param string $tableName Name of the table to be registered
73 * @param string $fieldName Name of the field to be registered
74 * @param array $options Additional configuration options
75 * + fieldList: field configuration to be added to showitems
76 * + typesList: list of types that shall visualize the categories field
77 * + position: insert position of the categories field
78 * + label: backend label of the categories field
79 * + fieldConfiguration: TCA field config array to override defaults
80 * @param bool $override If TRUE, any category configuration for the same table / field is removed before the new configuration is added
81 * @return bool
82 * @throws \InvalidArgumentException
83 * @throws \RuntimeException
84 */
85 public function add($extensionKey, $tableName, $fieldName = 'categories', array $options = [], $override = false)
86 {
87 $didRegister = false;
88 if (empty($tableName) || !is_string($tableName)) {
89 throw new \InvalidArgumentException('No or invalid table name "' . $tableName . '" given.', 1369122038);
90 }
91 if (empty($extensionKey) || !is_string($extensionKey)) {
92 throw new \InvalidArgumentException('No or invalid extension key "' . $extensionKey . '" given.', 1397836158);
93 }
94
95 if ($override) {
96 $this->remove($tableName, $fieldName);
97 }
98
99 if (!$this->isRegistered($tableName, $fieldName)) {
100 $this->registry[$tableName][$fieldName] = $options;
101 $this->extensions[$extensionKey][$tableName][$fieldName] = $fieldName;
102
103 if (isset($GLOBALS['TCA'][$tableName]['columns'])) {
104 $this->applyTcaForTableAndField($tableName, $fieldName);
105 $didRegister = true;
106 }
107 }
108
109 return $didRegister;
110 }
111
112 /**
113 * Gets all extension keys that registered a category configuration.
114 *
115 * @return array
116 */
117 public function getExtensionKeys()
118 {
119 return array_keys($this->extensions);
120 }
121
122 /**
123 * Gets all categorized tables
124 *
125 * @return array
126 */
127 public function getCategorizedTables()
128 {
129 return array_keys($this->registry);
130 }
131
132 /**
133 * Returns a list of category fields for a given table for populating selector "category_field"
134 * in tt_content table (called as itemsProcFunc).
135 *
136 * @param array $configuration Current field configuration
137 * @throws \UnexpectedValueException
138 * @return void
139 */
140 public function getCategoryFieldsForTable(array &$configuration)
141 {
142 $table = $configuration['config']['itemsProcConfig']['table'] ?? '';
143 // Lookup table for legacy menu content element
144 if (empty($table)) {
145 $menuType = $configuration['row']['menu_type'][0] ?? '';
146 // Define the table being looked up from the type of menu
147 if ($menuType === 'categorized_pages') {
148 $table = 'pages';
149 } elseif ($menuType === 'categorized_content') {
150 $table = 'tt_content';
151 }
152 }
153 // Return early if no table is defined
154 if (empty($table)) {
155 throw new \UnexpectedValueException('The given menu_type is not supported.', 1381823570);
156 }
157 // Loop on all registries and find entries for the correct table
158 foreach ($this->registry as $tableName => $fields) {
159 if ($tableName === $table) {
160 foreach ($fields as $fieldName => $options) {
161 $fieldLabel = $this->getLanguageService()->sL($GLOBALS['TCA'][$tableName]['columns'][$fieldName]['label']);
162 $configuration['items'][] = [$fieldLabel, $fieldName];
163 }
164 }
165 }
166 }
167
168 /**
169 * Tells whether a table has a category configuration in the registry.
170 *
171 * @param string $tableName Name of the table to be looked up
172 * @param string $fieldName Name of the field to be looked up
173 * @return bool
174 */
175 public function isRegistered($tableName, $fieldName = 'categories')
176 {
177 return isset($this->registry[$tableName][$fieldName]);
178 }
179
180 /**
181 * Generates tables definitions for all registered tables.
182 *
183 * @return string
184 */
185 public function getDatabaseTableDefinitions()
186 {
187 $sql = '';
188 foreach ($this->getExtensionKeys() as $extensionKey) {
189 $sql .= $this->getDatabaseTableDefinition($extensionKey);
190 }
191 return $sql;
192 }
193
194 /**
195 * Generates table definitions for registered tables by an extension.
196 *
197 * @param string $extensionKey Extension key to have the database definitions created for
198 * @return string
199 */
200 public function getDatabaseTableDefinition($extensionKey)
201 {
202 if (!isset($this->extensions[$extensionKey]) || !is_array($this->extensions[$extensionKey])) {
203 return '';
204 }
205 $sql = '';
206
207 foreach ($this->extensions[$extensionKey] as $tableName => $fields) {
208 foreach ($fields as $fieldName) {
209 $sql .= sprintf($this->template, $tableName, $fieldName);
210 }
211 }
212 return $sql;
213 }
214
215 /**
216 * Apply TCA to all registered tables
217 *
218 * @return void
219 * @internal
220 */
221 public function applyTcaForPreRegisteredTables()
222 {
223 $this->registerDefaultCategorizedTables();
224 foreach ($this->registry as $tableName => $fields) {
225 foreach ($fields as $fieldName => $_) {
226 $this->applyTcaForTableAndField($tableName, $fieldName);
227 }
228 }
229 }
230
231 /**
232 * Applies the additions directly to the TCA
233 *
234 * @param string $tableName
235 * @param string $fieldName
236 */
237 protected function applyTcaForTableAndField($tableName, $fieldName)
238 {
239 $this->addTcaColumn($tableName, $fieldName, $this->registry[$tableName][$fieldName]);
240 $this->addToAllTCAtypes($tableName, $fieldName, $this->registry[$tableName][$fieldName]);
241 }
242
243 /**
244 * Add default categorized tables to the registry
245 *
246 * @return void
247 */
248 protected function registerDefaultCategorizedTables()
249 {
250 $defaultCategorizedTables = GeneralUtility::trimExplode(
251 ',',
252 $GLOBALS['TYPO3_CONF_VARS']['SYS']['defaultCategorizedTables'],
253 true
254 );
255 foreach ($defaultCategorizedTables as $defaultCategorizedTable) {
256 if (!$this->isRegistered($defaultCategorizedTable)) {
257 $this->add('core', $defaultCategorizedTable, 'categories');
258 }
259 }
260 }
261
262 /**
263 * Add a new field into the TCA types -> showitem
264 *
265 * @param string $tableName Name of the table to be categorized
266 * @param string $fieldName Name of the field to be used to store categories
267 * @param array $options Additional configuration options
268 * + fieldList: field configuration to be added to showitems
269 * + typesList: list of types that shall visualize the categories field
270 * + position: insert position of the categories field
271 * @return void
272 */
273 protected function addToAllTCAtypes($tableName, $fieldName, array $options)
274 {
275
276 // Makes sure to add more TCA to an existing structure
277 if (isset($GLOBALS['TCA'][$tableName]['columns'])) {
278 if (empty($options['fieldList'])) {
279 $fieldList = $this->addCategoryTab($tableName, $fieldName);
280 } else {
281 $fieldList = $options['fieldList'];
282 }
283
284 $typesList = '';
285 if (isset($options['typesList']) && $options['typesList'] !== '') {
286 $typesList = $options['typesList'];
287 }
288
289 $position = '';
290 if (!empty($options['position'])) {
291 $position = $options['position'];
292 }
293
294 // Makes the new "categories" field to be visible in TSFE.
295 ExtensionManagementUtility::addToAllTCAtypes($tableName, $fieldList, $typesList, $position);
296 }
297 }
298
299 /**
300 * Creates the 'fieldList' string for $fieldName which includes a categories tab.
301 * But only one categories tab is added per table.
302 *
303 * @param string $tableName
304 * @param string $fieldName
305 * @return string
306 */
307 protected function addCategoryTab($tableName, $fieldName)
308 {
309 $fieldList = '';
310 if (!isset($this->addedCategoryTabs[$tableName])) {
311 $fieldList .= '--div--;LLL:EXT:lang/Resources/Private/Language/locallang_tca.xlf:sys_category.tabs.category, ';
312 $this->addedCategoryTabs[$tableName] = $tableName;
313 }
314 $fieldList .= $fieldName;
315 return $fieldList;
316 }
317
318 /**
319 * Add a new TCA Column
320 *
321 * @param string $tableName Name of the table to be categorized
322 * @param string $fieldName Name of the field to be used to store categories
323 * @param array $options Additional configuration options
324 * + fieldConfiguration: TCA field config array to override defaults
325 * + label: backend label of the categories field
326 * + interface: boolean if the category should be included in the "interface" section of the TCA table
327 * + l10n_mode
328 * + l10n_display
329 * @return void
330 */
331 protected function addTcaColumn($tableName, $fieldName, array $options)
332 {
333 // Makes sure to add more TCA to an existing structure
334 if (isset($GLOBALS['TCA'][$tableName]['columns'])) {
335 // Take specific label into account
336 $label = 'LLL:EXT:lang/Resources/Private/Language/locallang_tca.xlf:sys_category.categories';
337 if (!empty($options['label'])) {
338 $label = $options['label'];
339 }
340
341 // Take specific value of exclude flag into account
342 $exclude = true;
343 if (isset($options['exclude'])) {
344 $exclude = (bool)$options['exclude'];
345 }
346
347 $fieldConfiguration = empty($options['fieldConfiguration']) ? [] : $options['fieldConfiguration'];
348
349 $columns = [
350 $fieldName => [
351 'exclude' => $exclude,
352 'label' => $label,
353 'config' => static::getTcaFieldConfiguration($tableName, $fieldName, $fieldConfiguration),
354 ],
355 ];
356
357 if (isset($options['l10n_mode'])) {
358 $columns[$fieldName]['l10n_mode'] = $options['l10n_mode'];
359 }
360 if (isset($options['l10n_display'])) {
361 $columns[$fieldName]['l10n_display'] = $options['l10n_display'];
362 }
363 if (isset($options['displayCond'])) {
364 $columns[$fieldName]['displayCond'] = $options['displayCond'];
365 }
366
367 // Register opposite references for the foreign side of a relation
368 if (empty($GLOBALS['TCA']['sys_category']['columns']['items']['config']['MM_oppositeUsage'][$tableName])) {
369 $GLOBALS['TCA']['sys_category']['columns']['items']['config']['MM_oppositeUsage'][$tableName] = [];
370 }
371 if (!in_array($fieldName, $GLOBALS['TCA']['sys_category']['columns']['items']['config']['MM_oppositeUsage'][$tableName])) {
372 $GLOBALS['TCA']['sys_category']['columns']['items']['config']['MM_oppositeUsage'][$tableName][] = $fieldName;
373 }
374
375 // Add field to interface list per default (unless the 'interface' property is FALSE)
376 if (
377 (!isset($options['interface']) || $options['interface'])
378 && !empty($GLOBALS['TCA'][$tableName]['interface']['showRecordFieldList'])
379 && !GeneralUtility::inList($GLOBALS['TCA'][$tableName]['interface']['showRecordFieldList'], $fieldName)
380 ) {
381 $GLOBALS['TCA'][$tableName]['interface']['showRecordFieldList'] .= ',' . $fieldName;
382 }
383
384 // Adding fields to an existing table definition
385 ExtensionManagementUtility::addTCAcolumns($tableName, $columns);
386 }
387 }
388
389 /**
390 * Get the config array for given table and field.
391 * This method does NOT take care of adding sql fields, adding the field to TCA types
392 * nor does it set the MM_oppositeUsage in the sys_category TCA. This has to be taken care of manually!
393 *
394 * @param string $tableName The table name
395 * @param string $fieldName The field name (default categories)
396 * @param array $fieldConfigurationOverride Changes to the default configuration
397 * @return array
398 * @api
399 */
400 public static function getTcaFieldConfiguration($tableName, $fieldName = 'categories', array $fieldConfigurationOverride = [])
401 {
402 // Forges a new field, default name is "categories"
403 $fieldConfiguration = [
404 'type' => 'select',
405 'renderType' => 'selectTree',
406 'foreign_table' => 'sys_category',
407 'foreign_table_where' => ' AND sys_category.sys_language_uid IN (-1, 0) ORDER BY sys_category.sorting ASC',
408 'MM' => 'sys_category_record_mm',
409 'MM_opposite_field' => 'items',
410 'MM_match_fields' => [
411 'tablenames' => $tableName,
412 'fieldname' => $fieldName,
413 ],
414 'size' => 20,
415 'maxitems' => 9999,
416 'treeConfig' => [
417 'parentField' => 'parent',
418 'appearance' => [
419 'expandAll' => true,
420 'showHeader' => true,
421 'maxLevels' => 99,
422 ],
423 ],
424 ];
425
426 // Merge changes to TCA configuration
427 if (!empty($fieldConfigurationOverride)) {
428 ArrayUtility::mergeRecursiveWithOverrule(
429 $fieldConfiguration,
430 $fieldConfigurationOverride
431 );
432 }
433
434 return $fieldConfiguration;
435 }
436
437 /**
438 * A slot method to inject the required category database fields to the
439 * tables definition string
440 *
441 * @param array $sqlString
442 * @return array
443 */
444 public function addCategoryDatabaseSchemaToTablesDefinition(array $sqlString)
445 {
446 $this->registerDefaultCategorizedTables();
447 $sqlString[] = $this->getDatabaseTableDefinitions();
448 return ['sqlString' => $sqlString];
449 }
450
451 /**
452 * A slot method to inject the required category database fields of an
453 * extension to the tables definition string
454 *
455 * @param array $sqlString
456 * @param string $extensionKey
457 * @return array
458 */
459 public function addExtensionCategoryDatabaseSchemaToTablesDefinition(array $sqlString, $extensionKey)
460 {
461 $sqlString[] = $this->getDatabaseTableDefinition($extensionKey);
462 return ['sqlString' => $sqlString, 'extensionKey' => $extensionKey];
463 }
464
465 /**
466 * @return LanguageService
467 */
468 protected function getLanguageService()
469 {
470 return $GLOBALS['LANG'];
471 }
472
473 /**
474 * Removes the given field in the given table from the registry if it is found.
475 *
476 * @param string $tableName The name of the table for which the registration should be removed.
477 * @param string $fieldName The name of the field for which the registration should be removed.
478 */
479 protected function remove($tableName, $fieldName)
480 {
481 if (!$this->isRegistered($tableName, $fieldName)) {
482 return;
483 }
484
485 unset($this->registry[$tableName][$fieldName]);
486
487 foreach ($this->extensions as $extensionKey => $tableFieldConfig) {
488 foreach ($tableFieldConfig as $extTableName => $fieldNameArray) {
489 if ($extTableName === $tableName && isset($fieldNameArray[$fieldName])) {
490 unset($this->extensions[$extensionKey][$tableName][$fieldName]);
491 break;
492 }
493 }
494 }
495
496 // If no more fields are configured we unregister the categories tab.
497 if (empty($this->registry[$tableName]) && isset($this->addedCategoryTabs[$tableName])) {
498 unset($this->addedCategoryTabs[$tableName]);
499 }
500 }
501 }