[BUGFIX] Respect DateTimeImmutable in Extbase
[Packages/TYPO3.CMS.git] / typo3 / sysext / extbase / Classes / Persistence / Generic / Mapper / DataMapper.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\Database\Query\QueryHelper;
18 use TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface;
19 use TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface;
20 use TYPO3\CMS\Extbase\Object\Exception\CannotReconstituteObjectException;
21 use TYPO3\CMS\Extbase\Persistence;
22 use TYPO3\CMS\Extbase\Persistence\Generic\Exception\UnexpectedTypeException;
23 use TYPO3\CMS\Extbase\Persistence\QueryInterface;
24 use TYPO3\CMS\Extbase\Utility\TypeHandlingUtility;
25
26 /**
27 * A mapper to map database tables configured in $TCA on domain objects.
28 * @internal only to be used within Extbase, not part of TYPO3 Core API.
29 */
30 class DataMapper
31 {
32 /**
33 * @var \TYPO3\CMS\Extbase\Reflection\ReflectionService
34 */
35 protected $reflectionService;
36
37 /**
38 * @var \TYPO3\CMS\Extbase\Persistence\Generic\Qom\QueryObjectModelFactory
39 */
40 protected $qomFactory;
41
42 /**
43 * @var \TYPO3\CMS\Extbase\Persistence\Generic\Session
44 */
45 protected $persistenceSession;
46
47 /**
48 * A reference to the page select object providing methods to perform language and work space overlays
49 *
50 * @var \TYPO3\CMS\Frontend\Page\PageRepository
51 */
52 protected $pageSelectObject;
53
54 /**
55 * @var \TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMapFactory
56 */
57 protected $dataMapFactory;
58
59 /**
60 * @var \TYPO3\CMS\Extbase\Persistence\Generic\QueryFactoryInterface
61 */
62 protected $queryFactory;
63
64 /**
65 * @var \TYPO3\CMS\Extbase\Object\ObjectManagerInterface
66 */
67 protected $objectManager;
68
69 /**
70 * @var \TYPO3\CMS\Extbase\SignalSlot\Dispatcher
71 */
72 protected $signalSlotDispatcher;
73
74 /**
75 * @var ConfigurationManagerInterface
76 */
77 protected $configurationManager;
78
79 /**
80 * @var ?QueryInterface
81 */
82 protected $query;
83
84 /**
85 * DataMapper constructor.
86 * @param ?QueryInterface $query
87 */
88 public function __construct(?QueryInterface $query = null)
89 {
90 $this->query = $query;
91 }
92
93 /**
94 * @param \TYPO3\CMS\Extbase\Reflection\ReflectionService $reflectionService
95 */
96 public function injectReflectionService(\TYPO3\CMS\Extbase\Reflection\ReflectionService $reflectionService)
97 {
98 $this->reflectionService = $reflectionService;
99 }
100
101 /**
102 * @param \TYPO3\CMS\Extbase\Persistence\Generic\Qom\QueryObjectModelFactory $qomFactory
103 */
104 public function injectQomFactory(\TYPO3\CMS\Extbase\Persistence\Generic\Qom\QueryObjectModelFactory $qomFactory)
105 {
106 $this->qomFactory = $qomFactory;
107 }
108
109 /**
110 * @param \TYPO3\CMS\Extbase\Persistence\Generic\Session $persistenceSession
111 */
112 public function injectPersistenceSession(\TYPO3\CMS\Extbase\Persistence\Generic\Session $persistenceSession)
113 {
114 $this->persistenceSession = $persistenceSession;
115 }
116
117 /**
118 * @param \TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMapFactory $dataMapFactory
119 */
120 public function injectDataMapFactory(\TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMapFactory $dataMapFactory)
121 {
122 $this->dataMapFactory = $dataMapFactory;
123 }
124
125 /**
126 * @param \TYPO3\CMS\Extbase\Persistence\Generic\QueryFactoryInterface $queryFactory
127 */
128 public function injectQueryFactory(\TYPO3\CMS\Extbase\Persistence\Generic\QueryFactoryInterface $queryFactory)
129 {
130 $this->queryFactory = $queryFactory;
131 }
132
133 /**
134 * @param \TYPO3\CMS\Extbase\Object\ObjectManagerInterface $objectManager
135 */
136 public function injectObjectManager(\TYPO3\CMS\Extbase\Object\ObjectManagerInterface $objectManager)
137 {
138 $this->objectManager = $objectManager;
139 }
140
141 /**
142 * @param \TYPO3\CMS\Extbase\SignalSlot\Dispatcher $signalSlotDispatcher
143 */
144 public function injectSignalSlotDispatcher(\TYPO3\CMS\Extbase\SignalSlot\Dispatcher $signalSlotDispatcher)
145 {
146 $this->signalSlotDispatcher = $signalSlotDispatcher;
147 }
148
149 /**
150 * @param \TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface $configurationManager
151 */
152 public function injectConfigurationManager(ConfigurationManagerInterface $configurationManager)
153 {
154 $this->configurationManager = $configurationManager;
155 }
156
157 /**
158 * Maps the given rows on objects
159 *
160 * @param string $className The name of the class
161 * @param array $rows An array of arrays with field_name => value pairs
162 * @return array An array of objects of the given class
163 */
164 public function map($className, array $rows)
165 {
166 $objects = [];
167 foreach ($rows as $row) {
168 $objects[] = $this->mapSingleRow($this->getTargetType($className, $row), $row);
169 }
170 return $objects;
171 }
172
173 /**
174 * Returns the target type for the given row.
175 *
176 * @param string $className The name of the class
177 * @param array $row A single array with field_name => value pairs
178 * @return string The target type (a class name)
179 */
180 public function getTargetType($className, array $row)
181 {
182 $dataMap = $this->getDataMap($className);
183 $targetType = $className;
184 if ($dataMap->getRecordTypeColumnName() !== null) {
185 foreach ($dataMap->getSubclasses() as $subclassName) {
186 $recordSubtype = $this->getDataMap($subclassName)->getRecordType();
187 if ((string)$row[$dataMap->getRecordTypeColumnName()] === (string)$recordSubtype) {
188 $targetType = $subclassName;
189 break;
190 }
191 }
192 }
193 return $targetType;
194 }
195
196 /**
197 * Maps a single row on an object of the given class
198 *
199 * @param string $className The name of the target class
200 * @param array $row A single array with field_name => value pairs
201 * @return object An object of the given class
202 */
203 protected function mapSingleRow($className, array $row)
204 {
205 if ($this->persistenceSession->hasIdentifier($row['uid'], $className)) {
206 $object = $this->persistenceSession->getObjectByIdentifier($row['uid'], $className);
207 } else {
208 $object = $this->createEmptyObject($className);
209 $this->persistenceSession->registerObject($object, $row['uid']);
210 $this->thawProperties($object, $row);
211 $this->emitAfterMappingSingleRow($object);
212 $object->_memorizeCleanState();
213 $this->persistenceSession->registerReconstitutedEntity($object);
214 }
215 return $object;
216 }
217
218 /**
219 * Emits a signal after mapping a single row.
220 *
221 * @param DomainObjectInterface $object The mapped object
222 */
223 protected function emitAfterMappingSingleRow(DomainObjectInterface $object)
224 {
225 $this->signalSlotDispatcher->dispatch(__CLASS__, 'afterMappingSingleRow', [$object]);
226 }
227
228 /**
229 * Creates a skeleton of the specified object
230 *
231 * @param string $className Name of the class to create a skeleton for
232 * @throws CannotReconstituteObjectException
233 * @return object The object skeleton
234 */
235 protected function createEmptyObject($className)
236 {
237 // Note: The class_implements() function also invokes autoload to assure that the interfaces
238 // and the class are loaded. Would end up with __PHP_Incomplete_Class without it.
239 if (!in_array(\TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface::class, class_implements($className))) {
240 throw new CannotReconstituteObjectException('Cannot create empty instance of the class "' . $className
241 . '" because it does not implement the TYPO3\\CMS\\Extbase\\DomainObject\\DomainObjectInterface.', 1234386924);
242 }
243 $object = $this->objectManager->getEmptyObject($className);
244 return $object;
245 }
246
247 /**
248 * Sets the given properties on the object.
249 *
250 * @param DomainObjectInterface $object The object to set properties on
251 * @param array $row
252 */
253 protected function thawProperties(DomainObjectInterface $object, array $row)
254 {
255 $className = get_class($object);
256 $classSchema = $this->reflectionService->getClassSchema($className);
257 $dataMap = $this->getDataMap($className);
258 $object->_setProperty('uid', (int)$row['uid']);
259 $object->_setProperty('pid', (int)($row['pid'] ?? 0));
260 $object->_setProperty('_localizedUid', (int)$row['uid']);
261 $object->_setProperty('_versionedUid', (int)$row['uid']);
262 if ($dataMap->getLanguageIdColumnName() !== null) {
263 $object->_setProperty('_languageUid', (int)$row[$dataMap->getLanguageIdColumnName()]);
264 if (isset($row['_LOCALIZED_UID'])) {
265 $object->_setProperty('_localizedUid', (int)$row['_LOCALIZED_UID']);
266 }
267 }
268 if (!empty($row['_ORIG_uid']) && !empty($GLOBALS['TCA'][$dataMap->getTableName()]['ctrl']['versioningWS'])) {
269 $object->_setProperty('_versionedUid', (int)$row['_ORIG_uid']);
270 }
271 $properties = $object->_getProperties();
272 foreach ($properties as $propertyName => $propertyValue) {
273 if (!$dataMap->isPersistableProperty($propertyName)) {
274 continue;
275 }
276 $columnMap = $dataMap->getColumnMap($propertyName);
277 $columnName = $columnMap->getColumnName();
278 $propertyData = $classSchema->getProperty($propertyName);
279 $propertyValue = null;
280 if (isset($row[$columnName])) {
281 switch ($propertyData['type']) {
282 case 'integer':
283 $propertyValue = (int)$row[$columnName];
284 break;
285 case 'float':
286 $propertyValue = (double)$row[$columnName];
287 break;
288 case 'boolean':
289 $propertyValue = (bool)$row[$columnName];
290 break;
291 case 'string':
292 $propertyValue = (string)$row[$columnName];
293 break;
294 case 'array':
295 // $propertyValue = $this->mapArray($row[$columnName]); // Not supported, yet!
296 break;
297 case 'SplObjectStorage':
298 case Persistence\ObjectStorage::class:
299 $propertyValue = $this->mapResultToPropertyValue(
300 $object,
301 $propertyName,
302 $this->fetchRelated($object, $propertyName, $row[$columnName])
303 );
304 break;
305 case is_subclass_of($propertyData['type'], \DateTimeInterface::class):
306 $propertyValue = $this->mapDateTime(
307 $row[$columnName],
308 $columnMap->getDateTimeStorageFormat(),
309 $propertyData['type']
310 );
311 break;
312 default:
313 if (TypeHandlingUtility::isCoreType($propertyData['type'])) {
314 $propertyValue = $this->mapCoreType($propertyData['type'], $row[$columnName]);
315 } else {
316 $propertyValue = $this->mapObjectToClassProperty(
317 $object,
318 $propertyName,
319 $row[$columnName]
320 );
321 }
322
323 }
324 }
325 if ($propertyValue !== null) {
326 $object->_setProperty($propertyName, $propertyValue);
327 }
328 }
329 }
330
331 /**
332 * Map value to a core type
333 *
334 * @param string $type
335 * @param mixed $value
336 * @return \TYPO3\CMS\Core\Type\TypeInterface
337 */
338 protected function mapCoreType($type, $value)
339 {
340 return new $type($value);
341 }
342
343 /**
344 * Creates a DateTime from an unix timestamp or date/datetime/time value.
345 * If the input is empty, NULL is returned.
346 *
347 * @param int|string $value Unix timestamp or date/datetime/time value
348 * @param string|null $storageFormat Storage format for native date/datetime/time fields
349 * @param string|null $targetType The object class name to be created
350 * @return \DateTimeInterface
351 */
352 protected function mapDateTime($value, $storageFormat = null, $targetType = \DateTime::class)
353 {
354 $dateTimeTypes = QueryHelper::getDateTimeTypes();
355
356 if (empty($value) || $value === '0000-00-00' || $value === '0000-00-00 00:00:00' || $value === '00:00:00') {
357 // 0 -> NULL !!!
358 return null;
359 }
360 if (in_array($storageFormat, $dateTimeTypes, true)) {
361 // native date/datetime/time values are stored in UTC
362 $utcTimeZone = new \DateTimeZone('UTC');
363 $utcDateTime = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance($targetType, $value, $utcTimeZone);
364 $currentTimeZone = new \DateTimeZone(date_default_timezone_get());
365 return $utcDateTime->setTimezone($currentTimeZone);
366 }
367 // integer timestamps are local server time
368 return \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance($targetType, date('c', (int)$value));
369 }
370
371 /**
372 * Fetches a collection of objects related to a property of a parent object
373 *
374 * @param DomainObjectInterface $parentObject The object instance this proxy is part of
375 * @param string $propertyName The name of the proxied property in it's parent
376 * @param mixed $fieldValue The raw field value.
377 * @param bool $enableLazyLoading A flag indication if the related objects should be lazy loaded
378 * @return \TYPO3\CMS\Extbase\Persistence\Generic\LazyObjectStorage|Persistence\QueryResultInterface The result
379 */
380 public function fetchRelated(DomainObjectInterface $parentObject, $propertyName, $fieldValue = '', $enableLazyLoading = true)
381 {
382 $propertyMetaData = $this->reflectionService->getClassSchema(get_class($parentObject))->getProperty($propertyName);
383 if ($enableLazyLoading === true && $propertyMetaData['annotations']['lazy']) {
384 if ($propertyMetaData['type'] === Persistence\ObjectStorage::class) {
385 $result = $this->objectManager->get(\TYPO3\CMS\Extbase\Persistence\Generic\LazyObjectStorage::class, $parentObject, $propertyName, $fieldValue, $this);
386 } else {
387 if (empty($fieldValue)) {
388 $result = null;
389 } else {
390 $result = $this->objectManager->get(\TYPO3\CMS\Extbase\Persistence\Generic\LazyLoadingProxy::class, $parentObject, $propertyName, $fieldValue, $this);
391 }
392 }
393 } else {
394 $result = $this->fetchRelatedEager($parentObject, $propertyName, $fieldValue);
395 }
396 return $result;
397 }
398
399 /**
400 * Fetches the related objects from the storage backend.
401 *
402 * @param DomainObjectInterface $parentObject The object instance this proxy is part of
403 * @param string $propertyName The name of the proxied property in it's parent
404 * @param mixed $fieldValue The raw field value.
405 * @return mixed
406 */
407 protected function fetchRelatedEager(DomainObjectInterface $parentObject, $propertyName, $fieldValue = '')
408 {
409 return $fieldValue === '' ? $this->getEmptyRelationValue($parentObject, $propertyName) : $this->getNonEmptyRelationValue($parentObject, $propertyName, $fieldValue);
410 }
411
412 /**
413 * @param DomainObjectInterface $parentObject
414 * @param string $propertyName
415 * @return array|null
416 */
417 protected function getEmptyRelationValue(DomainObjectInterface $parentObject, $propertyName)
418 {
419 $columnMap = $this->getDataMap(get_class($parentObject))->getColumnMap($propertyName);
420 $relatesToOne = $columnMap->getTypeOfRelation() == ColumnMap::RELATION_HAS_ONE;
421 return $relatesToOne ? null : [];
422 }
423
424 /**
425 * @param DomainObjectInterface $parentObject
426 * @param string $propertyName
427 * @param string $fieldValue
428 * @return Persistence\QueryResultInterface
429 */
430 protected function getNonEmptyRelationValue(DomainObjectInterface $parentObject, $propertyName, $fieldValue)
431 {
432 $query = $this->getPreparedQuery($parentObject, $propertyName, $fieldValue);
433 return $query->execute();
434 }
435
436 /**
437 * Builds and returns the prepared query, ready to be executed.
438 *
439 * @param DomainObjectInterface $parentObject
440 * @param string $propertyName
441 * @param string $fieldValue
442 * @return Persistence\QueryInterface
443 */
444 protected function getPreparedQuery(DomainObjectInterface $parentObject, $propertyName, $fieldValue = '')
445 {
446 $dataMap = $this->getDataMap(get_class($parentObject));
447 $columnMap = $dataMap->getColumnMap($propertyName);
448 $type = $this->getType(get_class($parentObject), $propertyName);
449 $query = $this->queryFactory->create($type);
450 if ($this->query && $query instanceof Persistence\Generic\Query) {
451 $query->setParentQuery($this->query);
452 }
453 $query->getQuerySettings()->setRespectStoragePage(false);
454 $query->getQuerySettings()->setRespectSysLanguage(false);
455
456 if ($this->configurationManager->isFeatureEnabled('consistentTranslationOverlayHandling')) {
457 //we always want to overlay relations as most of the time they are stored in db using default lang uids
458 $query->getQuerySettings()->setLanguageOverlayMode(true);
459 if ($this->query) {
460 $query->getQuerySettings()->setLanguageUid($this->query->getQuerySettings()->getLanguageUid());
461
462 if ($dataMap->getLanguageIdColumnName() !== null && !$this->query->getQuerySettings()->getRespectSysLanguage()) {
463 //pass language of parent record to child objects, so they can be overlaid correctly in case
464 //e.g. findByUid is used.
465 //the languageUid is used for getRecordOverlay later on, despite RespectSysLanguage being false
466 $languageUid = (int)$parentObject->_getProperty('_languageUid');
467 $query->getQuerySettings()->setLanguageUid($languageUid);
468 }
469 }
470 }
471
472 if ($columnMap->getTypeOfRelation() === ColumnMap::RELATION_HAS_MANY) {
473 if ($columnMap->getChildSortByFieldName() !== null) {
474 $query->setOrderings([$columnMap->getChildSortByFieldName() => Persistence\QueryInterface::ORDER_ASCENDING]);
475 }
476 } elseif ($columnMap->getTypeOfRelation() === ColumnMap::RELATION_HAS_AND_BELONGS_TO_MANY) {
477 $query->setSource($this->getSource($parentObject, $propertyName));
478 if ($columnMap->getChildSortByFieldName() !== null) {
479 $query->setOrderings([$columnMap->getChildSortByFieldName() => Persistence\QueryInterface::ORDER_ASCENDING]);
480 }
481 }
482 $query->matching($this->getConstraint($query, $parentObject, $propertyName, $fieldValue, $columnMap->getRelationTableMatchFields()));
483 return $query;
484 }
485
486 /**
487 * Builds and returns the constraint for multi value properties.
488 *
489 * @param Persistence\QueryInterface $query
490 * @param DomainObjectInterface $parentObject
491 * @param string $propertyName
492 * @param string $fieldValue
493 * @param array $relationTableMatchFields
494 * @return \TYPO3\CMS\Extbase\Persistence\Generic\Qom\ConstraintInterface $constraint
495 */
496 protected function getConstraint(Persistence\QueryInterface $query, DomainObjectInterface $parentObject, $propertyName, $fieldValue = '', $relationTableMatchFields = [])
497 {
498 $columnMap = $this->getDataMap(get_class($parentObject))->getColumnMap($propertyName);
499 if ($columnMap->getParentKeyFieldName() !== null) {
500 $constraint = $query->equals($columnMap->getParentKeyFieldName(), $parentObject);
501 if ($columnMap->getParentTableFieldName() !== null) {
502 $constraint = $query->logicalAnd(
503 $constraint,
504 $query->equals($columnMap->getParentTableFieldName(), $this->getDataMap(get_class($parentObject))->getTableName())
505 );
506 }
507 } else {
508 $constraint = $query->in('uid', \TYPO3\CMS\Core\Utility\GeneralUtility::intExplode(',', $fieldValue));
509 }
510 if (!empty($relationTableMatchFields)) {
511 foreach ($relationTableMatchFields as $relationTableMatchFieldName => $relationTableMatchFieldValue) {
512 $constraint = $query->logicalAnd($constraint, $query->equals($relationTableMatchFieldName, $relationTableMatchFieldValue));
513 }
514 }
515 return $constraint;
516 }
517
518 /**
519 * Builds and returns the source to build a join for a m:n relation.
520 *
521 * @param DomainObjectInterface $parentObject
522 * @param string $propertyName
523 * @return \TYPO3\CMS\Extbase\Persistence\Generic\Qom\SourceInterface $source
524 */
525 protected function getSource(DomainObjectInterface $parentObject, $propertyName)
526 {
527 $columnMap = $this->getDataMap(get_class($parentObject))->getColumnMap($propertyName);
528 $left = $this->qomFactory->selector(null, $columnMap->getRelationTableName());
529 $childClassName = $this->getType(get_class($parentObject), $propertyName);
530 $right = $this->qomFactory->selector($childClassName, $columnMap->getChildTableName());
531 $joinCondition = $this->qomFactory->equiJoinCondition($columnMap->getRelationTableName(), $columnMap->getChildKeyFieldName(), $columnMap->getChildTableName(), 'uid');
532 $source = $this->qomFactory->join($left, $right, Persistence\Generic\Query::JCR_JOIN_TYPE_INNER, $joinCondition);
533 return $source;
534 }
535
536 /**
537 * Returns the mapped classProperty from the identiyMap or
538 * mapResultToPropertyValue()
539 *
540 * If the field value is empty and the column map has no parent key field name,
541 * the relation will be empty. If the persistence session has a registered object of
542 * the correct type and identity (fieldValue), this function returns that object.
543 * Otherwise, it proceeds with mapResultToPropertyValue().
544 *
545 * @param DomainObjectInterface $parentObject
546 * @param string $propertyName
547 * @param mixed $fieldValue the raw field value
548 * @return mixed
549 * @see mapResultToPropertyValue()
550 */
551 protected function mapObjectToClassProperty(DomainObjectInterface $parentObject, $propertyName, $fieldValue)
552 {
553 if ($this->propertyMapsByForeignKey($parentObject, $propertyName)) {
554 $result = $this->fetchRelated($parentObject, $propertyName, $fieldValue);
555 $propertyValue = $this->mapResultToPropertyValue($parentObject, $propertyName, $result);
556 } else {
557 if ($fieldValue === '') {
558 $propertyValue = $this->getEmptyRelationValue($parentObject, $propertyName);
559 } else {
560 $propertyMetaData = $this->reflectionService->getClassSchema(get_class($parentObject))->getProperty($propertyName);
561 if ($this->persistenceSession->hasIdentifier($fieldValue, $propertyMetaData['type'])) {
562 $propertyValue = $this->persistenceSession->getObjectByIdentifier($fieldValue, $propertyMetaData['type']);
563 } else {
564 $result = $this->fetchRelated($parentObject, $propertyName, $fieldValue);
565 $propertyValue = $this->mapResultToPropertyValue($parentObject, $propertyName, $result);
566 }
567 }
568 }
569
570 return $propertyValue;
571 }
572
573 /**
574 * Checks if the relation is based on a foreign key.
575 *
576 * @param DomainObjectInterface $parentObject
577 * @param string $propertyName
578 * @return bool TRUE if the property is mapped
579 */
580 protected function propertyMapsByForeignKey(DomainObjectInterface $parentObject, $propertyName)
581 {
582 $columnMap = $this->getDataMap(get_class($parentObject))->getColumnMap($propertyName);
583 return $columnMap->getParentKeyFieldName() !== null;
584 }
585
586 /**
587 * Returns the given result as property value of the specified property type.
588 *
589 * @param DomainObjectInterface $parentObject
590 * @param string $propertyName
591 * @param mixed $result The result
592 * @return mixed
593 */
594 public function mapResultToPropertyValue(DomainObjectInterface $parentObject, $propertyName, $result)
595 {
596 $propertyValue = null;
597 if ($result instanceof Persistence\Generic\LoadingStrategyInterface) {
598 $propertyValue = $result;
599 } else {
600 $propertyMetaData = $this->reflectionService->getClassSchema(get_class($parentObject))->getProperty($propertyName);
601 if (in_array($propertyMetaData['type'], ['array', 'ArrayObject', 'SplObjectStorage', Persistence\ObjectStorage::class], true)) {
602 $objects = [];
603 foreach ($result as $value) {
604 $objects[] = $value;
605 }
606 if ($propertyMetaData['type'] === 'ArrayObject') {
607 $propertyValue = new \ArrayObject($objects);
608 } elseif ($propertyMetaData['type'] === Persistence\ObjectStorage::class) {
609 $propertyValue = new Persistence\ObjectStorage();
610 foreach ($objects as $object) {
611 $propertyValue->attach($object);
612 }
613 $propertyValue->_memorizeCleanState();
614 } else {
615 $propertyValue = $objects;
616 }
617 } elseif (strpbrk($propertyMetaData['type'], '_\\') !== false) {
618 if (is_object($result) && $result instanceof Persistence\QueryResultInterface) {
619 $propertyValue = $result->getFirst();
620 } else {
621 $propertyValue = $result;
622 }
623 }
624 }
625 return $propertyValue;
626 }
627
628 /**
629 * Counts the number of related objects assigned to a property of a parent object
630 *
631 * @param DomainObjectInterface $parentObject The object instance this proxy is part of
632 * @param string $propertyName The name of the proxied property in it's parent
633 * @param mixed $fieldValue The raw field value.
634 * @return int
635 */
636 public function countRelated(DomainObjectInterface $parentObject, $propertyName, $fieldValue = '')
637 {
638 $query = $this->getPreparedQuery($parentObject, $propertyName, $fieldValue);
639 return $query->execute()->count();
640 }
641
642 /**
643 * Delegates the call to the Data Map.
644 * Returns TRUE if the property is persistable (configured in $TCA)
645 *
646 * @param string $className The property name
647 * @param string $propertyName The property name
648 * @return bool TRUE if the property is persistable (configured in $TCA)
649 */
650 public function isPersistableProperty($className, $propertyName)
651 {
652 $dataMap = $this->getDataMap($className);
653 return $dataMap->isPersistableProperty($propertyName);
654 }
655
656 /**
657 * Returns a data map for a given class name
658 *
659 * @param string $className The class name you want to fetch the Data Map for
660 * @throws Persistence\Generic\Exception
661 * @return DataMap The data map
662 */
663 public function getDataMap($className)
664 {
665 if (!is_string($className) || $className === '') {
666 throw new Persistence\Generic\Exception('No class name was given to retrieve the Data Map for.', 1251315965);
667 }
668 return $this->dataMapFactory->buildDataMap($className);
669 }
670
671 /**
672 * Returns the selector (table) name for a given class name.
673 *
674 * @param string $className
675 * @return string The selector name
676 */
677 public function convertClassNameToTableName($className)
678 {
679 return $this->getDataMap($className)->getTableName();
680 }
681
682 /**
683 * Returns the column name for a given property name of the specified class.
684 *
685 * @param string $propertyName
686 * @param string $className
687 * @return string The column name
688 */
689 public function convertPropertyNameToColumnName($propertyName, $className = null)
690 {
691 if (!empty($className)) {
692 $dataMap = $this->getDataMap($className);
693 if ($dataMap !== null) {
694 $columnMap = $dataMap->getColumnMap($propertyName);
695 if ($columnMap !== null) {
696 return $columnMap->getColumnName();
697 }
698 }
699 }
700 return \TYPO3\CMS\Core\Utility\GeneralUtility::camelCaseToLowerCaseUnderscored($propertyName);
701 }
702
703 /**
704 * Returns the type of a child object.
705 *
706 * @param string $parentClassName The class name of the object this proxy is part of
707 * @param string $propertyName The name of the proxied property in it's parent
708 * @throws UnexpectedTypeException
709 * @return string The class name of the child object
710 */
711 public function getType($parentClassName, $propertyName)
712 {
713 $propertyMetaData = $this->reflectionService->getClassSchema($parentClassName)->getProperty($propertyName);
714 if (!empty($propertyMetaData['elementType'])) {
715 $type = $propertyMetaData['elementType'];
716 } elseif (!empty($propertyMetaData['type'])) {
717 $type = $propertyMetaData['type'];
718 } else {
719 throw new UnexpectedTypeException('Could not determine the child object type.', 1251315967);
720 }
721 return $type;
722 }
723
724 /**
725 * Returns a plain value, i.e. objects are flattened out if possible.
726 * Multi value objects or arrays will be converted to a comma-separated list for use in IN SQL queries.
727 *
728 * @param mixed $input The value that will be converted.
729 * @param ColumnMap $columnMap Optional column map for retrieving the date storage format.
730 * @throws \InvalidArgumentException
731 * @throws UnexpectedTypeException
732 * @return int|string
733 */
734 public function getPlainValue($input, $columnMap = null)
735 {
736 if ($input === null) {
737 return 'NULL';
738 }
739 if ($input instanceof Persistence\Generic\LazyLoadingProxy) {
740 $input = $input->_loadRealInstance();
741 }
742
743 if (is_bool($input)) {
744 $parameter = (int)$input;
745 } elseif (is_int($input)) {
746 $parameter = $input;
747 } elseif ($input instanceof \DateTimeInterface) {
748 if ($columnMap !== null && $columnMap->getDateTimeStorageFormat() !== null) {
749 $storageFormat = $columnMap->getDateTimeStorageFormat();
750 $timeZoneToStore = clone $input;
751 // set to UTC to store in database
752 $timeZoneToStore->setTimezone(new \DateTimeZone('UTC'));
753 switch ($storageFormat) {
754 case 'datetime':
755 $parameter = $timeZoneToStore->format('Y-m-d H:i:s');
756 break;
757 case 'date':
758 $parameter = $timeZoneToStore->format('Y-m-d');
759 break;
760 case 'time':
761 $parameter = $timeZoneToStore->format('H:i');
762 break;
763 default:
764 throw new \InvalidArgumentException('Column map DateTime format "' . $storageFormat . '" is unknown. Allowed values are date, datetime or time.', 1395353470);
765 }
766 } else {
767 $parameter = $input->format('U');
768 }
769 } elseif ($input instanceof DomainObjectInterface) {
770 $parameter = (int)$input->getUid();
771 } elseif (TypeHandlingUtility::isValidTypeForMultiValueComparison($input)) {
772 $plainValueArray = [];
773 foreach ($input as $inputElement) {
774 $plainValueArray[] = $this->getPlainValue($inputElement, $columnMap);
775 }
776 $parameter = implode(',', $plainValueArray);
777 } elseif (is_object($input)) {
778 if (TypeHandlingUtility::isCoreType($input)) {
779 $parameter = (string)$input;
780 } else {
781 throw new UnexpectedTypeException('An object of class "' . get_class($input) . '" could not be converted to a plain value.', 1274799934);
782 }
783 } else {
784 $parameter = (string)$input;
785 }
786 return $parameter;
787 }
788 }