22d3ca784a0dcd60edcb035f16b305bbcbc48ff4
[Packages/TYPO3.CMS.git] / typo3 / sysext / extbase / Classes / Persistence / Generic / Mapper / DataMapFactory.php
1 <?php
2 namespace TYPO3\CMS\Extbase\Persistence\Generic\Mapper;
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\Database\Query\QueryHelper;
18 use TYPO3\CMS\Extbase\Reflection\ClassSchema\Exception\NoSuchPropertyException;
19
20 /**
21 * A factory for a data map to map a single table configured in $TCA on a domain object.
22 * @internal only to be used within Extbase, not part of TYPO3 Core API.
23 */
24 class DataMapFactory implements \TYPO3\CMS\Core\SingletonInterface
25 {
26 /**
27 * @var \TYPO3\CMS\Extbase\Reflection\ReflectionService
28 */
29 protected $reflectionService;
30
31 /**
32 * @var \TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface
33 */
34 protected $configurationManager;
35
36 /**
37 * @var \TYPO3\CMS\Extbase\Object\ObjectManagerInterface
38 */
39 protected $objectManager;
40
41 /**
42 * @var \TYPO3\CMS\Core\Cache\CacheManager
43 */
44 protected $cacheManager;
45
46 /**
47 * @var \TYPO3\CMS\Core\Cache\Frontend\FrontendInterface
48 */
49 protected $dataMapCache;
50
51 /**
52 * Runtime cache for data maps, to reduce number of calls to cache backend.
53 *
54 * @var array
55 */
56 protected $dataMaps = [];
57
58 /**
59 * @param \TYPO3\CMS\Extbase\Reflection\ReflectionService $reflectionService
60 * @param \TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface $configurationManager
61 * @param \TYPO3\CMS\Extbase\Object\ObjectManagerInterface $objectManager
62 * @param \TYPO3\CMS\Core\Cache\CacheManager $cacheManager
63 */
64 public function __construct(
65 \TYPO3\CMS\Extbase\Reflection\ReflectionService $reflectionService,
66 \TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface $configurationManager,
67 \TYPO3\CMS\Extbase\Object\ObjectManagerInterface $objectManager,
68 \TYPO3\CMS\Core\Cache\CacheManager $cacheManager
69 ) {
70 $this->reflectionService = $reflectionService;
71 $this->configurationManager = $configurationManager;
72 $this->objectManager = $objectManager;
73 $this->cacheManager = $cacheManager;
74
75 $this->dataMapCache = $this->cacheManager->getCache('extbase_datamapfactory_datamap');
76 }
77
78 /**
79 * Builds a data map by adding column maps for all the configured columns in the $TCA.
80 * It also resolves the type of values the column is holding and the typo of relation the column
81 * represents.
82 *
83 * @param string $className The class name you want to fetch the Data Map for
84 * @return \TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMap The data map
85 */
86 public function buildDataMap($className)
87 {
88 if (isset($this->dataMaps[$className])) {
89 return $this->dataMaps[$className];
90 }
91 $dataMap = $this->dataMapCache->get(str_replace('\\', '%', $className));
92 if ($dataMap === false) {
93 $dataMap = $this->buildDataMapInternal($className);
94 $this->dataMapCache->set(str_replace('\\', '%', $className), $dataMap);
95 }
96 $this->dataMaps[$className] = $dataMap;
97 return $dataMap;
98 }
99
100 /**
101 * Builds a data map by adding column maps for all the configured columns in the $TCA.
102 * It also resolves the type of values the column is holding and the typo of relation the column
103 * represents.
104 *
105 * @param string $className The class name you want to fetch the Data Map for
106 * @throws \TYPO3\CMS\Extbase\Persistence\Generic\Exception\InvalidClassException
107 * @return \TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMap The data map
108 */
109 protected function buildDataMapInternal($className)
110 {
111 if (!class_exists($className)) {
112 throw new \TYPO3\CMS\Extbase\Persistence\Generic\Exception\InvalidClassException(
113 'Could not find class definition for name "' . $className . '". This could be caused by a mis-spelling of the class name in the class definition.',
114 1476045117
115 );
116 }
117 $recordType = null;
118 $subclasses = [];
119 $tableName = $this->resolveTableName($className);
120 $columnMapping = [];
121 $frameworkConfiguration = $this->configurationManager->getConfiguration(\TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface::CONFIGURATION_TYPE_FRAMEWORK);
122 $classSettings = $frameworkConfiguration['persistence']['classes'][$className] ?? null;
123 if ($classSettings !== null) {
124 if (isset($classSettings['subclasses']) && is_array($classSettings['subclasses'])) {
125 $subclasses = $this->resolveSubclassesRecursive($frameworkConfiguration['persistence']['classes'], $classSettings['subclasses']);
126 }
127 if (isset($classSettings['mapping']['recordType']) && $classSettings['mapping']['recordType'] !== '') {
128 $recordType = $classSettings['mapping']['recordType'];
129 }
130 if (isset($classSettings['mapping']['tableName']) && $classSettings['mapping']['tableName'] !== '') {
131 $tableName = $classSettings['mapping']['tableName'];
132 }
133 $classHierarchy = array_merge([$className], class_parents($className));
134 foreach ($classHierarchy as $currentClassName) {
135 if (in_array($currentClassName, [\TYPO3\CMS\Extbase\DomainObject\AbstractEntity::class, \TYPO3\CMS\Extbase\DomainObject\AbstractValueObject::class])) {
136 break;
137 }
138 $currentClassSettings = $frameworkConfiguration['persistence']['classes'][$currentClassName];
139 if ($currentClassSettings !== null) {
140 if (isset($currentClassSettings['mapping']['columns']) && is_array($currentClassSettings['mapping']['columns'])) {
141 \TYPO3\CMS\Core\Utility\ArrayUtility::mergeRecursiveWithOverrule($columnMapping, $currentClassSettings['mapping']['columns'], true, false);
142 }
143 }
144 }
145 }
146 /** @var \TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMap $dataMap */
147 $dataMap = $this->objectManager->get(\TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMap::class, $className, $tableName, $recordType, $subclasses);
148 $dataMap = $this->addMetaDataColumnNames($dataMap, $tableName);
149 // $classPropertyNames = $this->reflectionService->getClassPropertyNames($className);
150 $tcaColumnsDefinition = $this->getColumnsDefinition($tableName);
151 \TYPO3\CMS\Core\Utility\ArrayUtility::mergeRecursiveWithOverrule($tcaColumnsDefinition, $columnMapping);
152 // @todo Is this is too powerful?
153
154 foreach ($tcaColumnsDefinition as $columnName => $columnDefinition) {
155 if (isset($columnDefinition['mapOnProperty'])) {
156 $propertyName = $columnDefinition['mapOnProperty'];
157 } else {
158 $propertyName = \TYPO3\CMS\Core\Utility\GeneralUtility::underscoredToLowerCamelCase($columnName);
159 }
160 // @todo: shall we really create column maps for non existing properties?
161 // @todo: check why this could happen in the first place. TCA definitions for non existing model properties?
162 $columnMap = $this->createColumnMap($columnName, $propertyName);
163 try {
164 $property = $this->reflectionService->getClassSchema($className)->getProperty($propertyName);
165 [$type, $elementType] = [$property->getType(), $property->getElementType()];
166 } catch (NoSuchPropertyException $e) {
167 [$type, $elementType] = [null, null];
168 }
169 $columnMap = $this->setType($columnMap, $columnDefinition['config']);
170 $columnMap = $this->setRelations($columnMap, $columnDefinition['config'], $type, $elementType);
171 $columnMap = $this->setFieldEvaluations($columnMap, $columnDefinition['config']);
172 $dataMap->addColumnMap($columnMap);
173 }
174 return $dataMap;
175 }
176
177 /**
178 * Resolve the table name for the given class name
179 *
180 * @param string $className
181 * @return string The table name
182 */
183 protected function resolveTableName($className)
184 {
185 $className = ltrim($className, '\\');
186 $classNameParts = explode('\\', $className);
187 // Skip vendor and product name for core classes
188 if (strpos($className, 'TYPO3\\CMS\\') === 0) {
189 $classPartsToSkip = 2;
190 } else {
191 $classPartsToSkip = 1;
192 }
193 $tableName = 'tx_' . strtolower(implode('_', array_slice($classNameParts, $classPartsToSkip)));
194
195 return $tableName;
196 }
197
198 /**
199 * Resolves all subclasses for the given set of (sub-)classes.
200 * The whole classes configuration is used to determine all subclasses recursively.
201 *
202 * @param array $classesConfiguration The framework configuration part [persistence][classes].
203 * @param array $subclasses An array of subclasses defined via TypoScript
204 * @return array An numeric array that contains all available subclasses-strings as values.
205 */
206 protected function resolveSubclassesRecursive(array $classesConfiguration, array $subclasses)
207 {
208 $allSubclasses = [];
209 foreach ($subclasses as $subclass) {
210 $allSubclasses[] = $subclass;
211 if (isset($classesConfiguration[$subclass]['subclasses']) && is_array($classesConfiguration[$subclass]['subclasses'])) {
212 $childSubclasses = $this->resolveSubclassesRecursive($classesConfiguration, $classesConfiguration[$subclass]['subclasses']);
213 $allSubclasses = array_merge($allSubclasses, $childSubclasses);
214 }
215 }
216 return $allSubclasses;
217 }
218
219 /**
220 * Returns the TCA ctrl section of the specified table; or NULL if not set
221 *
222 * @param string $tableName An optional table name to fetch the columns definition from
223 * @return array The TCA columns definition
224 */
225 protected function getControlSection($tableName)
226 {
227 return (isset($GLOBALS['TCA'][$tableName]['ctrl']) && is_array($GLOBALS['TCA'][$tableName]['ctrl']))
228 ? $GLOBALS['TCA'][$tableName]['ctrl']
229 : null;
230 }
231
232 /**
233 * Returns the TCA columns array of the specified table
234 *
235 * @param string $tableName An optional table name to fetch the columns definition from
236 * @return array The TCA columns definition
237 */
238 protected function getColumnsDefinition($tableName)
239 {
240 return is_array($GLOBALS['TCA'][$tableName]['columns']) ? $GLOBALS['TCA'][$tableName]['columns'] : [];
241 }
242
243 /**
244 * @param DataMap $dataMap
245 * @param string $tableName
246 * @return DataMap
247 */
248 protected function addMetaDataColumnNames(\TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMap $dataMap, $tableName)
249 {
250 $controlSection = $GLOBALS['TCA'][$tableName]['ctrl'];
251 $dataMap->setPageIdColumnName('pid');
252 if (isset($controlSection['tstamp'])) {
253 $dataMap->setModificationDateColumnName($controlSection['tstamp']);
254 }
255 if (isset($controlSection['crdate'])) {
256 $dataMap->setCreationDateColumnName($controlSection['crdate']);
257 }
258 if (isset($controlSection['cruser_id'])) {
259 $dataMap->setCreatorColumnName($controlSection['cruser_id']);
260 }
261 if (isset($controlSection['delete'])) {
262 $dataMap->setDeletedFlagColumnName($controlSection['delete']);
263 }
264 if (isset($controlSection['languageField'])) {
265 $dataMap->setLanguageIdColumnName($controlSection['languageField']);
266 }
267 if (isset($controlSection['transOrigPointerField'])) {
268 $dataMap->setTranslationOriginColumnName($controlSection['transOrigPointerField']);
269 }
270 if (isset($controlSection['transOrigDiffSourceField'])) {
271 $dataMap->setTranslationOriginDiffSourceName($controlSection['transOrigDiffSourceField']);
272 }
273 if (isset($controlSection['type'])) {
274 $dataMap->setRecordTypeColumnName($controlSection['type']);
275 }
276 if (isset($controlSection['rootLevel'])) {
277 $dataMap->setRootLevel($controlSection['rootLevel']);
278 }
279 if (isset($controlSection['is_static'])) {
280 $dataMap->setIsStatic($controlSection['is_static']);
281 }
282 if (isset($controlSection['enablecolumns']['disabled'])) {
283 $dataMap->setDisabledFlagColumnName($controlSection['enablecolumns']['disabled']);
284 }
285 if (isset($controlSection['enablecolumns']['starttime'])) {
286 $dataMap->setStartTimeColumnName($controlSection['enablecolumns']['starttime']);
287 }
288 if (isset($controlSection['enablecolumns']['endtime'])) {
289 $dataMap->setEndTimeColumnName($controlSection['enablecolumns']['endtime']);
290 }
291 if (isset($controlSection['enablecolumns']['fe_group'])) {
292 $dataMap->setFrontEndUserGroupColumnName($controlSection['enablecolumns']['fe_group']);
293 }
294 return $dataMap;
295 }
296
297 /**
298 * Set the table column type
299 *
300 * @param ColumnMap $columnMap
301 * @param array $columnConfiguration
302 * @return ColumnMap
303 */
304 protected function setType(ColumnMap $columnMap, $columnConfiguration)
305 {
306 $tableColumnType = $columnConfiguration['type'] ?? null;
307 $columnMap->setType(\TYPO3\CMS\Core\DataHandling\TableColumnType::cast($tableColumnType));
308 $tableColumnSubType = $columnConfiguration['internal_type'] ?? null;
309 $columnMap->setInternalType(\TYPO3\CMS\Core\DataHandling\TableColumnSubType::cast($tableColumnSubType));
310
311 return $columnMap;
312 }
313
314 /**
315 * This method tries to determine the type of type of relation to other tables and sets it based on
316 * the $TCA column configuration
317 *
318 * @param ColumnMap $columnMap The column map
319 * @param array|null $columnConfiguration The column configuration from $TCA
320 * @param string|null $type
321 * @param string|null $elementType
322 * @return ColumnMap
323 */
324 protected function setRelations(ColumnMap $columnMap, $columnConfiguration, ?string $type, ?string $elementType)
325 {
326 if (isset($columnConfiguration)) {
327 if (isset($columnConfiguration['MM'])) {
328 $columnMap = $this->setManyToManyRelation($columnMap, $columnConfiguration);
329 } elseif ($elementType !== null) {
330 $columnMap = $this->setOneToManyRelation($columnMap, $columnConfiguration);
331 } elseif ($type !== null && strpbrk($type, '_\\') !== false) {
332 // @todo: check the strpbrk function call. Seems to be a check for Tx_Foo_Bar style class names
333 $columnMap = $this->setOneToOneRelation($columnMap, $columnConfiguration);
334 } elseif (
335 isset($columnConfiguration['type'], $columnConfiguration['renderType'])
336 && $columnConfiguration['type'] === 'select'
337 && (
338 $columnConfiguration['renderType'] !== 'selectSingle'
339 || (isset($columnConfiguration['maxitems']) && $columnConfiguration['maxitems'] > 1)
340 )
341 ) {
342 $columnMap->setTypeOfRelation(ColumnMap::RELATION_HAS_MANY);
343 } elseif (
344 isset($columnConfiguration['type']) && $columnConfiguration['type'] === 'group'
345 && (!isset($columnConfiguration['maxitems']) || $columnConfiguration['maxitems'] > 1)
346 ) {
347 $columnMap->setTypeOfRelation(ColumnMap::RELATION_HAS_MANY);
348 } else {
349 $columnMap->setTypeOfRelation(ColumnMap::RELATION_NONE);
350 }
351 } else {
352 $columnMap->setTypeOfRelation(ColumnMap::RELATION_NONE);
353 }
354 return $columnMap;
355 }
356
357 /**
358 * Sets field evaluations based on $TCA column configuration.
359 *
360 * @param ColumnMap $columnMap The column map
361 * @param array|null $columnConfiguration The column configuration from $TCA
362 * @return ColumnMap
363 */
364 protected function setFieldEvaluations(ColumnMap $columnMap, array $columnConfiguration = null)
365 {
366 if (!empty($columnConfiguration['eval'])) {
367 $fieldEvaluations = \TYPO3\CMS\Core\Utility\GeneralUtility::trimExplode(',', $columnConfiguration['eval'], true);
368 $dateTimeTypes = QueryHelper::getDateTimeTypes();
369
370 if (!empty(array_intersect($dateTimeTypes, $fieldEvaluations)) && !empty($columnConfiguration['dbType'])) {
371 $columnMap->setDateTimeStorageFormat($columnConfiguration['dbType']);
372 }
373 }
374
375 return $columnMap;
376 }
377
378 /**
379 * This method sets the configuration for a 1:1 relation based on
380 * the $TCA column configuration
381 *
382 * @param ColumnMap $columnMap The column map
383 * @param array|null $columnConfiguration The column configuration from $TCA
384 * @return ColumnMap
385 */
386 protected function setOneToOneRelation(ColumnMap $columnMap, array $columnConfiguration = null)
387 {
388 $columnMap->setTypeOfRelation(ColumnMap::RELATION_HAS_ONE);
389 $columnMap->setChildTableName($columnConfiguration['foreign_table']);
390 $columnMap->setChildTableWhereStatement($columnConfiguration['foreign_table_where'] ?? null);
391 $columnMap->setChildSortByFieldName($columnConfiguration['foreign_sortby'] ?? null);
392 $columnMap->setParentKeyFieldName($columnConfiguration['foreign_field'] ?? null);
393 $columnMap->setParentTableFieldName($columnConfiguration['foreign_table_field'] ?? null);
394 if (is_array($columnConfiguration['foreign_match_fields'])) {
395 $columnMap->setRelationTableMatchFields($columnConfiguration['foreign_match_fields']);
396 }
397 return $columnMap;
398 }
399
400 /**
401 * This method sets the configuration for a 1:n relation based on
402 * the $TCA column configuration
403 *
404 * @param ColumnMap $columnMap The column map
405 * @param array|null $columnConfiguration The column configuration from $TCA
406 * @return ColumnMap
407 */
408 protected function setOneToManyRelation(ColumnMap $columnMap, array $columnConfiguration = null)
409 {
410 $columnMap->setTypeOfRelation(ColumnMap::RELATION_HAS_MANY);
411 $columnMap->setChildTableName($columnConfiguration['foreign_table']);
412 $columnMap->setChildTableWhereStatement($columnConfiguration['foreign_table_where'] ?? null);
413 $columnMap->setChildSortByFieldName($columnConfiguration['foreign_sortby'] ?? null);
414 $columnMap->setParentKeyFieldName($columnConfiguration['foreign_field'] ?? null);
415 $columnMap->setParentTableFieldName($columnConfiguration['foreign_table_field'] ?? null);
416 if (is_array($columnConfiguration['foreign_match_fields'] ?? null)) {
417 $columnMap->setRelationTableMatchFields($columnConfiguration['foreign_match_fields']);
418 }
419 return $columnMap;
420 }
421
422 /**
423 * This method sets the configuration for a m:n relation based on
424 * the $TCA column configuration
425 *
426 * @param ColumnMap $columnMap The column map
427 * @param array|null $columnConfiguration The column configuration from $TCA
428 * @throws \TYPO3\CMS\Extbase\Persistence\Generic\Exception\UnsupportedRelationException
429 * @return ColumnMap
430 */
431 protected function setManyToManyRelation(ColumnMap $columnMap, array $columnConfiguration = null)
432 {
433 if (isset($columnConfiguration['MM'])) {
434 $columnMap->setTypeOfRelation(ColumnMap::RELATION_HAS_AND_BELONGS_TO_MANY);
435 $columnMap->setChildTableName($columnConfiguration['foreign_table']);
436 $columnMap->setChildTableWhereStatement($columnConfiguration['foreign_table_where'] ?? null);
437 $columnMap->setRelationTableName($columnConfiguration['MM']);
438 if (isset($columnConfiguration['MM_match_fields']) && is_array($columnConfiguration['MM_match_fields'])) {
439 $columnMap->setRelationTableMatchFields($columnConfiguration['MM_match_fields']);
440 }
441 if (isset($columnConfiguration['MM_insert_fields']) && is_array($columnConfiguration['MM_insert_fields'])) {
442 $columnMap->setRelationTableInsertFields($columnConfiguration['MM_insert_fields']);
443 }
444 $columnMap->setRelationTableWhereStatement($columnConfiguration['MM_table_where'] ?? null);
445 if (!empty($columnConfiguration['MM_opposite_field'])) {
446 $columnMap->setParentKeyFieldName('uid_foreign');
447 $columnMap->setChildKeyFieldName('uid_local');
448 $columnMap->setChildSortByFieldName('sorting_foreign');
449 } else {
450 $columnMap->setParentKeyFieldName('uid_local');
451 $columnMap->setChildKeyFieldName('uid_foreign');
452 $columnMap->setChildSortByFieldName('sorting');
453 }
454 } else {
455 throw new \TYPO3\CMS\Extbase\Persistence\Generic\Exception\UnsupportedRelationException('The given information to build a many-to-many-relation was not sufficient. Check your TCA definitions. mm-relations with IRRE must have at least a defined "MM" or "foreign_selector".', 1268817963);
456 }
457 if ($this->getControlSection($columnMap->getRelationTableName()) !== null) {
458 $columnMap->setRelationTablePageIdColumnName('pid');
459 }
460 return $columnMap;
461 }
462
463 /**
464 * Creates the ColumnMap object for the given columnName and propertyName
465 *
466 * @param string $columnName
467 * @param string $propertyName
468 *
469 * @return ColumnMap
470 */
471 protected function createColumnMap($columnName, $propertyName)
472 {
473 return $this->objectManager->get(\TYPO3\CMS\Extbase\Persistence\Generic\Mapper\ColumnMap::class, $columnName, $propertyName);
474 }
475 }