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