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