1<?php 2 3/* 4 * This file is part of the TYPO3 CMS project. 5 * 6 * It is free software; you can redistribute it and/or modify it under 7 * the terms of the GNU General Public License, either version 2 8 * of the License, or any later version. 9 * 10 * For the full copyright and license information, please read the 11 * LICENSE.txt file that was distributed with this source code. 12 * 13 * The TYPO3 project - inspiring people to share! 14 */ 15 16namespace TYPO3\CMS\Extbase\Persistence\Generic\Mapper; 17 18use Psr\EventDispatcher\EventDispatcherInterface; 19use TYPO3\CMS\Core\Context\Context; 20use TYPO3\CMS\Core\Database\Query\QueryHelper; 21use TYPO3\CMS\Core\Database\RelationHandler; 22use TYPO3\CMS\Core\Utility\GeneralUtility; 23use TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface; 24use TYPO3\CMS\Extbase\Event\Persistence\AfterObjectThawedEvent; 25use TYPO3\CMS\Extbase\Object\Exception\CannotReconstituteObjectException; 26use TYPO3\CMS\Extbase\Object\ObjectManagerInterface; 27use TYPO3\CMS\Extbase\Persistence; 28use TYPO3\CMS\Extbase\Persistence\Generic\Exception; 29use TYPO3\CMS\Extbase\Persistence\Generic\Exception\UnexpectedTypeException; 30use TYPO3\CMS\Extbase\Persistence\Generic\LazyLoadingProxy; 31use TYPO3\CMS\Extbase\Persistence\Generic\LazyObjectStorage; 32use TYPO3\CMS\Extbase\Persistence\Generic\LoadingStrategyInterface; 33use TYPO3\CMS\Extbase\Persistence\Generic\Mapper\Exception\NonExistentPropertyException; 34use TYPO3\CMS\Extbase\Persistence\Generic\Mapper\Exception\UnknownPropertyTypeException; 35use TYPO3\CMS\Extbase\Persistence\Generic\Qom\QueryObjectModelFactory; 36use TYPO3\CMS\Extbase\Persistence\Generic\Query; 37use TYPO3\CMS\Extbase\Persistence\Generic\QueryFactoryInterface; 38use TYPO3\CMS\Extbase\Persistence\Generic\Session; 39use TYPO3\CMS\Extbase\Persistence\ObjectStorage; 40use TYPO3\CMS\Extbase\Persistence\QueryInterface; 41use TYPO3\CMS\Extbase\Persistence\QueryResultInterface; 42use TYPO3\CMS\Extbase\Reflection\ClassSchema; 43use TYPO3\CMS\Extbase\Reflection\ClassSchema\Exception\NoSuchPropertyException; 44use TYPO3\CMS\Extbase\Reflection\ReflectionService; 45use TYPO3\CMS\Extbase\Utility\TypeHandlingUtility; 46 47/** 48 * A mapper to map database tables configured in $TCA on domain objects. 49 * @internal only to be used within Extbase, not part of TYPO3 Core API. 50 */ 51class DataMapper 52{ 53 /** 54 * @var \TYPO3\CMS\Extbase\Reflection\ReflectionService 55 */ 56 protected $reflectionService; 57 58 /** 59 * @var \TYPO3\CMS\Extbase\Persistence\Generic\Qom\QueryObjectModelFactory 60 */ 61 protected $qomFactory; 62 63 /** 64 * @var \TYPO3\CMS\Extbase\Persistence\Generic\Session 65 */ 66 protected $persistenceSession; 67 68 /** 69 * @var \TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMapFactory 70 */ 71 protected $dataMapFactory; 72 73 /** 74 * @var \TYPO3\CMS\Extbase\Persistence\Generic\QueryFactoryInterface 75 */ 76 protected $queryFactory; 77 78 /** 79 * @var \TYPO3\CMS\Extbase\Object\ObjectManagerInterface 80 */ 81 protected $objectManager; 82 83 /** 84 * @var EventDispatcherInterface 85 */ 86 protected $eventDispatcher; 87 88 /** 89 * @var QueryInterface|null 90 */ 91 protected $query; 92 93 /** 94 * DataMapper constructor. 95 * @param \TYPO3\CMS\Extbase\Reflection\ReflectionService $reflectionService 96 * @param \TYPO3\CMS\Extbase\Persistence\Generic\Qom\QueryObjectModelFactory $qomFactory 97 * @param \TYPO3\CMS\Extbase\Persistence\Generic\Session $persistenceSession 98 * @param \TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMapFactory $dataMapFactory 99 * @param \TYPO3\CMS\Extbase\Persistence\Generic\QueryFactoryInterface $queryFactory 100 * @param \TYPO3\CMS\Extbase\Object\ObjectManagerInterface $objectManager 101 * @param EventDispatcherInterface $eventDispatcher 102 * @param QueryInterface|null $query 103 */ 104 public function __construct( 105 ReflectionService $reflectionService, 106 QueryObjectModelFactory $qomFactory, 107 Session $persistenceSession, 108 DataMapFactory $dataMapFactory, 109 QueryFactoryInterface $queryFactory, 110 ObjectManagerInterface $objectManager, 111 EventDispatcherInterface $eventDispatcher, 112 ?QueryInterface $query = null 113 ) { 114 $this->query = $query; 115 $this->reflectionService = $reflectionService; 116 $this->qomFactory = $qomFactory; 117 $this->persistenceSession = $persistenceSession; 118 $this->dataMapFactory = $dataMapFactory; 119 $this->queryFactory = $queryFactory; 120 $this->objectManager = $objectManager; 121 $this->eventDispatcher = $eventDispatcher; 122 123 if ($query !== null) { 124 trigger_error( 125 'Constructor argument $query will be removed in TYPO3 v11.0, use setQuery method instead.', 126 E_USER_DEPRECATED 127 ); 128 $this->query = $query; 129 } 130 } 131 132 /** 133 * @param QueryInterface $query 134 */ 135 public function setQuery(QueryInterface $query): void 136 { 137 $this->query = $query; 138 } 139 140 /** 141 * Maps the given rows on objects 142 * 143 * @param string $className The name of the class 144 * @param array $rows An array of arrays with field_name => value pairs 145 * @return array An array of objects of the given class 146 */ 147 public function map($className, array $rows) 148 { 149 $objects = []; 150 foreach ($rows as $row) { 151 $objects[] = $this->mapSingleRow($this->getTargetType($className, $row), $row); 152 } 153 return $objects; 154 } 155 156 /** 157 * Returns the target type for the given row. 158 * 159 * @param string $className The name of the class 160 * @param array $row A single array with field_name => value pairs 161 * @return string The target type (a class name) 162 */ 163 public function getTargetType($className, array $row) 164 { 165 $dataMap = $this->getDataMap($className); 166 $targetType = $className; 167 if ($dataMap->getRecordTypeColumnName() !== null) { 168 foreach ($dataMap->getSubclasses() as $subclassName) { 169 $recordSubtype = $this->getDataMap($subclassName)->getRecordType(); 170 if ((string)$row[$dataMap->getRecordTypeColumnName()] === (string)$recordSubtype) { 171 $targetType = $subclassName; 172 break; 173 } 174 } 175 } 176 return $targetType; 177 } 178 179 /** 180 * Maps a single row on an object of the given class 181 * 182 * @param string $className The name of the target class 183 * @param array $row A single array with field_name => value pairs 184 * @return object An object of the given class 185 */ 186 protected function mapSingleRow($className, array $row) 187 { 188 if ($this->persistenceSession->hasIdentifier($row['uid'], $className)) { 189 $object = $this->persistenceSession->getObjectByIdentifier($row['uid'], $className); 190 } else { 191 $object = $this->createEmptyObject($className); 192 $this->persistenceSession->registerObject($object, $row['uid']); 193 $this->thawProperties($object, $row); 194 $event = new AfterObjectThawedEvent($object, $row); 195 $this->eventDispatcher->dispatch($event); 196 $object->_memorizeCleanState(); 197 $this->persistenceSession->registerReconstitutedEntity($object); 198 } 199 return $object; 200 } 201 202 /** 203 * Creates a skeleton of the specified object 204 * 205 * @param string $className Name of the class to create a skeleton for 206 * @throws CannotReconstituteObjectException 207 * @return DomainObjectInterface The object skeleton 208 */ 209 protected function createEmptyObject($className) 210 { 211 // Note: The class_implements() function also invokes autoload to assure that the interfaces 212 // and the class are loaded. Would end up with __PHP_Incomplete_Class without it. 213 if (!in_array(DomainObjectInterface::class, class_implements($className))) { 214 throw new CannotReconstituteObjectException('Cannot create empty instance of the class "' . $className 215 . '" because it does not implement the TYPO3\\CMS\\Extbase\\DomainObject\\DomainObjectInterface.', 1234386924); 216 } 217 $object = $this->objectManager->getEmptyObject($className); 218 return $object; 219 } 220 221 /** 222 * Sets the given properties on the object. 223 * 224 * @param DomainObjectInterface $object The object to set properties on 225 * @param array $row 226 * @throws NonExistentPropertyException 227 * @throws UnknownPropertyTypeException 228 */ 229 protected function thawProperties(DomainObjectInterface $object, array $row) 230 { 231 $className = get_class($object); 232 $classSchema = $this->reflectionService->getClassSchema($className); 233 $dataMap = $this->getDataMap($className); 234 $object->_setProperty('uid', (int)$row['uid']); 235 $object->_setProperty('pid', (int)($row['pid'] ?? 0)); 236 $object->_setProperty('_localizedUid', (int)$row['uid']); 237 $object->_setProperty('_versionedUid', (int)$row['uid']); 238 if ($dataMap->getLanguageIdColumnName() !== null) { 239 $object->_setProperty('_languageUid', (int)$row[$dataMap->getLanguageIdColumnName()]); 240 if (isset($row['_LOCALIZED_UID'])) { 241 $object->_setProperty('_localizedUid', (int)$row['_LOCALIZED_UID']); 242 } 243 } 244 if (!empty($row['_ORIG_uid']) && !empty($GLOBALS['TCA'][$dataMap->getTableName()]['ctrl']['versioningWS'])) { 245 $object->_setProperty('_versionedUid', (int)$row['_ORIG_uid']); 246 } 247 $properties = $object->_getProperties(); 248 foreach ($properties as $propertyName => $propertyValue) { 249 if (!$dataMap->isPersistableProperty($propertyName)) { 250 continue; 251 } 252 $columnMap = $dataMap->getColumnMap($propertyName); 253 $columnName = $columnMap->getColumnName(); 254 255 try { 256 $property = $classSchema->getProperty($propertyName); 257 } catch (NoSuchPropertyException $e) { 258 throw new NonExistentPropertyException( 259 'The type of property ' . $className . '::' . $propertyName . ' could not be identified, ' . 260 'as property ' . $propertyName . ' is unknown to the ' . ClassSchema::class . ' instance of class ' . 261 $className . '. Please make sure said property exists and that you cleared all caches to trigger ' . 262 'a new build of said ' . ClassSchema::class . ' instance.', 263 1580056272 264 ); 265 } 266 267 $propertyType = $property->getType(); 268 if ($propertyType === null) { 269 throw new UnknownPropertyTypeException( 270 'The type of property ' . $className . '::' . $propertyName . ' could not be identified, therefore the desired value (' . 271 var_export($propertyValue, true) . ') cannot be mapped onto it. The type of a class property is usually defined via php doc blocks. ' . 272 'Make sure the property has a valid @var tag set which defines the type.', 273 1579965021 274 ); 275 } 276 $propertyValue = null; 277 if (isset($row[$columnName])) { 278 switch ($propertyType) { 279 case 'int': 280 case 'integer': 281 $propertyValue = (int)$row[$columnName]; 282 break; 283 case 'float': 284 $propertyValue = (double)$row[$columnName]; 285 break; 286 case 'bool': 287 case 'boolean': 288 $propertyValue = (bool)$row[$columnName]; 289 break; 290 case 'string': 291 $propertyValue = (string)$row[$columnName]; 292 break; 293 case 'array': 294 // $propertyValue = $this->mapArray($row[$columnName]); // Not supported, yet! 295 break; 296 case \SplObjectStorage::class: 297 case ObjectStorage::class: 298 $propertyValue = $this->mapResultToPropertyValue( 299 $object, 300 $propertyName, 301 $this->fetchRelated($object, $propertyName, $row[$columnName]) 302 ); 303 break; 304 default: 305 if (is_subclass_of($propertyType, \DateTimeInterface::class)) { 306 $propertyValue = $this->mapDateTime( 307 $row[$columnName], 308 $columnMap->getDateTimeStorageFormat(), 309 $propertyType 310 ); 311 } elseif (TypeHandlingUtility::isCoreType($propertyType)) { 312 $propertyValue = $this->mapCoreType($propertyType, $row[$columnName]); 313 } else { 314 $propertyValue = $this->mapObjectToClassProperty( 315 $object, 316 $propertyName, 317 $row[$columnName] 318 ); 319 } 320 321 } 322 } 323 if ($propertyValue !== null) { 324 $object->_setProperty($propertyName, $propertyValue); 325 } 326 } 327 } 328 329 /** 330 * Map value to a core type 331 * 332 * @param string $type 333 * @param mixed $value 334 * @return \TYPO3\CMS\Core\Type\TypeInterface 335 */ 336 protected function mapCoreType($type, $value) 337 { 338 return new $type($value); 339 } 340 341 /** 342 * Creates a DateTime from a unix timestamp or date/datetime/time value. 343 * If the input is empty, NULL is returned. 344 * 345 * @param int|string $value Unix timestamp or date/datetime/time value 346 * @param string|null $storageFormat Storage format for native date/datetime/time fields 347 * @param string $targetType The object class name to be created 348 * @return \DateTimeInterface 349 */ 350 protected function mapDateTime($value, $storageFormat = null, $targetType = \DateTime::class) 351 { 352 $dateTimeTypes = QueryHelper::getDateTimeTypes(); 353 354 if (empty($value) || $value === '0000-00-00' || $value === '0000-00-00 00:00:00' || $value === '00:00:00') { 355 // 0 -> NULL !!! 356 return null; 357 } 358 if (in_array($storageFormat, $dateTimeTypes, true)) { 359 // native date/datetime/time values are stored in UTC 360 $utcTimeZone = new \DateTimeZone('UTC'); 361 $utcDateTime = GeneralUtility::makeInstance($targetType, $value, $utcTimeZone); 362 $currentTimeZone = new \DateTimeZone(date_default_timezone_get()); 363 return $utcDateTime->setTimezone($currentTimeZone); 364 } 365 // integer timestamps are local server time 366 return GeneralUtility::makeInstance($targetType, date('c', (int)$value)); 367 } 368 369 /** 370 * Fetches a collection of objects related to a property of a parent object 371 * 372 * @param DomainObjectInterface $parentObject The object instance this proxy is part of 373 * @param string $propertyName The name of the proxied property in it's parent 374 * @param mixed $fieldValue The raw field value. 375 * @param bool $enableLazyLoading A flag indication if the related objects should be lazy loaded 376 * @return \TYPO3\CMS\Extbase\Persistence\Generic\LazyObjectStorage|Persistence\QueryResultInterface The result 377 */ 378 public function fetchRelated(DomainObjectInterface $parentObject, $propertyName, $fieldValue = '', $enableLazyLoading = true) 379 { 380 $property = $this->reflectionService->getClassSchema(get_class($parentObject))->getProperty($propertyName); 381 if ($enableLazyLoading === true && $property->isLazy()) { 382 if ($property->getType() === ObjectStorage::class) { 383 $result = $this->objectManager->get(LazyObjectStorage::class, $parentObject, $propertyName, $fieldValue, $this); 384 } else { 385 if (empty($fieldValue)) { 386 $result = null; 387 } else { 388 $result = $this->objectManager->get(LazyLoadingProxy::class, $parentObject, $propertyName, $fieldValue, $this); 389 } 390 } 391 } else { 392 $result = $this->fetchRelatedEager($parentObject, $propertyName, $fieldValue); 393 } 394 return $result; 395 } 396 397 /** 398 * Fetches the related objects from the storage backend. 399 * 400 * @param DomainObjectInterface $parentObject The object instance this proxy is part of 401 * @param string $propertyName The name of the proxied property in it's parent 402 * @param mixed $fieldValue The raw field value. 403 * @return mixed 404 */ 405 protected function fetchRelatedEager(DomainObjectInterface $parentObject, $propertyName, $fieldValue = '') 406 { 407 return $fieldValue === '' ? $this->getEmptyRelationValue($parentObject, $propertyName) : $this->getNonEmptyRelationValue($parentObject, $propertyName, $fieldValue); 408 } 409 410 /** 411 * @param DomainObjectInterface $parentObject 412 * @param string $propertyName 413 * @return array|null 414 */ 415 protected function getEmptyRelationValue(DomainObjectInterface $parentObject, $propertyName) 416 { 417 $columnMap = $this->getDataMap(get_class($parentObject))->getColumnMap($propertyName); 418 $relatesToOne = $columnMap->getTypeOfRelation() == ColumnMap::RELATION_HAS_ONE; 419 return $relatesToOne ? null : []; 420 } 421 422 /** 423 * @param DomainObjectInterface $parentObject 424 * @param string $propertyName 425 * @param string $fieldValue 426 * @return Persistence\QueryResultInterface 427 */ 428 protected function getNonEmptyRelationValue(DomainObjectInterface $parentObject, $propertyName, $fieldValue) 429 { 430 $query = $this->getPreparedQuery($parentObject, $propertyName, $fieldValue); 431 return $query->execute(); 432 } 433 434 /** 435 * Builds and returns the prepared query, ready to be executed. 436 * 437 * @param DomainObjectInterface $parentObject 438 * @param string $propertyName 439 * @param string $fieldValue 440 * @return Persistence\QueryInterface 441 */ 442 protected function getPreparedQuery(DomainObjectInterface $parentObject, $propertyName, $fieldValue = '') 443 { 444 $dataMap = $this->getDataMap(get_class($parentObject)); 445 $columnMap = $dataMap->getColumnMap($propertyName); 446 $type = $this->getType(get_class($parentObject), $propertyName); 447 $query = $this->queryFactory->create($type); 448 if ($this->query && $query instanceof Query) { 449 $query->setParentQuery($this->query); 450 } 451 $query->getQuerySettings()->setRespectStoragePage(false); 452 $query->getQuerySettings()->setRespectSysLanguage(false); 453 454 // we always want to overlay relations as most of the time they are stored in db using default lang uids 455 $query->getQuerySettings()->setLanguageOverlayMode(true); 456 if ($this->query) { 457 $query->getQuerySettings()->setLanguageUid($this->query->getQuerySettings()->getLanguageUid()); 458 459 if ($dataMap->getLanguageIdColumnName() !== null && !$this->query->getQuerySettings()->getRespectSysLanguage()) { 460 //pass language of parent record to child objects, so they can be overlaid correctly in case 461 //e.g. findByUid is used. 462 //the languageUid is used for getRecordOverlay later on, despite RespectSysLanguage being false 463 $languageUid = (int)$parentObject->_getProperty('_languageUid'); 464 $query->getQuerySettings()->setLanguageUid($languageUid); 465 } 466 } 467 468 if ($columnMap->getTypeOfRelation() === ColumnMap::RELATION_HAS_MANY) { 469 if ($columnMap->getChildSortByFieldName() !== null) { 470 $query->setOrderings([$columnMap->getChildSortByFieldName() => QueryInterface::ORDER_ASCENDING]); 471 } 472 } elseif ($columnMap->getTypeOfRelation() === ColumnMap::RELATION_HAS_AND_BELONGS_TO_MANY) { 473 $query->setSource($this->getSource($parentObject, $propertyName)); 474 if ($columnMap->getChildSortByFieldName() !== null) { 475 $query->setOrderings([$columnMap->getChildSortByFieldName() => QueryInterface::ORDER_ASCENDING]); 476 } 477 } 478 $query->matching($this->getConstraint($query, $parentObject, $propertyName, $fieldValue, (array)$columnMap->getRelationTableMatchFields())); 479 return $query; 480 } 481 482 /** 483 * Builds and returns the constraint for multi value properties. 484 * 485 * @param Persistence\QueryInterface $query 486 * @param DomainObjectInterface $parentObject 487 * @param string $propertyName 488 * @param string $fieldValue 489 * @param array $relationTableMatchFields 490 * @return \TYPO3\CMS\Extbase\Persistence\Generic\Qom\ConstraintInterface $constraint 491 */ 492 protected function getConstraint(QueryInterface $query, DomainObjectInterface $parentObject, $propertyName, $fieldValue = '', $relationTableMatchFields = []) 493 { 494 $dataMap = $this->getDataMap(get_class($parentObject)); 495 $columnMap = $dataMap->getColumnMap($propertyName); 496 $workspaceId = GeneralUtility::makeInstance(Context::class)->getPropertyFromAspect('workspace', 'id'); 497 if ($columnMap && $workspaceId > 0) { 498 $resolvedRelationIds = $this->resolveRelationValuesOfField($dataMap, $columnMap, $parentObject, $fieldValue, $workspaceId); 499 } else { 500 $resolvedRelationIds = []; 501 } 502 // Work with the UIDs directly in a workspace 503 if (!empty($resolvedRelationIds)) { 504 if ($query->getSource() instanceof Persistence\Generic\Qom\JoinInterface) { 505 $constraint = $query->in($query->getSource()->getJoinCondition()->getProperty1Name(), $resolvedRelationIds); 506 // When querying MM relations directly, Typo3DbQueryParser uses enableFields and thus, filters 507 // out versioned records by default. However, we directly query versioned UIDs here, so we want 508 // to include the versioned records explicitly. 509 if ($columnMap->getTypeOfRelation() === ColumnMap::RELATION_HAS_AND_BELONGS_TO_MANY) { 510 $query->getQuerySettings()->setEnableFieldsToBeIgnored(['pid']); 511 $query->getQuerySettings()->setIgnoreEnableFields(true); 512 } 513 } else { 514 $constraint = $query->in('uid', $resolvedRelationIds); 515 } 516 if ($columnMap->getParentTableFieldName() !== null) { 517 $constraint = $query->logicalAnd( 518 $constraint, 519 $query->equals($columnMap->getParentTableFieldName(), $dataMap->getTableName()) 520 ); 521 } 522 } elseif ($columnMap->getParentKeyFieldName() !== null) { 523 $value = $parentObject; 524 // If this a MM relation, and MM relations do not know about workspaces, the MM relations always point to the 525 // versioned record, so this must be taken into account here and the versioned record's UID must be used. 526 if ($columnMap->getTypeOfRelation() === ColumnMap::RELATION_HAS_AND_BELONGS_TO_MANY) { 527 // The versioned UID is used ideally the version ID of a translated record, so this takes precedence over the localized UID 528 if ($value->_hasProperty('_versionedUid') && $value->_getProperty('_versionedUid') > 0 && $value->_getProperty('_versionedUid') !== $value->getUid()) { 529 $value = (int)$value->_getProperty('_versionedUid'); 530 } 531 } 532 $constraint = $query->equals($columnMap->getParentKeyFieldName(), $value); 533 if ($columnMap->getParentTableFieldName() !== null) { 534 $constraint = $query->logicalAnd( 535 $constraint, 536 $query->equals($columnMap->getParentTableFieldName(), $dataMap->getTableName()) 537 ); 538 } 539 } else { 540 $constraint = $query->in('uid', GeneralUtility::intExplode(',', $fieldValue)); 541 } 542 if (!empty($relationTableMatchFields)) { 543 foreach ($relationTableMatchFields as $relationTableMatchFieldName => $relationTableMatchFieldValue) { 544 $constraint = $query->logicalAnd($constraint, $query->equals($relationTableMatchFieldName, $relationTableMatchFieldValue)); 545 } 546 } 547 return $constraint; 548 } 549 550 /** 551 * This resolves relations via RelationHandler and returns their UIDs respectively, and works for MM/ForeignField/CSV in IRRE + Select + Group. 552 * 553 * Note: This only happens for resolving properties for models. When limiting a parentQuery, the Typo3DbQueryParser is taking care of it. 554 * 555 * By using the RelationHandler, the localized, deleted and moved records turn out to be properly resolved 556 * without having to build intermediate queries. 557 * 558 * This is currently only used in workspaces' context, as it is 1 additional DB query needed. 559 * 560 * @param DataMap $dataMap 561 * @param ColumnMap $columnMap 562 * @param DomainObjectInterface $parentObject 563 * @param string $fieldValue 564 * @param int $workspaceId 565 * @return array|false|mixed 566 */ 567 protected function resolveRelationValuesOfField(DataMap $dataMap, ColumnMap $columnMap, DomainObjectInterface $parentObject, $fieldValue, int $workspaceId) 568 { 569 $parentId = $parentObject->getUid(); 570 // versionedUid in a multi-language setup is the overlaid versioned AND translated ID 571 if ($parentObject->_hasProperty('_versionedUid') && $parentObject->_getProperty('_versionedUid') > 0 && $parentObject->_getProperty('_versionedUid') !== $parentId) { 572 $parentId = $parentObject->_getProperty('_versionedUid'); 573 } elseif ($parentObject->_hasProperty('_languageUid') && $parentObject->_getProperty('_languageUid') > 0) { 574 $parentId = $parentObject->_getProperty('_localizedUid'); 575 } 576 $relationHandler = GeneralUtility::makeInstance(RelationHandler::class); 577 $relationHandler->setWorkspaceId($workspaceId); 578 $relationHandler->setUseLiveReferenceIds(true); 579 $relationHandler->setUseLiveParentIds(true); 580 $tableName = $dataMap->getTableName(); 581 $fieldName = $columnMap->getColumnName(); 582 $fieldConfiguration = $GLOBALS['TCA'][$tableName]['columns'][$fieldName]['config'] ?? null; 583 if (!is_array($fieldConfiguration)) { 584 return []; 585 } 586 $relationHandler->start( 587 $fieldValue, 588 $fieldConfiguration['allowed'] ?? $fieldConfiguration['foreign_table'] ?? '', 589 $fieldConfiguration['MM'] ?? '', 590 $parentId, 591 $tableName, 592 $fieldConfiguration 593 ); 594 $relationHandler->processDeletePlaceholder(); 595 $relatedUids = []; 596 if (!empty($relationHandler->tableArray)) { 597 $relatedUids = reset($relationHandler->tableArray); 598 } 599 return $relatedUids; 600 } 601 602 /** 603 * Builds and returns the source to build a join for a m:n relation. 604 * 605 * @param DomainObjectInterface $parentObject 606 * @param string $propertyName 607 * @return \TYPO3\CMS\Extbase\Persistence\Generic\Qom\SourceInterface $source 608 */ 609 protected function getSource(DomainObjectInterface $parentObject, $propertyName) 610 { 611 $columnMap = $this->getDataMap(get_class($parentObject))->getColumnMap($propertyName); 612 $left = $this->qomFactory->selector(null, $columnMap->getRelationTableName()); 613 $childClassName = $this->getType(get_class($parentObject), $propertyName); 614 $right = $this->qomFactory->selector($childClassName, $columnMap->getChildTableName()); 615 $joinCondition = $this->qomFactory->equiJoinCondition($columnMap->getRelationTableName(), $columnMap->getChildKeyFieldName(), $columnMap->getChildTableName(), 'uid'); 616 $source = $this->qomFactory->join($left, $right, Query::JCR_JOIN_TYPE_INNER, $joinCondition); 617 return $source; 618 } 619 620 /** 621 * Returns the mapped classProperty from the identityMap or 622 * mapResultToPropertyValue() 623 * 624 * If the field value is empty and the column map has no parent key field name, 625 * the relation will be empty. If the persistence session has a registered object of 626 * the correct type and identity (fieldValue), this function returns that object. 627 * Otherwise, it proceeds with mapResultToPropertyValue(). 628 * 629 * @param DomainObjectInterface $parentObject 630 * @param string $propertyName 631 * @param mixed $fieldValue the raw field value 632 * @return mixed 633 * @see mapResultToPropertyValue() 634 */ 635 protected function mapObjectToClassProperty(DomainObjectInterface $parentObject, $propertyName, $fieldValue) 636 { 637 if ($this->propertyMapsByForeignKey($parentObject, $propertyName)) { 638 $result = $this->fetchRelated($parentObject, $propertyName, $fieldValue); 639 $propertyValue = $this->mapResultToPropertyValue($parentObject, $propertyName, $result); 640 } else { 641 if ($fieldValue === '') { 642 $propertyValue = $this->getEmptyRelationValue($parentObject, $propertyName); 643 } else { 644 $property = $this->reflectionService->getClassSchema(get_class($parentObject))->getProperty($propertyName); 645 if ($this->persistenceSession->hasIdentifier($fieldValue, $property->getType())) { 646 $propertyValue = $this->persistenceSession->getObjectByIdentifier($fieldValue, $property->getType()); 647 } else { 648 $result = $this->fetchRelated($parentObject, $propertyName, $fieldValue); 649 $propertyValue = $this->mapResultToPropertyValue($parentObject, $propertyName, $result); 650 } 651 } 652 } 653 654 return $propertyValue; 655 } 656 657 /** 658 * Checks if the relation is based on a foreign key. 659 * 660 * @param DomainObjectInterface $parentObject 661 * @param string $propertyName 662 * @return bool TRUE if the property is mapped 663 */ 664 protected function propertyMapsByForeignKey(DomainObjectInterface $parentObject, $propertyName) 665 { 666 $columnMap = $this->getDataMap(get_class($parentObject))->getColumnMap($propertyName); 667 return $columnMap->getParentKeyFieldName() !== null; 668 } 669 670 /** 671 * Returns the given result as property value of the specified property type. 672 * 673 * @param DomainObjectInterface $parentObject 674 * @param string $propertyName 675 * @param mixed $result The result 676 * @return mixed 677 */ 678 public function mapResultToPropertyValue(DomainObjectInterface $parentObject, $propertyName, $result) 679 { 680 $propertyValue = null; 681 if ($result instanceof LoadingStrategyInterface) { 682 $propertyValue = $result; 683 } else { 684 $property = $this->reflectionService->getClassSchema(get_class($parentObject))->getProperty($propertyName); 685 if (in_array($property->getType(), ['array', \ArrayObject::class, \SplObjectStorage::class, ObjectStorage::class], true)) { 686 $objects = []; 687 foreach ($result as $value) { 688 $objects[] = $value; 689 } 690 if ($property->getType() === \ArrayObject::class) { 691 $propertyValue = new \ArrayObject($objects); 692 } elseif ($property->getType() === ObjectStorage::class) { 693 $propertyValue = new ObjectStorage(); 694 foreach ($objects as $object) { 695 $propertyValue->attach($object); 696 } 697 $propertyValue->_memorizeCleanState(); 698 } else { 699 $propertyValue = $objects; 700 } 701 } elseif (strpbrk((string)$property->getType(), '_\\') !== false) { 702 // @todo: check the strpbrk function call. Seems to be a check for Tx_Foo_Bar style class names 703 if (is_object($result) && $result instanceof QueryResultInterface) { 704 $propertyValue = $result->getFirst(); 705 } else { 706 $propertyValue = $result; 707 } 708 } 709 } 710 return $propertyValue; 711 } 712 713 /** 714 * Counts the number of related objects assigned to a property of a parent object 715 * 716 * @param DomainObjectInterface $parentObject The object instance this proxy is part of 717 * @param string $propertyName The name of the proxied property in it's parent 718 * @param mixed $fieldValue The raw field value. 719 * @return int 720 */ 721 public function countRelated(DomainObjectInterface $parentObject, $propertyName, $fieldValue = '') 722 { 723 $query = $this->getPreparedQuery($parentObject, $propertyName, $fieldValue); 724 return $query->execute()->count(); 725 } 726 727 /** 728 * Delegates the call to the Data Map. 729 * Returns TRUE if the property is persistable (configured in $TCA) 730 * 731 * @param string $className The property name 732 * @param string $propertyName The property name 733 * @return bool TRUE if the property is persistable (configured in $TCA) 734 */ 735 public function isPersistableProperty($className, $propertyName) 736 { 737 $dataMap = $this->getDataMap($className); 738 return $dataMap->isPersistableProperty($propertyName); 739 } 740 741 /** 742 * Returns a data map for a given class name 743 * 744 * @param string $className The class name you want to fetch the Data Map for 745 * @throws Persistence\Generic\Exception 746 * @return DataMap The data map 747 */ 748 public function getDataMap($className) 749 { 750 if (!is_string($className) || $className === '') { 751 throw new Exception('No class name was given to retrieve the Data Map for.', 1251315965); 752 } 753 return $this->dataMapFactory->buildDataMap($className); 754 } 755 756 /** 757 * Returns the selector (table) name for a given class name. 758 * 759 * @param string $className 760 * @return string The selector name 761 */ 762 public function convertClassNameToTableName($className) 763 { 764 return $this->getDataMap($className)->getTableName(); 765 } 766 767 /** 768 * Returns the column name for a given property name of the specified class. 769 * 770 * @param string $propertyName 771 * @param string $className 772 * @return string The column name 773 */ 774 public function convertPropertyNameToColumnName($propertyName, $className = null) 775 { 776 if (!empty($className)) { 777 $dataMap = $this->getDataMap($className); 778 if ($dataMap !== null) { 779 $columnMap = $dataMap->getColumnMap($propertyName); 780 if ($columnMap !== null) { 781 return $columnMap->getColumnName(); 782 } 783 } 784 } 785 return GeneralUtility::camelCaseToLowerCaseUnderscored($propertyName); 786 } 787 788 /** 789 * Returns the type of a child object. 790 * 791 * @param string $parentClassName The class name of the object this proxy is part of 792 * @param string $propertyName The name of the proxied property in it's parent 793 * @throws UnexpectedTypeException 794 * @return string The class name of the child object 795 */ 796 public function getType($parentClassName, $propertyName) 797 { 798 try { 799 $property = $this->reflectionService->getClassSchema($parentClassName)->getProperty($propertyName); 800 801 if ($property->getElementType() !== null) { 802 return $property->getElementType(); 803 } 804 805 if ($property->getType() !== null) { 806 return $property->getType(); 807 } 808 } catch (NoSuchPropertyException $e) { 809 } 810 811 throw new UnexpectedTypeException('Could not determine the child object type.', 1251315967); 812 } 813 814 /** 815 * Returns a plain value, i.e. objects are flattened out if possible. 816 * Multi value objects or arrays will be converted to a comma-separated list for use in IN SQL queries. 817 * 818 * @param mixed $input The value that will be converted. 819 * @param ColumnMap $columnMap Optional column map for retrieving the date storage format. 820 * @throws \InvalidArgumentException 821 * @throws UnexpectedTypeException 822 * @return int|string 823 */ 824 public function getPlainValue($input, $columnMap = null) 825 { 826 if ($input === null) { 827 return 'NULL'; 828 } 829 if ($input instanceof LazyLoadingProxy) { 830 $input = $input->_loadRealInstance(); 831 } 832 833 if (is_bool($input)) { 834 $parameter = (int)$input; 835 } elseif (is_int($input)) { 836 $parameter = $input; 837 } elseif ($input instanceof \DateTimeInterface) { 838 if ($columnMap !== null && $columnMap->getDateTimeStorageFormat() !== null) { 839 $storageFormat = $columnMap->getDateTimeStorageFormat(); 840 $timeZoneToStore = clone $input; 841 // set to UTC to store in database 842 $timeZoneToStore->setTimezone(new \DateTimeZone('UTC')); 843 switch ($storageFormat) { 844 case 'datetime': 845 $parameter = $timeZoneToStore->format('Y-m-d H:i:s'); 846 break; 847 case 'date': 848 $parameter = $timeZoneToStore->format('Y-m-d'); 849 break; 850 case 'time': 851 $parameter = $timeZoneToStore->format('H:i'); 852 break; 853 default: 854 throw new \InvalidArgumentException('Column map DateTime format "' . $storageFormat . '" is unknown. Allowed values are date, datetime or time.', 1395353470); 855 } 856 } else { 857 $parameter = $input->format('U'); 858 } 859 } elseif ($input instanceof DomainObjectInterface) { 860 $parameter = (int)$input->getUid(); 861 } elseif (TypeHandlingUtility::isValidTypeForMultiValueComparison($input)) { 862 $plainValueArray = []; 863 foreach ($input as $inputElement) { 864 $plainValueArray[] = $this->getPlainValue($inputElement, $columnMap); 865 } 866 $parameter = implode(',', $plainValueArray); 867 } elseif (is_object($input)) { 868 if (TypeHandlingUtility::isCoreType($input)) { 869 $parameter = (string)$input; 870 } else { 871 throw new UnexpectedTypeException('An object of class "' . get_class($input) . '" could not be converted to a plain value.', 1274799934); 872 } 873 } else { 874 $parameter = (string)$input; 875 } 876 return $parameter; 877 } 878} 879