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