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