1<?php 2namespace 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 17use 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 */ 23class 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} 488