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