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