TYPO3 v15 dev-main snapshot ()

This commit is contained in:
2026-08-10 22:31:15 +02:00
commit 6830982e7d
295 changed files with 31995 additions and 0 deletions
@@ -0,0 +1,71 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Extbase\Persistence;
class ClassesConfiguration
{
/**
* @var array
*/
private $configuration;
public function __construct(array $configuration)
{
$this->configuration = $configuration;
}
public function hasClass(string $className): bool
{
return array_key_exists($className, $this->configuration);
}
public function getConfigurationFor(string $className): ?array
{
return $this->configuration[$className] ?? null;
}
/**
* Resolves all subclasses for the given set of (sub-)classes.
* The whole classes configuration is used to determine all subclasses recursively.
*
* @return array A numeric array that contains all available subclasses-strings as values.
*/
public function getSubClasses(string $className): array
{
return $this->resolveSubClassesRecursive($className);
}
private function resolveSubClassesRecursive(string $className, array $subClasses = []): array
{
foreach ($this->configuration[$className]['subclasses'] ?? [] as $subclass) {
if (in_array($subclass, $subClasses, true)) {
continue;
}
$subClasses[] = $subclass;
$subClasses = $this->resolveSubClassesRecursive($subclass, $subClasses);
}
return $subClasses;
}
public function getConfiguration(): array
{
return $this->configuration;
}
}
@@ -0,0 +1,111 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Extbase\Persistence;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
use TYPO3\CMS\Core\Package\PackageManager;
use TYPO3\CMS\Core\Utility\ArrayUtility;
use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;
use TYPO3\CMS\Extbase\DomainObject\AbstractValueObject;
final readonly class ClassesConfigurationFactory
{
public function __construct(
#[Autowire(service: 'cache.extbase')]
private FrontendInterface $cache,
private PackageManager $packageManager,
#[Autowire(expression: 'service("package-dependent-cache-identifier").withPrefix("PersistenceClasses").toString()')]
private string $cacheIdentifier,
) {}
public function createClassesConfiguration(): ClassesConfiguration
{
$classesConfigurationCache = $this->cache->get($this->cacheIdentifier);
if ($classesConfigurationCache !== false) {
return new ClassesConfiguration($classesConfigurationCache);
}
$classes = [];
foreach ($this->packageManager->getActivePackages() as $activePackage) {
$persistenceClassesFile = $activePackage->getPackagePath() . 'Configuration/Extbase/Persistence/Classes.php';
if (file_exists($persistenceClassesFile)) {
$definedClasses = require $persistenceClassesFile;
if (is_array($definedClasses)) {
ArrayUtility::mergeRecursiveWithOverrule(
$classes,
$definedClasses,
true,
false
);
}
}
}
$classes = $this->inheritPropertiesFromParentClasses($classes);
$this->cache->set($this->cacheIdentifier, $classes);
return new ClassesConfiguration($classes);
}
/**
* todo: this method is flawed, see https://forge.typo3.org/issues/87566
*/
private function inheritPropertiesFromParentClasses(array $classes): array
{
foreach (array_keys($classes) as $className) {
if (!isset($classes[$className]['properties'])) {
$classes[$className]['properties'] = [];
}
/*
* At first we need to clean the list of parent classes.
* This methods is expected to be called for models that either inherit
* AbstractEntity or AbstractValueObject, therefore we want to know all
* parents of $className until one of these parents.
*/
$relevantParentClasses = [];
$parentClasses = class_parents($className) ?: [];
while (null !== $parentClass = array_shift($parentClasses)) {
if (in_array($parentClass, [AbstractEntity::class, AbstractValueObject::class], true)) {
break;
}
$relevantParentClasses[] = $parentClass;
}
/*
* Once we found all relevant parent classes of $class, we can check their
* property configuration and merge theirs with the current one. This is necessary
* to get the property configuration of parent classes in the current one to not
* miss data in the model later on.
*/
foreach ($relevantParentClasses as $currentClassName) {
if (null === $properties = $classes[$currentClassName]['properties'] ?? null) {
continue;
}
// Merge new properties over existing ones.
$classes[$className]['properties'] = array_replace_recursive($properties, $classes[$className]['properties'] ?? []);
}
}
return $classes;
}
}
+25
View File
@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Extbase\Persistence;
use TYPO3\CMS\Extbase\Exception as ExtbaseException;
/**
* A generic Persistence exception
*/
class Exception extends ExtbaseException {}
@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Extbase\Persistence\Exception;
use TYPO3\CMS\Extbase\Persistence\Exception;
/**
* An "Invalid Object Type" exception
*/
class IllegalObjectTypeException extends Exception {}
@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Extbase\Persistence\Exception;
use TYPO3\CMS\Extbase\Persistence\Exception;
/**
* An "Illegal Relation Type" exception
*/
class IllegalRelationTypeException extends Exception {}
@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Extbase\Persistence\Exception;
use TYPO3\CMS\Extbase\Exception;
/**
* An "Invalid Query" Exception
*/
class InvalidQueryException extends Exception {}
@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Extbase\Persistence\Exception;
use TYPO3\CMS\Extbase\Persistence\Exception;
/**
* An "Unknown Object" exception
*/
class UnknownObjectException extends Exception {}
+923
View File
@@ -0,0 +1,923 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Extbase\Persistence\Generic;
use Psr\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
use TYPO3\CMS\Core\Context\LanguageAspect;
use TYPO3\CMS\Core\Database\Query\QueryHelper;
use TYPO3\CMS\Core\Database\ReferenceIndex;
use TYPO3\CMS\Core\DataHandling\TableColumnType;
use TYPO3\CMS\Core\Schema\TcaSchemaFactory;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface;
use TYPO3\CMS\Extbase\Configuration\Exception\NoServerRequestGivenException;
use TYPO3\CMS\Extbase\DomainObject\AbstractDomainObject;
use TYPO3\CMS\Extbase\DomainObject\AbstractValueObject;
use TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface;
use TYPO3\CMS\Extbase\Event\Persistence\EntityAddedToPersistenceEvent;
use TYPO3\CMS\Extbase\Event\Persistence\EntityFinalizedAfterPersistenceEvent;
use TYPO3\CMS\Extbase\Event\Persistence\EntityPersistedEvent;
use TYPO3\CMS\Extbase\Event\Persistence\EntityRemovedFromPersistenceEvent;
use TYPO3\CMS\Extbase\Event\Persistence\EntityUpdatedInPersistenceEvent;
use TYPO3\CMS\Extbase\Event\Persistence\ModifyQueryBeforeFetchingObjectCountEvent;
use TYPO3\CMS\Extbase\Event\Persistence\ModifyQueryBeforeFetchingObjectDataEvent;
use TYPO3\CMS\Extbase\Event\Persistence\ModifyResultAfterFetchingObjectCountEvent;
use TYPO3\CMS\Extbase\Event\Persistence\ModifyResultAfterFetchingObjectDataEvent;
use TYPO3\CMS\Extbase\Persistence\Exception\IllegalRelationTypeException;
use TYPO3\CMS\Extbase\Persistence\Generic\Mapper\ColumnMap;
use TYPO3\CMS\Extbase\Persistence\Generic\Mapper\ColumnMap\Relation;
use TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMapFactory;
use TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMapper;
use TYPO3\CMS\Extbase\Persistence\Generic\Storage\BackendInterface as StorageBackendInterface;
use TYPO3\CMS\Extbase\Persistence\ObjectStorage;
use TYPO3\CMS\Extbase\Persistence\PersistenceManagerInterface;
use TYPO3\CMS\Extbase\Persistence\QueryInterface;
use TYPO3\CMS\Extbase\Reflection\ClassSchema\Property;
use TYPO3\CMS\Extbase\Reflection\ObjectAccess;
use TYPO3\CMS\Extbase\Reflection\ReflectionService;
/**
* A persistence backend. This backend maps objects to the relational model of the storage backend.
* It persists all added, removed and changed objects.
*
* Warning: This is a stateful-shared service!
*
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
#[Autoconfigure(public: true)]
class Backend implements BackendInterface
{
protected PersistenceManagerInterface $persistenceManager;
protected ObjectStorage $aggregateRootObjects;
protected ObjectStorage $deletedEntities;
protected ObjectStorage $changedEntities;
protected ObjectStorage $visitedDuringPersistence;
public function __construct(
protected readonly ConfigurationManagerInterface $configurationManager,
protected readonly Session $session,
protected readonly ReflectionService $reflectionService,
protected readonly StorageBackendInterface $storageBackend,
protected readonly DataMapFactory $dataMapFactory,
protected readonly EventDispatcherInterface $eventDispatcher,
protected readonly ReferenceIndex $referenceIndex,
protected readonly TcaSchemaFactory $tcaSchemaFactory,
) {
$this->aggregateRootObjects = new ObjectStorage();
$this->deletedEntities = new ObjectStorage();
$this->changedEntities = new ObjectStorage();
}
public function setPersistenceManager(PersistenceManagerInterface $persistenceManager): void
{
$this->persistenceManager = $persistenceManager;
}
/**
* Returns the number of records matching the query.
*
* @return int
*/
public function getObjectCountByQuery(QueryInterface $query)
{
$event = new ModifyQueryBeforeFetchingObjectCountEvent($query);
$this->eventDispatcher->dispatch($event);
$query = $event->getQuery();
$result = $this->storageBackend->getObjectCountByQuery($query);
$event = new ModifyResultAfterFetchingObjectCountEvent($query, $result);
$this->eventDispatcher->dispatch($event);
return $event->getResult();
}
/**
* Returns the object data matching the $query.
*
* @return list<array<string,mixed>>
*/
public function getObjectDataByQuery(QueryInterface $query)
{
$event = new ModifyQueryBeforeFetchingObjectDataEvent($query);
$this->eventDispatcher->dispatch($event);
$query = $event->getQuery();
$result = $this->storageBackend->getObjectDataByQuery($query);
$event = new ModifyResultAfterFetchingObjectDataEvent($query, $result);
$this->eventDispatcher->dispatch($event);
return $event->getResult();
}
/**
* Returns the (internal) identifier for the object, if it is known to the
* backend. Otherwise NULL is returned.
*
* The returned identifier is the base identifier (UID or UID_localizedUID)
* without the language content identifier suffix, suitable for use as an external identifier.
*
* @param object $object
* @return string|null The identifier for the object if it is known, or NULL
*/
public function getIdentifierByObject($object)
{
if ($object instanceof LazyLoadingProxy) {
$object = $object->_loadRealInstance();
}
if (!is_object($object)) {
return null;
}
$identifier = $this->session->getIdentifierByObject($object);
if ($identifier === null) {
return null;
}
return $this->session->getBaseIdentifier($identifier);
}
/**
* Returns the object with the (internal) identifier, if it is known to the
* backend. Otherwise NULL is returned.
*
* @param string $identifier
* @param string $className
* @return object|null The object for the identifier if it is known, or NULL
*/
public function getObjectByIdentifier($identifier, $className)
{
$query = $this->persistenceManager->createQueryForType($className);
// This allows to fetch IDs for languages for default language AND language IDs
// This is especially important when using the PropertyMapper of the Extbase MVC part to get
// an object of the translated version of the incoming ID of a record.
// "Free" mode (OVERLAYS_OFF) is mapped to OVERLAYS_MIXED - overlays need to be enabled for the
// identity lookup, but hiding untranslated records is not a configured intent in free mode.
// This is consistent with the same handling for related objects in DataMapper->getPreparedQuery().
$languageAspect = $query->getQuerySettings()->getLanguageAspect();
$languageAspect = new LanguageAspect(
$languageAspect->getId(),
$languageAspect->getContentId(),
$languageAspect->getOverlayType() === LanguageAspect::OVERLAYS_OFF ? LanguageAspect::OVERLAYS_MIXED : $languageAspect->getOverlayType(),
$languageAspect->getFallbackChain()
);
// Build language-aware session identifier
$sessionIdentifier = $this->session->buildIdentifier($identifier, $languageAspect);
if ($this->session->hasIdentifier($sessionIdentifier, $className)) {
return $this->session->getObjectByIdentifier($sessionIdentifier, $className);
}
$query->getQuerySettings()->setLanguageAspect($languageAspect);
$query->getQuerySettings()->setRespectStoragePage(false);
$query->getQuerySettings()->setRespectSysLanguage(false);
return $query->matching($query->equals('uid', $identifier))->execute()->getFirst();
}
/**
* Checks if the given object has ever been persisted.
*
* @param object $object The object to check
* @return bool TRUE if the object is new, FALSE if the object exists in the repository
*/
public function isNewObject($object)
{
return $this->getIdentifierByObject($object) === null;
}
/**
* Sets the aggregate root objects
*/
public function setAggregateRootObjects(ObjectStorage $objects)
{
$this->aggregateRootObjects = $objects;
}
/**
* Sets the changed objects
*/
public function setChangedEntities(ObjectStorage $entities)
{
$this->changedEntities = $entities;
}
/**
* Sets the deleted objects
*/
public function setDeletedEntities(ObjectStorage $entities)
{
$this->deletedEntities = $entities;
}
/**
* Commits the current persistence session.
*/
public function commit()
{
$this->persistObjects();
$this->processDeletedObjects();
}
/**
* Traverse and persist all aggregate roots and their object graph.
*/
protected function persistObjects(): void
{
$this->visitedDuringPersistence = new ObjectStorage();
foreach ($this->aggregateRootObjects as $object) {
/** @var DomainObjectInterface $object */
if ($object->_isNew()) {
$this->insertObject($object);
}
$this->persistObject($object);
}
foreach ($this->changedEntities as $object) {
$this->persistObject($object);
}
}
/**
* Persists the given object.
*/
protected function persistObject(DomainObjectInterface $object): void
{
if (isset($this->visitedDuringPersistence[$object])) {
return;
}
$row = [];
$queue = [];
$className = get_class($object);
$dataMap = $this->dataMapFactory->buildDataMap($className);
$classSchema = $this->reflectionService->getClassSchema($className);
foreach ($classSchema->getDomainObjectProperties() as $property) {
$propertyName = $property->getName();
if (!$dataMap->isPersistableProperty($propertyName)) {
continue;
}
$propertyValue = $object->_getProperty($propertyName);
if ($this->propertyValueIsLazyLoaded($propertyValue)) {
continue;
}
$columnMap = $dataMap->getColumnMap($propertyName);
if ($propertyValue instanceof ObjectStorage) {
$cleanProperty = $object->_getCleanProperty($propertyName);
// objectstorage needs to be persisted if the object is new, the objectstorage is dirty, meaning it has
// been changed after initial build, or an empty objectstorage is present and the cleanstate objectstorage
// has childelements, meaning all elements should been removed from the objectstorage
if ($object->_isNew() || $propertyValue->_isDirty() || ($propertyValue->count() === 0 && $cleanProperty && $cleanProperty->count() > 0)) {
$this->persistObjectStorage($propertyValue, $object, $propertyName, $row);
$propertyValue->_memorizeCleanState();
}
foreach ($propertyValue as $containedObject) {
if ($containedObject instanceof DomainObjectInterface) {
$queue[] = $containedObject;
}
}
} elseif ($propertyValue instanceof DomainObjectInterface) {
if ($object->_isDirty($propertyName)) {
if ($propertyValue->_isNew()) {
$this->insertObject($propertyValue, $object, $propertyName);
}
$row[$columnMap->columnName] = $this->getPlainValue($propertyValue, null, $property);
}
$queue[] = $propertyValue;
} elseif ($object->_isNew() || $object->_isDirty($propertyName)) {
$row[$columnMap->columnName] = $this->getPlainValue($propertyValue, $columnMap, $property);
}
}
if (!empty($row)) {
$this->updateObject($object, $row);
$object->_memorizeCleanState();
}
$this->visitedDuringPersistence[$object] = $object->getUid();
foreach ($queue as $queuedObject) {
$this->persistObject($queuedObject);
}
$this->eventDispatcher->dispatch(new EntityPersistedEvent($object));
}
/**
* Checks, if the property value is lazy loaded and was not initialized
*/
protected function propertyValueIsLazyLoaded(mixed $propertyValue): bool
{
if ($propertyValue instanceof LazyLoadingProxy) {
return true;
}
if (($propertyValue instanceof LazyObjectStorage) && $propertyValue->isInitialized() === false) {
return true;
}
return false;
}
/**
* Persists an object storage. Objects of a 1:n or m:n relation are queued and processed with the parent object.
* A 1:1 relation gets persisted immediately. Objects which were removed from the property were detached from
* the parent object. They will not be deleted by default. You have to add the attribute
* #[\TYPO3\CMS\Extbase\Attribute\ORM\Cascade(['value' => 'remove'])] to the property if you want them to
* be deleted as well.
*
* @param \TYPO3\CMS\Extbase\Persistence\ObjectStorage $objectStorage The object storage to be persisted.
* @param DomainObjectInterface $parentObject The parent object. One of the properties holds the object storage.
* @param string $propertyName The name of the property holding the object storage.
* @param array $row The row array of the parent object to be persisted. It's passed by reference and gets filled with either a comma separated list of uids (csv) or the number of contained objects.
*/
protected function persistObjectStorage(
ObjectStorage $objectStorage,
DomainObjectInterface $parentObject,
string $propertyName,
array &$row
): void {
$className = get_class($parentObject);
$dataMapper = GeneralUtility::makeInstance(DataMapper::class);
$columnMap = $this->dataMapFactory->buildDataMap($className)->getColumnMap($propertyName);
$property = $this->reflectionService->getClassSchema($className)->getProperty($propertyName);
foreach ($this->getRemovedChildObjects($parentObject, $propertyName) as $removedObject) {
$this->detachObjectFromParentObject($removedObject, $parentObject, $propertyName);
if ($columnMap->typeOfRelation === Relation::HAS_MANY && $property->getCascadeValue() === 'remove') {
$this->removeEntity($removedObject);
}
}
$currentUids = [];
$sortingPosition = 1;
$updateSortingOfFollowing = false;
foreach ($objectStorage as $object) {
/** @var DomainObjectInterface $object */
if (empty($currentUids)) {
$sortingPosition = 1;
} else {
$sortingPosition++;
}
$cleanProperty = $parentObject->_getCleanProperty($propertyName);
if ($object->_isNew()) {
$this->insertObject($object, $parentObject);
$this->attachObjectToParentObject($object, $parentObject, $propertyName, $sortingPosition);
// if a new object is inserted, all objects after this need to have their sorting updated
$updateSortingOfFollowing = true;
} elseif ($cleanProperty === null || $cleanProperty->getPosition($object) === null) {
// if parent object is new then it doesn't have cleanProperty yet; before attaching object it's clean position is null
$this->attachObjectToParentObject($object, $parentObject, $propertyName, $sortingPosition);
// if a relation is dirty (speaking the same object is removed and added again at a different position), all objects after this needs to be updated the sorting
$updateSortingOfFollowing = true;
} elseif ($objectStorage->isRelationDirty($object) || $cleanProperty->getPosition($object) !== $objectStorage->getPosition($object)) {
$this->updateRelationOfObjectToParentObject($object, $parentObject, $propertyName, $sortingPosition);
$updateSortingOfFollowing = true;
} elseif ($updateSortingOfFollowing) {
if ($sortingPosition > $objectStorage->getPosition($object)) {
$this->updateRelationOfObjectToParentObject($object, $parentObject, $propertyName, $sortingPosition);
} else {
$sortingPosition = $objectStorage->getPosition($object);
}
}
$currentUids[] = $object->getUid();
}
if ($columnMap->parentKeyFieldName === null) {
$row[$columnMap->columnName] = implode(',', $currentUids);
} else {
$row[$columnMap->columnName] = $dataMapper->countRelated($parentObject, $propertyName);
}
}
/**
* Returns the removed objects determined by a comparison of the clean property value
* with the actual property value.
*/
protected function getRemovedChildObjects(DomainObjectInterface $object, string $propertyName): array
{
$removedObjects = [];
$cleanPropertyValue = $object->_getCleanProperty($propertyName);
if (is_array($cleanPropertyValue) || $cleanPropertyValue instanceof \Iterator) {
$propertyValue = $object->_getProperty($propertyName);
foreach ($cleanPropertyValue as $containedObject) {
if (!$propertyValue->contains($containedObject)) {
$removedObjects[] = $containedObject;
}
}
}
return $removedObjects;
}
/**
* Updates the fields defining the relation between the object and the parent object.
*/
protected function attachObjectToParentObject(
DomainObjectInterface $object,
DomainObjectInterface $parentObject,
string $parentPropertyName,
int $sortingPosition = 0
): void {
$parentDataMap = $this->dataMapFactory->buildDataMap(get_class($parentObject));
$parentColumnMap = $parentDataMap->getColumnMap($parentPropertyName);
if ($parentColumnMap->typeOfRelation === Relation::HAS_MANY) {
$this->attachObjectToParentObjectRelationHasMany($object, $parentObject, $parentPropertyName, $sortingPosition);
} elseif ($parentColumnMap->typeOfRelation === Relation::HAS_AND_BELONGS_TO_MANY) {
$this->insertRelationInRelationtable($object, $parentObject, $parentPropertyName, $sortingPosition);
}
}
/**
* Updates the fields defining the relation between the object and the parent object.
*/
protected function updateRelationOfObjectToParentObject(
DomainObjectInterface $object,
DomainObjectInterface $parentObject,
string $parentPropertyName,
int $sortingPosition = 0
): void {
$parentDataMap = $this->dataMapFactory->buildDataMap(get_class($parentObject));
$parentColumnMap = $parentDataMap->getColumnMap($parentPropertyName);
if ($parentColumnMap->typeOfRelation === Relation::HAS_MANY) {
$this->attachObjectToParentObjectRelationHasMany($object, $parentObject, $parentPropertyName, $sortingPosition);
} elseif ($parentColumnMap->typeOfRelation === Relation::HAS_AND_BELONGS_TO_MANY) {
$this->updateRelationInRelationTable($object, $parentObject, $parentPropertyName, $sortingPosition);
}
}
/**
* Updates fields defining the relation between the object and the parent object in relation has-many.
*
* @throws IllegalRelationTypeException
*/
protected function attachObjectToParentObjectRelationHasMany(
DomainObjectInterface $object,
DomainObjectInterface $parentObject,
string $parentPropertyName,
int $sortingPosition = 0
): void {
$parentDataMap = $this->dataMapFactory->buildDataMap(get_class($parentObject));
$parentColumnMap = $parentDataMap->getColumnMap($parentPropertyName);
if ($parentColumnMap->typeOfRelation !== Relation::HAS_MANY) {
throw new IllegalRelationTypeException(
'Parent column relation type is ' . Relation::class . '::' . $parentColumnMap->typeOfRelation->name
. ' but should be ' . Relation::class . '::' . Relation::HAS_MANY->name,
1345368105
);
}
$row = [];
if ($parentColumnMap->parentKeyFieldName !== null) {
$row[$parentColumnMap->parentKeyFieldName] = $parentObject->_getProperty(AbstractDomainObject::PROPERTY_LOCALIZED_UID) ?: $parentObject->getUid();
if ($parentColumnMap->parentTableFieldName !== null) {
$row[$parentColumnMap->parentTableFieldName] = $parentDataMap->tableName;
}
$row = array_merge($parentColumnMap->relationTableMatchFields, $row);
}
$childSortByFieldName = $parentColumnMap->childSortByFieldName;
if (!empty($childSortByFieldName)) {
$row[$childSortByFieldName] = $sortingPosition;
}
if (!empty($row)) {
$this->updateObject($object, $row);
}
}
/**
* Updates the fields defining the relation between the object and the parent object.
*/
protected function detachObjectFromParentObject(
DomainObjectInterface $object,
DomainObjectInterface $parentObject,
string $parentPropertyName
): void {
$parentDataMap = $this->dataMapFactory->buildDataMap(get_class($parentObject));
$parentColumnMap = $parentDataMap->getColumnMap($parentPropertyName);
if ($parentColumnMap->typeOfRelation === Relation::HAS_MANY) {
$row = [];
if ($parentColumnMap->parentKeyFieldName !== null) {
$row[$parentColumnMap->parentKeyFieldName] = 0;
if ($parentColumnMap->parentTableFieldName !== null) {
$row[$parentColumnMap->parentTableFieldName] = '';
}
if (!empty($parentColumnMap->relationTableMatchFields)) {
$row = array_merge(array_fill_keys(array_keys($parentColumnMap->relationTableMatchFields), ''), $row);
}
}
if (!empty($parentColumnMap->childSortByFieldName)) {
$row[$parentColumnMap->childSortByFieldName] = 0;
}
if (!empty($row)) {
$this->updateObject($object, $row);
}
} elseif ($parentColumnMap->typeOfRelation === Relation::HAS_AND_BELONGS_TO_MANY) {
$this->deleteRelationFromRelationtable($object, $parentObject, $parentPropertyName);
}
}
/**
* Inserts an object in the storage backend
*/
protected function insertObject(
DomainObjectInterface $object,
?DomainObjectInterface $parentObject = null,
string $parentPropertyName = ''
): void {
if ($object instanceof AbstractValueObject) {
$result = $this->getUidOfAlreadyPersistedValueObject($object);
if ($result !== null) {
$object->_setProperty(AbstractDomainObject::PROPERTY_UID, $result);
return;
}
}
$className = get_class($object);
$dataMap = $this->dataMapFactory->buildDataMap($className);
$row = [];
$classSchema = $this->reflectionService->getClassSchema($className);
foreach ($classSchema->getDomainObjectProperties() as $property) {
$propertyName = $property->getName();
if (!$dataMap->isPersistableProperty($propertyName)) {
continue;
}
$propertyValue = $object->_getProperty($propertyName);
if ($this->propertyValueIsLazyLoaded($propertyValue)) {
continue;
}
$columnMap = $dataMap->getColumnMap($propertyName);
if ($columnMap->typeOfRelation === Relation::HAS_ONE) {
$row[$columnMap->columnName] = 0;
} elseif ($columnMap->typeOfRelation !== Relation::NONE) {
if ($columnMap->parentKeyFieldName === null) {
// CSV type relation
$row[$columnMap->columnName] = '';
} else {
// MM type relation
$row[$columnMap->columnName] = 0;
}
} elseif ($propertyValue !== null) {
$row[$columnMap->columnName] = $this->getPlainValue($propertyValue, $columnMap, $property);
}
}
$this->addCommonFieldsToRow($object, $row);
if ($dataMap->languageIdColumnName !== null && $object->_getProperty(AbstractDomainObject::PROPERTY_LANGUAGE_UID) === null) {
$row[$dataMap->languageIdColumnName] = 0;
$object->_setProperty(AbstractDomainObject::PROPERTY_LANGUAGE_UID, 0);
}
if ($dataMap->translationOriginColumnName !== null) {
$row[$dataMap->translationOriginColumnName] = 0;
}
if ($dataMap->translationOriginDiffSourceName !== null) {
$row[$dataMap->translationOriginDiffSourceName] = '';
}
if ($parentObject !== null && $parentPropertyName) {
$parentColumnDataMap = $this->dataMapFactory->buildDataMap(get_class($parentObject))->getColumnMap($parentPropertyName);
$row = array_merge($parentColumnDataMap->relationTableMatchFields, $row);
if ($parentColumnDataMap->parentKeyFieldName !== null) {
$row[$parentColumnDataMap->parentKeyFieldName] = (int)$parentObject->getUid();
}
}
if ($parentObject) {
// Ensure a nested object respects the storage PID for new records or inherits the storage PID from
// the parent object.
$storagePidForObject = $this->determineStoragePageIdForNewRecord($object);
if ($storagePidForObject === 0) {
$storagePidForObject = $parentObject->getPid() ?? 0;
}
$row['pid'] = $storagePidForObject;
}
$uid = $this->storageBackend->addRow($dataMap->tableName, $row);
$localizedUid = $object->_getProperty(AbstractDomainObject::PROPERTY_LOCALIZED_UID);
$identifier = $this->session->buildIdentifier(['uid' => $uid, '_LOCALIZED_UID' => $localizedUid]);
$object->_setProperty(AbstractDomainObject::PROPERTY_UID, $uid);
$object->setPid((int)$row['pid']);
if ($uid >= 1) {
$this->eventDispatcher->dispatch(new EntityAddedToPersistenceEvent($object));
}
$this->referenceIndex->updateRefIndexTable($dataMap->tableName, $uid);
$this->session->registerObject($object, $identifier);
if ($uid >= 1) {
$this->eventDispatcher->dispatch(new EntityFinalizedAfterPersistenceEvent($object));
}
}
/**
* Tests, if the given Value Object already exists in the storage backend and if so, it returns the uid.
*
* @return int|null The matching uid if an object was found, else null
*/
protected function getUidOfAlreadyPersistedValueObject(AbstractValueObject $object): ?int
{
return $this->storageBackend->getUidOfAlreadyPersistedValueObject($object);
}
/**
* Inserts mm-relation into a relation table
*
* @return int The uid of the inserted row
*/
protected function insertRelationInRelationtable(
DomainObjectInterface $object,
DomainObjectInterface $parentObject,
string $propertyName,
?int $sortingPosition = null
): int {
$dataMap = $this->dataMapFactory->buildDataMap(get_class($parentObject));
$columnMap = $dataMap->getColumnMap($propertyName);
$parentUid = $parentObject->getUid();
if ($parentObject->_getProperty(AbstractDomainObject::PROPERTY_LOCALIZED_UID) !== null) {
$parentUid = $parentObject->_getProperty(AbstractDomainObject::PROPERTY_LOCALIZED_UID);
}
$row = [];
if ($columnMap->parentKeyFieldName !== null) {
$row[$columnMap->parentKeyFieldName] = (int)$parentUid;
}
if ($columnMap->childKeyFieldName !== null) {
$row[$columnMap->childKeyFieldName] = (int)$object->getUid();
}
if ($columnMap->childSortByFieldName !== null) {
$row[$columnMap->childSortByFieldName] = $sortingPosition ?? 0;
}
$relationTableName = $columnMap->relationTableName;
if ($this->tcaSchemaFactory->has($relationTableName)) {
$row[AbstractDomainObject::PROPERTY_PID] = $this->determineStoragePageIdForNewRecord();
}
$row = array_merge($columnMap->relationTableMatchFields, $row);
return $this->storageBackend->addRow($relationTableName, $row, true);
}
/**
* Updates mm-relation in a relation table
*
* @return bool TRUE if update was successfully
*/
protected function updateRelationInRelationTable(
DomainObjectInterface $object,
DomainObjectInterface $parentObject,
string $propertyName,
int $sortingPosition = 0
): bool {
$dataMap = $this->dataMapFactory->buildDataMap(get_class($parentObject));
$columnMap = $dataMap->getColumnMap($propertyName);
$row = [];
if ($columnMap->parentKeyFieldName !== null) {
$row[$columnMap->parentKeyFieldName] = (int)$parentObject->getUid();
}
if ($columnMap->childKeyFieldName !== null) {
$row[$columnMap->childKeyFieldName] = (int)$object->getUid();
}
if ($columnMap->childSortByFieldName !== null) {
$row[$columnMap->childSortByFieldName] = $sortingPosition;
}
$relationTableName = $columnMap->relationTableName;
$row = array_merge($columnMap->relationTableMatchFields, $row);
$this->storageBackend->updateRelationTableRow($relationTableName, $row);
return true;
}
/**
* Delete all mm-relations of a parent from a relation table
*
* @return bool TRUE if delete was successfully
*/
protected function deleteAllRelationsFromRelationtable(
DomainObjectInterface $parentObject,
string $parentPropertyName
): bool {
$dataMap = $this->dataMapFactory->buildDataMap(get_class($parentObject));
$columnMap = $dataMap->getColumnMap($parentPropertyName);
$relationTableName = $columnMap->relationTableName;
$relationMatchFields = [];
if ($columnMap->parentKeyFieldName !== null) {
$relationMatchFields[$columnMap->parentKeyFieldName] = (int)$parentObject->getUid();
}
$relationMatchFields = array_merge($columnMap->relationTableMatchFields, $relationMatchFields);
$this->storageBackend->removeRow($relationTableName, $relationMatchFields);
return true;
}
/**
* Delete an mm-relation from a relation table
*/
protected function deleteRelationFromRelationtable(
DomainObjectInterface $relatedObject,
DomainObjectInterface $parentObject,
string $parentPropertyName
): bool {
$dataMap = $this->dataMapFactory->buildDataMap(get_class($parentObject));
$columnMap = $dataMap->getColumnMap($parentPropertyName);
$relationTableName = $columnMap->relationTableName;
$relationMatchFields = [];
if ($columnMap->parentKeyFieldName !== null) {
$relationMatchFields[$columnMap->parentKeyFieldName] = (int)$parentObject->getUid();
}
if ($columnMap->childKeyFieldName !== null) {
$relationMatchFields[$columnMap->childKeyFieldName] = (int)$relatedObject->getUid();
}
$relationMatchFields = array_merge($columnMap->relationTableMatchFields, $relationMatchFields);
$this->storageBackend->removeRow($relationTableName, $relationMatchFields);
return true;
}
/**
* Updates a given object in the storage
*/
protected function updateObject(DomainObjectInterface $object, array $row): void
{
$dataMap = $this->dataMapFactory->buildDataMap(get_class($object));
$this->addCommonFieldsToRow($object, $row);
$row['uid'] = $object->getUid();
if ($dataMap->languageIdColumnName !== null) {
$row[$dataMap->languageIdColumnName] = (int)$object->_getProperty(AbstractDomainObject::PROPERTY_LANGUAGE_UID);
if ($object->_getProperty(AbstractDomainObject::PROPERTY_LOCALIZED_UID) !== null) {
$row['uid'] = $object->_getProperty(AbstractDomainObject::PROPERTY_LOCALIZED_UID);
}
}
$this->storageBackend->updateRow($dataMap->tableName, $row);
$this->eventDispatcher->dispatch(new EntityUpdatedInPersistenceEvent($object));
$this->referenceIndex->updateRefIndexTable($dataMap->tableName, (int)$row['uid']);
}
/**
* Adds common database fields to a row
*/
protected function addCommonFieldsToRow(DomainObjectInterface $object, array &$row): void
{
$dataMap = $this->dataMapFactory->buildDataMap(get_class($object));
$this->addCommonDateFieldsToRow($object, $row);
if ($dataMap->recordTypeColumnName !== null && $dataMap->recordType !== null) {
$row[$dataMap->recordTypeColumnName] = $dataMap->recordType;
}
if ($object->_isNew() && !isset($row['pid'])) {
$row['pid'] = $this->determineStoragePageIdForNewRecord($object);
}
}
/**
* Adjusts the common date fields of the given row to the current time
*/
protected function addCommonDateFieldsToRow(DomainObjectInterface $object, array &$row): void
{
$dataMap = $this->dataMapFactory->buildDataMap(get_class($object));
if ($object->_isNew() && $dataMap->creationDateColumnName !== null) {
$row[$dataMap->creationDateColumnName] = $GLOBALS['EXEC_TIME'];
}
if ($dataMap->modificationDateColumnName !== null) {
$row[$dataMap->modificationDateColumnName] = $GLOBALS['EXEC_TIME'];
}
}
/**
* Iterate over deleted aggregate root objects and process them
*/
protected function processDeletedObjects(): void
{
foreach ($this->deletedEntities as $entity) {
if ($this->session->hasObject($entity)) {
$this->removeEntity($entity);
$this->session->unregisterReconstitutedEntity($entity);
$this->session->unregisterObject($entity);
}
}
$this->deletedEntities = new ObjectStorage();
}
/**
* Deletes an object
*/
protected function removeEntity(DomainObjectInterface $object, bool $markAsDeleted = true): void
{
$dataMap = $this->dataMapFactory->buildDataMap(get_class($object));
if ($markAsDeleted === true && $dataMap->deletedFlagColumnName !== null) {
$deletedColumnName = $dataMap->deletedFlagColumnName;
$row = [
'uid' => $object->getUid(),
$deletedColumnName => 1,
];
$this->addCommonDateFieldsToRow($object, $row);
$this->storageBackend->updateRow($dataMap->tableName, $row);
} else {
$this->storageBackend->removeRow($dataMap->tableName, ['uid' => $object->getUid()]);
}
$this->eventDispatcher->dispatch(new EntityRemovedFromPersistenceEvent($object));
$this->removeRelatedObjects($object);
$this->referenceIndex->updateRefIndexTable($dataMap->tableName, $object->getUid());
}
/**
* Remove related objects
*/
protected function removeRelatedObjects(DomainObjectInterface $object): void
{
$className = get_class($object);
$dataMap = $this->dataMapFactory->buildDataMap($className);
$classSchema = $this->reflectionService->getClassSchema($className);
foreach ($classSchema->getDomainObjectProperties() as $property) {
$propertyName = $property->getName();
$columnMap = $dataMap->getColumnMap($propertyName);
if ($columnMap === null) {
continue;
}
$propertyValue = $object->_getProperty($propertyName);
if ($property->getCascadeValue() === 'remove') {
if ($columnMap->typeOfRelation === Relation::HAS_MANY) {
foreach ($propertyValue as $containedObject) {
$this->removeEntity($containedObject);
}
} elseif ($propertyValue instanceof DomainObjectInterface) {
$this->removeEntity($propertyValue);
}
} elseif ($dataMap->deletedFlagColumnName === null
&& $columnMap->typeOfRelation === Relation::HAS_AND_BELONGS_TO_MANY
) {
$this->deleteAllRelationsFromRelationtable($object, $propertyName);
}
}
}
/**
* Determine the storage page ID for a given NEW record
*
* This does the following:
* - If the domain object has an accessible property 'pid' (i.e. through a getPid() method), that is used to store the record.
* - If there is a TypoScript configuration "classes.CLASSNAME.newRecordStoragePid", that is used to store new records.
* - If there is no such TypoScript configuration, it uses the first value of The "storagePid" taken for reading records.
*
* @return int the storage Page ID where the object should be stored
*/
protected function determineStoragePageIdForNewRecord(?DomainObjectInterface $object = null): int
{
$frameworkConfiguration = [];
try {
$frameworkConfiguration = $this->configurationManager->getConfiguration(ConfigurationManagerInterface::CONFIGURATION_TYPE_FRAMEWORK);
} catch (NoServerRequestGivenException) {
// Fallback to empty array if ConfigurationManager has not been initialized with a Request.
// This implies storagePid 0. This is a measure to specifically allow running the extbase
// persistence layer without a Request, which may be useful in some CLI scenarios (and can
// be convenient in tests) when no other code branches of extbase that have a hard dependency
// to the Request (e.g. controllers / view) are used.
}
if ($object !== null) {
if (ObjectAccess::isPropertyGettable($object, AbstractDomainObject::PROPERTY_PID)) {
$pid = ObjectAccess::getProperty($object, AbstractDomainObject::PROPERTY_PID);
if (isset($pid)) {
return (int)$pid;
}
}
$className = get_class($object);
if (isset($frameworkConfiguration['persistence']['classes'][$className]) && !empty($frameworkConfiguration['persistence']['classes'][$className]['newRecordStoragePid'])) {
return (int)$frameworkConfiguration['persistence']['classes'][$className]['newRecordStoragePid'];
}
}
$storagePidList = GeneralUtility::intExplode(',', (string)($frameworkConfiguration['persistence']['storagePid'] ?? '0'));
return $storagePidList[0];
}
/**
* Returns a plain value, i.e. objects are flattened out if possible.
* Checks explicitly for null values as DataMapper's getPlainValue would convert this to 'NULL'.
* For null values, the expected DB null value will be considered.
*
* @param mixed $input The value that will be converted
* @param ColumnMap|null $columnMap Optional column map for retrieving the date storage format
* @param Property|null $property The current property
* @return int|string|null
*/
protected function getPlainValue(mixed $input, ?ColumnMap $columnMap = null, ?Property $property = null)
{
if ($input !== null) {
return GeneralUtility::makeInstance(DataMapper::class)->getPlainValue($input, $columnMap);
}
if ($columnMap?->type === TableColumnType::DATETIME) {
return QueryHelper::transformDateTimeToDatabaseValue(
null,
$columnMap->isNullable,
$columnMap->dateTimeFormat ?? 'datetime',
$columnMap->dateTimeStorageFormat
);
}
if ($property === null) {
return null;
}
$className = $property->getPrimaryType()->getClassName() ?? null;
if ($className === null) {
return null;
}
// Nullable domain model property
if (is_subclass_of($className, DomainObjectInterface::class)) {
return 0;
}
return null;
}
}
@@ -0,0 +1,92 @@
<?php
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Extbase\Persistence\Generic;
use TYPO3\CMS\Extbase\Persistence\ObjectStorage;
use TYPO3\CMS\Extbase\Persistence\PersistenceManagerInterface;
use TYPO3\CMS\Extbase\Persistence\QueryInterface;
/**
* A persistence backend interface
*/
interface BackendInterface
{
/**
* Set a PersistenceManager instance.
*/
public function setPersistenceManager(PersistenceManagerInterface $persistenceManager);
/**
* Sets the aggregate root objects
*/
public function setAggregateRootObjects(ObjectStorage $objects);
/**
* Sets the deleted entities
*/
public function setDeletedEntities(ObjectStorage $entities);
/**
* Sets the changed objects
*/
public function setChangedEntities(ObjectStorage $entities);
/**
* Commits the current persistence session
*/
public function commit();
/**
* Returns the (internal) identifier for the object, if it is known to the
* backend. Otherwise NULL is returned.
*
* @param object $object
* @return string|null The identifier for the object if it is known, or NULL
*/
public function getIdentifierByObject($object);
/**
* Returns the object with the (internal) identifier, if it is known to the
* backend. Otherwise NULL is returned.
*
* @param string $identifier
* @param string $className
* @return object|null The object for the identifier if it is known, or NULL
*/
public function getObjectByIdentifier($identifier, $className);
/**
* Checks if the given object has ever been persisted.
*
* @param object $object The object to check
* @return bool TRUE if the object is new, FALSE if the object exists in the repository
*/
public function isNewObject($object);
/**
* Returns the number of records matching the query.
*
* @return int
*/
public function getObjectCountByQuery(QueryInterface $query);
/**
* Returns the object data matching the $query.
*
* @return list<array<string,mixed>>
*/
public function getObjectDataByQuery(QueryInterface $query);
}
+23
View File
@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Extbase\Persistence\Generic;
/**
* A generic Persistence exception
*/
class Exception extends \TYPO3\CMS\Extbase\Persistence\Exception {}
@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Extbase\Persistence\Generic\Exception;
use TYPO3\CMS\Extbase\Persistence\Generic\Exception;
/**
* Thrown if a setting set is not available in the current context.
*/
class InconsistentQuerySettingsException extends Exception {}
@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Extbase\Persistence\Generic\Exception;
use TYPO3\CMS\Extbase\Persistence\Generic\Exception;
/**
* An "Invalid Class" exception
*/
class InvalidClassException extends Exception {}
@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Extbase\Persistence\Generic\Exception;
use TYPO3\CMS\Extbase\Persistence\Generic\Exception;
/**
* An "InvalidRelationConfigurationException" exception
*/
class InvalidRelationConfigurationException extends Exception {}
@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Extbase\Persistence\Generic\Exception;
use TYPO3\CMS\Extbase\Persistence\Generic\Exception;
/**
* A "Missing ColumnMap" exception
*/
class MissingColumnMapException extends Exception {}
@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Extbase\Persistence\Generic\Exception;
use TYPO3\CMS\Extbase\Persistence\Exception;
/**
* An "NotImplementedException" exception
*/
class NotImplementedException extends Exception
{
public function __construct(string $method, ?int $exceptionCode = null)
{
parent::__construct(
sprintf('Method %s is not supported by generic persistence"', $method),
$exceptionCode ?? 1350213237
);
}
}
@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Extbase\Persistence\Generic\Exception;
/**
* Main exception thrown by classes in this package. May contain an error
* message and/or another nested exception.
*/
class RepositoryException extends \RuntimeException {}
@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Extbase\Persistence\Generic\Exception;
use TYPO3\CMS\Extbase\Persistence\Generic\Exception;
/**
* A "Too Dirty" exception
*/
class TooDirtyException extends Exception {}
@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Extbase\Persistence\Generic\Exception;
use TYPO3\CMS\Extbase\Persistence\Generic\Exception;
/**
* An "Unexpected Type" exception.
*/
class UnexpectedTypeException extends Exception {}
@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Extbase\Persistence\Generic\Exception;
use TYPO3\CMS\Extbase\Persistence\Generic\Exception;
/**
* An "Unsupported Order" exception: The order you specified in the query is not supported by now.
*/
class UnsupportedOrderException extends Exception {}
@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Extbase\Persistence\Generic\Exception;
use TYPO3\CMS\Extbase\Persistence\Generic\Exception;
/**
* An "Unsupported Order" exception: The order you specified in the query is not supported by now.
*/
class UnsupportedRelationException extends Exception {}
@@ -0,0 +1,250 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Extbase\Persistence\Generic;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface;
use TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMapper;
/**
* A proxy that can replace any object and replaces itself in it's parent on
* first access (call, get, set, isset, unset).
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
class LazyLoadingProxy implements \Iterator, LoadingStrategyInterface
{
protected ?DataMapper $dataMapper = null;
/**
* The object this property is contained in.
*
* @var DomainObjectInterface
*/
private $parentObject;
/**
* The name of the property represented by this proxy.
*
* @var string
*/
private $propertyName;
/**
* The raw field value.
*
* @var mixed
*/
private $fieldValue;
/**
* Constructs this proxy instance.
*
* @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.
*/
public function __construct($parentObject, $propertyName, $fieldValue, ?DataMapper $dataMapper = null)
{
$this->parentObject = $parentObject;
$this->propertyName = $propertyName;
$this->fieldValue = $fieldValue;
if ($dataMapper === null) {
$dataMapper = GeneralUtility::makeInstance(DataMapper::class);
}
$this->dataMapper = $dataMapper;
}
/**
* Populate this proxy by asking the $population closure.
*
* @return object|null The instance (hopefully) returned
*/
public function _loadRealInstance()
{
// this check safeguards against a proxy being activated multiple times
// usually that does not happen, but if the proxy is held from outside
// its parent ... the result would be weird.
if ($this->parentObject->_getProperty($this->propertyName) instanceof LazyLoadingProxy && $this->dataMapper) {
$objects = $this->dataMapper->fetchRelated($this->parentObject, $this->propertyName, $this->fieldValue, false);
$propertyValue = $this->dataMapper->mapResultToPropertyValue($this->parentObject, $this->propertyName, $objects);
$this->parentObject->_setProperty($this->propertyName, $propertyValue);
$this->parentObject->_memorizeCleanState($this->propertyName);
return $propertyValue;
}
return $this->parentObject->_getProperty($this->propertyName);
}
/**
* @return string
*/
public function _getTypeAndUidString()
{
$type = $this->dataMapper->getType(get_class($this->parentObject), $this->propertyName);
return $type . ':' . $this->fieldValue;
}
public function getUid(): int
{
return (int)$this->fieldValue;
}
/**
* Magic method call implementation.
*
* @param string $methodName The name of the property to get
* @param array $arguments The arguments given to the call
* @return mixed
*/
public function __call($methodName, $arguments)
{
$realInstance = $this->_loadRealInstance();
if (!is_object($realInstance)) {
return null;
}
/** @var callable $callable */
$callable = [$realInstance, $methodName];
return $callable(...$arguments);
}
/**
* Magic get call implementation.
*
* @param string $propertyName The name of the property to get
* @return mixed
*/
public function __get($propertyName)
{
$realInstance = $this->_loadRealInstance();
if ($realInstance instanceof DomainObjectInterface) {
return $realInstance->_getProperty($propertyName);
}
return $realInstance?->{$propertyName};
}
/**
* Magic set call implementation.
*
* @param string $propertyName The name of the property to set
* @param mixed $value The value for the property to set
*/
public function __set($propertyName, $value)
{
$realInstance = $this->_loadRealInstance();
$realInstance->{$propertyName} = $value;
}
/**
* Magic isset call implementation.
*
* @param string $propertyName The name of the property to check
* @return bool
*/
public function __isset($propertyName)
{
$realInstance = $this->_loadRealInstance();
return isset($realInstance->{$propertyName});
}
/**
* Magic unset call implementation.
*
* @param string $propertyName The name of the property to unset
*/
public function __unset($propertyName)
{
$realInstance = $this->_loadRealInstance();
unset($realInstance->{$propertyName});
}
/**
* Magic toString call implementation.
*
* @return string
*/
public function __toString()
{
$realInstance = $this->_loadRealInstance();
return $realInstance->__toString();
}
/**
* Returns the current value of the storage array
*/
public function current(): mixed
{
// todo: make sure current() can be performed on $realInstance
$realInstance = $this->_loadRealInstance();
return current($realInstance);
}
/**
* Returns the current key storage array
* @return int|string|null
*/
public function key(): mixed
{
// todo: make sure key() can be performed on $realInstance
$realInstance = $this->_loadRealInstance();
return key($realInstance);
}
/**
* Returns the next position of the storage array
*/
public function next(): void
{
// todo: make sure next() can be performed on $realInstance
$realInstance = $this->_loadRealInstance();
next($realInstance);
}
/**
* Resets the array pointer of the storage
*/
public function rewind(): void
{
// todo: make sure reset() can be performed on $realInstance
$realInstance = $this->_loadRealInstance();
reset($realInstance);
}
/**
* Checks if the array pointer of the storage points to a valid position
*/
public function valid(): bool
{
return $this->current() !== false;
}
public function __serialize(): array
{
$properties = get_object_vars($this);
unset($properties['dataMapper']);
return $properties;
}
public function __unserialize(array $data): void
{
foreach ($data as $propertyName => $propertyValue) {
$this->{$propertyName} = $propertyValue;
}
$this->dataMapper = GeneralUtility::getContainer()->get(DataMapper::class);
}
}
@@ -0,0 +1,317 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Extbase\Persistence\Generic;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface;
use TYPO3\CMS\Extbase\Persistence\Generic\Mapper\ColumnMap\Relation;
use TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMapper;
use TYPO3\CMS\Extbase\Persistence\ObjectStorage;
/**
* A proxy that can replace any object and replaces itself in its parent on the first access
* (`call`, `get`, `set`, `isset`, `unset`).
*
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*
* @template TEntity of object
* @extends ObjectStorage<TEntity>
*/
class LazyObjectStorage extends ObjectStorage implements LoadingStrategyInterface
{
/**
* This field is only needed to make debugging easier:
*
* If you call current() on a class that implements Iterator, PHP will return the first field of the object
* instead of calling the current() method of the interface.
*
* We use this unusual behavior of PHP to return the warning below in this case.
*/
private string $warning = 'You should never see this warning. If you do, you probably used PHP array functions like current() on the TYPO3\\CMS\\Extbase\\Persistence\\Generic\\LazyObjectStorage. To retrieve the first result, you can use the rewind() and current() methods.';
protected DataMapper $dataMapper;
/**
* The object this property is contained in.
*/
protected DomainObjectInterface $parentObject;
/**
* The name of the property represented by this proxy.
*/
protected string $propertyName;
/**
* The raw field value.
*/
protected mixed $fieldValue;
protected bool $isInitialized = false;
public function isInitialized(): bool
{
return $this->isInitialized;
}
/**
* @param TEntity $parentObject The object instance this proxy is part of
* @param string $propertyName The name of the proxied property in its parent
* @param mixed $fieldValue The raw field value.
*/
public function __construct(object $parentObject, string $propertyName, mixed $fieldValue, ?DataMapper $dataMapper = null)
{
$this->parentObject = $parentObject;
$this->propertyName = $propertyName;
$this->fieldValue = $fieldValue;
reset($this->storage);
if ($dataMapper === null) {
$dataMapper = GeneralUtility::makeInstance(DataMapper::class);
}
$this->dataMapper = $dataMapper;
}
/**
* Lazily initializes the object storage.
*/
protected function initialize(): void
{
if ($this->isInitialized) {
return;
}
$this->isInitialized = true;
$objects = $this->dataMapper->fetchRelated($this->parentObject, $this->propertyName, $this->fieldValue, false);
foreach ($objects as $object) {
parent::attach($object);
}
$this->_memorizeCleanState();
if (!$this->isStorageAlreadyMemorizedInParentCleanState()) {
$this->parentObject->_memorizeCleanState($this->propertyName);
}
}
protected function isStorageAlreadyMemorizedInParentCleanState(): bool
{
return $this->parentObject->_getCleanProperty($this->propertyName) === $this;
}
// Delegation to the ObjectStorage methods below
/**
* @see `ObjectStorage::addAll`
*/
public function addAll(ObjectStorage $storage): void
{
$this->initialize();
parent::addAll($storage);
}
/**
* @param TEntity $object The object to add.
* @param mixed $information The information to associate with the object.
*
* @see `ObjectStorage::attach`
*/
public function attach(object $object, mixed $information = null): void
{
$this->initialize();
parent::attach($object, $information);
}
/**
* @param TEntity $object The object to look for.
*
* @see `ObjectStorage::contains`
*/
public function contains(object $object): bool
{
$this->initialize();
return parent::contains($object);
}
/**
* Counts the elements in the storage array
*
* @throws Exception
* @return 0|positive-int The number of objects in the storage.
*/
public function count(): int
{
$columnMap = $this->dataMapper->getDataMap(get_class($this->parentObject))->getColumnMap($this->propertyName);
if (!$this->isInitialized && $columnMap->typeOfRelation === Relation::HAS_MANY) {
$numberOfElements = $this->dataMapper->countRelated($this->parentObject, $this->propertyName, $this->fieldValue);
} else {
$this->initialize();
$numberOfElements = count($this->storage);
}
return $numberOfElements;
}
/**
* @return TEntity|null The object at the current iterator position.
* @see `ObjectStorage::current`
*/
public function current(): ?object
{
$this->initialize();
return parent::current();
}
/**
* @param TEntity $object The object to remove.
*
* @see `ObjectStorage::detach`
*/
public function detach(object $object): void
{
$this->initialize();
parent::detach($object);
}
/**
* @return string The index corresponding to the position of the iterator.
*
* @see `ObjectStorage::key`
*/
public function key(): string
{
$this->initialize();
return parent::key();
}
/**
* @see `ObjectStorage::next`
*/
public function next(): void
{
$this->initialize();
parent::next();
}
/**
* @param TEntity|int|string $value The object to look for, or the key in the storage.
*
* @see `ObjectStorage::offsetExists`
*/
public function offsetExists(mixed $value): bool
{
$this->initialize();
return parent::offsetExists($value);
}
/**
* @param TEntity|int|string $value The object to look for, or its key in the storage.
*
* @see `ObjectStorage::offsetGet`
*/
public function offsetGet(mixed $value): mixed
{
$this->initialize();
return parent::offsetGet($value);
}
/**
* @param TEntity|string|null $object The object to add.
* @param mixed $information The information to associate with the object.
*
* @see `ObjectStorage::offsetSet`
*/
public function offsetSet(mixed $object, mixed $information): void
{
$this->initialize();
parent::offsetSet($object, $information);
}
/**
* @param TEntity|int|string $value The object to remove, or its key in the storage.
*
* @see `ObjectStorage::offsetUnset`
*/
public function offsetUnset(mixed $value): void
{
$this->initialize();
parent::offsetUnset($value);
}
/**
* @param ObjectStorage $storage The storage containing the elements to remove.
*
* @see `ObjectStorage::removeAll`
*/
public function removeAll(ObjectStorage $storage): void
{
$this->initialize();
parent::removeAll($storage);
}
/**
* @see `ObjectStorage::rewind`
*/
public function rewind(): void
{
$this->initialize();
parent::rewind();
}
/**
* @see `ObjectStorage::valid`
*/
public function valid(): bool
{
$this->initialize();
return parent::valid();
}
/**
* @see `ObjectStorage::toArray`
*/
public function toArray(): array
{
$this->initialize();
return parent::toArray();
}
/**
* @param mixed $object
*/
public function getPosition($object): ?int
{
$this->initialize();
return parent::getPosition($object);
}
public function __serialize(): array
{
$properties = get_object_vars($this);
unset(
$properties['warning'],
$properties['dataMapper']
);
return $properties;
}
public function __unserialize(array $data): void
{
foreach ($data as $propertyName => $propertyValue) {
if (property_exists($this, $propertyName)) {
$this->{$propertyName} = $propertyValue;
}
}
$this->dataMapper = GeneralUtility::getContainer()->get(DataMapper::class);
}
}
@@ -0,0 +1,21 @@
<?php
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Extbase\Persistence\Generic;
/**
* An interface for the lazy loading strategies.
*/
interface LoadingStrategyInterface {}
@@ -0,0 +1,136 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Extbase\Persistence\Generic\Mapper;
use TYPO3\CMS\Core\DataHandling\TableColumnType;
use TYPO3\CMS\Extbase\Persistence\Generic\Mapper\ColumnMap\Relation;
/**
* A column map to map a column configured in $TCA on a property of a domain object.
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
final readonly class ColumnMap
{
/**
* @param string $columnName Name of the DB column
* @param TableColumnType $type TCA column type like "input", "inline"
* @param string|null $dateTimeFormat DataTime format (TCA "format" property). Allowed: "date", "datetime", "time", "timesec", "datetimesec"
* @param string|null $dateTimeStorageFormat Alternative DataTime format instead of using unix timestamps (TCA "dbType" property). Allowed: "date", "datetime", "time"
* @param Relation|null $typeOfRelation Extbase "Relation" enum if any
* @param string|null $childTableName TCA "foreign_table" if any, @todo: Does not consider group "allowed" for multi table relations
* @param string|null $relationTableName TCA "MM" if any
* @param array $relationTableMatchFields TCA "MM_match_fields" if any in MM, TCA "foreign_match_fields" if any
* @param string|null $parentKeyFieldName TCA "uid_local" or "uid_foreign" with TCA "MM" depending on "opposite",
* TCA "foreign_field" with TCA "foreign_table" relations
* @param string|null $parentTableFieldName TCA "foreign_table_field" with TCA "foreign_table" relations
* @param string|null $childKeyFieldName TCA "uid_local" or "uid_foreign" with TCA "MM" depending on "opposite"
* @param string|null $childSortByFieldName Name of the field results from child's table are sorted by:
* TCA "sorting" or "sorting_foreign" with TCA "MM" depending on TCA "opposite" situation,
* TCA "foreign_sortby" with TCA "foreign_table"
* @param string|null $childTableDefaultSortings name of the fields with direction results from child's table are sorted by default:
* TCA "foreign_default_sortby" with TCA "foreign_table"
*/
public function __construct(
public string $columnName,
public TableColumnType $type,
public ?string $dateTimeFormat = null,
public ?string $dateTimeStorageFormat = null,
public ?Relation $typeOfRelation = Relation::NONE,
public ?string $childTableName = null,
public ?string $relationTableName = null,
public array $relationTableMatchFields = [],
public ?string $parentKeyFieldName = null,
public ?string $parentTableFieldName = null,
public ?string $childKeyFieldName = null,
public ?string $childSortByFieldName = null,
public ?string $childTableDefaultSortings = null,
public bool $isNullable = false,
) {}
// Getters below could be removed but don't harm much and kept as b/w compat for now.
public function getTypeOfRelation(): Relation
{
return $this->typeOfRelation;
}
public function getColumnName(): string
{
return $this->columnName;
}
public function getChildTableName(): ?string
{
return $this->childTableName;
}
public function getChildTableDefaultSortings(): ?string
{
return $this->childTableDefaultSortings;
}
public function getChildSortByFieldName(): ?string
{
return $this->childSortByFieldName;
}
public function getRelationTableName(): ?string
{
return $this->relationTableName;
}
public function getRelationTableMatchFields(): array
{
return $this->relationTableMatchFields;
}
public function getParentKeyFieldName(): ?string
{
return $this->parentKeyFieldName;
}
public function getParentTableFieldName(): ?string
{
return $this->parentTableFieldName;
}
public function getChildKeyFieldName(): ?string
{
return $this->childKeyFieldName;
}
public function getDateTimeFormat(): ?string
{
return $this->dateTimeFormat;
}
public function getDateTimeStorageFormat(): ?string
{
return $this->dateTimeStorageFormat;
}
public function getType(): TableColumnType
{
return $this->type;
}
public function isNullable(): bool
{
return $this->isNullable;
}
}
@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Extbase\Persistence\Generic\Mapper\ColumnMap;
/**
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
enum Relation
{
case NONE;
case HAS_ONE;
case HAS_MANY;
case BELONGS_TO_MANY;
case HAS_AND_BELONGS_TO_MANY;
}
@@ -0,0 +1,210 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Extbase\Persistence\Generic\Mapper;
use TYPO3\CMS\Core\DataHandling\TableColumnType;
use TYPO3\CMS\Core\Schema\Field\CountryFieldType;
use TYPO3\CMS\Core\Schema\Field\DateTimeFieldType;
use TYPO3\CMS\Core\Schema\Field\FieldTypeInterface;
use TYPO3\CMS\Core\Schema\Field\FolderFieldType;
use TYPO3\CMS\Core\Schema\Field\RelationalFieldTypeInterface;
use TYPO3\CMS\Core\Schema\Field\StaticSelectFieldType;
use TYPO3\CMS\Core\Schema\RelationshipType;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Persistence\Generic\Mapper\ColumnMap\Relation;
use TYPO3\CMS\Extbase\Reflection\ClassSchema\Exception\NoSuchPropertyException;
use TYPO3\CMS\Extbase\Reflection\ReflectionService;
/**
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
readonly class ColumnMapFactory
{
public function __construct(
private ReflectionService $reflectionService,
) {}
public function create(FieldTypeInterface $field, string $propertyName, string $className): ColumnMap
{
$propertyType = null;
$propertyCollectionValueType = null;
try {
$property = $this->reflectionService->getClassSchema($className)->getProperty($propertyName);
$nonProxyPropertyTypes = $property->getFilteredTypes([$property, 'filterLazyLoadingProxyAndLazyObjectStorage']);
$primaryType = $nonProxyPropertyTypes[0] ?? null;
$propertyType = $primaryType?->getClassName() ?? $primaryType?->getBuiltinType() ?? null;
if ($primaryType?->isCollection() && $primaryType->getCollectionValueTypes() !== []) {
$primaryCollectionValueType = $primaryType->getCollectionValueTypes()[0];
$propertyCollectionValueType = $primaryCollectionValueType->getClassName() ?? $primaryCollectionValueType->getBuiltinType();
}
} catch (NoSuchPropertyException) {
// $type and $propertyCollectionValueType kept null
}
// @todo: The relation related handling below smells fishy at various places. Some TCA
// details are ignored, some are at least opinionated, some are wrong. The combination
// of fetching details from TCA *and* the model class makes everything quite complex.
// This should be consolidated.
// Also, the mixture of extbase internal "Relation", core TableColumnType, plus
// core TcaSchema details is complex and should be simplified to what we really need.
// Last, ColumnMap is not fully used throughout extbase, various details tend to
// still access TCA details directly.
// In the end, we may be better off removing extbase "Relation" altogether and
// add TcaSchema $columnConfiguration to ColumnMap to sort out TCA details at
// the few places where needed directly? This would be more in-line with DataHandler
// as well and raises fewer state questions in consumers, which reduces complexity.
$columnConfiguration = $field->getConfiguration();
$columnName = $field->getName();
$tableColumnType = TableColumnType::tryFrom($field->getType());
$childTableName = null;
if ($field->isType(TableColumnType::GROUP)) {
// TCA type="group" has no TCA property "foreign_table" and can only deal with single-table
// relations in extbase (no support for union types). That means `allowed` should only
// contain ONE table entry, as Extbase can only evaluate the first one, if multiple
// are defined.
$allowed = GeneralUtility::trimExplode(',', $columnConfiguration['allowed'] ?? '', true);
$childTableName = $allowed[0] ?? $columnConfiguration['foreign_table'] ?? null;
} elseif ($field instanceof RelationalFieldTypeInterface) {
$childTableName = $columnConfiguration['foreign_table'] ?? null;
}
if ($field instanceof DateTimeFieldType) {
// TCA type="datetime" considers "dbtype" and is done.
return new ColumnMap(
columnName: $columnName,
type: $tableColumnType,
dateTimeFormat: $field->getFormat(),
dateTimeStorageFormat: $field->getPersistenceType(),
isNullable: $field->isNullable(),
);
}
if (($field instanceof RelationalFieldTypeInterface) && $field->getRelationshipType() === RelationshipType::ManyToMany) {
if (!isset($columnConfiguration['MM'])) {
throw new \LogicException(
'TCA schema of column ' . $columnName . ' is "ManytoMany", but TCA config has no MM property set',
1733560101
);
}
return new ColumnMap(
columnName: $columnName,
type: $tableColumnType,
typeOfRelation: Relation::HAS_AND_BELONGS_TO_MANY,
childTableName: $childTableName,
relationTableName: $columnConfiguration['MM'],
relationTableMatchFields: is_array($columnConfiguration['MM_match_fields'] ?? false) ? $columnConfiguration['MM_match_fields'] : [],
parentKeyFieldName: !empty($columnConfiguration['MM_opposite_field']) ? 'uid_foreign' : 'uid_local',
childKeyFieldName: !empty($columnConfiguration['MM_opposite_field']) ? 'uid_local' : 'uid_foreign',
childSortByFieldName: !empty($columnConfiguration['MM_opposite_field']) ? 'sorting_foreign' : 'sorting',
isNullable: $field->isNullable(),
);
}
if ($propertyCollectionValueType !== null) {
// The field might not be a RelationFieldType, e.g. for TCA type "passthrough" or type "select"
// without items. However, the model defines a relation and therefore overrules the TCA schema lookup.
// This also overrules any "maxitems" or "renderType" configuration!
return new ColumnMap(
columnName: $columnName,
type: $tableColumnType,
typeOfRelation: Relation::HAS_MANY,
childTableName: $childTableName,
relationTableMatchFields: is_array($columnConfiguration['foreign_match_fields'] ?? false) ? $columnConfiguration['foreign_match_fields'] : [],
parentKeyFieldName: $columnConfiguration['foreign_field'] ?? null,
parentTableFieldName: $columnConfiguration['foreign_table_field'] ?? null,
childSortByFieldName: $columnConfiguration['foreign_sortby'] ?? null,
childTableDefaultSortings: $columnConfiguration['foreign_default_sortby'] ?? null,
isNullable: $field->isNullable(),
);
}
if ($propertyType !== null && strpbrk($propertyType, '_\\') !== false) {
// @todo: Check this. Seems to be a check for Tx_Foo_Bar style class names?!
return new ColumnMap(
columnName: $columnName,
type: $tableColumnType,
typeOfRelation: Relation::HAS_ONE,
childTableName: $childTableName,
relationTableMatchFields: is_array($columnConfiguration['foreign_match_fields'] ?? false) ? $columnConfiguration['foreign_match_fields'] : [],
parentKeyFieldName: $columnConfiguration['foreign_field'] ?? null,
parentTableFieldName: $columnConfiguration['foreign_table_field'] ?? null,
childSortByFieldName: $columnConfiguration['foreign_sortby'] ?? null,
isNullable: $field->isNullable(),
);
}
if ($field instanceof FolderFieldType) {
// Folder is a special case which always has a relation to one or many "folders".
// In case "maxitems" is set to > 1 and relationship is not explicitly set to "*toOne"
// it's HAS_MANY, in all other cases it's HAS_ONE. It can never belong to many.
// @todo: Get rid of the "maxitems" and rely purely on the evaluated relationship type
// @todo: TCA type="folder" has no TCA property "relationship"!
$relation = Relation::HAS_ONE;
if (!in_array((string)($columnConfiguration['relationship'] ?? ''), ['oneToOne', 'manyToOne'], true)
&& (!isset($columnConfiguration['maxitems']) || $columnConfiguration['maxitems'] > 1)
) {
$relation = Relation::HAS_MANY;
}
return new ColumnMap(
columnName: $columnName,
type: $tableColumnType,
typeOfRelation: $relation,
isNullable: $field->isNullable(),
);
}
if ($field instanceof CountryFieldType) {
$relation = Relation::HAS_ONE;
return new ColumnMap(
columnName: $columnName,
type: $tableColumnType,
typeOfRelation: $relation,
);
}
if (
(
$field instanceof RelationalFieldTypeInterface
&& $field->getRelationshipType()->hasMany()
&& (
!$field->isType(TableColumnType::GROUP, TableColumnType::SELECT)
|| ($field->isType(TableColumnType::GROUP) && (!isset($columnConfiguration['maxitems']) || $columnConfiguration['maxitems'] > 1))
|| ($field->isType(TableColumnType::SELECT) && (($columnConfiguration['renderType'] ?? '') !== 'selectSingle' || (int)($columnConfiguration['maxitems'] ?? 0) > 1))
)
)
|| (
$field instanceof StaticSelectFieldType
&& (int)($columnConfiguration['maxitems'] ?? 0) > 1 // @todo: Get rid of the "maxitems" and rely purely on the relationship type
)
) {
return new ColumnMap(
columnName: $columnName,
type: $tableColumnType,
typeOfRelation: Relation::HAS_MANY,
isNullable: $field->isNullable(),
);
}
return new ColumnMap(
columnName: $columnName,
type: $tableColumnType,
isNullable: $field->isNullable(),
);
}
}
@@ -0,0 +1,157 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Extbase\Persistence\Generic\Mapper;
/**
* A data map to map a single table configured in $TCA on a domain object.
*
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
final readonly class DataMap
{
/**
* @param string $className Name of the class this column map represents
* @param string $tableName Name of the DB table this column map is located on
* @param string|null $recordType The record type stored in the "type" field as configured in $TCA
* @param array $subclasses List of subclasses of the current class
* @param array<non-empty-string, ColumnMap> $columnMaps List of TCA columns with their ColumnMap representation
* @param string|null $languageIdColumnName Name of a column holding the language id of the record, often "sys_language_uid"
* @param string|null $translationOriginColumnName Name of a column holding the uid of the record this record is a translation of, often "l10n_parent" or "l18n_parent"
* @param string|null $translationOriginDiffSourceName Name of a column holding the diff data for the record this record is a translation of, often "l10n_diffsource" or "l10n_diffsource"
* @param string|null $modificationDateColumnName Name of a column holding the timestamp the record was last modified, often "tstamp"
* @param string|null $creationDateColumnName Name of a column holding the creation date timestamp, often "crdate"
* @param string|null $deletedFlagColumnName Name of a column indicating the soft deleted state of the row, often "deleted"
* @param string|null $disabledFlagColumnName Name of a column indicating the "hidden in frontend" state of the row, often "hidden" or "disabled"
* @param string|null $startTimeColumnName Name of a column holding the timestamp the record should not be displayed before, often "starttime"
* @param string|null $endTimeColumnName Name of a column holding the timestamp the record should not be displayed afterward, often "endtime"
* @param string|null $frontendUserGroupColumnName Name of a column holding the uid of the front-end user group which is allowed to edit this record
* @param string|null $recordTypeColumnName Name of a column holding the record type, example: "CType" in table "tt_content"
* @param bool $rootLevel Bool cast of TCA[$tableName]['ctrl']['rootLevel']
*/
public function __construct(
public string $className,
public string $tableName,
public ?string $recordType = null,
public array $subclasses = [],
public array $columnMaps = [],
public ?string $languageIdColumnName = null,
public ?string $translationOriginColumnName = null,
public ?string $translationOriginDiffSourceName = null,
public ?string $modificationDateColumnName = null,
public ?string $creationDateColumnName = null,
public ?string $deletedFlagColumnName = null,
public ?string $disabledFlagColumnName = null,
public ?string $startTimeColumnName = null,
public ?string $endTimeColumnName = null,
public ?string $frontendUserGroupColumnName = null,
public ?string $recordTypeColumnName = null,
public bool $rootLevel = false,
) {}
public function getColumnMap(string $propertyName): ?ColumnMap
{
return $this->columnMaps[$propertyName] ?? null;
}
public function isPersistableProperty(string $propertyName): bool
{
return isset($this->columnMaps[$propertyName]);
}
// Getters below could be removed but don't harm much and kept as b/w compat for now.
public function getClassName(): string
{
return $this->className;
}
public function getTableName(): string
{
return $this->tableName;
}
public function getRecordType(): ?string
{
return $this->recordType;
}
public function getSubclasses(): array
{
return $this->subclasses;
}
public function getLanguageIdColumnName(): ?string
{
return $this->languageIdColumnName;
}
public function getTranslationOriginColumnName(): ?string
{
return $this->translationOriginColumnName;
}
public function getTranslationOriginDiffSourceName(): ?string
{
return $this->translationOriginDiffSourceName;
}
public function getModificationDateColumnName(): ?string
{
return $this->modificationDateColumnName;
}
public function getCreationDateColumnName(): ?string
{
return $this->creationDateColumnName;
}
public function getDeletedFlagColumnName(): ?string
{
return $this->deletedFlagColumnName;
}
public function getDisabledFlagColumnName(): ?string
{
return $this->disabledFlagColumnName;
}
public function getStartTimeColumnName(): ?string
{
return $this->startTimeColumnName;
}
public function getEndTimeColumnName(): ?string
{
return $this->endTimeColumnName;
}
public function getFrontEndUserGroupColumnName(): ?string
{
return $this->frontendUserGroupColumnName;
}
public function getRecordTypeColumnName(): ?string
{
return $this->recordTypeColumnName;
}
public function getRootLevel(): bool
{
return $this->rootLevel;
}
}
@@ -0,0 +1,178 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Extbase\Persistence\Generic\Mapper;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
use TYPO3\CMS\Core\Schema\Capability\TcaSchemaCapability;
use TYPO3\CMS\Core\Schema\TcaSchemaFactory;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Persistence\ClassesConfiguration;
use TYPO3\CMS\Extbase\Persistence\Generic\Exception\InvalidClassException;
/**
* A factory for a data map to map a single table configured in $TCA on a domain object.
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
readonly class DataMapFactory
{
public function __construct(
private ClassesConfiguration $classesConfiguration,
private ColumnMapFactory $columnMapFactory,
private TcaSchemaFactory $tcaSchemaFactory,
#[Autowire(expression: 'service("package-dependent-cache-identifier").toString()')]
private string $baseCacheIdentifier,
#[Autowire(service: 'cache.runtime')]
private FrontendInterface $firstLevelCache,
#[Autowire(service: 'cache.extbase')]
private FrontendInterface $secondLevelCache,
) {}
/**
* Builds a data map by adding column maps for all the configured columns in the $TCA.
* It also resolves the type of values the column is holding and the typo of relation the column
* represents.
*
* @param string $className The class name you want to fetch the Data Map for
*/
public function buildDataMap(string $className): DataMap
{
$className = ltrim($className, '\\');
$cacheIdentifierClassName = str_replace('\\', '', $className) . '_';
$cacheIdentifier = 'DataMap_' . $cacheIdentifierClassName . $this->baseCacheIdentifier;
$dataMap = $this->firstLevelCache->get($cacheIdentifier);
if ($dataMap instanceof DataMap) {
return $dataMap;
}
$dataMap = $this->secondLevelCache->get($cacheIdentifier);
if ($dataMap instanceof DataMap) {
$this->firstLevelCache->set($cacheIdentifier, $dataMap);
return $dataMap;
}
$dataMap = $this->buildDataMapInternal($className);
$this->firstLevelCache->set($cacheIdentifier, $dataMap);
$this->secondLevelCache->set($cacheIdentifier, $dataMap);
return $dataMap;
}
/**
* Builds a data map by adding column maps for all the configured columns in the $TCA.
* It also resolves the type of values the column is holding and the typo of relation the column
* represents.
*
* @param string $className The class name you want to fetch the Data Map for
* @throws InvalidClassException
*/
protected function buildDataMapInternal(string $className): DataMap
{
if (!class_exists($className)) {
throw new InvalidClassException(
'Could not find class definition for name "' . $className . '". This could be caused by a mis-spelling of the class name in the class definition.',
1476045117
);
}
$recordType = null;
$subclasses = [];
$tableName = $this->resolveTableName($className);
$fieldNameToPropertyNameMapping = [];
if ($this->classesConfiguration->hasClass($className)) {
$classSettings = $this->classesConfiguration->getConfigurationFor($className);
$subclasses = $this->classesConfiguration->getSubClasses($className);
if (isset($classSettings['recordType']) && $classSettings['recordType'] !== '') {
$recordType = (string)$classSettings['recordType'];
}
if (isset($classSettings['tableName']) && $classSettings['tableName'] !== '') {
$tableName = $classSettings['tableName'];
}
foreach ($classSettings['properties'] ?? [] as $propertyName => $propertyDefinition) {
$fieldNameToPropertyNameMapping[$propertyDefinition['fieldName']] = $propertyName;
}
}
$schema = null;
$languageCapability = null;
$columnMaps = [];
if ($this->tcaSchemaFactory->has($tableName)) {
$schema = $this->tcaSchemaFactory->get($tableName);
if ($schema->hasCapability(TcaSchemaCapability::Language)) {
$languageCapability = $schema->getCapability(TcaSchemaCapability::Language);
}
foreach ($schema->getFields() as $columnName => $columnDefinition) {
$propertyName = $fieldNameToPropertyNameMapping[$columnName] ?? GeneralUtility::underscoredToLowerCamelCase($columnName);
$columnMaps[$propertyName] = $this->columnMapFactory->create($columnDefinition, $propertyName, $className);
}
}
return new DataMap(
className: $className,
tableName: $tableName,
recordType: $recordType,
subclasses: $subclasses,
columnMaps: $columnMaps,
languageIdColumnName: $languageCapability?->getLanguageField()->getName(),
translationOriginColumnName: $languageCapability?->getTranslationOriginPointerField()->getName(),
translationOriginDiffSourceName: $languageCapability?->hasDiffSourceField()
? $languageCapability->getDiffSourceField()->getName()
: null,
modificationDateColumnName: $schema?->hasCapability(TcaSchemaCapability::UpdatedAt)
? (string)$schema->getCapability(TcaSchemaCapability::UpdatedAt)
: null,
creationDateColumnName: $schema?->hasCapability(TcaSchemaCapability::CreatedAt)
? (string)$schema->getCapability(TcaSchemaCapability::CreatedAt)
: null,
deletedFlagColumnName: $schema?->hasCapability(TcaSchemaCapability::SoftDelete)
? (string)$schema->getCapability(TcaSchemaCapability::SoftDelete)
: null,
disabledFlagColumnName: $schema?->hasCapability(TcaSchemaCapability::RestrictionDisabledField)
? (string)$schema->getCapability(TcaSchemaCapability::RestrictionDisabledField)
: null,
startTimeColumnName: $schema?->hasCapability(TcaSchemaCapability::RestrictionStartTime)
? (string)$schema->getCapability(TcaSchemaCapability::RestrictionStartTime)
: null,
endTimeColumnName: $schema?->hasCapability(TcaSchemaCapability::RestrictionEndTime)
? (string)$schema->getCapability(TcaSchemaCapability::RestrictionEndTime)
: null,
frontendUserGroupColumnName: $schema?->hasCapability(TcaSchemaCapability::RestrictionUserGroup)
? (string)$schema->getCapability(TcaSchemaCapability::RestrictionUserGroup)
: null,
// @todo Check how to resolve foreign table types properly - if possible at all in this scenario
recordTypeColumnName: $schema?->supportsSubSchema() && !$schema->getSubSchemaTypeInformation()->isPointerToForeignFieldInForeignSchema()
? $schema->getSubSchemaTypeInformation()->getFieldName()
: null,
// @todo We should remove DataMap in order to use TcaSchema directly
rootLevel: (bool)($schema?->getCapability(TcaSchemaCapability::RestrictionRootLevel)->getRootLevelType()),
);
}
/**
* Resolve the table name for the given class name
*/
protected function resolveTableName(string $className): string
{
$className = ltrim($className, '\\');
$classNameParts = explode('\\', $className);
// Skip vendor and product name for core classes
if (str_starts_with($className, 'TYPO3\\CMS\\')) {
$classPartsToSkip = 2;
} else {
$classPartsToSkip = 1;
}
return 'tx_' . strtolower(implode('_', array_slice($classNameParts, $classPartsToSkip)));
}
}
@@ -0,0 +1,977 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Extbase\Persistence\Generic\Mapper;
use Doctrine\Instantiator\InstantiatorInterface;
use Psr\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
use TYPO3\CMS\Core\Context\Context;
use TYPO3\CMS\Core\Context\LanguageAspect;
use TYPO3\CMS\Core\Country\Country;
use TYPO3\CMS\Core\Country\CountryProvider;
use TYPO3\CMS\Core\Database\Query\QueryHelper;
use TYPO3\CMS\Core\Database\RelationHandler;
use TYPO3\CMS\Core\DataHandling\TableColumnType;
use TYPO3\CMS\Core\Domain\DateTimeFactory;
use TYPO3\CMS\Core\Schema\TcaSchemaFactory;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\DomainObject\AbstractDomainObject;
use TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface;
use TYPO3\CMS\Extbase\Event\Persistence\AfterObjectThawedEvent;
use TYPO3\CMS\Extbase\Persistence;
use TYPO3\CMS\Extbase\Persistence\Generic\Exception;
use TYPO3\CMS\Extbase\Persistence\Generic\Exception\InvalidClassException;
use TYPO3\CMS\Extbase\Persistence\Generic\Exception\UnexpectedTypeException;
use TYPO3\CMS\Extbase\Persistence\Generic\LazyLoadingProxy;
use TYPO3\CMS\Extbase\Persistence\Generic\LazyObjectStorage;
use TYPO3\CMS\Extbase\Persistence\Generic\LoadingStrategyInterface;
use TYPO3\CMS\Extbase\Persistence\Generic\Mapper\ColumnMap\Relation;
use TYPO3\CMS\Extbase\Persistence\Generic\Mapper\Exception\NonExistentPropertyException;
use TYPO3\CMS\Extbase\Persistence\Generic\Mapper\Exception\UnknownPropertyTypeException;
use TYPO3\CMS\Extbase\Persistence\Generic\Qom\JoinInterface;
use TYPO3\CMS\Extbase\Persistence\Generic\Qom\QueryObjectModelFactory;
use TYPO3\CMS\Extbase\Persistence\Generic\Qom\SourceInterface;
use TYPO3\CMS\Extbase\Persistence\Generic\Query;
use TYPO3\CMS\Extbase\Persistence\Generic\QueryFactoryInterface;
use TYPO3\CMS\Extbase\Persistence\Generic\Session;
use TYPO3\CMS\Extbase\Persistence\ObjectStorage;
use TYPO3\CMS\Extbase\Persistence\QueryInterface;
use TYPO3\CMS\Extbase\Persistence\QueryResultInterface;
use TYPO3\CMS\Extbase\Reflection\ClassSchema\Exception\NoPropertyTypesException;
use TYPO3\CMS\Extbase\Reflection\ClassSchema\Exception\NoSuchPropertyException;
use TYPO3\CMS\Extbase\Reflection\ClassSchema\Property;
use TYPO3\CMS\Extbase\Reflection\ReflectionService;
use TYPO3\CMS\Extbase\Utility\TypeHandlingUtility;
/**
* A mapper to map database tables configured in $TCA on domain objects.
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
#[Autoconfigure(public: true, shared: false)]
class DataMapper
{
/**
* @var QueryInterface|null
*/
protected $query;
public function __construct(
private readonly ReflectionService $reflectionService,
private readonly QueryObjectModelFactory $qomFactory,
private readonly Session $persistenceSession,
private readonly DataMapFactory $dataMapFactory,
private readonly QueryFactoryInterface $queryFactory,
private readonly EventDispatcherInterface $eventDispatcher,
private readonly InstantiatorInterface $instantiator,
private readonly TcaSchemaFactory $tcaSchemaFactory,
private readonly CountryProvider $countryProvider,
) {}
public function setQuery(QueryInterface $query): void
{
$this->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<T> $className
* @phpstan-return list<T>
*/
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<T> $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<T> $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<non-empty-string, QueryInterface::ORDER_*>|null
* @return array<string, string>|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;
}
}
@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Extbase\Persistence\Generic\Mapper;
class Exception extends \TYPO3\CMS\Extbase\Persistence\Exception {}
@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Extbase\Persistence\Generic\Mapper\Exception;
use TYPO3\CMS\Extbase\Persistence\Generic\Mapper\Exception;
class NonExistentPropertyException extends Exception {}
@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Extbase\Persistence\Generic\Mapper\Exception;
use TYPO3\CMS\Extbase\Persistence\Generic\Mapper\Exception;
class UnknownPropertyTypeException extends Exception {}
@@ -0,0 +1,258 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Extbase\Persistence\Generic;
use TYPO3\CMS\Core\SingletonInterface;
use TYPO3\CMS\Extbase\Persistence\Exception\UnknownObjectException;
use TYPO3\CMS\Extbase\Persistence\ObjectStorage;
use TYPO3\CMS\Extbase\Persistence\PersistenceManagerInterface;
use TYPO3\CMS\Extbase\Persistence\QueryInterface;
/**
* The Extbase Persistence Manager
*/
class PersistenceManager implements PersistenceManagerInterface, SingletonInterface
{
protected array $newObjects = [];
protected ObjectStorage $changedObjects;
protected ObjectStorage $addedObjects;
protected ObjectStorage $removedObjects;
protected QueryFactoryInterface $queryFactory;
protected BackendInterface $backend;
protected Session $persistenceSession;
/**
* Create new instance
*
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
public function __construct(
QueryFactoryInterface $queryFactory,
BackendInterface $backend,
Session $persistenceSession
) {
$this->queryFactory = $queryFactory;
$this->backend = $backend;
$this->persistenceSession = $persistenceSession;
$this->addedObjects = new ObjectStorage();
$this->removedObjects = new ObjectStorage();
$this->changedObjects = new ObjectStorage();
}
/**
* Registers a repository
*
* @param string $className The class name of the repository to be registered
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
public function registerRepositoryClassName(string $className): void {}
/**
* Returns the number of records matching the query.
*/
public function getObjectCountByQuery(QueryInterface $query): int
{
return $this->backend->getObjectCountByQuery($query);
}
/**
* Returns the object data matching the $query.
* @return list<array<string,mixed>>
*/
public function getObjectDataByQuery(QueryInterface $query): array
{
return $this->backend->getObjectDataByQuery($query);
}
/**
* Returns the (internal) identifier for the object, if it is known to the
* backend. Otherwise NULL is returned.
*
* Note: this returns an identifier even if the object has not been
* persisted in case of AOP-managed entities. Use isNewObject() if you need
* to distinguish those cases.
*
* @return string|null The identifier for the object if it is known, or NULL
*/
public function getIdentifierByObject(object $object): ?string
{
return $this->backend->getIdentifierByObject($object);
}
/**
* Returns the object with the (internal) identifier, if it is known to the
* backend. Otherwise NULL is returned.
*
* @param bool $useLazyLoading Set to TRUE if you want to use lazy loading for this object
* @return object|null The object for the identifier if it is known, or NULL
*/
public function getObjectByIdentifier(string|int $identifier, ?string $objectType = null, bool $useLazyLoading = false): ?object
{
if (isset($this->newObjects[$identifier])) {
return $this->newObjects[$identifier];
}
// Delegate to backend which handles language-aware session lookup
return $this->backend->getObjectByIdentifier((string)$identifier, $objectType);
}
/**
* Commits new objects and changes to objects in the current persistence
* session into the backend.
*/
public function persistAll(): void
{
// hand in only aggregate roots, leaving handling of subobjects to
// the underlying storage layer
// reconstituted entities must be fetched from the session and checked
// for changes by the underlying backend as well!
$this->backend->setAggregateRootObjects($this->addedObjects);
$this->backend->setChangedEntities($this->changedObjects);
$this->backend->setDeletedEntities($this->removedObjects);
$this->backend->commit();
$this->addedObjects = new ObjectStorage();
$this->removedObjects = new ObjectStorage();
$this->changedObjects = new ObjectStorage();
}
/**
* Return a query object for the given type.
*
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*
* @template T of object
* @param class-string<T> $type
* @return QueryInterface<T>
*/
public function createQueryForType(string $type): QueryInterface
{
return $this->queryFactory->create($type);
}
/**
* Adds an object to the persistence.
*
* @param object $object The object to add
*/
public function add(object $object): void
{
$this->addedObjects->attach($object);
$this->removedObjects->detach($object);
}
/**
* Removes an object to the persistence.
*
* @param object $object The object to remove
*/
public function remove(object $object): void
{
if ($this->addedObjects->contains($object)) {
$this->addedObjects->detach($object);
} else {
$this->removedObjects->attach($object);
}
}
/**
* Update an object in the persistence.
*
* @param object $object The modified object
* @throws \TYPO3\CMS\Extbase\Persistence\Exception\UnknownObjectException
*/
public function update(object $object): void
{
if ($this->isNewObject($object)) {
throw new UnknownObjectException('The object of type "' . get_class($object) . '" given to update must be persisted already, but is new.', 1249479819);
}
$this->changedObjects->attach($object);
}
/**
* Initializes the persistence manager, called by Extbase.
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
public function initializeObject(): void
{
$this->backend->setPersistenceManager($this);
}
/**
* Clears the in-memory state of the persistence.
*
* Managed instances become detached, any fetches will
* return data directly from the persistence "backend".
*
* @throws \TYPO3\CMS\Extbase\Persistence\Generic\Exception\NotImplementedException
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
public function clearState(): void
{
$this->newObjects = [];
$this->addedObjects = new ObjectStorage();
$this->removedObjects = new ObjectStorage();
$this->changedObjects = new ObjectStorage();
$this->persistenceSession->destroy();
}
/**
* Checks if the given object has ever been persisted.
*
* @param object $object The object to check
* @return bool TRUE if the object is new, FALSE if the object exists in the persistence session
*/
public function isNewObject(object $object): bool
{
return $this->persistenceSession->hasObject($object) === false;
}
/**
* Registers an object which has been created or cloned during this request.
*
* A "new" object does not necessarily
* have to be known by any repository or be persisted in the end.
*
* Objects registered with this method must be known to the getObjectByIdentifier()
* method.
*
* @param object $object The new object to register
*
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
public function registerNewObject(object $object): void
{
$identifier = $this->getIdentifierByObject($object);
$this->newObjects[$identifier] = $object;
}
/**
* Tear down the persistence
*
* This method is called in functional tests to reset the storage between tests.
* The implementation is optional and depends on the underlying persistence backend.
*
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
public function tearDown(): void
{
if (method_exists($this->backend, 'tearDown')) {
$this->backend->tearDown();
}
}
}
@@ -0,0 +1,361 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Extbase\Persistence\Generic;
/**
* The property types supported by the JCR standard.
*
* The STRING property type is used to store strings.
* BINARY properties are used to store binary data.
* The LONG property type is used to store integers.
* The DECIMAL property type is used to store precise decimal numbers.
* The DOUBLE property type is used to store floating point numbers.
* The DATE property type is used to store time and date information. See 4.2.6.1 Date in the specification.
* The BOOLEAN property type is used to store boolean values.
* A NAME is a pairing of a namespace and a local name. When read, the namespace is mapped to the current prefix. See 4.2.6.2 Name in the specification.
* A PATH property is an ordered list of path elements. A path element is a NAME with an optional index. When read, the NAMEs within the path are mapped to their current prefix. A path may be absolute or relative. See 4.2.6.3 Path in the specification.
* A REFERENCE property stores the identifier of a referenceable node (one having type mix:referenceable), which must exist within the same workspace or session as the REFERENCE property. A REFERENCE property enforces this referential integrity by preventing (in level 2 implementations) the removal of its target node. See 4.2.6.4 Reference in the specification.
* A WEAKREFERENCE property stores the identifier of a referenceable node (one having type mix:referenceable). A WEAKREFERENCE property does not enforce referential integrity. See 4.2.6.5 Weak Reference in the specification.
* A URI property is identical to STRING property except that it only accepts values that conform to the syntax of a URI-reference as defined in RFC 3986. See also 4.2.6.6 URI in the specification.
* UNDEFINED can be used within a property definition (see 4.7.5 Property Definitions) to specify that the property in question may be of any type. However, it cannot be the actual type of any property instance. For example it will never be returned by Property.getType() and (in level 2 implementations) it cannot be assigned as the type when creating a new property.
*
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
class PropertyType
{
/**
* This constant can be used within a property definition to specify that
* the property in question may be of any type.
* However, it cannot be the actual type of any property instance. For
* example, it will never be returned by Property#getType and it cannot be
* assigned as the type when creating a new property.
*/
public const UNDEFINED = 0;
/**
* The STRING property type is used to store strings.
*/
public const STRING = 1;
/**
* BINARY properties are used to store binary data.
*/
public const BINARY = 2;
/**
* The LONG property type is used to store integers.
*/
public const LONG = 3;
/**
* The DOUBLE property type is used to store floating point numbers.
*/
public const DOUBLE = 4;
/**
* The DATE property type is used to store time and date information.
*/
public const DATE = 5;
/**
* The BOOLEAN property type is used to store boolean values.
*/
public const BOOLEAN = 6;
/**
* A NAME is a pairing of a namespace and a local name. When read, the
* namespace is mapped to the current prefix.
*
* WE DO NOT USE THIS IN EXTBASE!
*/
public const NAME = 7;
/**
* A PATH property is an ordered list of path elements. A path element is a
* NAME with an optional index. When read, the NAMEs within the path are
* mapped to their current prefix. A path may be absolute or relative.
*
* WE DO NOT USE THIS IN EXTBASE!
*/
public const PATH = 8;
/**
* A REFERENCE property stores the identifier of a referenceable node (one
* having type mix:referenceable), which must exist within the same
* workspace or session as the REFERENCE property. A REFERENCE property
* enforces this referential integrity by preventing the removal of its
* target node.
*/
public const REFERENCE = 9;
/**
* A WEAKREFERENCE property stores the identifier of a referenceable node
* (one having type mix:referenceable). A WEAKREFERENCE property does not
* enforce referential integrity.
*
* WE DO NOT USE THIS IN EXTBASE!
*/
public const WEAKREFERENCE = 10;
/**
* A URI property is identical to STRING property except that it only
* accepts values that conform to the syntax of a URI-reference as defined
* in RFC 3986.
*
* WE DO NOT USE THIS IN EXTBASE!
*/
public const URI = 11;
/**
* The DECIMAL property type is used to store precise decimal numbers.
*
* WE DO NOT USE THIS IN EXTBASE!
*/
public const DECIMAL = 12;
/**
* The INTEGER property type is used to store precise decimal numbers.
*
* WE DO NOT USE THIS IN EXTBASE!
*/
public const INTEGER = 13;
/**
* String constant for type name as used in serialization.
*/
public const TYPENAME_UNDEFINED = 'undefined';
/**
* String constant for type name as used in serialization.
*/
public const TYPENAME_STRING = 'String';
/**
* String constant for type name as used in serialization.
*/
public const TYPENAME_BINARY = 'Binary';
/**
* String constant for type name as used in serialization.
*/
public const TYPENAME_LONG = 'Long';
/**
* String constant for type name as used in serialization.
*/
public const TYPENAME_DOUBLE = 'Double';
/**
* String constant for type name as used in serialization.
*/
public const TYPENAME_DATE = 'Date';
/**
* String constant for type name as used in serialization.
*/
public const TYPENAME_BOOLEAN = 'Boolean';
/**
* String constant for type name as used in serialization.
*/
public const TYPENAME_NAME = 'Name';
/**
* String constant for type name as used in serialization.
*/
public const TYPENAME_PATH = 'Path';
/**
* String constant for type name as used in serialization.
*/
public const TYPENAME_REFERENCE = 'Reference';
/**
* String constant for type name as used in serialization.
*/
public const TYPENAME_WEAKREFERENCE = 'WeakReference';
/**
* String constant for type name as used in serialization.
*/
public const TYPENAME_URI = 'URI';
/**
* String constant for type name as used in serialization.
*/
public const TYPENAME_DECIMAL = 'Decimal';
/**
* String constant for type name as used in serialization.
*/
public const TYPENAME_INTEGER = 'Integer';
/**
* Make instantiation impossible...
*/
private function __construct() {}
/**
* Returns the name of the specified type, as used in serialization.
*
* @param int $type type the property type
* @return string name of the specified type
*/
public static function nameFromValue($type)
{
switch ((int)$type) {
case self::STRING:
$name = self::TYPENAME_STRING;
break;
case self::BINARY:
$name = self::TYPENAME_BINARY;
break;
case self::BOOLEAN:
$name = self::TYPENAME_BOOLEAN;
break;
case self::LONG:
$name = self::TYPENAME_LONG;
break;
case self::DOUBLE:
$name = self::TYPENAME_DOUBLE;
break;
case self::DECIMAL:
$name = self::TYPENAME_DECIMAL;
break;
case self::INTEGER:
$name = self::TYPENAME_INTEGER;
break;
case self::DATE:
$name = self::TYPENAME_DATE;
break;
case self::NAME:
$name = self::TYPENAME_NAME;
break;
case self::PATH:
$name = self::TYPENAME_PATH;
break;
case self::REFERENCE:
$name = self::TYPENAME_REFERENCE;
break;
case self::WEAKREFERENCE:
$name = self::TYPENAME_WEAKREFERENCE;
break;
case self::URI:
$name = self::TYPENAME_URI;
break;
default:
// case self::UNDEFINED:
$name = self::TYPENAME_UNDEFINED;
}
return $name;
}
/**
* Returns the numeric constant value of the type with the specified name.
*
* @param string $name The name of the property type
* @return int The numeric constant value
*/
public static function valueFromName($name)
{
switch ($name) {
case self::TYPENAME_STRING:
$value = self::STRING;
break;
case self::TYPENAME_BINARY:
$value = self::BINARY;
break;
case self::TYPENAME_LONG:
$value = self::LONG;
break;
case self::TYPENAME_DOUBLE:
$value = self::DOUBLE;
break;
case self::TYPENAME_DECIMAL:
$value = self::DECIMAL;
break;
case self::TYPENAME_INTEGER:
$value = self::INTEGER;
break;
case self::TYPENAME_DATE:
$value = self::DATE;
break;
case self::TYPENAME_BOOLEAN:
$value = self::BOOLEAN;
break;
case self::TYPENAME_NAME:
$value = self::NAME;
break;
case self::TYPENAME_PATH:
$value = self::PATH;
break;
case self::TYPENAME_REFERENCE:
$value = self::REFERENCE;
break;
case self::TYPENAME_WEAKREFERENCE:
$value = self::WEAKREFERENCE;
break;
case self::TYPENAME_URI:
$value = self::URI;
break;
default:
// case self::TYPENAME_UNDEFINED:
$value = self::UNDEFINED;
}
return $value;
}
/**
* Returns the numeric constant value of the type for the given PHP type
* name as returned by gettype().
*
* @param string $type
* @return int
*/
public static function valueFromType($type)
{
switch (strtolower($type)) {
case 'string':
$value = \TYPO3\CMS\Extbase\Persistence\Generic\PropertyType::STRING;
break;
case 'boolean':
$value = \TYPO3\CMS\Extbase\Persistence\Generic\PropertyType::BOOLEAN;
break;
case 'integer':
$value = \TYPO3\CMS\Extbase\Persistence\Generic\PropertyType::LONG;
break;
case 'float':
case 'double':
$value = \TYPO3\CMS\Extbase\Persistence\Generic\PropertyType::DOUBLE;
break;
case 'int':
$value = \TYPO3\CMS\Extbase\Persistence\Generic\PropertyType::INTEGER;
break;
case 'datetime':
$value = \TYPO3\CMS\Extbase\Persistence\Generic\PropertyType::DATE;
break;
default:
$value = \TYPO3\CMS\Extbase\Persistence\Generic\PropertyType::UNDEFINED;
}
return $value;
}
}
@@ -0,0 +1,30 @@
<?php
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Extbase\Persistence\Generic\Qom;
/**
* Performs a logical conjunction of two other constraints.
*
* To satisfy the And constraint, a node-tuple must satisfy both constraint1 and
* constraint2.
*
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
interface AndInterface extends ConstraintInterface
{
public function getConstraint1(): ConstraintInterface;
public function getConstraint2(): ConstraintInterface;
}
@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Extbase\Persistence\Generic\Qom;
/**
* Evaluates to the value of a bind variable.
*
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
final readonly class BindVariableValue implements BindVariableValueInterface
{
public function __construct(private string $variableName) {}
public function collectBoundVariableNames(array &$boundVariables): void
{
$boundVariables[$this->variableName] = null;
}
public function getBindVariableName(): string
{
return $this->variableName;
}
}
@@ -0,0 +1,26 @@
<?php
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Extbase\Persistence\Generic\Qom;
/**
* Evaluates to the value of a bind variable.
*
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
interface BindVariableValueInterface extends StaticOperandInterface
{
public function getBindVariableName(): string;
}
@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Extbase\Persistence\Generic\Qom;
/**
* Evaluates to the first non-NULL value among the operands.
*
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
final readonly class Coalesce implements CoalesceInterface
{
/**
* @param array<DynamicOperandInterface|string> $operands
*/
public function __construct(
private array $operands
) {}
public function getOperands(): array
{
return $this->operands;
}
public function getFunctionName(): string
{
return 'COALESCE';
}
}
@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Extbase\Persistence\Generic\Qom;
/**
* Evaluates to the first non-NULL value among the operands.
*
* Usage example:
* $query->orderBy($query->coalesce('nickname', 'firstName'), QueryInterface::ORDER_ASCENDING);
*
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
interface CoalesceInterface extends FunctionExpressionInterface {}
@@ -0,0 +1,110 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Extbase\Persistence\Generic\Qom;
use TYPO3\CMS\Extbase\Persistence\QueryInterface;
/**
* Filters node-tuples based on the outcome of a binary operation.
*
* For any comparison, operand2 always evaluates to a scalar value. In contrast,
* operand1 may evaluate to an array of values (for example, the value of a multi-valued
* property), in which case the comparison is separately performed for each element
* of the array, and the Comparison constraint is satisfied as a whole if the
* comparison against any element of the array is satisfied.
*
* If operand1 and operand2 evaluate to values of different property types, the
* value of operand2 is converted to the property type of the value of operand1.
* If the type conversion fails, the query is invalid.
*
* If operator is not supported for the property type of operand1, the query is invalid.
*
* If operand1 evaluates to null (for example, if the operand evaluates the value
* of a property which does not exist), the constraint is not satisfied.
*
* The OPERATOR_EQUAL_TO operator is satisfied only if the value of operand1
* equals the value of operand2.
*
* The OPERATOR_NOT_EQUAL_TO operator is satisfied unless the value of
* operand1 equals the value of operand2.
*
* The OPERATOR_LESS_THAN operator is satisfied only if the value of
* operand1 is ordered before the value of operand2.
*
* The OPERATOR_LESS_THAN_OR_EQUAL_TO operator is satisfied unless the value
* of operand1 is ordered after the value of operand2.
*
* The OPERATOR_GREATER_THAN operator is satisfied only if the value of
* operand1 is ordered after the value of operand2.
*
* The OPERATOR_GREATER_THAN_OR_EQUAL_TO operator is satisfied unless the
* value of operand1 is ordered before the value of operand2.
*
* The OPERATOR_LIKE operator is satisfied only if the value of operand1
* matches the pattern specified by the value of operand2, where in the pattern:
* the character "%" matches zero or more characters, and
* the character "_" (underscore) matches exactly one character, and
* the string "\x" matches the character "x", and
* all other characters match themselves.
*
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
final readonly class Comparison implements ComparisonInterface
{
/**
* @param QueryInterface::OPERATOR_* $operator
*/
public function __construct(
private PropertyValueInterface $operand1,
private int $operator,
private mixed $operand2
) {}
public function getOperand1(): PropertyValueInterface
{
return $this->operand1;
}
/**
* @return QueryInterface::OPERATOR_*
*/
public function getOperator(): int
{
$operator = $this->operator;
if ($this->getOperand2() === null) {
if ($operator === QueryInterface::OPERATOR_EQUAL_TO) {
$operator = QueryInterface::OPERATOR_EQUAL_TO_NULL;
} elseif ($operator === QueryInterface::OPERATOR_NOT_EQUAL_TO) {
$operator = QueryInterface::OPERATOR_NOT_EQUAL_TO_NULL;
}
}
return $operator;
}
public function getOperand2(): mixed
{
return $this->operand2;
}
public function collectBoundVariableNames(array &$boundVariables): array
{
return [];
}
}
@@ -0,0 +1,75 @@
<?php
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Extbase\Persistence\Generic\Qom;
use TYPO3\CMS\Extbase\Persistence\QueryInterface;
/**
* Filters node-tuples based on the outcome of a binary operation.
*
* For any comparison, operand2 always evaluates to a scalar value. In contrast,
* operand1 may evaluate to an array of values (for example, the value of a multi-valued
* property), in which case the comparison is separately performed for each element
* of the array, and the Comparison constraint is satisfied as a whole if the
* comparison against any element of the array is satisfied.
*
* If operand1 and operand2 evaluate to values of different property types, the
* value of operand2 is converted to the property type of the value of operand1.
* If the type conversion fails, the query is invalid.
*
* If operator is not supported for the property type of operand1, the query is invalid.
*
* If operand1 evaluates to null (for example, if the operand evaluates the value
* of a property which does not exist), the constraint is not satisfied.
*
* The JCR_OPERATOR_EQUAL_TO operator is satisfied only if the value of operand1
* equals the value of operand2.
*
* The JCR_OPERATOR_NOT_EQUAL_TO operator is satisfied unless the value of
* operand1 equals the value of operand2.
*
* The JCR_OPERATOR_LESS_THAN operator is satisfied only if the value of
* operand1 is ordered before the value of operand2.
*
* The JCR_OPERATOR_LESS_THAN_OR_EQUAL_TO operator is satisfied unless the value
* of operand1 is ordered after the value of operand2.
*
* The JCR_OPERATOR_GREATER_THAN operator is satisfied only if the value of
* operand1 is ordered after the value of operand2.
*
* The JCR_OPERATOR_GREATER_THAN_OR_EQUAL_TO operator is satisfied unless the
* value of operand1 is ordered before the value of operand2.
*
* The JCR_OPERATOR_LIKE operator is satisfied only if the value of operand1
* matches the pattern specified by the value of operand2, where in the pattern:
* the character "%" matches zero or more characters, and
* the character "_" (underscore) matches exactly one character, and
* the string "\x" matches the character "x", and
* all other characters match themselves.
*
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
interface ComparisonInterface extends ConstraintInterface
{
public function getOperand1(): PropertyValueInterface;
/**
* @return QueryInterface::OPERATOR_*
*/
public function getOperator(): int;
public function getOperand2(): mixed;
}
@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Extbase\Persistence\Generic\Qom;
/**
* Evaluates to the concatenated string value of the operands.
*
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
final readonly class Concat implements ConcatInterface
{
/**
* @param array<DynamicOperandInterface|string> $operands
*/
public function __construct(
private array $operands
) {}
public function getOperands(): array
{
return $this->operands;
}
public function getFunctionName(): string
{
return 'CONCAT';
}
}
@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Extbase\Persistence\Generic\Qom;
/**
* Evaluates to the concatenated string value of the operands.
*
* Usage example:
* $query->orderBy($query->concat('firstName', 'lastName'), QueryInterface::ORDER_ASCENDING);
*
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
interface ConcatInterface extends FunctionExpressionInterface {}
@@ -0,0 +1,29 @@
<?php
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Extbase\Persistence\Generic\Qom;
/**
* Filters the set of tuples formed by evaluating the query's sources and
* the joins between them.
*
* To be included in the query results, a tuple must satisfy the constraint.
*
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
interface ConstraintInterface
{
public function collectBoundVariableNames(array &$boundVariables);
}
@@ -0,0 +1,23 @@
<?php
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Extbase\Persistence\Generic\Qom;
/**
* An operand whose value can only be determined in evaluating the query.
*
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
interface DynamicOperandInterface extends OperandInterface {}
@@ -0,0 +1,69 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Extbase\Persistence\Generic\Qom;
/**
* Tests whether the value of a property in a first selector is equal to the value of a
* property in a second selector.
* A node-tuple satisfies the constraint only if: the selector1Name node has a property named property1Name, and
* the selector2Name node has a property named property2Name, and
* the value of property property1Name is equal to the value of property property2Name.
*
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
final readonly class EquiJoinCondition implements EquiJoinConditionInterface
{
public function __construct(
private string $selector1Name,
private string $property1Name,
private string $selector2Name,
private string $property2Name
) {
// @todo Test for selector1Name = selector2Name -> exception
}
public function getSelector1Name(): string
{
return $this->selector1Name;
}
public function getProperty1Name(): string
{
return $this->property1Name;
}
public function getSelector2Name(): string
{
return $this->selector2Name;
}
public function getProperty2Name(): string
{
return $this->property2Name;
}
public function getChildSelectorName(): string
{
return '';
}
public function getParentSelectorName(): string
{
return '';
}
}
@@ -0,0 +1,32 @@
<?php
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Extbase\Persistence\Generic\Qom;
/**
* Tests whether the childSelector node is a child of the parentSelector node. A
* node-tuple satisfies the constraint only if:
* childSelectorNode.getParent().isSame(parentSelectorNode)
* would return true, where childSelectorNode is the node for childSelector and
* parentSelectorNode is the node for parentSelector.
*
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
interface EquiJoinConditionInterface extends JoinConditionInterface
{
public function getChildSelectorName(): string;
public function getParentSelectorName(): string;
}
@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Extbase\Persistence\Generic\Qom;
/**
* Base interface for SQL function expressions with multiple operands.
*
* Function expressions can be used in ORDER BY clauses to sort results
* by computed values such as CONCAT, TRIM, COALESCE, etc.
*
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
interface FunctionExpressionInterface extends DynamicOperandInterface
{
/**
* Returns the operands of this function expression.
*
* @return array<DynamicOperandInterface|string> The operands
*/
public function getOperands(): array;
/**
* Returns the SQL function name.
*
* @return string The function name (e.g., 'CONCAT', 'TRIM', 'COALESCE')
*/
public function getFunctionName(): string;
}
+59
View File
@@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Extbase\Persistence\Generic\Qom;
/**
* Performs a join between two node-tuple sources.
*
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
final readonly class Join implements SourceInterface, JoinInterface
{
/**
* @param string $joinType One of Query::JCR_JOIN_TYPE_*
*/
public function __construct(
private SourceInterface&SelectorInterface $left,
private SourceInterface&SelectorInterface $right,
private string $joinType,
private JoinConditionInterface $joinCondition
) {}
public function getLeft(): SourceInterface&SelectorInterface
{
return $this->left;
}
public function getRight(): SourceInterface&SelectorInterface
{
return $this->right;
}
/**
* @return string one of QueryObjectModelConstants.JCR_JOIN_TYPE_*
*/
public function getJoinType(): string
{
return $this->joinType;
}
public function getJoinCondition(): JoinConditionInterface
{
return $this->joinCondition;
}
}
@@ -0,0 +1,26 @@
<?php
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Extbase\Persistence\Generic\Qom;
/**
* Filters the set of node-tuples formed from a join.
*
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
interface JoinConditionInterface
{
public function getSelector1Name(): string;
}
@@ -0,0 +1,35 @@
<?php
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Extbase\Persistence\Generic\Qom;
/**
* Performs a join between two node-tuple sources.
*
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
interface JoinInterface
{
public function getLeft(): SourceInterface&SelectorInterface;
public function getRight(): SourceInterface&SelectorInterface;
/**
* @return string one of QueryObjectModelConstants.JCR_JOIN_TYPE_*
*/
public function getJoinType(): string;
public function getJoinCondition(): JoinConditionInterface;
}
@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Extbase\Persistence\Generic\Qom;
/**
* Performs a logical conjunction of two other constraints.
*
* To satisfy the And constraint, a node-tuple must satisfy both constraint1 and
* constraint2.
*
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
final readonly class LogicalAnd implements AndInterface
{
public function __construct(
private ConstraintInterface $constraint1,
private ConstraintInterface $constraint2
) {}
public function collectBoundVariableNames(array &$boundVariables): void
{
$this->constraint1->collectBoundVariableNames($boundVariables);
$this->constraint2->collectBoundVariableNames($boundVariables);
}
public function getConstraint1(): ConstraintInterface
{
return $this->constraint1;
}
public function getConstraint2(): ConstraintInterface
{
return $this->constraint2;
}
}
@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Extbase\Persistence\Generic\Qom;
/**
* Performs a logical negation of another constraint.
*
* To satisfy the Not constraint, the node-tuple must not satisfy constraint.
*
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
final readonly class LogicalNot implements NotInterface
{
public function __construct(private ConstraintInterface $constraint) {}
public function collectBoundVariableNames(array &$boundVariables): void
{
$this->constraint->collectBoundVariableNames($boundVariables);
}
public function getConstraint(): ConstraintInterface
{
return $this->constraint;
}
}
@@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Extbase\Persistence\Generic\Qom;
/**
* Performs a logical disjunction of two other constraints.
*
* To satisfy the Or constraint, the node-tuple must either:
* satisfy constraint1 but not constraint2, or
* satisfy constraint2 but not constraint1, or
* satisfy both constraint1 and constraint2.
*
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
final readonly class LogicalOr implements OrInterface
{
public function __construct(
private ConstraintInterface $constraint1,
private ConstraintInterface $constraint2
) {}
public function collectBoundVariableNames(array &$boundVariables): void
{
$this->constraint1->collectBoundVariableNames($boundVariables);
$this->constraint2->collectBoundVariableNames($boundVariables);
}
public function getConstraint1(): ConstraintInterface
{
return $this->constraint1;
}
public function getConstraint2(): ConstraintInterface
{
return $this->constraint2;
}
}
@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Extbase\Persistence\Generic\Qom;
/**
* Evaluates to the lower-case string value (or values, if multi-valued) of
* operand.
*
* If operand does not evaluate to a string value, its value is first converted
* to a string.
*
* If operand evaluates to null, the LowerCase operand also evaluates to null.
*
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
final readonly class LowerCase implements LowerCaseInterface
{
public function __construct(private PropertyValueInterface $operand) {}
public function getOperand(): PropertyValueInterface
{
return $this->operand;
}
public function getSelectorName(): string
{
return $this->operand->getSelectorName();
}
public function getPropertyName(): string
{
return 'LOWER' . $this->operand->getPropertyName();
}
}
@@ -0,0 +1,32 @@
<?php
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Extbase\Persistence\Generic\Qom;
/**
* Evaluates to the lower-case string value (or values, if multi-valued) of
* operand.
*
* If operand does not evaluate to a string value, its value is first converted
* to a string.
*
* If operand evaluates to null, the LowerCase operand also evaluates to null.
*
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
interface LowerCaseInterface extends PropertyValueInterface
{
public function getOperand(): PropertyValueInterface;
}
@@ -0,0 +1,28 @@
<?php
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Extbase\Persistence\Generic\Qom;
/**
* Performs a logical negation of another constraint.
*
* To satisfy the Not constraint, the node-tuple must not satisfy constraint.
*
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
interface NotInterface extends ConstraintInterface
{
public function getConstraint(): ConstraintInterface;
}
@@ -0,0 +1,23 @@
<?php
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Extbase\Persistence\Generic\Qom;
/**
* An operand to a binary operation specified by a Comparison.
*
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
interface OperandInterface {}
@@ -0,0 +1,32 @@
<?php
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Extbase\Persistence\Generic\Qom;
/**
* Performs a logical disjunction of two other constraints.
*
* To satisfy the Or constraint, the node-tuple must either:
* satisfy constraint1 but not constraint2, or
* satisfy constraint2 but not constraint1, or
* satisfy both constraint1 and constraint2.
*
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
interface OrInterface extends ConstraintInterface
{
public function getConstraint1(): ConstraintInterface;
public function getConstraint2(): ConstraintInterface;
}
@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Extbase\Persistence\Generic\Qom;
use TYPO3\CMS\Extbase\Persistence\QueryInterface;
/**
* Determines the relative order of two rows in the result set by evaluating operand for
* each.
*
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
final readonly class Ordering implements OrderingInterface
{
/**
* @param string $order One of QueryInterface::ORDER_*
*/
public function __construct(
private DynamicOperandInterface $operand,
private string $order = QueryInterface::ORDER_ASCENDING
) {}
public function getOperand(): DynamicOperandInterface
{
return $this->operand;
}
/**
* @return string One of QueryInterface::ORDER_*
*/
public function getOrder(): string
{
return $this->order;
}
}
@@ -0,0 +1,32 @@
<?php
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Extbase\Persistence\Generic\Qom;
/**
* Determines the relative order of two rows in the result set by evaluating operand for
* each.
*
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
interface OrderingInterface
{
public function getOperand(): DynamicOperandInterface;
/**
* @return string One of QueryInterface::ORDER_*
*/
public function getOrder(): string;
}
@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Extbase\Persistence\Generic\Qom;
/**
* Evaluates to the value (or values, if multi-valued) of a property.
*
* If, for a node-tuple, the selector node does not have a property named property,
* the operand evaluates to null.
*
* The query is invalid if:
*
* selector is not the name of a selector in the query, or
* property is not a syntactically valid JCR name.
*
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
final readonly class PropertyValue implements PropertyValueInterface
{
public function __construct(
private string $propertyName,
private string $selectorName = '',
) {}
public function getSelectorName(): string
{
return $this->selectorName;
}
public function getPropertyName(): string
{
return $this->propertyName;
}
}
@@ -0,0 +1,36 @@
<?php
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Extbase\Persistence\Generic\Qom;
/**
* Evaluates to the value (or values, if multi-valued) of a property.
*
* If, for a node-tuple, the selector node does not have a property named property,
* the operand evaluates to null.
*
* The query is invalid if:
*
* selector is not the name of a selector in the query, or
* property is not a syntactically valid JCR name.
*
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
interface PropertyValueInterface extends DynamicOperandInterface
{
public function getSelectorName(): string;
public function getPropertyName(): string;
}
@@ -0,0 +1,184 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Extbase\Persistence\Generic\Qom;
use TYPO3\CMS\Core\Database\Query\QueryBuilder;
use TYPO3\CMS\Core\SingletonInterface;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Persistence\QueryInterface;
/**
* The Query Object Model Factory
*
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
class QueryObjectModelFactory implements SingletonInterface
{
/**
* Selects a subset of the nodes in the repository based on node type.
*/
public function selector(?string $nodeTypeName = null, string $selectorName = ''): SourceInterface&SelectorInterface
{
if ($selectorName === '') {
$selectorName = $nodeTypeName;
}
return new Selector($selectorName, $nodeTypeName);
}
/**
* Sets a statement as constraint. This is not part of the JCR 2.0 Specification!
*/
public function statement(string|\Doctrine\DBAL\Statement|QueryBuilder $statement, array $boundVariables = []): Statement
{
return GeneralUtility::makeInstance(Statement::class, $statement, $boundVariables);
}
/**
* Performs a join between two node-tuple sources.
*/
public function join(
SourceInterface&SelectorInterface $left,
SourceInterface&SelectorInterface $right,
string $joinType,
JoinConditionInterface $joinCondition
): SourceInterface&JoinInterface {
return new Join($left, $right, $joinType, $joinCondition);
}
/**
* Tests whether the value of a property in a first selector is equal to the value of a property in a second selector.
*/
public function equiJoinCondition(string $selector1Name, string $property1Name, string $selector2Name, string $property2Name): EquiJoinConditionInterface
{
return GeneralUtility::makeInstance(EquiJoinCondition::class, $selector1Name, $property1Name, $selector2Name, $property2Name);
}
/**
* Performs a logical conjunction of two other constraints.
*/
public function _and(ConstraintInterface $constraint1, ConstraintInterface $constraint2): AndInterface
{
return GeneralUtility::makeInstance(LogicalAnd::class, $constraint1, $constraint2);
}
/**
* Performs a logical disjunction of two other constraints.
*/
public function _or(ConstraintInterface $constraint1, ConstraintInterface $constraint2): OrInterface
{
return GeneralUtility::makeInstance(LogicalOr::class, $constraint1, $constraint2);
}
/**
* Performs a logical negation of another constraint.
*/
public function not(ConstraintInterface $constraint): NotInterface
{
return GeneralUtility::makeInstance(LogicalNot::class, $constraint);
}
/**
* Filters node-tuples based on the outcome of a binary operation.
*/
public function comparison(PropertyValueInterface $operand1, int $operator, mixed $operand2): ComparisonInterface
{
return GeneralUtility::makeInstance(Comparison::class, $operand1, $operator, $operand2);
}
/**
* Evaluates to the value (or values, if multi-valued) of a property in the specified or default selector.
*/
public function propertyValue(string $propertyName, string $selectorName = ''): PropertyValueInterface
{
return GeneralUtility::makeInstance(PropertyValue::class, $propertyName, $selectorName);
}
/**
* Evaluates to the lower-case string value (or values, if multi-valued) of an operand.
*/
public function lowerCase(PropertyValueInterface $operand): LowerCaseInterface
{
return GeneralUtility::makeInstance(LowerCase::class, $operand);
}
/**
* Evaluates to the upper-case string value (or values, if multi-valued) of an operand.
*/
public function upperCase(PropertyValueInterface $operand): UpperCaseInterface
{
return GeneralUtility::makeInstance(UpperCase::class, $operand);
}
/**
* Orders by the value of the specified operand, in ascending order.
*
* The query is invalid if $operand does not evaluate to a scalar value.
*/
public function ascending(DynamicOperandInterface $operand): OrderingInterface
{
return GeneralUtility::makeInstance(Ordering::class, $operand, QueryInterface::ORDER_ASCENDING);
}
/**
* Orders by the value of the specified operand, in descending order.
*
* The query is invalid if $operand does not evaluate to a scalar value.
*/
public function descending(DynamicOperandInterface $operand): OrderingInterface
{
return GeneralUtility::makeInstance(Ordering::class, $operand, QueryInterface::ORDER_DESCENDING);
}
/**
* Evaluates to the value of a bind variable.
*/
public function bindVariable(string $bindVariableName): BindVariableValueInterface
{
return GeneralUtility::makeInstance(BindVariableValue::class, $bindVariableName);
}
/**
* Evaluates to the concatenated string value of the operands.
*
* @param DynamicOperandInterface|string ...$operands Property names or operand objects to concatenate
*/
public function concat(DynamicOperandInterface|string ...$operands): ConcatInterface
{
return GeneralUtility::makeInstance(Concat::class, $operands);
}
/**
* Evaluates to the trimmed string value of the operand.
*
* @param DynamicOperandInterface $operand The operand to trim
*/
public function trim(DynamicOperandInterface $operand): TrimInterface
{
return GeneralUtility::makeInstance(Trim::class, $operand);
}
/**
* Evaluates to the first non-NULL value among the operands.
*
* @param DynamicOperandInterface|string ...$operands Property names or operand objects
*/
public function coalesce(DynamicOperandInterface|string ...$operands): CoalesceInterface
{
return GeneralUtility::makeInstance(Coalesce::class, $operands);
}
}
@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Extbase\Persistence\Generic\Qom;
/**
* Selects a subset of the nodes in the repository based on node type.
*
* A selector selects every node in the repository, subject to access control
* constraints, that satisfies at least one of the following conditions:
*
* the node's primary node type is nodeType, or
* the node's primary node type is a subtype of nodeType, or
* the node has a mixin node type that is nodeType, or
* the node has a mixin node type that is a subtype of nodeType.
*
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
final readonly class Selector implements SourceInterface, SelectorInterface
{
public function __construct(
private string $selectorName,
private ?string $nodeTypeName,
) {}
public function getNodeTypeName(): ?string
{
return $this->nodeTypeName;
}
public function getSelectorName(): string
{
return $this->selectorName;
}
}
@@ -0,0 +1,43 @@
<?php
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Extbase\Persistence\Generic\Qom;
/**
* Selects a subset of the nodes in the repository based on node type.
*
* A selector selects every node in the repository, subject to access control
* constraints, that satisfies at least one of the following conditions:
*
* the node's primary node type is nodeType, or
* the node's primary node type is a subtype of nodeType, or
* the node has a mixin node type that is nodeType, or
* the node has a mixin node type that is a subtype of nodeType.
*
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
interface SelectorInterface
{
/**
* Gets the name of the required node type.
*/
public function getNodeTypeName(): ?string;
/**
* Gets the selector name.
* A selector's name can be used elsewhere in the query to identify the selector.
*/
public function getSelectorName(): string;
}
@@ -0,0 +1,23 @@
<?php
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Extbase\Persistence\Generic\Qom;
/**
* Evaluates to a set of node-tuples.
*
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
interface SourceInterface {}
@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Extbase\Persistence\Generic\Qom;
use TYPO3\CMS\Core\Database\Query\QueryBuilder;
/**
* A statement acting as a constraint.
*
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
final readonly class Statement implements ConstraintInterface
{
/**
* @param array $boundVariables An array of variables to bind to the statement, only to be used with prepared statements
*/
public function __construct(
private string|\Doctrine\DBAL\Statement|QueryBuilder $statement,
private array $boundVariables = []
) {}
public function getStatement(): string|\Doctrine\DBAL\Statement|QueryBuilder
{
return $this->statement;
}
public function getBoundVariables(): array
{
return $this->boundVariables;
}
public function collectBoundVariableNames(array &$boundVariables) {}
}
@@ -0,0 +1,24 @@
<?php
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Extbase\Persistence\Generic\Qom;
/**
* An operand whose value can be determined from static analysis of the query,
* prior to its evaluation.
*
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
interface StaticOperandInterface extends OperandInterface {}
+45
View File
@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Extbase\Persistence\Generic\Qom;
/**
* Evaluates to the trimmed string value of the operand.
*
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
final readonly class Trim implements TrimInterface
{
public function __construct(
private DynamicOperandInterface $operand
) {}
public function getOperand(): DynamicOperandInterface
{
return $this->operand;
}
public function getOperands(): array
{
return [$this->operand];
}
public function getFunctionName(): string
{
return 'TRIM';
}
}
@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Extbase\Persistence\Generic\Qom;
/**
* Evaluates to the trimmed string value of the operand.
*
* Usage example:
* $query->orderBy($query->trim('title'), QueryInterface::ORDER_ASCENDING);
*
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
interface TrimInterface extends FunctionExpressionInterface
{
/**
* Returns the operand being trimmed.
*/
public function getOperand(): DynamicOperandInterface;
}
@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Extbase\Persistence\Generic\Qom;
/**
* Evaluates to the upper-case string value (or values, if multi-valued) of
* operand.
*
* If operand does not evaluate to a string value, its value is first converted
* to a string.
*
* If operand evaluates to null, the UpperCase operand also evaluates to null.
*
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
final readonly class UpperCase implements UpperCaseInterface
{
public function __construct(private PropertyValueInterface $operand) {}
public function getOperand(): PropertyValueInterface
{
return $this->operand;
}
public function getSelectorName(): string
{
return $this->operand->getSelectorName();
}
public function getPropertyName(): string
{
return 'UPPER' . $this->operand->getPropertyName();
}
}
@@ -0,0 +1,32 @@
<?php
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Extbase\Persistence\Generic\Qom;
/**
* Evaluates to the upper-case string value (or values, if multi-valued) of
* operand.
*
* If operand does not evaluate to a string value, its value is first converted
* to a string.
*
* If operand evaluates to null, the UpperCase operand also evaluates to null.
*
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
interface UpperCaseInterface extends PropertyValueInterface
{
public function getOperand(): PropertyValueInterface;
}
+674
View File
@@ -0,0 +1,674 @@
<?php
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Extbase\Persistence\Generic;
use Psr\Container\ContainerInterface;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Persistence\Generic\Exception\UnexpectedTypeException;
use TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMapFactory;
use TYPO3\CMS\Extbase\Persistence\Generic\Qom\AndInterface;
use TYPO3\CMS\Extbase\Persistence\Generic\Qom\CoalesceInterface;
use TYPO3\CMS\Extbase\Persistence\Generic\Qom\ConcatInterface;
use TYPO3\CMS\Extbase\Persistence\Generic\Qom\ConstraintInterface;
use TYPO3\CMS\Extbase\Persistence\Generic\Qom\DynamicOperandInterface;
use TYPO3\CMS\Extbase\Persistence\Generic\Qom\OrderingInterface;
use TYPO3\CMS\Extbase\Persistence\Generic\Qom\OrInterface;
use TYPO3\CMS\Extbase\Persistence\Generic\Qom\QueryObjectModelFactory;
use TYPO3\CMS\Extbase\Persistence\Generic\Qom\SelectorInterface;
use TYPO3\CMS\Extbase\Persistence\Generic\Qom\SourceInterface;
use TYPO3\CMS\Extbase\Persistence\Generic\Qom\TrimInterface;
use TYPO3\CMS\Extbase\Persistence\PersistenceManagerInterface;
use TYPO3\CMS\Extbase\Persistence\QueryInterface;
use TYPO3\CMS\Extbase\Persistence\QueryResultInterface;
use TYPO3\CMS\Extbase\Utility\TypeHandlingUtility;
/**
* The Query class used to run queries against the database
*
* @todo v12: Candidate to declare final - Can be decorated or standalone class implementing the interface
* @template T of object
* @implements QueryInterface<T>
*/
#[Autoconfigure(public: true, shared: false)]
class Query implements QueryInterface
{
/**
* An inner join.
*/
public const JCR_JOIN_TYPE_INNER = '{http://www.jcp.org/jcr/1.0}joinTypeInner';
/**
* A left-outer join.
*/
public const JCR_JOIN_TYPE_LEFT_OUTER = '{http://www.jcp.org/jcr/1.0}joinTypeLeftOuter';
/**
* A right-outer join.
*/
public const JCR_JOIN_TYPE_RIGHT_OUTER = '{http://www.jcp.org/jcr/1.0}joinTypeRightOuter';
/**
* Charset of strings in QOM
*/
public const CHARSET = 'utf-8';
/**
* @var string
* @phpstan-var class-string<T>
*/
protected $type;
protected DataMapFactory $dataMapFactory;
protected PersistenceManagerInterface $persistenceManager;
protected QueryObjectModelFactory $qomFactory;
protected ContainerInterface $container;
protected ?SourceInterface $source = null;
/**
* @var \TYPO3\CMS\Extbase\Persistence\Generic\Qom\ConstraintInterface
*/
protected $constraint;
/**
* @var \TYPO3\CMS\Extbase\Persistence\Generic\Qom\Statement
*/
protected $statement;
/**
* @var array<string, string>|array<OrderingInterface>
*/
protected array $orderings = [];
/**
* @var int|null
*/
protected $limit;
/**
* @var int
*/
protected $offset;
protected QuerySettingsInterface $querySettings;
/**
* @var QueryInterface|null
* @internal
*/
protected $parentQuery;
public function __construct(
DataMapFactory $dataMapFactory,
PersistenceManagerInterface $persistenceManager,
QueryObjectModelFactory $qomFactory,
ContainerInterface $container
) {
$this->dataMapFactory = $dataMapFactory;
$this->persistenceManager = $persistenceManager;
$this->qomFactory = $qomFactory;
$this->container = $container;
}
/**
* @phpstan-param class-string<T> $type
*/
public function setType(string $type): void
{
$this->type = $type;
}
/**
* @internal
*/
public function getParentQuery(): ?QueryInterface
{
return $this->parentQuery;
}
/**
* @internal
*/
public function setParentQuery(?QueryInterface $parentQuery): void
{
$this->parentQuery = $parentQuery;
}
/**
* Sets the Query Settings. These Query settings must match the settings expected by
* the specific Storage Backend.
*/
public function setQuerySettings(QuerySettingsInterface $querySettings)
{
$this->querySettings = $querySettings;
}
public function getQuerySettings(): QuerySettingsInterface
{
return $this->querySettings;
}
/**
* Returns the type this query cares for.
*
* @return string
* @phpstan-return class-string<T>
*/
public function getType()
{
return $this->type;
}
public function setSource(SourceInterface $source): void
{
$this->source = $source;
}
/**
* Returns the selector's name or an empty string, if the source is not a selector
* @todo This has to be checked at another place
*
* @return string The selector name
*/
protected function getSelectorName()
{
$source = $this->getSource();
if ($source instanceof SelectorInterface) {
return $source->getSelectorName();
}
return '';
}
public function getSource(): SourceInterface
{
if ($this->source === null) {
$this->source = $this->qomFactory->selector($this->getType(), $this->dataMapFactory->buildDataMap($this->getType())->tableName);
}
return $this->source;
}
/**
* Executes the query against the database and returns the result
*
* @param bool $returnRawQueryResult avoids the object mapping by the persistence
* @return QueryResultInterface|list<array<string,mixed>> The query result object or an array if $returnRawQueryResult is TRUE
* @phpstan-return ($returnRawQueryResult is true ? list<array<string,mixed>> : QueryResultInterface<int,T>)
*/
public function execute($returnRawQueryResult = false)
{
if ($returnRawQueryResult) {
return $this->persistenceManager->getObjectDataByQuery($this);
}
/** @phpstan-var QueryResultInterface<int,T> $queryResult */
$queryResult = $this->container->get(QueryResultInterface::class);
$queryResult->setQuery($this);
return $queryResult;
}
/**
* Sets the property names to order the result by. Expected like this:
* array(
* 'foo' => \TYPO3\CMS\Extbase\Persistence\QueryInterface::ORDER_ASCENDING,
* 'bar' => \TYPO3\CMS\Extbase\Persistence\QueryInterface::ORDER_DESCENDING
* )
* where 'foo' and 'bar' are property names.
*
* @param array $orderings The property names to order by
* @return QueryInterface
* @phpstan-return QueryInterface<T>
*/
public function setOrderings(array $orderings)
{
$this->orderings = $orderings;
return $this;
}
/**
* Returns the property names to order the result by. Like this:
* array(
* 'foo' => \TYPO3\CMS\Extbase\Persistence\QueryInterface::ORDER_ASCENDING,
* 'bar' => \TYPO3\CMS\Extbase\Persistence\QueryInterface::ORDER_DESCENDING
* )
*
* @return array<string, string>|array<OrderingInterface>
*/
public function getOrderings()
{
return $this->orderings;
}
/**
* Sets the ordering for the result by a single operand. Replaces any existing orderings.
*
* @param string|DynamicOperandInterface $operand The property name or a dynamic operand
* @param string $order The order direction
* @return QueryInterface
* @phpstan-return QueryInterface<T>
*/
public function orderBy(string|DynamicOperandInterface $operand, string $order = QueryInterface::ORDER_ASCENDING)
{
$this->orderings = [];
return $this->addOrderBy($operand, $order);
}
/**
* Adds an ordering for the result. Appends to any existing orderings.
*
* @param string|DynamicOperandInterface $operand The property name or a dynamic operand
* @param string $order The order direction
* @return QueryInterface
* @phpstan-return QueryInterface<T>
*/
public function addOrderBy(string|DynamicOperandInterface $operand, string $order = QueryInterface::ORDER_ASCENDING)
{
if (is_string($operand)) {
$operand = $this->qomFactory->propertyValue($operand, $this->getSelectorName());
}
if ($order === QueryInterface::ORDER_ASCENDING) {
$this->orderings[] = $this->qomFactory->ascending($operand);
} else {
$this->orderings[] = $this->qomFactory->descending($operand);
}
return $this;
}
/**
* Creates a CONCAT expression for ordering.
*
* @param string|DynamicOperandInterface ...$operands Property names or operand objects to concatenate
*/
public function concat(string|DynamicOperandInterface ...$operands): ConcatInterface
{
$resolvedOperands = [];
foreach ($operands as $operand) {
if (is_string($operand)) {
$resolvedOperands[] = $this->qomFactory->propertyValue($operand, $this->getSelectorName());
} else {
$resolvedOperands[] = $operand;
}
}
return $this->qomFactory->concat(...$resolvedOperands);
}
/**
* Creates a TRIM expression for ordering.
*
* @param string|DynamicOperandInterface $operand The property name or operand to trim
*/
public function trim(string|DynamicOperandInterface $operand): TrimInterface
{
if (is_string($operand)) {
$operand = $this->qomFactory->propertyValue($operand, $this->getSelectorName());
}
return $this->qomFactory->trim($operand);
}
/**
* Creates a COALESCE expression for ordering.
*
* @param string|DynamicOperandInterface ...$operands Property names or operand objects
*/
public function coalesce(string|DynamicOperandInterface ...$operands): CoalesceInterface
{
$resolvedOperands = [];
foreach ($operands as $operand) {
if (is_string($operand)) {
$resolvedOperands[] = $this->qomFactory->propertyValue($operand, $this->getSelectorName());
} else {
$resolvedOperands[] = $operand;
}
}
return $this->qomFactory->coalesce(...$resolvedOperands);
}
/**
* Sets the maximum size of the result set to limit. Returns $this to allow
* for chaining (fluid interface)
*
* @param int $limit
* @throws \InvalidArgumentException
* @return QueryInterface
* @phpstan-return QueryInterface<T>
*/
public function setLimit($limit)
{
if (!is_int($limit) || $limit < 1) {
throw new \InvalidArgumentException('The limit must be an integer >= 1', 1245071870);
}
$this->limit = $limit;
return $this;
}
/**
* Resets a previously set maximum size of the result set. Returns $this to allow
* for chaining (fluid interface)
*
* @return QueryInterface
*/
public function unsetLimit()
{
$this->limit = null;
return $this;
}
/**
* Returns the maximum size of the result set to limit.
*
* @return int|null
*/
public function getLimit()
{
return $this->limit;
}
/**
* Sets the start offset of the result set to offset. Returns $this to
* allow for chaining (fluid interface)
*
* @param int $offset
* @throws \InvalidArgumentException
* @return QueryInterface
* @phpstan-return QueryInterface<T>
*/
public function setOffset($offset)
{
if (!is_int($offset) || $offset < 0) {
throw new \InvalidArgumentException('The offset must be a positive integer', 1245071872);
}
$this->offset = $offset;
return $this;
}
/**
* Returns the start offset of the result set.
*
* @return int
*/
public function getOffset()
{
return $this->offset;
}
/**
* The constraint used to limit the result set. Returns $this to allow
* for chaining (fluid interface)
*
* @param \TYPO3\CMS\Extbase\Persistence\Generic\Qom\ConstraintInterface $constraint
* @return QueryInterface
* @phpstan-return QueryInterface<T>
*/
public function matching($constraint)
{
$this->constraint = $constraint;
return $this;
}
/**
* Sets the statement of this query. If you use this, you will lose the abstraction from a concrete storage
* backend (database).
*
* @param string|\TYPO3\CMS\Core\Database\Query\QueryBuilder|\Doctrine\DBAL\Statement $statement The statement
* @param array $parameters An array of parameters. These will be bound to placeholders '?' in the $statement.
* @return QueryInterface
*/
public function statement($statement, array $parameters = [])
{
$this->statement = $this->qomFactory->statement($statement, $parameters);
return $this;
}
/**
* Returns the statement of this query.
*
* @return \TYPO3\CMS\Extbase\Persistence\Generic\Qom\Statement
*/
public function getStatement()
{
return $this->statement;
}
/**
* Gets the constraint for this query.
*
* @return \TYPO3\CMS\Extbase\Persistence\Generic\Qom\ConstraintInterface|null the constraint, or null if none
*/
public function getConstraint()
{
return $this->constraint;
}
/**
* Performs a logical conjunction of multiple given constraints. The method
* takes an arbitrary number of constraints and concatenates them with a boolean AND.
*/
public function logicalAnd(ConstraintInterface ...$constraints): AndInterface
{
switch (count($constraints)) {
case 0:
$alwaysTrue = $this->greaterThan('uid', 0);
return $this->qomFactory->_and($alwaysTrue, $alwaysTrue);
case 1:
$alwaysTrue = $this->greaterThan('uid', 0);
return $this->qomFactory->_and(array_shift($constraints), $alwaysTrue);
default:
$resultingConstraint = $this->qomFactory->_and(array_shift($constraints), array_shift($constraints));
foreach ($constraints as $furtherConstraint) {
$resultingConstraint = $this->qomFactory->_and($resultingConstraint, $furtherConstraint);
}
return $resultingConstraint;
}
}
/**
* Performs a logical disjunction of multiple given constraints. The method
* takes an arbitrary number of constraints and concatenates them with a boolean OR.
*/
public function logicalOr(ConstraintInterface ...$constraints): OrInterface
{
switch (count($constraints)) {
case 0:
$alwaysFalse = $this->equals('uid', 0);
return $this->qomFactory->_or($alwaysFalse, $alwaysFalse);
case 1:
$alwaysFalse = $this->equals('uid', 0);
return $this->qomFactory->_or(array_shift($constraints), $alwaysFalse);
default:
$resultingConstraint = $this->qomFactory->_or(array_shift($constraints), array_shift($constraints));
foreach ($constraints as $furtherConstraint) {
$resultingConstraint = $this->qomFactory->_or($resultingConstraint, $furtherConstraint);
}
return $resultingConstraint;
}
}
/**
* Performs a logical negation of the given constraint
*
* @param \TYPO3\CMS\Extbase\Persistence\Generic\Qom\ConstraintInterface $constraint Constraint to negate
* @throws \RuntimeException
* @return \TYPO3\CMS\Extbase\Persistence\Generic\Qom\NotInterface
*/
public function logicalNot(ConstraintInterface $constraint)
{
return $this->qomFactory->not($constraint);
}
/**
* Returns an equals criterion used for matching objects against a query
*
* @param string $propertyName The name of the property to compare against
* @param mixed $operand The value to compare with
* @param bool $caseSensitive Whether the equality test should be done case-sensitive
* @return \TYPO3\CMS\Extbase\Persistence\Generic\Qom\ComparisonInterface
*/
public function equals($propertyName, $operand, $caseSensitive = true)
{
if (is_object($operand) || $caseSensitive) {
$comparison = $this->qomFactory->comparison(
$this->qomFactory->propertyValue($propertyName, $this->getSelectorName()),
QueryInterface::OPERATOR_EQUAL_TO,
$operand
);
} else {
$comparison = $this->qomFactory->comparison(
$this->qomFactory->lowerCase($this->qomFactory->propertyValue($propertyName, $this->getSelectorName())),
QueryInterface::OPERATOR_EQUAL_TO,
mb_strtolower($operand, \TYPO3\CMS\Extbase\Persistence\Generic\Query::CHARSET)
);
}
return $comparison;
}
/**
* Returns a like criterion used for matching objects against a query
*
* @param string $propertyName The name of the property to compare against
* @param mixed $operand The value to compare with
* @return \TYPO3\CMS\Extbase\Persistence\Generic\Qom\ComparisonInterface
*/
public function like($propertyName, $operand)
{
return $this->qomFactory->comparison(
$this->qomFactory->propertyValue($propertyName, $this->getSelectorName()),
QueryInterface::OPERATOR_LIKE,
$operand
);
}
/**
* Returns a "contains" criterion used for matching objects against a query.
* It matches if the multivalued property contains the given operand.
*
* @param string $propertyName The name of the (multivalued) property to compare against
* @param mixed $operand The value to compare with
* @return \TYPO3\CMS\Extbase\Persistence\Generic\Qom\ComparisonInterface
*/
public function contains($propertyName, $operand)
{
return $this->qomFactory->comparison($this->qomFactory->propertyValue($propertyName, $this->getSelectorName()), QueryInterface::OPERATOR_CONTAINS, $operand);
}
/**
* Returns an "in" criterion used for matching objects against a query. It
* matches if the property's value is contained in the multivalued operand.
*
* @param string $propertyName The name of the property to compare against
* @param mixed $operand The value to compare with, multivalued
* @throws Exception\UnexpectedTypeException
* @return \TYPO3\CMS\Extbase\Persistence\Generic\Qom\ComparisonInterface
*/
public function in($propertyName, $operand)
{
if (!TypeHandlingUtility::isValidTypeForMultiValueComparison($operand)) {
throw new UnexpectedTypeException('The "in" operator must be given a multivalued operand (array, ArrayAccess, Traversable).', 1264678095);
}
return $this->qomFactory->comparison($this->qomFactory->propertyValue($propertyName, $this->getSelectorName()), QueryInterface::OPERATOR_IN, $operand);
}
/**
* Returns a less than criterion used for matching objects against a query
*
* @param string $propertyName The name of the property to compare against
* @param mixed $operand The value to compare with
* @return \TYPO3\CMS\Extbase\Persistence\Generic\Qom\ComparisonInterface
*/
public function lessThan($propertyName, $operand)
{
return $this->qomFactory->comparison($this->qomFactory->propertyValue($propertyName, $this->getSelectorName()), QueryInterface::OPERATOR_LESS_THAN, $operand);
}
/**
* Returns a less or equal than criterion used for matching objects against a query
*
* @param string $propertyName The name of the property to compare against
* @param mixed $operand The value to compare with
* @return \TYPO3\CMS\Extbase\Persistence\Generic\Qom\ComparisonInterface
*/
public function lessThanOrEqual($propertyName, $operand)
{
return $this->qomFactory->comparison($this->qomFactory->propertyValue($propertyName, $this->getSelectorName()), QueryInterface::OPERATOR_LESS_THAN_OR_EQUAL_TO, $operand);
}
/**
* Returns a greater than criterion used for matching objects against a query
*
* @param string $propertyName The name of the property to compare against
* @param mixed $operand The value to compare with
* @return \TYPO3\CMS\Extbase\Persistence\Generic\Qom\ComparisonInterface
*/
public function greaterThan($propertyName, $operand)
{
return $this->qomFactory->comparison($this->qomFactory->propertyValue($propertyName, $this->getSelectorName()), QueryInterface::OPERATOR_GREATER_THAN, $operand);
}
/**
* Returns a greater than or equal criterion used for matching objects against a query
*
* @param string $propertyName The name of the property to compare against
* @param mixed $operand The value to compare with
* @return \TYPO3\CMS\Extbase\Persistence\Generic\Qom\ComparisonInterface
*/
public function greaterThanOrEqual($propertyName, $operand)
{
return $this->qomFactory->comparison($this->qomFactory->propertyValue($propertyName, $this->getSelectorName()), QueryInterface::OPERATOR_GREATER_THAN_OR_EQUAL_TO, $operand);
}
/**
* Returns a greater than or equal criterion used for matching objects against a query
*
* @param string $propertyName The name of the property to compare against
* @param mixed $operandLower The value of the lower boundary to compare against
* @param mixed $operandUpper The value of the upper boundary to compare against
* @return \TYPO3\CMS\Extbase\Persistence\Generic\Qom\AndInterface
*/
public function between($propertyName, $operandLower, $operandUpper)
{
return $this->logicalAnd(
$this->greaterThanOrEqual($propertyName, $operandLower),
$this->lessThanOrEqual($propertyName, $operandUpper)
);
}
/**
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
public function __wakeup()
{
$this->persistenceManager = GeneralUtility::makeInstance(PersistenceManagerInterface::class);
$this->dataMapFactory = GeneralUtility::makeInstance(DataMapFactory::class);
$this->qomFactory = GeneralUtility::makeInstance(QueryObjectModelFactory::class);
}
/**
* @return array
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
public function __sleep()
{
return ['type', 'source', 'constraint', 'statement', 'orderings', 'limit', 'offset', 'querySettings'];
}
/**
* Returns the query result count.
*
* @return int The query result count
*/
public function count()
{
return $this->execute()->count();
}
}
@@ -0,0 +1,71 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Extbase\Persistence\Generic;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface;
use TYPO3\CMS\Extbase\Configuration\Exception\NoServerRequestGivenException;
use TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMapFactory;
use TYPO3\CMS\Extbase\Persistence\QueryInterface;
/**
* The QueryFactory used to create queries against the storage backend
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
readonly class QueryFactory implements QueryFactoryInterface
{
public function __construct(
protected ConfigurationManagerInterface $configurationManager,
protected DataMapFactory $dataMapFactory,
) {}
/**
* Creates a query object working on the given class name
*
* @param string $className The class name
* @template T of object
* @phpstan-param class-string<T> $className
* @phpstan-return QueryInterface<T>
*/
public function create($className): QueryInterface
{
$query = GeneralUtility::makeInstance(QueryInterface::class);
$query->setType($className);
$querySettings = GeneralUtility::makeInstance(QuerySettingsInterface::class);
$dataMap = $this->dataMapFactory->buildDataMap($className);
if ($dataMap->rootLevel) {
$querySettings->setRespectStoragePage(false);
}
$storagePid = '0';
try {
$frameworkConfiguration = $this->configurationManager->getConfiguration(ConfigurationManagerInterface::CONFIGURATION_TYPE_FRAMEWORK);
$storagePid = (string)($frameworkConfiguration['persistence']['storagePid'] ?? '0');
} catch (NoServerRequestGivenException) {
// Fallback to storagePid 0 if ConfigurationManager has not been initialized with a Request. This
// is a measure to specifically allow running the extbase persistence layer without a Request, which
// may be useful in some CLI scenarios (and can be convenient in tests) when no other code branches
// of extbase that have a hard dependency to the Request (e.g. controllers / view) are used.
}
$querySettings->setStoragePageIds(GeneralUtility::intExplode(',', $storagePid));
$query->setQuerySettings($querySettings);
return $query;
}
}
@@ -0,0 +1,33 @@
<?php
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Extbase\Persistence\Generic;
/**
* A persistence query factory interface
*/
interface QueryFactoryInterface
{
/**
* Creates a query object working on the given class name
*
* @param string $className The class name
* @return \TYPO3\CMS\Extbase\Persistence\QueryInterface
* @template T of object
* @phpstan-param class-string<T> $className
* @phpstan-return \TYPO3\CMS\Extbase\Persistence\QueryInterface<T>
*/
public function create($className);
}
+260
View File
@@ -0,0 +1,260 @@
<?php
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Extbase\Persistence\Generic;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMapper;
use TYPO3\CMS\Extbase\Persistence\PersistenceManagerInterface;
use TYPO3\CMS\Extbase\Persistence\QueryInterface;
use TYPO3\CMS\Extbase\Persistence\QueryResultInterface;
/**
* A lazy result list that is returned by Query::execute()
*
* @todo v12: Candidate to declare final - Can be decorated or standalone class implementing the interface
* @template TValue of object
* @implements QueryResultInterface<int,TValue>
*/
#[Autoconfigure(public: true, shared: false)]
class QueryResult implements QueryResultInterface
{
protected DataMapper $dataMapper;
protected PersistenceManagerInterface $persistenceManager;
/**
* @var int|null
*/
protected $numberOfResults;
/**
* @phpstan-var QueryInterface<TValue>|null
*/
protected ?QueryInterface $query = null;
/**
* @var array|null
* @phpstan-var list<TValue>|null
*/
protected $queryResult;
public function __construct(
DataMapper $dataMapper,
PersistenceManagerInterface $persistenceManager
) {
$this->dataMapper = $dataMapper;
$this->persistenceManager = $persistenceManager;
}
/**
* @phpstan-param QueryInterface<TValue> $query
*/
public function setQuery(QueryInterface $query): void
{
$this->query = $query;
$this->dataMapper->setQuery($query);
}
/**
* Loads the objects this QueryResult is supposed to hold
*/
protected function initialize()
{
if (!is_array($this->queryResult)) {
$this->queryResult = $this->dataMapper->map($this->query->getType(), $this->persistenceManager->getObjectDataByQuery($this->query));
}
}
/**
* Returns a clone of the query object
*
* @return QueryInterface
* @phpstan-return QueryInterface<TValue>
*/
public function getQuery()
{
return clone $this->query;
}
/**
* Returns the first object in the result set
*
* @return object
* @phpstan-return TValue|null
*/
public function getFirst()
{
if (is_array($this->queryResult)) {
$queryResult = $this->queryResult;
reset($queryResult);
} else {
$query = $this->getQuery();
$query->setLimit(1);
$queryResult = $this->dataMapper->map($query->getType(), $this->persistenceManager->getObjectDataByQuery($query));
}
$firstResult = current($queryResult);
if ($firstResult === false) {
$firstResult = null;
}
return $firstResult;
}
/**
* Returns the number of objects in the result
*
* @return int The number of matching objects
*/
public function count(): int
{
if ($this->numberOfResults === null) {
if (is_array($this->queryResult)) {
$this->numberOfResults = count($this->queryResult);
} else {
$this->numberOfResults = $this->persistenceManager->getObjectCountByQuery($this->query);
}
}
return $this->numberOfResults;
}
/**
* Returns an array with the objects in the result set
*
* @return array
* @phpstan-return list<TValue>
*/
public function toArray()
{
$this->initialize();
return iterator_to_array($this);
}
/**
* This method is needed to implement the ArrayAccess interface,
* but it isn't very useful as the offset has to be an integer
*
* @param mixed $offset
*/
public function offsetExists($offset): bool
{
$this->initialize();
return isset($this->queryResult[$offset]);
}
/**
* @param mixed $offset
* @return TValue|null
*/
public function offsetGet($offset): mixed
{
$this->initialize();
return $this->queryResult[$offset] ?? null;
}
/**
* This method has no effect on the persisted objects but only on the result set
*
* @param mixed $offset
* @param mixed $value
* @phpstan-param TValue $value
*/
public function offsetSet($offset, $value): void
{
$this->initialize();
$this->numberOfResults = null;
$this->queryResult[$offset] = $value;
}
/**
* This method has no effect on the persisted objects but only on the result set
*
* @param mixed $offset
*/
public function offsetUnset($offset): void
{
$this->initialize();
$this->numberOfResults = null;
unset($this->queryResult[$offset]);
}
/**
* @return mixed
* @see Iterator::current()
* @return TValue|false
*/
public function current(): mixed
{
$this->initialize();
return current($this->queryResult);
}
/**
* @return mixed
* @see Iterator::key()
* @return int|null
*/
public function key(): mixed
{
$this->initialize();
return key($this->queryResult);
}
/**
* @see Iterator::next()
*/
public function next(): void
{
$this->initialize();
next($this->queryResult);
}
/**
* @see Iterator::rewind()
*/
public function rewind(): void
{
$this->initialize();
reset($this->queryResult);
}
/**
* @see Iterator::valid()
*/
public function valid(): bool
{
$this->initialize();
return current($this->queryResult) !== false;
}
/**
* Ensures that the persistenceManager and dataMapper are back when loading the QueryResult
* from the cache
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
public function __wakeup()
{
$this->persistenceManager = GeneralUtility::makeInstance(PersistenceManagerInterface::class);
$this->dataMapper = GeneralUtility::makeInstance(DataMapper::class);
}
/**
* @return array
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
public function __sleep()
{
return ['query'];
}
}
@@ -0,0 +1,134 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Extbase\Persistence\Generic;
use TYPO3\CMS\Core\Context\LanguageAspect;
/**
* A query settings interface. This interface is NOT part of the TYPO3.Flow API.
*/
interface QuerySettingsInterface
{
/**
* Sets the flag if the storage page should be respected for the query.
*
* @param bool $respectStoragePage If TRUE the storage page ID will be determined and the statement will be extended accordingly.
* @return $this fluent interface
*/
public function setRespectStoragePage(bool $respectStoragePage): self;
/**
* Returns the state, if the storage page should be respected for the query.
*
* @return bool TRUE, if the storage page should be respected; otherwise FALSE.
*/
public function getRespectStoragePage(): bool;
/**
* Sets the pid(s) of the storage page(s) that should be respected for the query.
*
* @param int[] $storagePageIds If TRUE the storage page ID will be determined and the statement will be extended accordingly.
* @return $this fluent interface
*/
public function setStoragePageIds(array $storagePageIds): self;
/**
* Returns the pid(s) of the storage page(s) that should be respected for the query.
*
* @return int[] list of integers that each represent a storage page id
*/
public function getStoragePageIds(): array;
/**
* Sets the flag if record language should be respected when querying.
* Other settings defines whether overlay should happen or not.
*
* @param bool $respectSysLanguage TRUE if only record language should be respected when querying
* @return $this fluent interface
*/
public function setRespectSysLanguage(bool $respectSysLanguage): self;
/**
* Returns the state, if record language should be checked when querying
*
* @return bool if TRUE record language is checked.
*/
public function getRespectSysLanguage(): bool;
/**
* Sets a flag indicating whether all or some enable fields should be ignored. If TRUE, all enable fields are ignored.
* If--in addition to this--enableFieldsToBeIgnored is set, only fields specified there are ignored. If FALSE, all
* enable fields are taken into account, regardless of the enableFieldsToBeIgnored setting.
*
* @param bool $ignoreEnableFields
* @return $this fluent interface
* @see setEnableFieldsToBeIgnored()
*/
public function setIgnoreEnableFields(bool $ignoreEnableFields): self;
/**
* The returned value indicates whether all or some enable fields should be ignored.
*
* If TRUE, all enable fields are ignored. If--in addition to this--enableFieldsToBeIgnored is set, only fields specified there are ignored.
* If FALSE, all enable fields are taken into account, regardless of the enableFieldsToBeIgnored setting.
*
* @see getEnableFieldsToBeIgnored()
*/
public function getIgnoreEnableFields(): bool;
/**
* An array of column names in the enable columns array (array keys in $GLOBALS['TCA'][$table]['ctrl']['enablecolumns']),
* to be ignored while building the query statement. Adding a column name here effectively switches off filtering
* by this column. This setting is only taken into account if $this->ignoreEnableFields = TRUE.
*
* @param string[] $enableFieldsToBeIgnored
* @return $this fluent interface
* @see setIgnoreEnableFields()
*/
public function setEnableFieldsToBeIgnored(array $enableFieldsToBeIgnored): self;
/**
* An array of column names in the enable columns array (array keys in $GLOBALS['TCA'][$table]['ctrl']['enablecolumns']),
* to be ignored while building the query statement.
*
* @return string[]
* @see getIgnoreEnableFields()
*/
public function getEnableFieldsToBeIgnored(): array;
/**
* Sets the flag if the query should return objects that are deleted.
*
* @param bool $includeDeleted
* @return $this fluent interface
*/
public function setIncludeDeleted(bool $includeDeleted): self;
/**
* Returns if the query should return objects that are deleted.
*/
public function getIncludeDeleted(): bool;
public function getLanguageAspect(): LanguageAspect;
/**
* Overrides the main language aspect, defined in the main Context API
* @return $this fluent interface
*/
public function setLanguageAspect(LanguageAspect $languageAspect): self;
}
+215
View File
@@ -0,0 +1,215 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Extbase\Persistence\Generic;
use TYPO3\CMS\Core\Context\LanguageAspect;
use TYPO3\CMS\Extbase\Persistence\ObjectStorage;
/**
* The persistence session - acts as a Unit of Work for Extbase persistence framework.
*
* Warning: This is a stateful-shared service!
*
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
class Session
{
protected ObjectStorage $reconstitutedEntities;
protected ObjectStorage $objectMap;
/**
* @var array<non-empty-string, array<non-empty-string, object>>
*/
protected array $identifierMap = [];
/**
* Constructs a new Session
*/
public function __construct()
{
$this->reconstitutedEntities = new ObjectStorage();
$this->objectMap = new ObjectStorage();
}
/**
* Registers data for a reconstituted object.
*
* $entityData format is described in
* "Documentation/PersistenceFramework object data format.txt"
*/
public function registerReconstitutedEntity(object $entity): void
{
$this->reconstitutedEntities->attach($entity);
}
/**
* Unregisters data for a reconstituted object
*/
public function unregisterReconstitutedEntity(object $entity): void
{
if ($this->reconstitutedEntities->contains($entity)) {
$this->reconstitutedEntities->detach($entity);
}
}
/**
* Returns all objects which have been registered as reconstituted
*/
public function getReconstitutedEntities(): ObjectStorage
{
return $this->reconstitutedEntities;
}
/**
* Checks whether the given object is known to the identity map
*/
public function hasObject(object $object): bool
{
return $this->objectMap->contains($object);
}
/**
* Checks whether the given identifier is known to the identity map
*
* @param non-empty-string $identifier
* @param class-string $className
*/
public function hasIdentifier(string $identifier, string $className): bool
{
return isset($this->identifierMap[$this->getClassIdentifier($className)][$identifier]);
}
/**
* Returns the object for the given identifier
*
* @param non-empty-string $identifier
* @param class-string $className
*/
public function getObjectByIdentifier(string $identifier, string $className): object
{
return $this->identifierMap[$this->getClassIdentifier($className)][$identifier];
}
/**
* Returns the identifier for the given object from
* the session, if the object was registered.
*
* @return non-empty-string|null
*/
public function getIdentifierByObject(object $object): ?string
{
if ($this->hasObject($object)) {
return $this->objectMap[$object];
}
return null;
}
/**
* Register an identifier for an object
*
* @param non-empty-string $identifier
*/
public function registerObject(object $object, string $identifier): void
{
$this->objectMap[$object] = $identifier;
$this->identifierMap[$this->getClassIdentifier(get_class($object))][$identifier] = $object;
}
/**
* Unregister an object
*/
public function unregisterObject(object $object): void
{
unset($this->identifierMap[$this->getClassIdentifier(get_class($object))][$this->objectMap[$object]]);
$this->objectMap->detach($object);
}
/**
* Destroy the state of the persistence session and reset
* all internal data.
*/
public function destroy(): void
{
$this->identifierMap = [];
$this->objectMap = new ObjectStorage();
$this->reconstitutedEntities = new ObjectStorage();
}
/**
* Objects are stored in the cache with their implementation class name
* to allow reusing instances of different classes that point to the same implementation
* Returns a unique class identifier respecting configured implementation class names
*
* @param class-string $className
* @return non-empty-string
*/
protected function getClassIdentifier(string $className): string
{
return strtolower($className);
}
/**
* Build a language-aware identifier for the identity map by combining
* a base identifier with a language content identifier.
*/
public function buildIdentifier(string|array $baseIdentifier, ?LanguageAspect $languageAspect = null): string
{
if (is_array($baseIdentifier)) {
$identifier = (string)$baseIdentifier['uid'];
if (isset($baseIdentifier['_LOCALIZED_UID'])) {
$identifier .= '_' . $baseIdentifier['_LOCALIZED_UID'];
}
$baseIdentifier = $identifier;
}
// Use default language context for newly inserted objects
$languageAspect ??= new LanguageAspect(0, 0, LanguageAspect::OVERLAYS_ON_WITH_FLOATING, []);
return $baseIdentifier . '@' . $this->getContentIdentifier($languageAspect);
}
/**
* Build a unique identifier representing the content-fetching configuration
* of the given LanguageAspect.
*
* This includes contentId, overlayType, and fallbackChain — everything
* that affects which record overlay is returned. The language ID is
* intentionally excluded because it only affects menus/links, not content.
*
* @internal
*/
protected function getContentIdentifier(LanguageAspect $languageAspect): string
{
return sprintf(
'%d-%s-%s',
$languageAspect->getContentId(),
$languageAspect->getOverlayType(),
implode(',', $languageAspect->getFallbackChain())
);
}
/**
* Extract the base identifier (before '@') from a full identity map identifier.
*/
public function getBaseIdentifier(string $identifier): string
{
$pos = strpos($identifier, '@');
if ($pos !== false) {
return substr($identifier, 0, $pos);
}
return $identifier;
}
}
@@ -0,0 +1,82 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Extbase\Persistence\Generic\Storage;
use TYPO3\CMS\Extbase\DomainObject\AbstractValueObject;
use TYPO3\CMS\Extbase\Persistence\QueryInterface;
/**
* Storage backend interface
*/
interface BackendInterface
{
/**
* Adds a row to the storage
*
* @param string $tableName The database table name
* @param array $fieldValues The fieldValues to insert
* @param bool $isRelation TRUE if we are currently inserting into a relation table, FALSE by default
* @return int the UID of the inserted row
*/
public function addRow(string $tableName, array $fieldValues, bool $isRelation = false): int;
/**
* Updates a row in the storage
*
* @param string $tableName The database table name
* @param array $fieldValues The fieldValues to update
* @param bool $isRelation TRUE if we are currently inserting into a relation table, FALSE by default
*/
public function updateRow(string $tableName, array $fieldValues, bool $isRelation = false): void;
/**
* Updates a relation row in the storage
*
* @param string $tableName The database relation table name
* @param array $fieldValues The fieldValues to be updated
*/
public function updateRelationTableRow(string $tableName, array $fieldValues): void;
/**
* Deletes a row in the storage
*
* @param string $tableName The database table name
* @param array $where An array of where array('fieldname' => value). This array will be transformed to a WHERE clause
* @param bool $isRelation TRUE if we are currently inserting into a relation table, FALSE by default
*/
public function removeRow(string $tableName, array $where, bool $isRelation = false): void;
/**
* Returns the number of items matching the query.
*/
public function getObjectCountByQuery(QueryInterface $query): int;
/**
* Returns the object data matching the $query.
*/
public function getObjectDataByQuery(QueryInterface $query): array;
/**
* Checks if a Value Object equal to the given Object exists in the data base
*
* @param \TYPO3\CMS\Extbase\DomainObject\AbstractValueObject $object The Value Object
* @return int|null The matching uid if an object was found, else null
* @todo this is the last monster in this persistence series. refactor!
*/
public function getUidOfAlreadyPersistedValueObject(AbstractValueObject $object): ?int;
}
@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Extbase\Persistence\Generic\Storage\Exception;
use TYPO3\CMS\Extbase\Persistence\Generic\Exception;
/**
* A Bad Constraint exception
*/
class BadConstraintException extends Exception {}
@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Extbase\Persistence\Generic\Storage\Exception;
use TYPO3\CMS\Extbase\Persistence\Generic\Exception;
/**
* An SQL exception
*/
class SqlErrorException extends Exception {}
@@ -0,0 +1,699 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Extbase\Persistence\Generic\Storage;
use Doctrine\DBAL\Exception as DBALException;
use Doctrine\DBAL\ParameterType;
use Doctrine\DBAL\Types\Exception\TypesException;
use Doctrine\DBAL\Types\Type;
use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\Http\Message\ServerRequestInterface;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use TYPO3\CMS\Core\Cache\CacheTag;
use TYPO3\CMS\Core\Cache\Event\AddCacheTagEvent;
use TYPO3\CMS\Core\Context\Context;
use TYPO3\CMS\Core\Context\LanguageAspect;
use TYPO3\CMS\Core\Database\Connection;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Query\QueryBuilder;
use TYPO3\CMS\Core\Database\Query\Restriction\FrontendRestrictionContainer;
use TYPO3\CMS\Core\Database\Query\Restriction\WorkspaceRestriction;
use TYPO3\CMS\Core\Domain\Repository\PageRepository;
use TYPO3\CMS\Core\Http\ApplicationType;
use TYPO3\CMS\Core\Schema\Capability\TcaSchemaCapability;
use TYPO3\CMS\Core\Schema\TcaSchemaFactory;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Versioning\VersionState;
use TYPO3\CMS\Extbase\DomainObject\AbstractDomainObject;
use TYPO3\CMS\Extbase\DomainObject\AbstractValueObject;
use TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMapper;
use TYPO3\CMS\Extbase\Persistence\Generic\Qom\JoinInterface;
use TYPO3\CMS\Extbase\Persistence\Generic\Qom\SelectorInterface;
use TYPO3\CMS\Extbase\Persistence\Generic\Qom\SourceInterface;
use TYPO3\CMS\Extbase\Persistence\Generic\Qom\Statement;
use TYPO3\CMS\Extbase\Persistence\Generic\Query;
use TYPO3\CMS\Extbase\Persistence\Generic\Storage\Exception\BadConstraintException;
use TYPO3\CMS\Extbase\Persistence\Generic\Storage\Exception\SqlErrorException;
use TYPO3\CMS\Extbase\Persistence\QueryInterface;
use TYPO3\CMS\Extbase\Reflection\ReflectionService;
use TYPO3\CMS\Extbase\Service\CacheService;
use TYPO3\CMS\Frontend\Cache\CacheLifetimeCalculator;
/**
* A Storage backend
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
#[Autoconfigure(public: true)]
readonly class Typo3DbBackend implements BackendInterface
{
public function __construct(
protected CacheService $cacheService,
protected ConnectionPool $connectionPool,
protected ReflectionService $reflectionService,
protected EventDispatcherInterface $eventDispatcher,
protected CacheLifetimeCalculator $cacheLifetimeCalculator,
protected TcaSchemaFactory $tcaSchemaFactory,
#[Autowire(expression: 'service("features").isFeatureEnabled("frontend.cache.autoTagging")')]
protected bool $autoTagging,
) {}
/**
* Adds a row to the storage
*
* @param string $tableName The database table name
* @param array $fieldValues The row to be inserted
* @param bool $isRelation TRUE if we are currently inserting into a relation table, FALSE by default
* @return int The uid of the inserted row
* @throws SqlErrorException
*/
public function addRow(string $tableName, array $fieldValues, bool $isRelation = false): int
{
if (isset($fieldValues['uid'])) {
unset($fieldValues['uid']);
}
try {
$connection = $this->connectionPool->getConnectionForTable($tableName);
$connection->insert($tableName, $fieldValues, $this->getTypesForDataset($tableName, $fieldValues));
} catch (DBALException $e) {
throw new SqlErrorException($e->getMessage(), 1470230766, $e);
}
$uid = 0;
if (!$isRelation) {
// Relation tables have no auto_increment column, so no retrieval must be tried.
$uid = (int)$connection->lastInsertId();
$this->cacheService->clearCacheForRecord($tableName, $uid);
}
return $uid;
}
/**
* Updates a row in the storage
*
* @param string $tableName The database table name
* @param array $fieldValues The row to be updated
* @param bool $isRelation TRUE if we are currently inserting into a relation table, FALSE by default
* @throws \InvalidArgumentException
* @throws SqlErrorException
*/
public function updateRow(string $tableName, array $fieldValues, bool $isRelation = false): void
{
if (!isset($fieldValues['uid'])) {
throw new \InvalidArgumentException('The given row must contain a value for "uid".', 1476045164);
}
$uid = (int)$fieldValues['uid'];
unset($fieldValues['uid']);
try {
$connection = $this->connectionPool->getConnectionForTable($tableName);
$connection->update($tableName, $fieldValues, ['uid' => $uid], $this->getTypesForDataset($tableName, $fieldValues));
} catch (DBALException $e) {
throw new SqlErrorException($e->getMessage(), 1470230767, $e);
}
if (!$isRelation) {
$this->cacheService->clearCacheForRecord($tableName, $uid);
}
}
/**
* Updates a relation row in the storage.
*
* @param string $tableName The database relation table name
* @param array $fieldValues The row to be updated
* @throws SqlErrorException
* @throws \InvalidArgumentException
*/
public function updateRelationTableRow(string $tableName, array $fieldValues): void
{
if (!isset($fieldValues['uid_local']) && !isset($fieldValues['uid_foreign'])) {
throw new \InvalidArgumentException(
'The given fieldValues must contain a value for "uid_local" and "uid_foreign".',
1360500126
);
}
$where = [];
$where['uid_local'] = (int)$fieldValues['uid_local'];
$where['uid_foreign'] = (int)$fieldValues['uid_foreign'];
unset($fieldValues['uid_local']);
unset($fieldValues['uid_foreign']);
if (!empty($fieldValues['tablenames'])) {
$where['tablenames'] = $fieldValues['tablenames'];
unset($fieldValues['tablenames']);
}
if (!empty($fieldValues['fieldname'])) {
$where['fieldname'] = $fieldValues['fieldname'];
unset($fieldValues['fieldname']);
}
try {
$this->connectionPool->getConnectionForTable($tableName)->update(
$tableName,
$fieldValues,
$where,
$this->getTypesForDataset($tableName, $fieldValues),
);
} catch (DBALException $e) {
throw new SqlErrorException($e->getMessage(), 1470230768, $e);
}
}
/**
* Deletes a row in the storage
*
* @param string $tableName The database table name
* @param array $where An array of where array('fieldname' => value).
* @param bool $isRelation TRUE if we are currently manipulating a relation table, FALSE by default
* @throws SqlErrorException
*/
public function removeRow(string $tableName, array $where, bool $isRelation = false): void
{
try {
$this->connectionPool->getConnectionForTable($tableName)->delete($tableName, $where);
} catch (DBALException $e) {
throw new SqlErrorException($e->getMessage(), 1470230769, $e);
}
if (!$isRelation && isset($where['uid'])) {
$this->cacheService->clearCacheForRecord($tableName, (int)$where['uid']);
}
}
/**
* Returns the object data matching the $query.
*
* @throws SqlErrorException
*/
public function getObjectDataByQuery(QueryInterface $query): array
{
$statement = $query->getStatement();
// A custom query is needed for the language, so a custom context is cloned
/** @var Context $context */
$context = clone GeneralUtility::makeInstance(Context::class);
$context->setAspect('language', $query->getQuerySettings()->getLanguageAspect());
if ($statement instanceof Statement && !$statement->getStatement() instanceof QueryBuilder) {
$rows = $this->getObjectDataByRawQuery($statement);
} else {
$queryParser = GeneralUtility::makeInstance(Typo3DbQueryParser::class);
if ($statement instanceof Statement
&& $statement->getStatement() instanceof QueryBuilder
) {
$queryBuilder = $statement->getStatement();
} else {
$queryBuilder = $queryParser->convertQueryToDoctrineQueryBuilder($query);
}
$selectParts = $queryBuilder->getSelect();
if ($queryParser->isDistinctQuerySuggested() && !empty($selectParts)) {
$selectParts[0] = 'DISTINCT ' . $selectParts[0];
$queryBuilder->selectLiteral(...$selectParts);
}
if ($query->getOffset()) {
$queryBuilder->setFirstResult($query->getOffset());
}
if ($query->getLimit()) {
// Only set the "real" limit in LIVE workspace, as we do not need to make WS overlays here
// And can calculate with the direct result from the RDBMS without needing to calculate this in
// PHP (see below).
// What we do in workspace, is making a "best guess". Why do we do this? If we have content that
// is hidden in a workspace, we need to get the "next" record in line, but we cannot do this
// with overlays in SQL. So we use the "best guess" by adding twice the limit. Imagine you have
// 2000 news records, and we need to manually calculate the first 10 records, we just take 20 records
// from SQL and hope that this matches for "most" usecases (Pareto Principle).
if ($context->getAspect('workspace')->isLive()) {
$queryBuilder->setMaxResults($query->getLimit());
} else {
$queryBuilder->setMaxResults($query->getLimit() * 2);
}
}
try {
$rows = $queryBuilder->executeQuery()->fetchAllAssociative();
} catch (DBALException $e) {
throw new SqlErrorException($e->getMessage(), 1472074485, $e);
}
}
if (!empty($rows)) {
$rows = $this->overlayLanguageAndWorkspace($query->getSource(), $rows, $query, $context);
if ($this->autoTagging) {
$source = $query->getSource();
if ($source instanceof JoinInterface) {
$source = $source->getRight();
}
if (!$source instanceof SelectorInterface) {
throw new \RuntimeException(get_class($source) . ' must implement SelectorInterface at this point.', 1726753183);
}
$tableName = $source->getSelectorName();
$this->addCacheTagsForRows($tableName, $rows);
}
}
return $rows;
}
/**
* Returns the object data using a custom statement
*
* @throws SqlErrorException when the raw SQL statement fails in the database
*/
protected function getObjectDataByRawQuery(Statement $statement): array
{
$realStatement = $statement->getStatement();
$parameters = $statement->getBoundVariables();
// The real statement is an instance of the Doctrine DBAL QueryBuilder, so fetching
// this directly is possible
if ($realStatement instanceof QueryBuilder) {
try {
$result = $realStatement->executeQuery();
} catch (DBALException $e) {
throw new SqlErrorException($e->getMessage(), 1472064721, $e);
}
$rows = $result->fetchAllAssociative();
// Prepared Doctrine DBAL statement
} elseif ($realStatement instanceof \Doctrine\DBAL\Statement) {
try {
foreach ($parameters as $parameterIdentifier => $parameterValue) {
$realStatement->bindValue($parameterIdentifier, $parameterValue);
}
$result = $realStatement->executeQuery();
} catch (DBALException $e) {
throw new SqlErrorException($e->getMessage(), 1481281404, $e);
}
$rows = $result->fetchAllAssociative();
} else {
// Do a real raw query. This is very stupid, as it does not allow to use DBAL's real power if
// several tables are on different databases, so this is used with caution and could be removed
// in the future
try {
$connection = $this->connectionPool->getConnectionByName(ConnectionPool::DEFAULT_CONNECTION_NAME);
$statement = $connection->executeQuery($realStatement, $parameters);
} catch (DBALException $e) {
throw new SqlErrorException($e->getMessage(), 1472064775, $e);
}
$rows = $statement->fetchAllAssociative();
}
return $rows;
}
/**
* Returns the number of tuples matching the query.
*
* @return int The number of matching tuples
* @throws BadConstraintException
* @throws SqlErrorException
*/
public function getObjectCountByQuery(QueryInterface $query): int
{
if ($query->getConstraint() instanceof Statement) {
throw new BadConstraintException('Could not execute count on queries with a constraint of type TYPO3\\CMS\\Extbase\\Persistence\\Generic\\Qom\\Statement', 1256661045);
}
$statement = $query->getStatement();
if ($statement instanceof Statement
&& !$statement->getStatement() instanceof QueryBuilder
) {
$rows = $this->getObjectDataByQuery($query);
$count = count($rows);
} else {
$queryParser = GeneralUtility::makeInstance(Typo3DbQueryParser::class);
$queryBuilder = $queryParser
->convertQueryToDoctrineQueryBuilder($query)
->resetOrderBy();
if ($queryParser->isDistinctQuerySuggested()) {
$source = $queryBuilder->getFrom()[0];
// Tablename is already quoted for the DBMS, we need to treat table and field names separately
$tableName = $source->alias ?: $source->table;
$fieldName = $queryBuilder->quoteIdentifier('uid');
$queryBuilder
->resetGroupBy()
->selectLiteral(sprintf('COUNT(DISTINCT %s.%s)', $tableName, $fieldName));
} else {
$queryBuilder->count('*');
}
// Ensure to count only records in the current workspace
$context = GeneralUtility::makeInstance(Context::class);
$workspaceUid = (int)$context->getPropertyFromAspect('workspace', 'id');
$queryBuilder->getRestrictions()->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, $workspaceUid));
try {
$count = $queryBuilder->executeQuery()->fetchOne();
} catch (DBALException $e) {
throw new SqlErrorException($e->getMessage(), 1472074379, $e);
}
if ($query->getOffset()) {
$count -= $query->getOffset();
}
if ($query->getLimit()) {
$count = min($count, $query->getLimit());
}
}
return (int)max(0, $count);
}
/**
* Checks if a Value Object equal to the given Object exists in the database
*
* @param AbstractValueObject $object The Value Object
* @return int|null The matching uid if an object was found, else FALSE
* @throws SqlErrorException
*/
public function getUidOfAlreadyPersistedValueObject(AbstractValueObject $object): ?int
{
$className = get_class($object);
/** @var DataMapper $dataMapper */
$dataMapper = GeneralUtility::makeInstance(DataMapper::class);
$dataMap = $dataMapper->getDataMap($className);
$queryBuilder = $this->connectionPool->getQueryBuilderForTable($dataMap->tableName);
if (($GLOBALS['TYPO3_REQUEST'] ?? null) instanceof ServerRequestInterface
&& ApplicationType::fromRequest($GLOBALS['TYPO3_REQUEST'])->isFrontend()
) {
$queryBuilder->setRestrictions(GeneralUtility::makeInstance(FrontendRestrictionContainer::class));
}
$whereClause = [];
// loop over all properties of the object to exactly set the values of each database field
$classSchema = $this->reflectionService->getClassSchema($className);
foreach ($classSchema->getDomainObjectProperties() as $property) {
$propertyName = $property->getName();
// @todo We couple the Backend to the Entity implementation (uid, isClone); changes there breaks this method
if ($dataMap->isPersistableProperty($propertyName) && $propertyName !== AbstractDomainObject::PROPERTY_UID && $propertyName !== AbstractDomainObject::PROPERTY_PID && $propertyName !== 'isClone') {
$propertyValue = $object->_getProperty($propertyName);
$columnMap = $dataMap->getColumnMap($propertyName);
$fieldName = $columnMap->columnName;
if ($propertyValue === null) {
$whereClause[] = $queryBuilder->expr()->isNull($fieldName);
} else {
$whereClause[] = $queryBuilder->expr()->eq($fieldName, $queryBuilder->createNamedParameter($dataMapper->getPlainValue($propertyValue, $columnMap)));
}
}
}
$queryBuilder
->select('uid')
->from($dataMap->tableName)
->where(...$whereClause);
try {
$uid = (int)$queryBuilder
->executeQuery()
->fetchOne();
if ($uid > 0) {
return $uid;
}
return null;
} catch (DBALException $e) {
throw new SqlErrorException($e->getMessage(), 1470231748, $e);
}
}
/**
* Performs workspace and language overlay on the given row array. The language and workspace id is automatically
* detected (depending on FE or BE context). You can also explicitly set the language/workspace id.
*/
protected function overlayLanguageAndWorkspace(SourceInterface $source, array $rows, QueryInterface $query, Context $context): array
{
$workspaceUid = (int)$context->getPropertyFromAspect('workspace', 'id');
$pageRepository = GeneralUtility::makeInstance(PageRepository::class, $context);
if ($source instanceof SelectorInterface) {
$tableName = $source->getSelectorName();
$rows = $this->resolveMovedRecordsInWorkspace($tableName, $rows, $workspaceUid);
return $this->overlayLanguageAndWorkspaceForSelect($tableName, $rows, $pageRepository, $query, $context);
}
if ($source instanceof JoinInterface) {
$tableName = $source->getRight()->getSelectorName();
// Special handling of joined select is only needed when doing workspace overlays, which does not happen
// in live workspace
if ($workspaceUid === 0) {
return $this->overlayLanguageAndWorkspaceForSelect($tableName, $rows, $pageRepository, $query, $context);
}
return $this->overlayLanguageAndWorkspaceForJoinedSelect($tableName, $rows, $pageRepository, $query, $context);
}
// No proper source, so we do not have a table name here
// we cannot do an overlay and return the original rows instead.
return $rows;
}
/**
* If the result is a plain SELECT (no JOIN) then the regular overlay process works for tables
* - overlay workspace
* - overlay language of versioned record again
*/
protected function overlayLanguageAndWorkspaceForSelect(string $tableName, array $rows, PageRepository $pageRepository, QueryInterface $query, Context $context): array
{
$limit = 0;
$overlaidRows = [];
$countOverlaidRows = 0;
if ($query->getLimit() && !$context->getAspect('workspace')->isLive()) {
$limit = $query->getLimit();
}
foreach ($rows as $row) {
$row = $this->overlayLanguageAndWorkspaceForSingleRecord($tableName, $row, $pageRepository, $query);
if (is_array($row)) {
$overlaidRows[] = $row;
$countOverlaidRows++;
// We need to calculate the number of overlaid rows manually in PHP
// (via the is_array() above), because some overlays do not exist in a Workspace
if ($limit === $countOverlaidRows) {
return $overlaidRows;
}
}
}
return $overlaidRows;
}
/**
* If the result consists of a JOIN (usually happens if a property is a relation with a MM table) then it is necessary
* to only do overlays for the fields that are contained in the main database table, otherwise a SQL error is thrown.
* In order to make this happen, a single SQL query is made to fetch all possible field names (= array keys) of
* a record (TCA[$tableName][columns] does not contain all needed information), which is then used to compute
* a separate subset of the row which can be overlaid properly.
*/
protected function overlayLanguageAndWorkspaceForJoinedSelect(string $tableName, array $rows, PageRepository $pageRepository, QueryInterface $query, Context $context): array
{
// No valid rows, so this is skipped
if (!isset($rows[0]['uid'])) {
return $rows;
}
$limit = 0;
$overlaidRows = [];
$countOverlaidRows = 0;
if ($query->getLimit() && !$context->getAspect('workspace')->isLive()) {
$limit = $query->getLimit();
}
// First, find out the fields that belong to the "main" selected table which is defined by TCA, and take the first
// record to find out all possible fields in this database table
$fieldsOfMainTable = $pageRepository->getRawRecord($tableName, (int)$rows[0]['uid']);
if (is_array($fieldsOfMainTable)) {
foreach ($rows as $row) {
$mainRow = array_intersect_key($row, $fieldsOfMainTable);
$joinRow = array_diff_key($row, $mainRow);
$mainRow = $this->overlayLanguageAndWorkspaceForSingleRecord($tableName, $mainRow, $pageRepository, $query);
if (is_array($mainRow)) {
$overlaidRows[] = array_replace($joinRow, $mainRow);
$countOverlaidRows++;
// We need to calculate the number of overlaid rows manually in PHP
// (via the is_array() above), because some overlays do not exist in a Workspace
if ($limit === $countOverlaidRows) {
return $overlaidRows;
}
}
}
}
return $overlaidRows;
}
/**
* Takes one specific row, as defined in TCA and does all overlays.
*
* @return array|int|mixed|null the overlaid row or false or null if overlay failed.
*/
protected function overlayLanguageAndWorkspaceForSingleRecord(string $tableName, array $row, PageRepository $pageRepository, QueryInterface $query)
{
$querySettings = $query->getQuerySettings();
$languageAspect = $querySettings->getLanguageAspect();
$languageUid = $languageAspect->getContentId();
$schema = $this->tcaSchemaFactory->get($tableName);
$languageOfCurrentRecord = 0;
$languageField = null;
$translationParentPointerField = null;
// If current row is a translation select its parent
if ($schema->isLanguageAware()) {
$languageCapability = $schema->getCapability(TcaSchemaCapability::Language);
$languageField = $languageCapability->getLanguageField()->getName();
$translationParentPointerField = $languageCapability->getTranslationOriginPointerField()->getName();
}
if ($languageField && ($row[$languageField] ?? false)) {
$languageOfCurrentRecord = $row[$languageField];
}
// Note #1: In case of ->findByUid([uid-of-translated-record]) the translated record should be fetched at all times
// Example: you've fetched a translation directly via findByUid(11) which is a translated record, but the
// request was to do overlays. In this case, the default record is loaded again, and then reapplied again.
// Note #2: We cannot use $languageAspect->doOverlays() as it also checks for ID > 0
$fetchLocalizedRecord = $languageAspect->getOverlayType() !== LanguageAspect::OVERLAYS_OFF;
// We have a translated record from the DB, but we do overlays, so let's take the default language record
// and do overlays again later-on
if ($languageOfCurrentRecord > 0
&& $fetchLocalizedRecord
&& ($row[$translationParentPointerField] ?? 0) > 0
) {
$row = $pageRepository->getRawRecord(
$tableName,
(int)$row[$translationParentPointerField]
);
$languageUid = $languageOfCurrentRecord;
}
// Handle workspace overlays
$pageRepository->versionOL($tableName, $row, true, $querySettings->getIgnoreEnableFields());
if (is_array($row) && $fetchLocalizedRecord) {
if ($tableName === 'pages') {
$row = $pageRepository->getLanguageOverlay($tableName, $row);
} else {
if (!$querySettings->getRespectSysLanguage()
&& $languageOfCurrentRecord > 0
&& (!$query instanceof Query || !$query->getParentQuery())
) {
// No parent query means we're processing the aggregate root.
// respectSysLanguage is false which means that records returned by the query
// might be from different languages (which is desired).
// So we must set the language used for overlay to the language of the current record
$languageUid = $languageOfCurrentRecord;
}
if ($translationParentPointerField
&& ($row[$translationParentPointerField] ?? 0) > 0
&& $languageOfCurrentRecord > 0
) {
// Force overlay by faking default language record, as getRecordOverlay can only handle default language records
$row['uid'] = $row[$translationParentPointerField];
$row[$languageField] = 0;
}
// The overlay type (and fallback chain) of the language aspect is respected, so translation
// behavior is consistent with the regular page / content rendering. The content language
// however may have been adjusted above to the language of the actually fetched record
// (see Note #1 and the respectSysLanguage handling), so a custom aspect is passed here.
$customLanguageAspect = new LanguageAspect(
$languageAspect->getId(),
$languageUid,
$languageAspect->getOverlayType(),
$languageAspect->getFallbackChain()
);
$row = $pageRepository->getLanguageOverlay($tableName, $row, $customLanguageAspect);
}
} elseif (is_array($row)) {
// If an already localized record is fetched, the "uid" of the default language is used
// as the record is re-fetched in the DataMapper
if ($translationParentPointerField
&& ($row[$translationParentPointerField] ?? 0) > 0
&& $languageOfCurrentRecord > 0
) {
$row['_LOCALIZED_UID'] = (int)$row['uid'];
$row['uid'] = $row[$translationParentPointerField];
}
}
return $row;
}
/**
* Fetches the moved record in case it is supported
* by the table and if there's only one row in the result set
* (applying this to all rows does not work, since the sorting
* order would be destroyed and possible limits are not met anymore)
* The move pointers are later unset (see versionOL() last argument)
*/
protected function resolveMovedRecordsInWorkspace(string $tableName, array $rows, int $workspaceUid): array
{
if ($workspaceUid === 0) {
return $rows;
}
if (!$this->tcaSchemaFactory->has($tableName) || !$this->tcaSchemaFactory->get($tableName)->hasCapability(TcaSchemaCapability::Workspace)) {
return $rows;
}
if (count($rows) !== 1) {
return $rows;
}
$queryBuilder = $this->connectionPool->getQueryBuilderForTable($tableName);
$queryBuilder->getRestrictions()->removeAll();
$movedRecords = $queryBuilder
->select('*')
->from($tableName)
->where(
$queryBuilder->expr()->eq('t3ver_state', $queryBuilder->createNamedParameter(VersionState::MOVE_POINTER->value, Connection::PARAM_INT)),
$queryBuilder->expr()->eq('t3ver_wsid', $queryBuilder->createNamedParameter($workspaceUid, Connection::PARAM_INT)),
$queryBuilder->expr()->eq('t3ver_oid', $queryBuilder->createNamedParameter($rows[0]['uid'], Connection::PARAM_INT))
)
->setMaxResults(1)
->executeQuery()
->fetchAllAssociative();
if (!empty($movedRecords)) {
$rows = $movedRecords;
}
return $rows;
}
protected function addCacheTagsForRows(string $tableName, array $rows): void
{
foreach ($rows as $row) {
$lifetime = $this->cacheLifetimeCalculator->calculateLifetimeForRow($tableName, $row);
$this->eventDispatcher->dispatch(
new AddCacheTagEvent(
new CacheTag(sprintf('%s_%s', $tableName, ($row['uid'] ?? 0)), $lifetime)
)
);
}
}
/**
* @param array<string, mixed> $fieldValues
* @return array<string, Type|ParameterType>
*/
private function getTypesForDataset(string $tableName, array $fieldValues): array
{
$connection = $this->connectionPool->getConnectionForTable($tableName);
$tableInfo = $connection->getSchemaInformation()->getTableInfo($tableName);
$types = [];
foreach ($fieldValues as $key => $value) {
if (!$tableInfo->hasColumnInfo($key)) {
// Field is not part of the database schema information, therefore no type is set here and
// Doctrine DBAL handles the value with its default binding type (ParameterType::STRING).
continue;
}
try {
// `ColumnInfo->getType()` returns the Doctrine type (e.g. JsonType), which carries the
// `PHP value <-> database value` conversion methods applied by Doctrine DBAL. Each Doctrine
// type maps to a plain binding type (e.g. ParameterType::STRING for VARCHAR/CHAR/TEXT/...),
// which binds the value as-is without applying any conversion. Extbase already performs that
// conversion itself, which is why the plain binding type is enforced here. This additionally
// prevents `Connection::ensureDatabaseValueTypes()` from adding the Doctrine type looked up
// from the database schema.
$types[$key] = $tableInfo->getColumnInfo($key)->getType()->getBindingType();
} catch (TypesException) {
// Ignore, no type to be set
}
}
return $types;
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,240 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Extbase\Persistence\Generic;
use Psr\Http\Message\ServerRequestInterface;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
use TYPO3\CMS\Core\Context\Context;
use TYPO3\CMS\Core\Context\LanguageAspect;
use TYPO3\CMS\Core\Http\ApplicationType;
use TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface;
/**
* Query settings, reflects the settings unique to TYPO3 CMS.
*
* The settings stored in this class are used by Extbase's persistence layer to control which data
* is retrieved from the database.
* It is possible for each Query to have a dedicated Typo3QuerySettings object, but those settings
* are not adhered to when reconstituting relations of entity objects. There a completely new
* Typo3QuerySettings object is used, with default settings applied.
* While this alone already may cause unexpected behavior, it is even worse when considering
* the contexts of TYPO3: Frontend, backend and CLI.
* Due to the nature of TYPO3, the enable-fields must be ignored in backend context, as an editor
* needs to see all records, regardless whether those are publicly (frontend) visible or not.
* This class has therefore the responsitiblity to provide different defaults, depending on the context.
* Unfortunately, the only way to determine the current context is by relying on the **global** request object.
*
* @todo: Future improvements should split this class into dedicated classes for backend, frontend (and possibly CLI) context.
* Furthermore, it must be re-evaluated if the object reconstitution within DataMapper should actually inherit
* the query settings the initial query was based upon.
*/
#[Autoconfigure(public: true, shared: false)]
class Typo3QuerySettings implements QuerySettingsInterface
{
protected ConfigurationManagerInterface $configurationManager;
protected Context $context;
/**
* Flag if the storage page should be respected for the query.
*/
protected bool $respectStoragePage = true;
/**
* the pid(s) of the storage page(s) that should be respected for the query.
*/
protected array $storagePageIds = [];
/**
* A flag indicating whether all or some enable fields should be ignored. If TRUE, all enable fields are ignored.
* If--in addition to this--enableFieldsToBeIgnored is set, only fields specified there are ignored. If FALSE, all
* enable fields are taken into account, regardless of the enableFieldsToBeIgnored setting.
*/
protected bool $ignoreEnableFields = false;
/**
* An array of column names in the enable columns array (array keys in $GLOBALS['TCA'][$table]['ctrl']['enablecolumns']),
* to be ignored while building the query statement
*/
protected array $enableFieldsToBeIgnored = [];
/**
* Flag whether deleted records should be included in the result set.
*/
protected bool $includeDeleted = false;
/**
* Flag if the sys_language_uid should be respected (default is TRUE).
*/
protected bool $respectSysLanguage = true;
protected LanguageAspect $languageAspect;
public function __construct(
Context $context,
ConfigurationManagerInterface $configurationManager
) {
// QuerySettings should always keep its own Context, as they can differ
// Currently this is only used for reading, but might be improved in the future
$this->context = clone $context;
$this->configurationManager = $configurationManager;
$this->languageAspect = $this->context->getAspect('language');
// see note in class' phpdoc about this condition
if (($GLOBALS['TYPO3_REQUEST'] ?? null) instanceof ServerRequestInterface
&& ApplicationType::fromRequest($GLOBALS['TYPO3_REQUEST'])->isBackend()
) {
$this->setIgnoreEnableFields(true);
}
}
/**
* Sets the flag if the storage page should be respected for the query.
*
* @param bool $respectStoragePage If TRUE the storage page ID will be determined and the statement will be extended accordingly.
*/
public function setRespectStoragePage(bool $respectStoragePage): QuerySettingsInterface
{
$this->respectStoragePage = $respectStoragePage;
return $this;
}
/**
* Returns the state, if the storage page should be respected for the query.
*
* @return bool TRUE, if the storage page should be respected; otherwise FALSE.
*/
public function getRespectStoragePage(): bool
{
return $this->respectStoragePage;
}
/**
* Sets the pid(s) of the storage page(s) that should be respected for the query.
*
* @param array $storagePageIds If given the storage page IDs will be determined and the statement will be extended accordingly.
*/
public function setStoragePageIds(array $storagePageIds): self
{
$this->storagePageIds = $storagePageIds;
return $this;
}
/**
* Returns the pid(s) of the storage page(s) that should be respected for the query.
*
* @return array list of integers that each represent a storage page id
*/
public function getStoragePageIds(): array
{
return $this->storagePageIds;
}
/**
* @param bool $respectSysLanguage TRUE if TYPO3 language settings are to be applied
*/
public function setRespectSysLanguage(bool $respectSysLanguage): self
{
$this->respectSysLanguage = $respectSysLanguage;
return $this;
}
/**
* @return bool TRUE if TYPO3 language settings are to be applied
*/
public function getRespectSysLanguage(): bool
{
return $this->respectSysLanguage;
}
public function getLanguageAspect(): LanguageAspect
{
return $this->languageAspect;
}
public function setLanguageAspect(LanguageAspect $languageAspect): self
{
$this->languageAspect = $languageAspect;
return $this;
}
/**
* Sets a flag indicating whether all or some enable fields should be ignored. If TRUE, all enable fields are ignored.
* If--in addition to this--enableFieldsToBeIgnored is set, only fields specified there are ignored. If FALSE, all
* enable fields are taken into account, regardless of the enableFieldsToBeIgnored setting.
*
* @see setEnableFieldsToBeIgnored()
*/
public function setIgnoreEnableFields(bool $ignoreEnableFields): self
{
$this->ignoreEnableFields = $ignoreEnableFields;
return $this;
}
/**
* The returned value indicates whether all or some enable fields should be ignored.
*
* If TRUE, all enable fields are ignored. If--in addition to this--enableFieldsToBeIgnored is set, only fields specified there are ignored.
* If FALSE, all enable fields are taken into account, regardless of the enableFieldsToBeIgnored setting.
*
* @see getEnableFieldsToBeIgnored()
*/
public function getIgnoreEnableFields(): bool
{
return $this->ignoreEnableFields;
}
/**
* An array of column names in the enable columns array (array keys in $GLOBALS['TCA'][$table]['ctrl']['enablecolumns']),
* to be ignored while building the query statement. Adding a column name here effectively switches off filtering
* by this column. This setting is only taken into account if $this->ignoreEnableFields = TRUE.
*
* @see setIgnoreEnableFields()
*/
public function setEnableFieldsToBeIgnored(array $enableFieldsToBeIgnored): self
{
$this->enableFieldsToBeIgnored = $enableFieldsToBeIgnored;
return $this;
}
/**
* An array of column names in the enable columns array (array keys in $GLOBALS['TCA'][$table]['ctrl']['enablecolumns']),
* to be ignored while building the query statement.
*
* @see getIgnoreEnableFields()
*/
public function getEnableFieldsToBeIgnored(): array
{
return $this->enableFieldsToBeIgnored;
}
/**
* Sets the flag if the query should return objects that are deleted.
*/
public function setIncludeDeleted(bool $includeDeleted): self
{
$this->includeDeleted = $includeDeleted;
return $this;
}
/**
* Returns if the query should return objects that are deleted.
*/
public function getIncludeDeleted(): bool
{
return $this->includeDeleted;
}
}
@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Extbase\Persistence;
/**
* An interface how to monitor changes on an object and its properties. All domain objects which should be persisted need to implement the below interface.
*
* @see \TYPO3\CMS\Extbase\DomainObject\AbstractEntity
* @see \TYPO3\CMS\Extbase\DomainObject\AbstractValueObject
*/
interface ObjectMonitoringInterface
{
/**
* Register an object's clean state, e.g. after it has been reconstituted
* from the database
*
* @param non-empty-string|null $propertyName
*/
public function _memorizeCleanState(?string $propertyName = null): void;
/**
* Returns TRUE if the properties were modified after reconstitution
*
* @param non-empty-string|null $propertyName
*/
public function _isDirty(?string $propertyName = null): bool;
}
+351
View File
@@ -0,0 +1,351 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Extbase\Persistence;
use TYPO3\CMS\Core\Utility\MathUtility;
use TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface;
/**
* The storage for objects. It ensures the uniqueness of an object in the storage. It's a remake of the
* `SplObjectStorage` introduced in PHP 5.3.
*
* Opposed to the `SplObjectStorage`, the `ObjectStorage` does not implement the `Serializable` interface.
*
* @template TEntity of object
* @implements \ArrayAccess<string, TEntity>
* @implements \Iterator<string, TEntity>
*/
class ObjectStorage implements \Countable, \Iterator, \ArrayAccess, ObjectMonitoringInterface
{
/**
* This field is only needed to make debugging easier:
*
* If you call `current()` on a class that implements `Iterator`, PHP will return the first field of the object
* instead of calling the `current()` method of the interface.
*
* We use this unusual behavior of PHP to return the warning below in this case.
*/
private string $warning = 'You should never see this warning. If you do, you probably used PHP array functions like current() on the TYPO3\\CMS\\Extbase\\Persistence\\ObjectStorage. To retrieve the first result, you can use the rewind() and current() methods.';
/**
* An array holding the objects and the stored information. The key of the array items ist the
* spl_object_hash of the given object.
*
* ```php
* [
* 'spl_object_hash' => [
* 'obj' => $object,
* 'inf' => $information,
* ],
* ]
* ```
*/
protected array $storage = [];
/**
* A flag indication if the object storage was modified after reconstitution (e.g., by adding a new object)
*/
protected bool $isModified = false;
/**
* An array holding the internal position the object was added.
*
* The object entry is unset when the object gets removed from the object storage.
*/
protected array $addedObjectsPositions = [];
/**
* An array holding the internal position the object was added before, when it would
* be removed from the object storage.
*/
protected array $removedObjectsPositions = [];
/**
* An internal variable holding the count of added objects to be stored as position.
*
* It will be reset when all objects are be removed from the object storage.
*/
protected int $positionCounter = 0;
/**
* Rewinds the iterator to the first storage element.
*/
public function rewind(): void
{
reset($this->storage);
}
/**
* Checks if the array pointer of the storage points to a valid position.
*/
public function valid(): bool
{
return current($this->storage) !== false;
}
/**
* Returns the index at which the iterator currently is.
*
* This is different from `SplObjectStorage` as the key in this implementation is the object hash (string).
*
* @return string The index corresponding to the position of the iterator.
*/
public function key(): string
{
return key($this->storage);
}
/**
* Returns the current storage entry.
*
* @return TEntity|null The object at the current iterator position.
*/
public function current(): ?object
{
$item = current($this->storage);
return $item['obj'] ?? null;
}
/**
* Moves to the next entry.
*/
public function next(): void
{
next($this->storage);
}
/**
* Returns the number of objects in the storage.
*
* @return 0|positive-int The number of objects in the storage.
*/
public function count(): int
{
return count($this->storage);
}
/**
* Associates information to an object in the storage. `offsetSet()` is an alias of `attach()`.
*
* @param TEntity|string|null $object The object to add.
* @param mixed $information The information to associate with the object.
*/
public function offsetSet(mixed $object, mixed $information): void
{
$this->isModified = true;
$this->storage[spl_object_hash($object)] = ['obj' => $object, 'inf' => $information];
$this->positionCounter++;
$this->addedObjectsPositions[spl_object_hash($object)] = $this->positionCounter;
}
/**
* Checks whether an object exists in the storage.
*
* @param TEntity|int|string $value The object to look for, or the key in the storage.
*/
public function offsetExists(mixed $value): bool
{
return (is_object($value) && isset($this->storage[spl_object_hash($value)]))
|| (MathUtility::canBeInterpretedAsInteger($value) && isset(array_values($this->storage)[$value]));
}
/**
* Removes an object from the storage. `offsetUnset()` is an alias of `detach()`.
*
* @param TEntity|int|string $value The object to remove, or its key in the storage.
*/
public function offsetUnset(mixed $value): void
{
$this->isModified = true;
$object = $value;
if (MathUtility::canBeInterpretedAsInteger($value)) {
$object = $this->offsetGet($value);
}
unset($this->storage[spl_object_hash($object)]);
if (empty($this->storage)) {
$this->positionCounter = 0;
}
$this->removedObjectsPositions[spl_object_hash($object)] = $this->addedObjectsPositions[spl_object_hash($object)] ?? null;
unset($this->addedObjectsPositions[spl_object_hash($object)]);
}
/**
* Returns the information associated with an object, or the object itself if an integer is passed.
*
* @param TEntity|int|string $value The object to look for, or its key in the storage.
* @return mixed The information associated with an object in the storage, or the object itself if an integer is passed.
*/
public function offsetGet(mixed $value): mixed
{
if (MathUtility::canBeInterpretedAsInteger($value)) {
return array_values($this->storage)[$value]['obj'] ?? null;
}
/** @var DomainObjectInterface $value */
return $this->storage[spl_object_hash($value)]['inf'] ?? null;
}
/**
* Checks if the storage contains a specific object.
*
* @param TEntity $object The object to look for.
*/
public function contains(object $object): bool
{
return $this->offsetExists($object);
}
/**
* Adds an object in the storage, and optionally associate it to some information.
*
* @param TEntity $object The object to add.
* @param mixed $information The information to associate with the object.
*/
public function attach(object $object, mixed $information = null): void
{
$this->offsetSet($object, $information);
}
/**
* Removes an object from the storage.
*
* @param TEntity $object The object to remove.
*/
public function detach(object $object): void
{
$this->offsetUnset($object);
}
/**
* Returns the information associated with the object pointed by the current iterator position.
*
* @return mixed The information associated with the current iterator position.
*/
public function getInfo(): mixed
{
$item = current($this->storage);
return $item['inf'] ?? null;
}
/**
* Associates information with the object currently pointed to by the iterator.
*/
public function setInfo(mixed $information): void
{
$this->isModified = true;
$key = key($this->storage);
$this->storage[$key]['inf'] = $information;
}
/**
* Adds all object-information pairs from a different storage in the current storage.
*
* @param ObjectStorage<TEntity> $storage
*/
public function addAll(ObjectStorage $storage): void
{
foreach ($storage as $object) {
$this->attach($object, $storage->getInfo());
}
}
/**
* Removes objects contained in another storage from the current storage.
*
* @param ObjectStorage<TEntity> $storage The storage containing the elements to remove.
*/
public function removeAll(ObjectStorage $storage): void
{
foreach ($storage as $object) {
$this->detach($object);
}
}
/**
* Returns this object storage as an array.
*
* @return list<TEntity>
*/
public function toArray(): array
{
$array = [];
$storage = array_values($this->storage);
foreach ($storage as $item) {
$array[] = $item['obj'];
}
return $array;
}
/**
* Alias of `toArray` which allows that method to be used from contexts which support
* for example dotted paths, e.g., `ObjectAccess::getPropertyPath($object, 'children.array.123')`
* to get exactly the 123rd item in the `children` property which is an `ObjectStorage`.
*
* @return list<TEntity>
*/
public function getArray(): array
{
return $this->toArray();
}
/**
* Register the storage's clean state, e.g., after it has been reconstituted from the database.
*
* @param non-empty-string|null $propertyName
*/
public function _memorizeCleanState(?string $propertyName = null): void
{
$this->isModified = false;
}
/**
* Returns `true` if the storage was modified after reconstitution.
*
* @param non-empty-string|null $propertyName
*/
public function _isDirty(?string $propertyName = null): bool
{
return $this->isModified;
}
/**
* Returns `true` if an object was added, then removed and added at a different position.
*/
public function isRelationDirty(object $object): bool
{
return isset($this->addedObjectsPositions[spl_object_hash($object)])
&& isset($this->removedObjectsPositions[spl_object_hash($object)])
&& ($this->addedObjectsPositions[spl_object_hash($object)] !== $this->removedObjectsPositions[spl_object_hash($object)]);
}
public function getPosition(object $object): ?int
{
if (!isset($this->addedObjectsPositions[spl_object_hash($object)])) {
return null;
}
return $this->addedObjectsPositions[spl_object_hash($object)];
}
}
@@ -0,0 +1,118 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Extbase\Persistence;
/**
* The Extbase Persistence Manager interface
*/
interface PersistenceManagerInterface
{
/**
* Commits new objects and changes to objects in the current persistence
* session into the backend
*/
public function persistAll(): void;
/**
* Clears the in-memory state of the persistence.
*
* Managed instances become detached, any fetches will
* return data directly from the persistence "backend".
*/
public function clearState(): void;
/**
* Checks if the given object has ever been persisted.
*
* @param object $object The object to check
* @return bool TRUE if the object is new, FALSE if the object exists in the repository
*/
public function isNewObject(object $object): bool;
/**
* Returns the (internal) identifier for the object, if it is known to the
* backend. Otherwise NULL is returned.
*
* Note: this returns an identifier even if the object has not been
* persisted in case of AOP-managed entities. Use isNewObject() if you need
* to distinguish those cases.
*
* @param object $object
* @return string|null The identifier for the object if it is known, or NULL
*/
public function getIdentifierByObject(object $object): ?string;
/**
* Returns the object with the (internal) identifier, if it is known to the
* backend. Otherwise NULL is returned.
*
* @param bool $useLazyLoading Set to TRUE if you want to use lazy loading for this object
* @return object|null The object for the identifier if it is known, or NULL
*/
public function getObjectByIdentifier(string|int $identifier, ?string $objectType = null, bool $useLazyLoading = false): ?object;
/**
* Returns the number of records matching the query.
*/
public function getObjectCountByQuery(QueryInterface $query): int;
/**
* Returns the object data matching the $query.
*/
public function getObjectDataByQuery(QueryInterface $query): array;
/**
* Registers a repository
*
* @param string $className The class name of the repository to be registered
*/
public function registerRepositoryClassName(string $className): void;
/**
* Adds an object to the persistence.
*
* @param object $object The object to add
*/
public function add(object $object): void;
/**
* Removes an object to the persistence.
*
* @param object $object The object to remove
*/
public function remove(object $object): void;
/**
* Update an object in the persistence.
*
* @param object $object The modified object
* @throws \TYPO3\CMS\Extbase\Persistence\Exception\UnknownObjectException
*/
public function update(object $object): void;
/**
* Return a query object for the given type.
*
* @internal only to be used within Extbase, not part of TYPO3 Core API
* .
* @template T of object
* @param class-string<T> $type
* @return QueryInterface<T>
*/
public function createQueryForType(string $type): QueryInterface;
}
+399
View File
@@ -0,0 +1,399 @@
<?php
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Extbase\Persistence;
use TYPO3\CMS\Extbase\Persistence\Generic\Qom\AndInterface;
use TYPO3\CMS\Extbase\Persistence\Generic\Qom\CoalesceInterface;
use TYPO3\CMS\Extbase\Persistence\Generic\Qom\ComparisonInterface;
use TYPO3\CMS\Extbase\Persistence\Generic\Qom\ConcatInterface;
use TYPO3\CMS\Extbase\Persistence\Generic\Qom\ConstraintInterface;
use TYPO3\CMS\Extbase\Persistence\Generic\Qom\DynamicOperandInterface;
use TYPO3\CMS\Extbase\Persistence\Generic\Qom\OrInterface;
use TYPO3\CMS\Extbase\Persistence\Generic\Qom\SourceInterface;
use TYPO3\CMS\Extbase\Persistence\Generic\Qom\TrimInterface;
use TYPO3\CMS\Extbase\Persistence\Generic\QuerySettingsInterface;
/**
* A persistence query interface
* @template T of object
*/
interface QueryInterface
{
/**
* The '=' comparison operator.
*/
public const OPERATOR_EQUAL_TO = 1;
/**
* For NULL we have to use 'IS' instead of '='
*/
public const OPERATOR_EQUAL_TO_NULL = 101;
/**
* The '!=' comparison operator.
*/
public const OPERATOR_NOT_EQUAL_TO = 2;
/**
* For NULL we have to use 'IS NOT' instead of '!='
*/
public const OPERATOR_NOT_EQUAL_TO_NULL = 202;
/**
* The '<' comparison operator.
*/
public const OPERATOR_LESS_THAN = 3;
/**
* The '<=' comparison operator.
*/
public const OPERATOR_LESS_THAN_OR_EQUAL_TO = 4;
/**
* The '>' comparison operator.
*/
public const OPERATOR_GREATER_THAN = 5;
/**
* The '>=' comparison operator.
*/
public const OPERATOR_GREATER_THAN_OR_EQUAL_TO = 6;
/**
* The 'like' comparison operator.
*/
public const OPERATOR_LIKE = 7;
/**
* The 'contains' comparison operator for collections.
*/
public const OPERATOR_CONTAINS = 8;
/**
* The 'in' comparison operator.
*/
public const OPERATOR_IN = 9;
/**
* The 'is NULL' comparison operator.
*/
public const OPERATOR_IS_NULL = 10;
/**
* The 'is empty' comparison operator for collections.
*/
public const OPERATOR_IS_EMPTY = 11;
/**
* Constants representing the direction when ordering result sets.
*/
public const ORDER_ASCENDING = 'ASC';
public const ORDER_DESCENDING = 'DESC';
/**
* Gets the node-tuple source for this query.
*
* @return SourceInterface
* @todo: Set SourceInterface as return type.
*/
public function getSource();
/**
* Executes the query and returns the result.
*
* @param bool $returnRawQueryResult avoids the object mapping by the persistence
* @return QueryResultInterface|list<array<string,mixed>> The query result object or an array if $returnRawQueryResult is TRUE
* @phpstan-return ($returnRawQueryResult is true ? list<array<string,mixed>> : QueryResultInterface<int,T>)
*/
public function execute($returnRawQueryResult = false);
/**
* Sets the property names to order the result by. Expected like this:
* array(
* 'foo' => \TYPO3\CMS\Extbase\Persistence\QueryInterface::ORDER_ASCENDING,
* 'bar' => \TYPO3\CMS\Extbase\Persistence\QueryInterface::ORDER_DESCENDING
* )
*
* @param array<string,string> $orderings The property names to order by
* @return \TYPO3\CMS\Extbase\Persistence\QueryInterface
* @phpstan-return QueryInterface<T>
*/
public function setOrderings(array $orderings);
/**
* Sets the ordering for the result by a single operand. Replaces any existing orderings.
*
* @param string|DynamicOperandInterface $operand The property name or a dynamic operand (e.g., concat(), trim())
* @param string $order The order direction (QueryInterface::ORDER_ASCENDING or ORDER_DESCENDING)
* @return \TYPO3\CMS\Extbase\Persistence\QueryInterface
* @phpstan-return QueryInterface<T>
*/
public function orderBy(string|DynamicOperandInterface $operand, string $order = self::ORDER_ASCENDING);
/**
* Adds an ordering for the result. Appends to any existing orderings.
*
* @param string|DynamicOperandInterface $operand The property name or a dynamic operand (e.g., concat(), trim())
* @param string $order The order direction (QueryInterface::ORDER_ASCENDING or ORDER_DESCENDING)
* @return \TYPO3\CMS\Extbase\Persistence\QueryInterface
* @phpstan-return QueryInterface<T>
*/
public function addOrderBy(string|DynamicOperandInterface $operand, string $order = self::ORDER_ASCENDING);
/**
* Creates a CONCAT expression for ordering.
*
* @param string|DynamicOperandInterface ...$operands Property names or operand objects to concatenate
*/
public function concat(string|DynamicOperandInterface ...$operands): ConcatInterface;
/**
* Creates a TRIM expression for ordering.
*
* @param string|DynamicOperandInterface $operand The property name or operand to trim
*/
public function trim(string|DynamicOperandInterface $operand): TrimInterface;
/**
* Creates a COALESCE expression for ordering.
*
* @param string|DynamicOperandInterface ...$operands Property names or operand objects
*/
public function coalesce(string|DynamicOperandInterface ...$operands): CoalesceInterface;
/**
* Sets the maximum size of the result set to limit. Returns $this to allow
* for chaining (fluid interface).
*
* @param int $limit
* @return \TYPO3\CMS\Extbase\Persistence\QueryInterface
* @phpstan-return QueryInterface<T>
*/
public function setLimit($limit);
/**
* Sets the start offset of the result set to offset. Returns $this to
* allow for chaining (fluid interface).
*
* @param int $offset
* @return \TYPO3\CMS\Extbase\Persistence\QueryInterface
* @phpstan-return QueryInterface<T>
*/
public function setOffset($offset);
/**
* The constraint used to limit the result set. Returns $this to allow
* for chaining (fluid interface).
*
* @param ConstraintInterface $constraint Some constraint, depending on the backend
* @return \TYPO3\CMS\Extbase\Persistence\QueryInterface
* @phpstan-return QueryInterface<T>
*/
public function matching($constraint);
/**
* Performs a logical conjunction of multiple given constraints. The method
* takes an arbitrary number of constraints and concatenates them with a boolean AND.
*/
public function logicalAnd(ConstraintInterface ...$constraints): AndInterface;
/**
* Performs a logical disjunction of multiple given constraints. The method
* takes an arbitrary number of constraints and concatenates them with a boolean OR.
*/
public function logicalOr(ConstraintInterface ...$constraints): OrInterface;
/**
* Performs a logical negation of the given constraint
*
* @param ConstraintInterface $constraint Constraint to negate
* @return \TYPO3\CMS\Extbase\Persistence\Generic\Qom\NotInterface
*/
public function logicalNot(ConstraintInterface $constraint);
/**
* Returns an equals criterion used for matching objects against a query.
*
* It matches if the $operand equals the value of the property named
* $propertyName. If $operand is NULL a strict check for NULL is done. For
* strings the comparison can be done with or without case-sensitivity.
*
* @param string $propertyName The name of the property to compare against
* @param mixed $operand The value to compare with
* @param bool $caseSensitive Whether the equality test should be done case-sensitive for strings
* @return \TYPO3\CMS\Extbase\Persistence\Generic\Qom\ComparisonInterface
*/
public function equals($propertyName, $operand, $caseSensitive = true);
/**
* Returns a like criterion used for matching objects against a query.
* Matches if the property named $propertyName is like the $operand, using
* standard SQL wildcards.
*
* @param string $propertyName The name of the property to compare against
* @param string $operand The value to compare with
* @return ComparisonInterface
* @throws \TYPO3\CMS\Extbase\Persistence\Exception\InvalidQueryException if used on a non-string property
*/
public function like($propertyName, $operand);
/**
* Returns a "contains" criterion used for matching objects against a query.
* It matches if the multivalued property contains the given operand.
*
* If NULL is given as $operand, there will never be a match!
*
* @param string $propertyName The name of the multivalued property to compare against
* @param mixed $operand The value to compare with
* @return ComparisonInterface
* @throws \TYPO3\CMS\Extbase\Persistence\Exception\InvalidQueryException if used on a single-valued property
*/
public function contains($propertyName, $operand);
/**
* Returns an "in" criterion used for matching objects against a query. It
* matches if the property's value is contained in the multivalued operand.
*
* @param string $propertyName The name of the property to compare against
* @param mixed $operand The value to compare with, multivalued
* @return ComparisonInterface
* @throws \TYPO3\CMS\Extbase\Persistence\Exception\InvalidQueryException if used on a multi-valued property
*/
public function in($propertyName, $operand);
/**
* Returns a less than criterion used for matching objects against a query
*
* @param string $propertyName The name of the property to compare against
* @param mixed $operand The value to compare with
* @return ComparisonInterface
* @throws \TYPO3\CMS\Extbase\Persistence\Exception\InvalidQueryException if used on a multi-valued property or with a non-literal/non-DateTime operand
*/
public function lessThan($propertyName, $operand);
/**
* Returns a less or equal than criterion used for matching objects against a query
*
* @param string $propertyName The name of the property to compare against
* @param mixed $operand The value to compare with
* @return ComparisonInterface
* @throws \TYPO3\CMS\Extbase\Persistence\Exception\InvalidQueryException if used on a multi-valued property or with a non-literal/non-DateTime operand
*/
public function lessThanOrEqual($propertyName, $operand);
/**
* Returns a greater than criterion used for matching objects against a query
*
* @param string $propertyName The name of the property to compare against
* @param mixed $operand The value to compare with
* @return ComparisonInterface
* @throws \TYPO3\CMS\Extbase\Persistence\Exception\InvalidQueryException if used on a multi-valued property or with a non-literal/non-DateTime operand
*/
public function greaterThan($propertyName, $operand);
/**
* Returns a greater than or equal criterion used for matching objects against a query
*
* @param string $propertyName The name of the property to compare against
* @param mixed $operand The value to compare with
* @return ComparisonInterface
* @throws \TYPO3\CMS\Extbase\Persistence\Exception\InvalidQueryException if used on a multi-valued property or with a non-literal/non-DateTime operand
*/
public function greaterThanOrEqual($propertyName, $operand);
/**
* Set the type this query cares for.
* @phpstan-param class-string<T> $type
*/
public function setType(string $type): void;
/**
* Returns the type this query cares for.
*
* @return string
* @phpstan-return class-string<T>
*/
public function getType();
/**
* Sets the Query Settings. These Query settings must match the settings expected by
* the specific Storage Backend.
*/
public function setQuerySettings(QuerySettingsInterface $querySettings);
/**
* Returns the Query Settings.
*
* @return QuerySettingsInterface $querySettings The Query Settings
*/
public function getQuerySettings();
/**
* Returns the query result count.
*
* @return int The query result count
*/
public function count();
/**
* Gets the orderings for this query.
*
* When using setOrderings(), returns legacy format:
* array(
* 'foo' => \TYPO3\CMS\Extbase\Persistence\QueryInterface::ORDER_ASCENDING,
* 'bar' => \TYPO3\CMS\Extbase\Persistence\QueryInterface::ORDER_DESCENDING
* )
*
* When using orderBy()/addOrderBy(), returns OrderingInterface objects:
* array(\TYPO3\CMS\Extbase\Persistence\Generic\Qom\OrderingInterface, ...)
*
* @return array<string,string>|array<\TYPO3\CMS\Extbase\Persistence\Generic\Qom\OrderingInterface>
*/
public function getOrderings();
/**
* Returns the maximum size of the result set to limit.
*
* @return int
*/
public function getLimit();
/**
* Returns the start offset of the result set.
*
* @return int
*/
public function getOffset();
/**
* Gets the constraint for this query.
*
* @return ConstraintInterface|null the constraint, or null if none
*/
public function getConstraint();
/**
* Sets the source to fetch the result from
*/
public function setSource(SourceInterface $source);
/**
* Returns the statement of this query.
*
* @return \TYPO3\CMS\Extbase\Persistence\Generic\Qom\Statement
*/
public function getStatement();
}
@@ -0,0 +1,55 @@
<?php
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Extbase\Persistence;
/**
* A lazy result list that is returned by Query::execute()
* @template TKey of int
* @template TValue of object
* @extends \Iterator<TKey,TValue>
* @extends \ArrayAccess<TKey,TValue>
*/
interface QueryResultInterface extends \Countable, \Iterator, \ArrayAccess
{
/**
* @phpstan-param QueryInterface<TValue> $query
*/
public function setQuery(QueryInterface $query): void;
/**
* Returns a clone of the query object
*
* @return \TYPO3\CMS\Extbase\Persistence\QueryInterface
* @phpstan-return QueryInterface<TValue>
*/
public function getQuery();
/**
* Returns the first object in the result set
*
* @return object|null
* @phpstan-return TValue|null
*/
public function getFirst();
/**
* Returns an array with the objects in the result set
*
* @return array
* @phpstan-return list<TValue>
*/
public function toArray();
}
+345
View File
@@ -0,0 +1,345 @@
<?php
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Extbase\Persistence;
use Psr\EventDispatcher\EventDispatcherInterface;
use TYPO3\CMS\Core\Cache\CacheTag;
use TYPO3\CMS\Core\Cache\Event\AddCacheTagEvent;
use TYPO3\CMS\Core\Configuration\Features;
use TYPO3\CMS\Core\SingletonInterface;
use TYPO3\CMS\Core\Utility\ClassNamingUtility;
use TYPO3\CMS\Extbase\Persistence\Exception\IllegalObjectTypeException;
use TYPO3\CMS\Extbase\Persistence\Generic\Qom\SelectorInterface;
use TYPO3\CMS\Extbase\Persistence\Generic\QuerySettingsInterface;
/**
* The base repository - will usually be extended by a more concrete repository.
* @template T of \TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface
* @implements RepositoryInterface<T>
*/
class Repository implements RepositoryInterface, SingletonInterface
{
protected PersistenceManagerInterface $persistenceManager;
protected EventDispatcherInterface $eventDispatcher;
protected bool $autoTagging;
/**
* @var string
* @phpstan-var class-string<T>
*/
protected $objectType;
/**
* @var array<non-empty-string, QueryInterface::ORDER_*>
*/
protected $defaultOrderings = [];
/**
* Override query settings created by extbase natively.
* Be careful if using this, see the comment on `setDefaultQuerySettings()` for more insights.
*
* @var QuerySettingsInterface
*/
protected $defaultQuerySettings;
public function injectPersistenceManager(PersistenceManagerInterface $persistenceManager)
{
$this->persistenceManager = $persistenceManager;
}
public function injectEventDispatcher(EventDispatcherInterface $eventDispatcher): void
{
$this->eventDispatcher = $eventDispatcher;
}
public function injectFeatures(Features $features): void
{
$this->autoTagging = $features->isFeatureEnabled('frontend.cache.autoTagging');
}
/**
* Constructs a new Repository
*/
public function __construct()
{
$this->objectType = ClassNamingUtility::translateRepositoryNameToModelName($this->getRepositoryClassName());
}
/**
* Adds an object to this repository
*
* @param object $object The object to add
* @phpstan-param T $object
* @throws Exception\IllegalObjectTypeException
*/
public function add($object)
{
if (!$object instanceof $this->objectType) {
throw new IllegalObjectTypeException('The object given to add() was not of the type (' . $this->objectType . ') this repository manages.', 1248363335);
}
$this->persistenceManager->add($object);
}
/**
* Removes an object from this repository.
*
* @param object $object The object to remove
* @phpstan-param T $object
* @throws Exception\IllegalObjectTypeException
*/
public function remove($object)
{
if (!$object instanceof $this->objectType) {
throw new IllegalObjectTypeException('The object given to remove() was not of the type (' . $this->objectType . ') this repository manages.', 1248363336);
}
$this->persistenceManager->remove($object);
}
/**
* Replaces an existing object with the same identifier by the given object
*
* @param object $modifiedObject The modified object
* @phpstan-param T $modifiedObject
* @throws Exception\UnknownObjectException
* @throws Exception\IllegalObjectTypeException
*/
public function update($modifiedObject)
{
if (!$modifiedObject instanceof $this->objectType) {
throw new IllegalObjectTypeException('The modified object given to update() was not of the type (' . $this->objectType . ') this repository manages.', 1249479625);
}
$this->persistenceManager->update($modifiedObject);
}
/**
* Returns all objects of this repository.
*
* @return QueryResultInterface
* @phpstan-return QueryResultInterface<int,T>
*/
public function findAll()
{
$query = $this->createQuery();
$this->addTableToCacheTags($query);
return $query->execute();
}
/**
* Returns the total number objects of this repository.
*
* @return int The object count
*/
public function countAll()
{
$query = $this->createQuery();
$this->addTableToCacheTags($query);
return $query->execute()->count();
}
/**
* Removes all objects of this repository as if remove() was called for
* all of them.
*/
public function removeAll()
{
foreach ($this->findAll() as $object) {
$this->remove($object);
}
}
/**
* Finds an object matching the given identifier.
*
* @param int $uid The identifier of the object to find
* @return object|null The matching object if found, otherwise NULL
* @phpstan-return T|null
*/
public function findByUid($uid)
{
return $this->findByIdentifier($uid);
}
/**
* Finds an object matching the given identifier.
*
* @param mixed $identifier The identifier of the object to find
* @return object|null The matching object if found, otherwise NULL
* @phpstan-return T|null
*/
public function findByIdentifier($identifier)
{
return $this->persistenceManager->getObjectByIdentifier($identifier, $this->objectType);
}
/**
* Sets the property names to order the result by per default.
* Expected like this:
* array(
* 'foo' => \TYPO3\CMS\Extbase\Persistence\QueryInterface::ORDER_ASCENDING,
* 'bar' => \TYPO3\CMS\Extbase\Persistence\QueryInterface::ORDER_DESCENDING
* )
*
* @param array<non-empty-string, QueryInterface::ORDER_*> $defaultOrderings The property names to order by
*/
public function setDefaultOrderings(array $defaultOrderings)
{
$this->defaultOrderings = $defaultOrderings;
}
/**
* Sets the default query settings to be used in this repository.
*
* A typical use case is an initializeObject() method that creates a QuerySettingsInterface
* object, configures it and sets it to be used for all queries created by the repository.
*
* Warning: Using this setter *fully overrides* native query settings created by
* QueryFactory->create(). This especially means that storagePid settings from
* configuration are not applied anymore, if not explicitly set. Make sure to apply these
* to your own QuerySettingsInterface object if needed, when using this method.
*/
public function setDefaultQuerySettings(QuerySettingsInterface $defaultQuerySettings)
{
$this->defaultQuerySettings = $defaultQuerySettings;
}
/**
* Returns a query for objects of this repository
*
* @return QueryInterface
* @phpstan-return QueryInterface<T>
*/
public function createQuery()
{
$query = $this->persistenceManager->createQueryForType($this->objectType);
if ($this->defaultOrderings !== []) {
$query->setOrderings($this->defaultOrderings);
}
if ($this->defaultQuerySettings !== null) {
$query->setQuerySettings(clone $this->defaultQuerySettings);
}
return $query;
}
/**
* @phpstan-param array<non-empty-string, mixed> $criteria
* @phpstan-param array<non-empty-string, QueryInterface::ORDER_*>|null $orderBy
* @phpstan-param 0|positive-int|null $limit
* @phpstan-param 0|positive-int|null $offset
* @phpstan-return QueryResultInterface<int,T>
*/
public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): QueryResultInterface
{
$query = $this->createQuery();
$constraints = [];
foreach ($criteria as $propertyName => $propertyValue) {
if (!is_string($propertyName)) {
throw new \RuntimeException('Repository::findBy() expects an array with string keys as first argument', 1741806517);
}
$constraints[] = $query->equals($propertyName, $propertyValue);
}
if (($numberOfConstraints = count($constraints)) === 1) {
$query->matching(...$constraints);
} elseif ($numberOfConstraints > 1) {
$query->matching($query->logicalAnd(...$constraints));
}
if (is_array($orderBy)) {
$query->setOrderings($orderBy);
}
if (is_int($limit)) {
$query->setLimit($limit);
}
if (is_int($offset)) {
$query->setOffset($offset);
}
$this->addStorageCacheTags($query);
return $query->execute();
}
/**
* @phpstan-param array<non-empty-string, mixed> $criteria
* @phpstan-param array<non-empty-string, QueryInterface::ORDER_*>|null $orderBy
* @phpstan-return T|null
*/
public function findOneBy(array $criteria, ?array $orderBy = null): ?object
{
return $this->findBy($criteria, $orderBy, 1)->getFirst();
}
/**
* @phpstan-param array<non-empty-string, mixed> $criteria
* @phpstan-return 0|positive-int
*/
public function count(array $criteria): int
{
return $this->findBy($criteria)->count();
}
/**
* Returns the class name of this class.
*
* @return class-string<static> Class name of the repository.
*/
protected function getRepositoryClassName()
{
return static::class;
}
/**
* Add the tablename to the cache tags, depending on the storage page settings.
*/
protected function addTableToCacheTags(QueryInterface $query): void
{
if (!$this->autoTagging) {
return;
}
$storagePageIds = $query->getQuerySettings()->getStoragePageIds();
if (empty($storagePageIds) || $query->getQuerySettings()->getRespectStoragePage() === false) {
$source = $query->getSource();
if ($source instanceof SelectorInterface) {
$this->eventDispatcher->dispatch(
new AddCacheTagEvent(new CacheTag($source->getSelectorName()))
);
}
} else {
$this->addStorageCacheTags($query);
}
}
/**
* Add the combination of tablename and storage pid as cache tag.
*/
protected function addStorageCacheTags(QueryInterface $query): void
{
if (!$this->autoTagging) {
return;
}
$source = $query->getSource();
if ($source instanceof SelectorInterface) {
$tableName = $source->getSelectorName();
$storagePageIds = $query->getQuerySettings()->getStoragePageIds();
foreach ($storagePageIds as $storagePageId) {
$this->eventDispatcher->dispatch(
new AddCacheTagEvent(new CacheTag(sprintf('%s_pid_%s', $tableName, $storagePageId)))
);
}
}
}
}
+115
View File
@@ -0,0 +1,115 @@
<?php
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Extbase\Persistence;
use TYPO3\CMS\Extbase\Persistence\Generic\QuerySettingsInterface;
/**
* Contract for a repository
* @template T of object
*/
interface RepositoryInterface
{
/**
* Adds an object to this repository.
*
* @param object $object The object to add
* @phpstan-param T $object
*/
public function add($object);
/**
* Removes an object from this repository.
*
* @param object $object The object to remove
* @phpstan-param T $object
*/
public function remove($object);
/**
* Replaces an existing object with the same identifier by the given object
*
* @param object $modifiedObject The modified object
* @phpstan-param T $modifiedObject
*/
public function update($modifiedObject);
/**
* Returns all objects of this repository.
*
* @return iterable The iterable query result
* @phpstan-return iterable<T>
*/
public function findAll();
/**
* Returns the total number objects of this repository.
*
* @return int The object count
*/
public function countAll();
/**
* Removes all objects of this repository as if remove() was called for
* all of them.
*/
public function removeAll();
/**
* Finds an object matching the given identifier.
*
* @param int $uid The identifier of the object to find
* @return object The matching object if found, otherwise NULL
* @phpstan-return T|null
*/
public function findByUid($uid);
/**
* Finds an object matching the given identifier.
*
* @param mixed $identifier The identifier of the object to find
* @return object The matching object if found, otherwise NULL
* @phpstan-return T|null
*/
public function findByIdentifier($identifier);
/**
* Sets the property names to order the result by per default.
* Expected like this:
* array(
* 'foo' => \TYPO3\CMS\Extbase\Persistence\QueryInterface::ORDER_ASCENDING,
* 'bar' => \TYPO3\CMS\Extbase\Persistence\QueryInterface::ORDER_DESCENDING
* )
*
* @param array $defaultOrderings The property names to order by
*/
public function setDefaultOrderings(array $defaultOrderings);
/**
* Sets the default query settings to be used in this repository
*
* @param \TYPO3\CMS\Extbase\Persistence\Generic\QuerySettingsInterface $defaultQuerySettings The query settings to be used by default
*/
public function setDefaultQuerySettings(QuerySettingsInterface $defaultQuerySettings);
/**
* Returns a query for objects of this repository
*
* @return \TYPO3\CMS\Extbase\Persistence\QueryInterface
* @phpstan-return QueryInterface<T>
*/
public function createQuery();
}