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