34bd3c068a029c66cebe72848012644ea59dbdeb
[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\Backend\Form\Utility\DisplayConditionEvaluator;
18 use TYPO3\CMS\Core\SingletonInterface;
19 use TYPO3\CMS\Core\Utility\ArrayUtility;
20 use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
21 use TYPO3\CMS\Core\Utility\GeneralUtility;
22 use TYPO3\CMS\Lang\LanguageService;
23
24 /**
25 * Class to register category configurations.
26 */
27 class CategoryRegistry implements SingletonInterface
28 {
29 /**
30 * @var array
31 */
32 protected $registry = [];
33
34 /**
35 * @var array
36 */
37 protected $extensions = [];
38
39 /**
40 * @var array
41 */
42 protected $addedCategoryTabs = [];
43
44 /**
45 * @var string
46 */
47 protected $template = '';
48
49 /**
50 * Returns a class instance
51 *
52 * @return CategoryRegistry
53 */
54 public static function getInstance()
55 {
56 return GeneralUtility::makeInstance(__CLASS__);
57 }
58
59 /**
60 * Creates this object.
61 */
62 public function __construct()
63 {
64 $this->template = str_repeat(PHP_EOL, 3) . 'CREATE TABLE %s (' . PHP_EOL
65 . ' %s int(11) DEFAULT \'0\' NOT NULL' . PHP_EOL . ');' . str_repeat(PHP_EOL, 3);
66 }
67
68 /**
69 * Adds a new category configuration to this registry.
70 * TCA changes are directly applied
71 *
72 * @param string $extensionKey Extension key to be used
73 * @param string $tableName Name of the table to be registered
74 * @param string $fieldName Name of the field to be registered
75 * @param array $options Additional configuration options
76 * + fieldList: field configuration to be added to showitems
77 * + typesList: list of types that shall visualize the categories field
78 * + position: insert position of the categories field
79 * + label: backend label of the categories field
80 * + fieldConfiguration: TCA field config array to override defaults
81 * @param bool $override If TRUE, any category configuration for the same table / field is removed before the new configuration is added
82 * @return bool
83 * @throws \InvalidArgumentException
84 * @throws \RuntimeException
85 */
86 public function add($extensionKey, $tableName, $fieldName = 'categories', array $options = [], $override = false)
87 {
88 $didRegister = false;
89 if (empty($tableName) || !is_string($tableName)) {
90 throw new \InvalidArgumentException('No or invalid table name "' . $tableName . '" given.', 1369122038);
91 }
92 if (empty($extensionKey) || !is_string($extensionKey)) {
93 throw new \InvalidArgumentException('No or invalid extension key "' . $extensionKey . '" given.', 1397836158);
94 }
95
96 if ($override) {
97 $this->remove($tableName, $fieldName);
98 }
99
100 if (!$this->isRegistered($tableName, $fieldName)) {
101 $this->registry[$tableName][$fieldName] = $options;
102 $this->extensions[$extensionKey][$tableName][$fieldName] = $fieldName;
103
104 if (isset($GLOBALS['TCA'][$tableName]['columns'])) {
105 $this->applyTcaForTableAndField($tableName, $fieldName);
106 $didRegister = true;
107 }
108 }
109
110 return $didRegister;
111 }
112
113 /**
114 * Gets all extension keys that registered a category configuration.
115 *
116 * @return array
117 */
118 public function getExtensionKeys()
119 {
120 return array_keys($this->extensions);
121 }
122
123 /**
124 * Gets all categorized tables
125 *
126 * @return array
127 */
128 public function getCategorizedTables()
129 {
130 return array_keys($this->registry);
131 }
132
133 /**
134 * Returns a list of category fields for the table configured in the categoryFieldsTable setting.
135 * For use in an itemsProcFunc of a TCA select field.
136 *
137 * @param array $configuration The TCA and row arrays passed to the itemsProcFunc.
138 * @return void
139 */
140 public function getCategoryFieldItems(array &$configuration)
141 {
142 $table = $this->getActiveCategoryFieldsTable($configuration);
143 // Loop on all registries and find entries for the correct table
144 foreach ($this->registry as $tableName => $fields) {
145 if ($tableName === $table) {
146 foreach ($fields as $fieldName => $options) {
147 $fieldLabel = $this->getLanguageService()->sL($GLOBALS['TCA'][$tableName]['columns'][$fieldName]['label']);
148 $configuration['items'][] = [$fieldLabel, $fieldName];
149 }
150 }
151 }
152 }
153
154 /**
155 * Tries to determine of which table the category fields should be collected.
156 * It looks in the categoryFieldsTable TCA entry in the config section of the current field.
157 *
158 * It is possible to pass a plain string with a table name or an array of table names
159 * that can be activated with an active condition. There must exactly be one active
160 * table at once. A possible array configuration might look like this:
161 *
162 * 'categoryFieldsTable' => array(
163 * 'categorized_pages' => array(
164 * 'table' => 'pages',
165 * 'activeCondition' => 'FIELD:menu_type:=:categorized_pages'
166 * ),
167 * 'categorized_content' => array(
168 * 'table' => 'tt_content',
169 * 'activeCondition' => 'FIELD:menu_type:=:categorized_content'
170 * )
171 * ),
172 *
173 * @param array $configuration The TCA and row arrays passed to the itemsProcFunc.
174 * @throws \RuntimeException In case of an invalid configuration.
175 * @return string
176 */
177 protected function getActiveCategoryFieldsTable(array $configuration)
178 {
179 $fieldAndTableInfo = sprintf(' (field: %s, table: %s)', $configuration['field'], $configuration['table']);
180
181 if (empty($configuration['config']['categoryFieldsTable'])) {
182 throw new \RuntimeException(
183 'The categoryFieldsTable setting is missing in the config section' . $fieldAndTableInfo,
184 1447273908
185 );
186 }
187
188 if (is_string($configuration['config']['categoryFieldsTable'])) {
189 return $configuration['config']['categoryFieldsTable'];
190 }
191
192 if (!is_array($configuration['config']['categoryFieldsTable'])) {
193 throw new \RuntimeException(
194 sprintf(
195 'The categoryFieldsTable table setting must be a string or an array, %s given' . $fieldAndTableInfo,
196 gettype($configuration['config']['categoryFieldsTable'])
197 ),
198 1447274126
199 );
200 }
201
202 $activeTable = null;
203
204 foreach ($configuration['config']['categoryFieldsTable'] as $configKey => $tableConfig) {
205 if (empty($tableConfig['table'])) {
206 throw new \RuntimeException(
207 sprintf(
208 'The table setting is missing for the categoryFieldsTable %s' . $fieldAndTableInfo,
209 $configKey
210 ),
211 1447274131
212 );
213 }
214 if (empty($tableConfig['activeCondition'])) {
215 throw new \RuntimeException(
216 sprintf(
217 'The activeCondition setting is missing for the categoryFieldsTable %s' . $fieldAndTableInfo,
218 $configKey
219 ),
220 1480786868
221 );
222 }
223
224 if ($this->getDisplayConditionEvaluator()->evaluateDisplayCondition(
225 $tableConfig['activeCondition'],
226 $configuration['row']
227 )
228 ) {
229 if (!empty($activeTable)) {
230 throw new \RuntimeException(
231 sprintf(
232 'There must only be one active categoryFieldsTable. Multiple active tables (%s, %s) '
233 . 'were found' . $fieldAndTableInfo,
234 $activeTable,
235 $tableConfig['table']
236 ),
237 1480787321
238 );
239 }
240 $activeTable = $tableConfig['table'];
241 }
242 }
243
244 if (empty($activeTable)) {
245 throw new \RuntimeException('No active was found' . $fieldAndTableInfo, 1447274507);
246 }
247
248 return $activeTable;
249 }
250
251 /**
252 * Returns the display condition evaluator utility class.
253 *
254 * @return DisplayConditionEvaluator
255 */
256 protected function getDisplayConditionEvaluator()
257 {
258 return GeneralUtility::makeInstance(DisplayConditionEvaluator::class);
259 }
260
261 /**
262 * Tells whether a table has a category configuration in the registry.
263 *
264 * @param string $tableName Name of the table to be looked up
265 * @param string $fieldName Name of the field to be looked up
266 * @return bool
267 */
268 public function isRegistered($tableName, $fieldName = 'categories')
269 {
270 return isset($this->registry[$tableName][$fieldName]);
271 }
272
273 /**
274 * Generates tables definitions for all registered tables.
275 *
276 * @return string
277 */
278 public function getDatabaseTableDefinitions()
279 {
280 $sql = '';
281 foreach ($this->getExtensionKeys() as $extensionKey) {
282 $sql .= $this->getDatabaseTableDefinition($extensionKey);
283 }
284 return $sql;
285 }
286
287 /**
288 * Generates table definitions for registered tables by an extension.
289 *
290 * @param string $extensionKey Extension key to have the database definitions created for
291 * @return string
292 */
293 public function getDatabaseTableDefinition($extensionKey)
294 {
295 if (!isset($this->extensions[$extensionKey]) || !is_array($this->extensions[$extensionKey])) {
296 return '';
297 }
298 $sql = '';
299
300 foreach ($this->extensions[$extensionKey] as $tableName => $fields) {
301 foreach ($fields as $fieldName) {
302 $sql .= sprintf($this->template, $tableName, $fieldName);
303 }
304 }
305 return $sql;
306 }
307
308 /**
309 * Apply TCA to all registered tables
310 *
311 * @return void
312 * @internal
313 */
314 public function applyTcaForPreRegisteredTables()
315 {
316 $this->registerDefaultCategorizedTables();
317 foreach ($this->registry as $tableName => $fields) {
318 foreach ($fields as $fieldName => $_) {
319 $this->applyTcaForTableAndField($tableName, $fieldName);
320 }
321 }
322 }
323
324 /**
325 * Applies the additions directly to the TCA
326 *
327 * @param string $tableName
328 * @param string $fieldName
329 */
330 protected function applyTcaForTableAndField($tableName, $fieldName)
331 {
332 $this->addTcaColumn($tableName, $fieldName, $this->registry[$tableName][$fieldName]);
333 $this->addToAllTCAtypes($tableName, $fieldName, $this->registry[$tableName][$fieldName]);
334 }
335
336 /**
337 * Add default categorized tables to the registry
338 *
339 * @return void
340 */
341 protected function registerDefaultCategorizedTables()
342 {
343 $defaultCategorizedTables = GeneralUtility::trimExplode(
344 ',',
345 $GLOBALS['TYPO3_CONF_VARS']['SYS']['defaultCategorizedTables'],
346 true
347 );
348 foreach ($defaultCategorizedTables as $defaultCategorizedTable) {
349 if (!$this->isRegistered($defaultCategorizedTable)) {
350 $this->add('core', $defaultCategorizedTable, 'categories');
351 }
352 }
353 }
354
355 /**
356 * Add a new field into the TCA types -> showitem
357 *
358 * @param string $tableName Name of the table to be categorized
359 * @param string $fieldName Name of the field to be used to store categories
360 * @param array $options Additional configuration options
361 * + fieldList: field configuration to be added to showitems
362 * + typesList: list of types that shall visualize the categories field
363 * + position: insert position of the categories field
364 * @return void
365 */
366 protected function addToAllTCAtypes($tableName, $fieldName, array $options)
367 {
368
369 // Makes sure to add more TCA to an existing structure
370 if (isset($GLOBALS['TCA'][$tableName]['columns'])) {
371 if (empty($options['fieldList'])) {
372 $fieldList = $this->addCategoryTab($tableName, $fieldName);
373 } else {
374 $fieldList = $options['fieldList'];
375 }
376
377 $typesList = '';
378 if (isset($options['typesList']) && $options['typesList'] !== '') {
379 $typesList = $options['typesList'];
380 }
381
382 $position = '';
383 if (!empty($options['position'])) {
384 $position = $options['position'];
385 }
386
387 // Makes the new "categories" field to be visible in TSFE.
388 ExtensionManagementUtility::addToAllTCAtypes($tableName, $fieldList, $typesList, $position);
389 }
390 }
391
392 /**
393 * Creates the 'fieldList' string for $fieldName which includes a categories tab.
394 * But only one categories tab is added per table.
395 *
396 * @param string $tableName
397 * @param string $fieldName
398 * @return string
399 */
400 protected function addCategoryTab($tableName, $fieldName)
401 {
402 $fieldList = '';
403 if (!isset($this->addedCategoryTabs[$tableName])) {
404 $fieldList .= '--div--;LLL:EXT:lang/Resources/Private/Language/locallang_tca.xlf:sys_category.tabs.category, ';
405 $this->addedCategoryTabs[$tableName] = $tableName;
406 }
407 $fieldList .= $fieldName;
408 return $fieldList;
409 }
410
411 /**
412 * Add a new TCA Column
413 *
414 * @param string $tableName Name of the table to be categorized
415 * @param string $fieldName Name of the field to be used to store categories
416 * @param array $options Additional configuration options
417 * + fieldConfiguration: TCA field config array to override defaults
418 * + label: backend label of the categories field
419 * + interface: boolean if the category should be included in the "interface" section of the TCA table
420 * + l10n_mode
421 * + l10n_display
422 * @return void
423 */
424 protected function addTcaColumn($tableName, $fieldName, array $options)
425 {
426 // Makes sure to add more TCA to an existing structure
427 if (isset($GLOBALS['TCA'][$tableName]['columns'])) {
428 // Take specific label into account
429 $label = 'LLL:EXT:lang/Resources/Private/Language/locallang_tca.xlf:sys_category.categories';
430 if (!empty($options['label'])) {
431 $label = $options['label'];
432 }
433
434 // Take specific value of exclude flag into account
435 $exclude = true;
436 if (isset($options['exclude'])) {
437 $exclude = (bool)$options['exclude'];
438 }
439
440 $fieldConfiguration = empty($options['fieldConfiguration']) ? [] : $options['fieldConfiguration'];
441
442 $columns = [
443 $fieldName => [
444 'exclude' => $exclude,
445 'label' => $label,
446 'config' => static::getTcaFieldConfiguration($tableName, $fieldName, $fieldConfiguration),
447 ],
448 ];
449
450 if (isset($options['l10n_mode'])) {
451 $columns[$fieldName]['l10n_mode'] = $options['l10n_mode'];
452 }
453 if (isset($options['l10n_display'])) {
454 $columns[$fieldName]['l10n_display'] = $options['l10n_display'];
455 }
456 if (isset($options['displayCond'])) {
457 $columns[$fieldName]['displayCond'] = $options['displayCond'];
458 }
459
460 // Register opposite references for the foreign side of a relation
461 if (empty($GLOBALS['TCA']['sys_category']['columns']['items']['config']['MM_oppositeUsage'][$tableName])) {
462 $GLOBALS['TCA']['sys_category']['columns']['items']['config']['MM_oppositeUsage'][$tableName] = [];
463 }
464 if (!in_array($fieldName, $GLOBALS['TCA']['sys_category']['columns']['items']['config']['MM_oppositeUsage'][$tableName])) {
465 $GLOBALS['TCA']['sys_category']['columns']['items']['config']['MM_oppositeUsage'][$tableName][] = $fieldName;
466 }
467
468 // Add field to interface list per default (unless the 'interface' property is FALSE)
469 if (
470 (!isset($options['interface']) || $options['interface'])
471 && !empty($GLOBALS['TCA'][$tableName]['interface']['showRecordFieldList'])
472 && !GeneralUtility::inList($GLOBALS['TCA'][$tableName]['interface']['showRecordFieldList'], $fieldName)
473 ) {
474 $GLOBALS['TCA'][$tableName]['interface']['showRecordFieldList'] .= ',' . $fieldName;
475 }
476
477 // Adding fields to an existing table definition
478 ExtensionManagementUtility::addTCAcolumns($tableName, $columns);
479 }
480 }
481
482 /**
483 * Get the config array for given table and field.
484 * This method does NOT take care of adding sql fields, adding the field to TCA types
485 * nor does it set the MM_oppositeUsage in the sys_category TCA. This has to be taken care of manually!
486 *
487 * @param string $tableName The table name
488 * @param string $fieldName The field name (default categories)
489 * @param array $fieldConfigurationOverride Changes to the default configuration
490 * @return array
491 * @api
492 */
493 public static function getTcaFieldConfiguration($tableName, $fieldName = 'categories', array $fieldConfigurationOverride = [])
494 {
495 // Forges a new field, default name is "categories"
496 $fieldConfiguration = [
497 'type' => 'select',
498 'renderType' => 'selectTree',
499 'foreign_table' => 'sys_category',
500 'foreign_table_where' => ' AND sys_category.sys_language_uid IN (-1, 0) ORDER BY sys_category.sorting ASC',
501 'MM' => 'sys_category_record_mm',
502 'MM_opposite_field' => 'items',
503 'MM_match_fields' => [
504 'tablenames' => $tableName,
505 'fieldname' => $fieldName,
506 ],
507 'size' => 20,
508 'maxitems' => 9999,
509 'treeConfig' => [
510 'parentField' => 'parent',
511 'appearance' => [
512 'expandAll' => true,
513 'showHeader' => true,
514 'maxLevels' => 99,
515 ],
516 ],
517 ];
518
519 // Merge changes to TCA configuration
520 if (!empty($fieldConfigurationOverride)) {
521 ArrayUtility::mergeRecursiveWithOverrule(
522 $fieldConfiguration,
523 $fieldConfigurationOverride
524 );
525 }
526
527 return $fieldConfiguration;
528 }
529
530 /**
531 * A slot method to inject the required category database fields to the
532 * tables definition string
533 *
534 * @param array $sqlString
535 * @return array
536 */
537 public function addCategoryDatabaseSchemaToTablesDefinition(array $sqlString)
538 {
539 $this->registerDefaultCategorizedTables();
540 $sqlString[] = $this->getDatabaseTableDefinitions();
541 return ['sqlString' => $sqlString];
542 }
543
544 /**
545 * A slot method to inject the required category database fields of an
546 * extension to the tables definition string
547 *
548 * @param array $sqlString
549 * @param string $extensionKey
550 * @return array
551 */
552 public function addExtensionCategoryDatabaseSchemaToTablesDefinition(array $sqlString, $extensionKey)
553 {
554 $sqlString[] = $this->getDatabaseTableDefinition($extensionKey);
555 return ['sqlString' => $sqlString, 'extensionKey' => $extensionKey];
556 }
557
558 /**
559 * @return LanguageService
560 */
561 protected function getLanguageService()
562 {
563 return $GLOBALS['LANG'];
564 }
565
566 /**
567 * Removes the given field in the given table from the registry if it is found.
568 *
569 * @param string $tableName The name of the table for which the registration should be removed.
570 * @param string $fieldName The name of the field for which the registration should be removed.
571 */
572 protected function remove($tableName, $fieldName)
573 {
574 if (!$this->isRegistered($tableName, $fieldName)) {
575 return;
576 }
577
578 unset($this->registry[$tableName][$fieldName]);
579
580 foreach ($this->extensions as $extensionKey => $tableFieldConfig) {
581 foreach ($tableFieldConfig as $extTableName => $fieldNameArray) {
582 if ($extTableName === $tableName && isset($fieldNameArray[$fieldName])) {
583 unset($this->extensions[$extensionKey][$tableName][$fieldName]);
584 break;
585 }
586 }
587 }
588
589 // If no more fields are configured we unregister the categories tab.
590 if (empty($this->registry[$tableName]) && isset($this->addedCategoryTabs[$tableName])) {
591 unset($this->addedCategoryTabs[$tableName]);
592 }
593 }
594 }