Files
cms-extbase/Classes/Persistence/Generic/Storage/Typo3DbBackend.php
T

700 lines
32 KiB
PHP

<?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;
}
}