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