31a9a86c55da0e56d28332d7390e2c9b68b4255f
[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 = '';
143 $menuType = isset($configuration['row']['menu_type'][0]) ? $configuration['row']['menu_type'][0] : '';
144 // Define the table being looked up from the type of menu
145 if ($menuType === 'categorized_pages') {
146 $table = 'pages';
147 } elseif ($menuType === 'categorized_content') {
148 $table = 'tt_content';
149 }
150 // Return early if no table is defined
151 if (empty($table)) {
152 throw new \UnexpectedValueException('The given menu_type is not supported.', 1381823570);
153 }
154 // Loop on all registries and find entries for the correct table
155 foreach ($this->registry as $tableName => $fields) {
156 if ($tableName === $table) {
157 foreach ($fields as $fieldName => $options) {
158 $fieldLabel = $this->getLanguageService()->sL($GLOBALS['TCA'][$tableName]['columns'][$fieldName]['label']);
159 $configuration['items'][] = [$fieldLabel, $fieldName];
160 }
161 }
162 }
163 }
164
165 /**
166 * Tells whether a table has a category configuration in the registry.
167 *
168 * @param string $tableName Name of the table to be looked up
169 * @param string $fieldName Name of the field to be looked up
170 * @return bool
171 */
172 public function isRegistered($tableName, $fieldName = 'categories')
173 {
174 return isset($this->registry[$tableName][$fieldName]);
175 }
176
177 /**
178 * Generates tables definitions for all registered tables.
179 *
180 * @return string
181 */
182 public function getDatabaseTableDefinitions()
183 {
184 $sql = '';
185 foreach ($this->getExtensionKeys() as $extensionKey) {
186 $sql .= $this->getDatabaseTableDefinition($extensionKey);
187 }
188 return $sql;
189 }
190
191 /**
192 * Generates table definitions for registered tables by an extension.
193 *
194 * @param string $extensionKey Extension key to have the database definitions created for
195 * @return string
196 */
197 public function getDatabaseTableDefinition($extensionKey)
198 {
199 if (!isset($this->extensions[$extensionKey]) || !is_array($this->extensions[$extensionKey])) {
200 return '';
201 }
202 $sql = '';
203
204 foreach ($this->extensions[$extensionKey] as $tableName => $fields) {
205 foreach ($fields as $fieldName) {
206 $sql .= sprintf($this->template, $tableName, $fieldName);
207 }
208 }
209 return $sql;
210 }
211
212 /**
213 * Apply TCA to all registered tables
214 *
215 * @return void
216 * @internal
217 */
218 public function applyTcaForPreRegisteredTables()
219 {
220 $this->registerDefaultCategorizedTables();
221 foreach ($this->registry as $tableName => $fields) {
222 foreach ($fields as $fieldName => $_) {
223 $this->applyTcaForTableAndField($tableName, $fieldName);
224 }
225 }
226 }
227
228 /**
229 * Applies the additions directly to the TCA
230 *
231 * @param string $tableName
232 * @param string $fieldName
233 */
234 protected function applyTcaForTableAndField($tableName, $fieldName)
235 {
236 $this->addTcaColumn($tableName, $fieldName, $this->registry[$tableName][$fieldName]);
237 $this->addToAllTCAtypes($tableName, $fieldName, $this->registry[$tableName][$fieldName]);
238 }
239
240 /**
241 * Add default categorized tables to the registry
242 *
243 * @return void
244 */
245 protected function registerDefaultCategorizedTables()
246 {
247 $defaultCategorizedTables = GeneralUtility::trimExplode(
248 ',',
249 $GLOBALS['TYPO3_CONF_VARS']['SYS']['defaultCategorizedTables'],
250 true
251 );
252 foreach ($defaultCategorizedTables as $defaultCategorizedTable) {
253 if (!$this->isRegistered($defaultCategorizedTable)) {
254 $this->add('core', $defaultCategorizedTable, 'categories');
255 }
256 }
257 }
258
259 /**
260 * Add a new field into the TCA types -> showitem
261 *
262 * @param string $tableName Name of the table to be categorized
263 * @param string $fieldName Name of the field to be used to store categories
264 * @param array $options Additional configuration options
265 * + fieldList: field configuration to be added to showitems
266 * + typesList: list of types that shall visualize the categories field
267 * + position: insert position of the categories field
268 * @return void
269 */
270 protected function addToAllTCAtypes($tableName, $fieldName, array $options)
271 {
272
273 // Makes sure to add more TCA to an existing structure
274 if (isset($GLOBALS['TCA'][$tableName]['columns'])) {
275 if (empty($options['fieldList'])) {
276 $fieldList = $this->addCategoryTab($tableName, $fieldName);
277 } else {
278 $fieldList = $options['fieldList'];
279 }
280
281 $typesList = '';
282 if (isset($options['typesList']) && $options['typesList'] !== '') {
283 $typesList = $options['typesList'];
284 }
285
286 $position = '';
287 if (!empty($options['position'])) {
288 $position = $options['position'];
289 }
290
291 // Makes the new "categories" field to be visible in TSFE.
292 ExtensionManagementUtility::addToAllTCAtypes($tableName, $fieldList, $typesList, $position);
293 }
294 }
295
296 /**
297 * Creates the 'fieldList' string for $fieldName which includes a categories tab.
298 * But only one categories tab is added per table.
299 *
300 * @param string $tableName
301 * @param string $fieldName
302 * @return string
303 */
304 protected function addCategoryTab($tableName, $fieldName)
305 {
306 $fieldList = '';
307 if (!isset($this->addedCategoryTabs[$tableName])) {
308 $fieldList .= '--div--;LLL:EXT:lang/Resources/Private/Language/locallang_tca.xlf:sys_category.tabs.category, ';
309 $this->addedCategoryTabs[$tableName] = $tableName;
310 }
311 $fieldList .= $fieldName;
312 return $fieldList;
313 }
314
315 /**
316 * Add a new TCA Column
317 *
318 * @param string $tableName Name of the table to be categorized
319 * @param string $fieldName Name of the field to be used to store categories
320 * @param array $options Additional configuration options
321 * + fieldConfiguration: TCA field config array to override defaults
322 * + label: backend label of the categories field
323 * + interface: boolean if the category should be included in the "interface" section of the TCA table
324 * + l10n_mode
325 * + l10n_display
326 * @return void
327 */
328 protected function addTcaColumn($tableName, $fieldName, array $options)
329 {
330 // Makes sure to add more TCA to an existing structure
331 if (isset($GLOBALS['TCA'][$tableName]['columns'])) {
332 // Take specific label into account
333 $label = 'LLL:EXT:lang/Resources/Private/Language/locallang_tca.xlf:sys_category.categories';
334 if (!empty($options['label'])) {
335 $label = $options['label'];
336 }
337
338 // Take specific value of exclude flag into account
339 $exclude = true;
340 if (isset($options['exclude'])) {
341 $exclude = (bool)$options['exclude'];
342 }
343
344 $fieldConfiguration = empty($options['fieldConfiguration']) ? [] : $options['fieldConfiguration'];
345
346 $columns = [
347 $fieldName => [
348 'exclude' => $exclude,
349 'label' => $label,
350 'config' => static::getTcaFieldConfiguration($tableName, $fieldName, $fieldConfiguration),
351 ],
352 ];
353
354 if (isset($options['l10n_mode'])) {
355 $columns[$fieldName]['l10n_mode'] = $options['l10n_mode'];
356 }
357 if (isset($options['l10n_display'])) {
358 $columns[$fieldName]['l10n_display'] = $options['l10n_display'];
359 }
360 if (isset($options['displayCond'])) {
361 $columns[$fieldName]['displayCond'] = $options['displayCond'];
362 }
363
364 // Register opposite references for the foreign side of a relation
365 if (empty($GLOBALS['TCA']['sys_category']['columns']['items']['config']['MM_oppositeUsage'][$tableName])) {
366 $GLOBALS['TCA']['sys_category']['columns']['items']['config']['MM_oppositeUsage'][$tableName] = [];
367 }
368 if (!in_array($fieldName, $GLOBALS['TCA']['sys_category']['columns']['items']['config']['MM_oppositeUsage'][$tableName])) {
369 $GLOBALS['TCA']['sys_category']['columns']['items']['config']['MM_oppositeUsage'][$tableName][] = $fieldName;
370 }
371
372 // Add field to interface list per default (unless the 'interface' property is FALSE)
373 if (
374 (!isset($options['interface']) || $options['interface'])
375 && !empty($GLOBALS['TCA'][$tableName]['interface']['showRecordFieldList'])
376 && !GeneralUtility::inList($GLOBALS['TCA'][$tableName]['interface']['showRecordFieldList'], $fieldName)
377 ) {
378 $GLOBALS['TCA'][$tableName]['interface']['showRecordFieldList'] .= ',' . $fieldName;
379 }
380
381 // Adding fields to an existing table definition
382 ExtensionManagementUtility::addTCAcolumns($tableName, $columns);
383 }
384 }
385
386 /**
387 * Get the config array for given table and field.
388 * This method does NOT take care of adding sql fields, adding the field to TCA types
389 * nor does it set the MM_oppositeUsage in the sys_category TCA. This has to be taken care of manually!
390 *
391 * @param string $tableName The table name
392 * @param string $fieldName The field name (default categories)
393 * @param array $fieldConfigurationOverride Changes to the default configuration
394 * @return array
395 * @api
396 */
397 public static function getTcaFieldConfiguration($tableName, $fieldName = 'categories', array $fieldConfigurationOverride = [])
398 {
399 // Forges a new field, default name is "categories"
400 $fieldConfiguration = [
401 'type' => 'select',
402 'renderType' => 'selectTree',
403 'foreign_table' => 'sys_category',
404 'foreign_table_where' => ' AND sys_category.sys_language_uid IN (-1, 0) ORDER BY sys_category.sorting ASC',
405 'MM' => 'sys_category_record_mm',
406 'MM_opposite_field' => 'items',
407 'MM_match_fields' => [
408 'tablenames' => $tableName,
409 'fieldname' => $fieldName,
410 ],
411 'size' => 20,
412 'maxitems' => 9999,
413 'treeConfig' => [
414 'parentField' => 'parent',
415 'appearance' => [
416 'expandAll' => true,
417 'showHeader' => true,
418 'maxLevels' => 99,
419 ],
420 ],
421 ];
422
423 // Merge changes to TCA configuration
424 if (!empty($fieldConfigurationOverride)) {
425 ArrayUtility::mergeRecursiveWithOverrule(
426 $fieldConfiguration,
427 $fieldConfigurationOverride
428 );
429 }
430
431 return $fieldConfiguration;
432 }
433
434 /**
435 * A slot method to inject the required category database fields to the
436 * tables definition string
437 *
438 * @param array $sqlString
439 * @return array
440 */
441 public function addCategoryDatabaseSchemaToTablesDefinition(array $sqlString)
442 {
443 $this->registerDefaultCategorizedTables();
444 $sqlString[] = $this->getDatabaseTableDefinitions();
445 return ['sqlString' => $sqlString];
446 }
447
448 /**
449 * A slot method to inject the required category database fields of an
450 * extension to the tables definition string
451 *
452 * @param array $sqlString
453 * @param string $extensionKey
454 * @return array
455 */
456 public function addExtensionCategoryDatabaseSchemaToTablesDefinition(array $sqlString, $extensionKey)
457 {
458 $sqlString[] = $this->getDatabaseTableDefinition($extensionKey);
459 return ['sqlString' => $sqlString, 'extensionKey' => $extensionKey];
460 }
461
462 /**
463 * @return LanguageService
464 */
465 protected function getLanguageService()
466 {
467 return $GLOBALS['LANG'];
468 }
469
470 /**
471 * Removes the given field in the given table from the registry if it is found.
472 *
473 * @param string $tableName The name of the table for which the registration should be removed.
474 * @param string $fieldName The name of the field for which the registration should be removed.
475 */
476 protected function remove($tableName, $fieldName)
477 {
478 if (!$this->isRegistered($tableName, $fieldName)) {
479 return;
480 }
481
482 unset($this->registry[$tableName][$fieldName]);
483
484 foreach ($this->extensions as $extensionKey => $tableFieldConfig) {
485 foreach ($tableFieldConfig as $extTableName => $fieldNameArray) {
486 if ($extTableName === $tableName && isset($fieldNameArray[$fieldName])) {
487 unset($this->extensions[$extensionKey][$tableName][$fieldName]);
488 break;
489 }
490 }
491 }
492
493 // If no more fields are configured we unregister the categories tab.
494 if (empty($this->registry[$tableName]) && isset($this->addedCategoryTabs[$tableName])) {
495 unset($this->addedCategoryTabs[$tableName]);
496 }
497 }
498 }