query = $query; } /** * Maps the given rows on objects * * @param string $className The name of the class * @param array $rows An array of arrays with field_name => value pairs * @return array An array of objects of the given class * @template T of DomainObjectInterface * @phpstan-param class-string $className * @phpstan-return list */ public function map($className, array $rows) { $objects = []; foreach ($rows as $row) { $objects[] = $this->mapSingleRow($this->getTargetType($className, $row), $row); } return $objects; } /** * Returns the target type for the given row. * * @param string $className The name of the class * @param array $row A single array with field_name => value pairs * @return string The target type (a class name) * @phpstan-param class-string $className * @phpstan-return class-string */ public function getTargetType($className, array $row) { $dataMap = $this->getDataMap($className); $targetType = $className; if ($dataMap->recordTypeColumnName !== null) { foreach ($dataMap->subclasses as $subclassName) { $recordSubtype = $this->getDataMap($subclassName)->recordType; if ((string)$row[$dataMap->recordTypeColumnName] === (string)$recordSubtype) { $targetType = $subclassName; break; } } } return $targetType; } /** * Maps a single row on an object of the given class * * @param string $className The name of the target class * @param array $row A single array with field_name => value pairs * @return object An object of the given class * @template T of DomainObjectInterface * @phpstan-param class-string $className * @phpstan-return T */ protected function mapSingleRow($className, array $row) { $identifier = $this->buildIdentifier($row); if ($this->persistenceSession->hasIdentifier($identifier, $className)) { $object = $this->persistenceSession->getObjectByIdentifier($identifier, $className); } else { $object = $this->createEmptyObject($className); $this->persistenceSession->registerObject($object, $identifier); $this->thawProperties($object, $row); $event = new AfterObjectThawedEvent($object, $row); $this->eventDispatcher->dispatch($event); $object->_memorizeCleanState(); $this->persistenceSession->registerReconstitutedEntity($object); } return $object; } /** * Build a language-aware identifier for the identity map. * * The identifier includes the UID, localized UID (if present), and the * language content identifier to ensure objects loaded with different * language configurations are cached separately. * * @param array $row A single array with field_name => value pairs * @return non-empty-string The identifier for the identity map */ protected function buildIdentifier(array $row): string { return $this->persistenceSession->buildIdentifier($row, $this->getEffectiveLanguageAspect()); } /** * Get the effective LanguageAspect for the current mapping context. * * Returns the LanguageAspect from the current query if available, * otherwise returns a default LanguageAspect for default language. */ protected function getEffectiveLanguageAspect(): LanguageAspect { return $this->query?->getQuerySettings()->getLanguageAspect() ?? new LanguageAspect(); } /** * Creates a skeleton of the specified object. This is * designed to *not* call class constructor when hydrating, * but *do call* initializeObject() if exists and obey * eventually registered implementation overrides ("xclass"). * * @param class-string $className Name of the class to create a skeleton for * @throws InvalidClassException * @template T of DomainObjectInterface * @phpstan-param class-string $className * @phpstan-return T */ protected function createEmptyObject(string $className): DomainObjectInterface { // Note: The class_implements() function also invokes autoload to assure that the interfaces // and the class are loaded. Would end up with __PHP_Incomplete_Class without it. if (!in_array(DomainObjectInterface::class, class_implements($className) ?: [])) { throw new InvalidClassException('Cannot create empty instance of the class "' . $className . '" because it does not implement the TYPO3\\CMS\\Extbase\\DomainObject\\DomainObjectInterface.', 1234386924); } // Use GU::getClassName() to obey class implementation overrides. $object = $this->instantiator->instantiate(GeneralUtility::getClassName($className)); if (is_callable($callable = [$object, 'initializeObject'])) { $callable(); } return $object; } /** * Sets the given properties on the object. * * @param DomainObjectInterface $object The object to set properties on * @throws NonExistentPropertyException * @throws UnknownPropertyTypeException */ protected function thawProperties(DomainObjectInterface $object, array $row) { $className = get_class($object); $classSchema = $this->reflectionService->getClassSchema($className); $dataMap = $this->getDataMap($className); $object->_setProperty(AbstractDomainObject::PROPERTY_UID, (int)$row['uid']); $object->_setProperty(AbstractDomainObject::PROPERTY_PID, (int)($row['pid'] ?? 0)); $object->_setProperty(AbstractDomainObject::PROPERTY_LOCALIZED_UID, (int)$row['uid']); $object->_setProperty(AbstractDomainObject::PROPERTY_VERSIONED_UID, (int)$row['uid']); if ($dataMap->languageIdColumnName !== null) { $object->_setProperty(AbstractDomainObject::PROPERTY_LANGUAGE_UID, (int)($row[$dataMap->languageIdColumnName] ?? 0)); if (isset($row['_LOCALIZED_UID'])) { $object->_setProperty(AbstractDomainObject::PROPERTY_LOCALIZED_UID, (int)$row['_LOCALIZED_UID']); } } if (!empty($row['_ORIG_uid']) && $this->tcaSchemaFactory->get($dataMap->tableName)->isWorkspaceAware()) { $object->_setProperty(AbstractDomainObject::PROPERTY_VERSIONED_UID, (int)$row['_ORIG_uid']); } foreach ($classSchema->getDomainObjectProperties() as $property) { $propertyName = $property->getName(); if (!$dataMap->isPersistableProperty($propertyName)) { continue; } $columnMap = $dataMap->getColumnMap($propertyName); if ($columnMap === null) { continue; } if (!isset($row[$columnMap->columnName])) { continue; } $propertyValue = $row[$columnMap->columnName]; $nonProxyPropertyTypes = $property->getFilteredTypes([$property, 'filterLazyLoadingProxyAndLazyObjectStorage']); if ($nonProxyPropertyTypes === []) { throw new UnknownPropertyTypeException( 'The type of property ' . $className . '::' . $propertyName . ' could not be identified, therefore the desired value (' . var_export($propertyValue, true) . ') cannot be mapped onto it. The type of a class property is usually defined via property types or php doc blocks. ' . 'Make sure the property has a property type or valid @var tag set which defines the type.', 1579965021 ); } if (count($nonProxyPropertyTypes) > 1) { throw new UnknownPropertyTypeException( 'The type of property ' . $className . '::' . $propertyName . ' could not be identified because the property is defined as union or intersection type, therefore the desired value (' . var_export($propertyValue, true) . ') cannot be mapped onto it. Make sure to use only a single type.', 1660215701 ); } $primaryType = $nonProxyPropertyTypes[0]; $propertyType = $primaryType->getBuiltinType(); $propertyClassName = $primaryType->getClassName(); $propertyValue = match ($propertyType) { 'int', 'integer' => (int)$propertyValue, 'bool', 'boolean' => (bool)$propertyValue, 'float' => (float)$propertyValue, 'string' => (string)$propertyValue, 'array' => null, // $this->mapArray($propertyValue); // Not supported, yet! 'object' => $this->thawObjectProperty($property, $columnMap, $object, $propertyName, $propertyValue, $propertyClassName), default => null, }; if ($propertyValue !== null || $property->isNullable()) { $object->_setProperty($propertyName, $propertyValue); } } } /** * @param non-empty-string $propertyName * @param class-string|null $targetClassName */ private function thawObjectProperty( Property $propertySchema, ColumnMap $columnMap, DomainObjectInterface $parent, string $propertyName, mixed $propertyValue, ?string $targetClassName ): ?object { if ($targetClassName === null) { return null; } if (is_subclass_of($targetClassName, \BackedEnum::class)) { return $propertySchema->isNullable() ? $targetClassName::tryFrom($propertyValue) : $targetClassName::from($propertyValue); } if (in_array($targetClassName, [\SplObjectStorage::class, ObjectStorage::class], true)) { return $this->mapResultToPropertyValue( $parent, $propertyName, $this->fetchRelated($parent, $propertyName, $propertyValue) ); } if (is_subclass_of($targetClassName, \DateTimeInterface::class)) { return $this->mapDateTime( $propertyValue, $columnMap->dateTimeFormat, $columnMap->dateTimeStorageFormat, $columnMap->isNullable, $targetClassName ); } if ($targetClassName === Country::class || is_subclass_of($targetClassName, Country::class)) { // @todo Check if this can be abstracted in a better way (for future TCA types) // @todo does alpha2 need to be configurable? All storage currently seems to depend on alpha2 in TCA FormEngine return $this->countryProvider->getByAlpha2IsoCode($propertyValue); } if (TypeHandlingUtility::isCoreType($targetClassName)) { return $this->mapCoreType($targetClassName, $propertyValue); } return $this->mapObjectToClassProperty( $parent, $propertyName, $propertyValue ); } /** * Map value to a core type * * @param string $type * @param mixed $value * @return \TYPO3\CMS\Core\Type\TypeInterface */ protected function mapCoreType($type, $value) { return new $type($value); } /** * Creates a DateTime from a unix timestamp or date/datetime/time value. * If the input is empty, NULL is returned. * * @param int|string $value Unix timestamp or date/datetime/datetimesec value or seconds for time/timesec * @param string|null $format Output format (date/datetime/time/timesec/datetimesec) * @param string|null $storageFormat Storage format for native date/datetime/time/datetimesec fields * @param string $targetType The object class name to be created * @return \DateTimeInterface|null */ protected function mapDateTime( $value, $format = null, $storageFormat = null, $isNullable = true, $targetType = \DateTime::class ) { $dateTime = DateTimeFactory::createFromDatabaseValueAndTCAConfig( $value, // Reconstruct TCA from our ColumnMap [ 'type' => 'datetime', 'format' => $format, 'dbType' => $storageFormat, 'nullable' => $isNullable, ] ); return $dateTime === null ? null : match ($targetType) { \DateTimeImmutable::class => $dateTime, \DateTime::class => \DateTime::createFromImmutable($dateTime), default => GeneralUtility::makeInstance($targetType, $dateTime->format('Y-m-d H:i:s.v e')), }; } /** * Fetches a collection of objects related to a property of a parent object * * @param DomainObjectInterface $parentObject The object instance this proxy is part of * @param string $propertyName The name of the proxied property in it's parent * @param mixed $fieldValue The raw field value. * @param bool $enableLazyLoading A flag indication if the related objects should be lazy loaded * @return \TYPO3\CMS\Extbase\Persistence\Generic\LazyObjectStorage|Persistence\QueryResultInterface The result */ public function fetchRelated(DomainObjectInterface $parentObject, $propertyName, $fieldValue = '', $enableLazyLoading = true) { $property = $this->reflectionService->getClassSchema(get_class($parentObject))->getProperty($propertyName); if ($enableLazyLoading && $property->isLazy()) { if ($property->isObjectStorageType()) { $result = GeneralUtility::makeInstance(LazyObjectStorage::class, $parentObject, $propertyName, $fieldValue, $this); } elseif (empty($fieldValue)) { $result = null; } else { $result = GeneralUtility::makeInstance(LazyLoadingProxy::class, $parentObject, $propertyName, $fieldValue, $this); } } else { $result = $this->fetchRelatedEager($parentObject, $propertyName, $fieldValue); } return $result; } /** * Fetches the related objects from the storage backend. * * @param DomainObjectInterface $parentObject The object instance this proxy is part of * @param string $propertyName The name of the proxied property in it's parent * @param mixed $fieldValue The raw field value. * @return mixed */ protected function fetchRelatedEager(DomainObjectInterface $parentObject, $propertyName, $fieldValue = '') { return $fieldValue === '' ? $this->getEmptyRelationValue($parentObject, $propertyName) : $this->getNonEmptyRelationValue($parentObject, $propertyName, $fieldValue); } /** * @param string $propertyName * @return array|null */ protected function getEmptyRelationValue(DomainObjectInterface $parentObject, $propertyName) { $columnMap = $this->getDataMap(get_class($parentObject))->getColumnMap($propertyName); $relatesToOne = $columnMap->typeOfRelation == Relation::HAS_ONE; return $relatesToOne ? null : []; } /** * @param string $propertyName * @param string $fieldValue * @return Persistence\QueryResultInterface */ protected function getNonEmptyRelationValue(DomainObjectInterface $parentObject, $propertyName, $fieldValue) { $query = $this->getPreparedQuery($parentObject, $propertyName, $fieldValue); return $query->execute(); } /** * Builds and returns the prepared query, ready to be executed. * * @param string $propertyName * @param string $fieldValue * @return Persistence\QueryInterface */ protected function getPreparedQuery(DomainObjectInterface $parentObject, $propertyName, $fieldValue = '') { $dataMap = $this->getDataMap(get_class($parentObject)); $columnMap = $dataMap->getColumnMap($propertyName); $type = $this->getType(get_class($parentObject), $propertyName); $query = $this->queryFactory->create($type); if ($this->query && $query instanceof Query) { $query->setParentQuery($this->query); } $query->getQuerySettings()->setRespectStoragePage(false); $query->getQuerySettings()->setRespectSysLanguage(false); $languageAspect = $query->getQuerySettings()->getLanguageAspect(); $languageUid = $languageAspect->getContentId(); if ($this->query) { $languageAspect = $this->query->getQuerySettings()->getLanguageAspect(); $languageUid = $languageAspect->getContentId(); if ($dataMap->languageIdColumnName !== null && !$this->query->getQuerySettings()->getRespectSysLanguage()) { //pass language of parent record to child objects, so they can be overlaid correctly in case //e.g. findByUid is used. //the languageUid is used for getRecordOverlay later on, despite RespectSysLanguage being false $parentLanguageUid = (int)$parentObject->_getProperty(AbstractDomainObject::PROPERTY_LANGUAGE_UID); // do not override the language when the parent language uid is set to all languages (-1) if ($parentLanguageUid !== -1) { $languageUid = $parentLanguageUid; } } } // we always want to overlay relations as most of the time they are stored in db using default language uids $languageAspect = new LanguageAspect( $languageUid, $languageUid, $languageAspect->getOverlayType() === LanguageAspect::OVERLAYS_OFF ? LanguageAspect::OVERLAYS_MIXED : $languageAspect->getOverlayType(), $languageAspect->getFallbackChain() ); $query->getQuerySettings()->setLanguageAspect($languageAspect); if ($columnMap->typeOfRelation === Relation::HAS_MANY) { if (null !== $orderings = $this->getOrderingsForColumnMap($columnMap)) { $query->setOrderings($orderings); } } elseif ($columnMap->typeOfRelation === Relation::HAS_AND_BELONGS_TO_MANY) { $query->setSource($this->getSource($parentObject, $propertyName)); if ($columnMap->childSortByFieldName !== null) { $query->setOrderings([$columnMap->childSortByFieldName => QueryInterface::ORDER_ASCENDING]); } } $query->matching($this->getConstraint($query, $parentObject, $propertyName, $fieldValue, $columnMap->relationTableMatchFields)); return $query; } /** * Get orderings array for extbase query by columnMap * * @phpstan-return array|null * @return array|null */ public function getOrderingsForColumnMap(ColumnMap $columnMap): ?array { if ($columnMap->childSortByFieldName !== null) { return [$columnMap->childSortByFieldName => QueryInterface::ORDER_ASCENDING]; } if ($columnMap->childTableDefaultSortings === null) { return null; } $orderings = []; $fields = QueryHelper::parseOrderBy($columnMap->childTableDefaultSortings); foreach ($fields as $field) { $fieldName = $field[0] ?? null; if ($fieldName === null) { continue; } if (($fieldOrdering = $field[1] ?? null) === null) { $orderings[$fieldName] = QueryInterface::ORDER_ASCENDING; continue; } $fieldOrdering = strtoupper($fieldOrdering); if (!in_array($fieldOrdering, [QueryInterface::ORDER_ASCENDING, QueryInterface::ORDER_DESCENDING], true)) { $orderings[$fieldName] = QueryInterface::ORDER_ASCENDING; continue; } $orderings[$fieldName] = $fieldOrdering; } return $orderings !== [] ? $orderings : null; } /** * Builds and returns the constraint for multi value properties. * * @param string $propertyName * @param string $fieldValue * @param array $relationTableMatchFields * @return \TYPO3\CMS\Extbase\Persistence\Generic\Qom\ConstraintInterface $constraint */ protected function getConstraint(QueryInterface $query, DomainObjectInterface $parentObject, $propertyName, $fieldValue = '', $relationTableMatchFields = []) { $dataMap = $this->getDataMap(get_class($parentObject)); $columnMap = $dataMap->getColumnMap($propertyName); $workspaceId = GeneralUtility::makeInstance(Context::class)->getPropertyFromAspect('workspace', 'id'); $parentId = $this->resolveParentId($parentObject, $workspaceId, $columnMap); if ($columnMap && $workspaceId > 0) { $resolvedRelationIds = $this->resolveRelationValuesOfField($dataMap, $columnMap, $parentId, $fieldValue, $workspaceId); } else { $resolvedRelationIds = []; } // Work with the UIDs directly in a workspace if (!empty($resolvedRelationIds)) { $source = $query->getSource(); if ($source instanceof JoinInterface) { $constraint = $query->in($source->getJoinCondition()->getProperty1Name(), $resolvedRelationIds); // When querying MM relations directly, Typo3DbQueryParser uses enableFields and thus, filters // out versioned records by default. However, we directly query versioned UIDs here, so we want // to include the versioned records explicitly. if ($columnMap->typeOfRelation === Relation::HAS_AND_BELONGS_TO_MANY) { $query->getQuerySettings()->setEnableFieldsToBeIgnored(['pid']); $query->getQuerySettings()->setIgnoreEnableFields(true); } // Also, we still need to restrict the MM on the foreign side if ($columnMap->getParentKeyFieldName() !== null) { $constraint = $query->logicalAnd( $constraint, $query->equals($columnMap->getParentKeyFieldName(), $parentId) ); } } else { $constraint = $query->in('uid', $resolvedRelationIds); } if ($columnMap->parentTableFieldName !== null) { $constraint = $query->logicalAnd( $constraint, $query->equals($columnMap->parentTableFieldName, $dataMap->tableName) ); } } elseif ($columnMap->parentKeyFieldName !== null) { $value = $parentObject; // If this a MM relation, and MM relations do not know about workspaces, the MM relations always point to the // versioned record, so this must be taken into account here and the versioned record's UID must be used. if ($columnMap->typeOfRelation === Relation::HAS_AND_BELONGS_TO_MANY) { // The versioned UID is used ideally the version ID of a translated record, so this takes precedence over the localized UID if ($value->_hasProperty(AbstractDomainObject::PROPERTY_VERSIONED_UID) && $value->_getProperty(AbstractDomainObject::PROPERTY_VERSIONED_UID) > 0 && $value->_getProperty(AbstractDomainObject::PROPERTY_VERSIONED_UID) !== $value->getUid()) { $value = (int)$value->_getProperty(AbstractDomainObject::PROPERTY_VERSIONED_UID); } } $constraint = $query->equals($columnMap->parentKeyFieldName, $value); if ($columnMap->parentTableFieldName !== null) { $constraint = $query->logicalAnd( $constraint, $query->equals($columnMap->parentTableFieldName, $dataMap->tableName) ); } } else { // Note: $fieldValue is annotated as a string, but this cannot be trusted as the callers do not ensure this. $constraint = $query->in('uid', GeneralUtility::intExplode(',', (string)$fieldValue)); } if (!empty($relationTableMatchFields)) { foreach ($relationTableMatchFields as $relationTableMatchFieldName => $relationTableMatchFieldValue) { $constraint = $query->logicalAnd($constraint, $query->equals($relationTableMatchFieldName, $relationTableMatchFieldValue)); } } return $constraint; } /** * Fetch the actual "uid" which we need to query to fetch relations to this UID. */ protected function resolveParentId(DomainObjectInterface $parentObject, int $workspaceId, ?ColumnMap $columnMap): ?int { $parentId = $parentObject->getUid(); if ($columnMap && $workspaceId > 0) { // versionedUid in a multi-language setup is the overlaid versioned AND translated ID if ($parentObject->_hasProperty(AbstractDomainObject::PROPERTY_VERSIONED_UID) && $parentObject->_getProperty(AbstractDomainObject::PROPERTY_VERSIONED_UID) > 0 && $parentObject->_getProperty(AbstractDomainObject::PROPERTY_VERSIONED_UID) !== $parentId) { $parentId = $parentObject->_getProperty(AbstractDomainObject::PROPERTY_VERSIONED_UID); } elseif ($parentObject->_hasProperty(AbstractDomainObject::PROPERTY_LANGUAGE_UID) && $parentObject->_getProperty(AbstractDomainObject::PROPERTY_LANGUAGE_UID) > 0) { $parentId = $parentObject->_getProperty(AbstractDomainObject::PROPERTY_LOCALIZED_UID); } } return $parentId; } /** * This resolves relations via RelationHandler and returns their UIDs respectively, and works for MM/ForeignField/CSV in IRRE + Select + Group. * * Note: This only happens for resolving properties for models. When limiting a parentQuery, the Typo3DbQueryParser is taking care of it. * * By using the RelationHandler, the localized, deleted and moved records turn out to be properly resolved * without having to build intermediate queries. * * This is currently only used in workspaces' context, as it is 1 additional DB query needed. * * @param DataMap $dataMap * @param ColumnMap $columnMap * @param int|null $parentId * @param string $fieldValue * @param int $workspaceId * @return array|false|mixed */ protected function resolveRelationValuesOfField(DataMap $dataMap, ColumnMap $columnMap, ?int $parentId, $fieldValue, int $workspaceId) { $relationHandler = GeneralUtility::makeInstance(RelationHandler::class); $relationHandler->setWorkspaceId($workspaceId); $relationHandler->setUseLiveReferenceIds(true); $relationHandler->setUseLiveParentIds(true); $tableName = $dataMap->tableName; $fieldName = $columnMap->columnName; if (!$this->tcaSchemaFactory->get($tableName)->hasField($fieldName)) { return []; } $fieldConfiguration = $this->tcaSchemaFactory->get($tableName)->getField($fieldName)->getConfiguration(); $relationHandler->start( $fieldValue, $fieldConfiguration['allowed'] ?? $fieldConfiguration['foreign_table'] ?? '', $fieldConfiguration['MM'] ?? '', $parentId, $tableName, $fieldConfiguration ); $relationHandler->processDeletePlaceholder(); $relatedUids = []; if (!empty($relationHandler->tableArray)) { $relatedUids = reset($relationHandler->tableArray); } return $relatedUids; } /** * Builds and returns the source to build a join for a m:n relation. * * @param string $propertyName */ protected function getSource(DomainObjectInterface $parentObject, $propertyName): SourceInterface { $columnMap = $this->getDataMap(get_class($parentObject))->getColumnMap($propertyName); $left = $this->qomFactory->selector(null, $columnMap->relationTableName); $childClassName = $this->getType(get_class($parentObject), $propertyName); $right = $this->qomFactory->selector($childClassName, $columnMap->childTableName); $joinCondition = $this->qomFactory->equiJoinCondition($columnMap->relationTableName, $columnMap->childKeyFieldName, $columnMap->childTableName, 'uid'); return $this->qomFactory->join($left, $right, Query::JCR_JOIN_TYPE_INNER, $joinCondition); } /** * Returns the mapped classProperty from the identityMap or * mapResultToPropertyValue() * * If the field value is empty and the column map has no parent key field name, * the relation will be empty. If the persistence session has a registered object of * the correct type and identity (fieldValue), this function returns that object. * Otherwise, it proceeds with mapResultToPropertyValue(). * * @param mixed $fieldValue the raw field value * @see mapResultToPropertyValue() */ protected function mapObjectToClassProperty(DomainObjectInterface $parentObject, string $propertyName, $fieldValue) { if ($this->propertyMapsByForeignKey($parentObject, $propertyName)) { $result = $this->fetchRelated($parentObject, $propertyName, $fieldValue); return $this->mapResultToPropertyValue($parentObject, $propertyName, $result); } if (empty($fieldValue)) { return $this->getEmptyRelationValue($parentObject, $propertyName); } $primaryType = $this->reflectionService ->getClassSchema(get_class($parentObject)) ->getProperty($propertyName) ->getPrimaryType(); if ($primaryType === null) { throw NoPropertyTypesException::create($parentObject::class, $propertyName); } $className = $primaryType->getClassName(); if ($className === null) { throw new \LogicException( sprintf('Evaluated type of class property %s::%s is not a class name. Check the type declaration of the property to use a valid class name.', $parentObject::class, $propertyName), 1660217846 ); } $identifier = $this->persistenceSession->buildIdentifier((string)$fieldValue, $this->getEffectiveLanguageAspect()); if ($this->persistenceSession->hasIdentifier($identifier, $className)) { return $this->persistenceSession->getObjectByIdentifier($identifier, $className); } $result = $this->fetchRelated($parentObject, $propertyName, $fieldValue); return $this->mapResultToPropertyValue($parentObject, $propertyName, $result); } /** * Checks if the relation is based on a foreign key. * * @param string $propertyName * @return bool TRUE if the property is mapped */ protected function propertyMapsByForeignKey(DomainObjectInterface $parentObject, $propertyName) { $columnMap = $this->getDataMap(get_class($parentObject))->getColumnMap($propertyName); return $columnMap->parentKeyFieldName !== null; } /** * Returns the given result as property value of the specified property type. * * @param string $propertyName * @param mixed $result The result * @return mixed */ public function mapResultToPropertyValue(DomainObjectInterface $parentObject, $propertyName, $result) { $propertyValue = null; if ($result instanceof LoadingStrategyInterface) { $propertyValue = $result; } else { $property = $this->reflectionService->getClassSchema(get_class($parentObject))->getProperty($propertyName); $primaryType = $property->getPrimaryType(); if ($primaryType === null) { throw NoPropertyTypesException::create($parentObject::class, $propertyName); } if ($primaryType->getBuiltinType() === 'array' || in_array($primaryType->getClassName(), [\ArrayObject::class, \SplObjectStorage::class, ObjectStorage::class], true)) { $objects = []; foreach ($result as $value) { $objects[] = $value; } if ($primaryType->getClassName() === \ArrayObject::class) { $propertyValue = new \ArrayObject($objects); } elseif ($primaryType->getClassName() === ObjectStorage::class) { $propertyValue = new ObjectStorage(); foreach ($objects as $object) { $propertyValue->attach($object); } $propertyValue->_memorizeCleanState(); } else { $propertyValue = $objects; } } elseif (strpbrk((string)$primaryType->getClassName(), '_\\') !== false) { // @todo: check the strpbrk function call. Seems to be a check for Tx_Foo_Bar style class names if ($result instanceof QueryResultInterface) { $propertyValue = $result->getFirst(); } else { $propertyValue = $result; } } } return $propertyValue; } /** * Counts the number of related objects assigned to a property of a parent object * * @param DomainObjectInterface $parentObject The object instance this proxy is part of * @param string $propertyName The name of the proxied property in it's parent * @param mixed $fieldValue The raw field value. * @return int */ public function countRelated(DomainObjectInterface $parentObject, $propertyName, $fieldValue = '') { $query = $this->getPreparedQuery($parentObject, $propertyName, $fieldValue); return $query->execute()->count(); } /** * Returns a data map for a given class name * * @param string $className The class name you want to fetch the Data Map for * @throws Persistence\Generic\Exception */ public function getDataMap($className): DataMap { if (!is_string($className) || $className === '') { throw new Exception('No class name was given to retrieve the Data Map for.', 1251315965); } return $this->dataMapFactory->buildDataMap($className); } /** * Returns the selector (table) name for a given class name. * * @param string $className * @return string The selector name */ public function convertClassNameToTableName($className) { return $this->getDataMap($className)->tableName; } /** * Returns the column name for a given property name of the specified class. * * @param string $propertyName * @param string $className * @return string The column name */ public function convertPropertyNameToColumnName($propertyName, $className = null) { if (!empty($className)) { $dataMap = $this->getDataMap($className); $columnMap = $dataMap->getColumnMap($propertyName); if ($columnMap !== null) { return $columnMap->columnName; } } return GeneralUtility::camelCaseToLowerCaseUnderscored($propertyName); } /** * Returns the type of a child object. * * @param string $parentClassName The class name of the object this proxy is part of * @param string $propertyName The name of the proxied property in it's parent * @throws UnexpectedTypeException * @return string The class name of the child object */ public function getType($parentClassName, $propertyName) { try { $primaryType = $this->reflectionService ->getClassSchema($parentClassName) ->getProperty($propertyName) ->getPrimaryType(); if ($primaryType === null) { throw NoPropertyTypesException::create($parentClassName, $propertyName); } if ($primaryType->isCollection() && $primaryType->getCollectionValueTypes() !== []) { $primaryCollectionValueType = $primaryType->getCollectionValueTypes()[0]; return $primaryCollectionValueType->getClassName() ?? $primaryCollectionValueType->getBuiltinType(); } return $primaryType->getClassName() ?? $primaryType->getBuiltinType(); } catch (NoSuchPropertyException|NoPropertyTypesException $e) { } throw new UnexpectedTypeException('Could not determine the child object type.', 1251315967); } /** * Returns a plain value, i.e. objects are flattened out if possible. * Multi value objects or arrays will be converted to a comma-separated list for use in "IN" SQL queries. * Caution: We do not return "null" values yet, if so, we need to adapt all places to handle null (see git history of this line) * * @param mixed $input The value that will be converted. * @param ColumnMap|null $columnMap Optional column map for retrieving the date storage format. */ public function getPlainValue(mixed $input, ?ColumnMap $columnMap = null): int|string { if ($input instanceof \DateTimeInterface || ($input === null && $columnMap?->type === TableColumnType::DATETIME)) { return QueryHelper::transformDateTimeToDatabaseValue( $input, $columnMap->isNullable ?? false, $columnMap->dateTimeFormat ?? 'datetime', $columnMap?->dateTimeStorageFormat ) ?? 'NULL'; } if ($input === null) { return 'NULL'; } if ($input instanceof \BackedEnum) { return $input->value; } if ($input instanceof LazyLoadingProxy) { $input = $input->_loadRealInstance(); } if (is_bool($input)) { return (int)$input; } if (is_int($input)) { return $input; } if ($input instanceof Country) { // @todo Check if this can be abstracted in a better way (for future TCA types) return $input->getAlpha2IsoCode(); } if ($input instanceof DomainObjectInterface) { return (int)$input->getUid(); } if (TypeHandlingUtility::isValidTypeForMultiValueComparison($input)) { $plainValueArray = []; foreach ($input as $inputElement) { $plainValueArray[] = $this->getPlainValue($inputElement, $columnMap); } return implode(',', $plainValueArray); } if (is_object($input)) { if (TypeHandlingUtility::isCoreType($input) || $input instanceof \Stringable) { return (string)$input; } throw new UnexpectedTypeException('An object of class "' . get_class($input) . '" could not be converted to a plain value.', 1274799934); } return (string)$input; } }