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