TYPO3 v15 dev-main snapshot ()
This commit is contained in:
@@ -0,0 +1,336 @@
|
||||
<?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\Core\Upgrades;
|
||||
|
||||
use TYPO3\CMS\Core\Attribute\UpgradeWizard;
|
||||
use TYPO3\CMS\Core\Database\Connection;
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
use TYPO3\CMS\Core\Registry;
|
||||
use TYPO3\CMS\Core\Upgrades\RowUpdater\RowUpdaterInterface;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* This is a generic updater to migrate content of TCA rows.
|
||||
*
|
||||
* Multiple classes implementing interface "RowUpdaterInterface" can be
|
||||
* registered here, each for a specific update purpose.
|
||||
*
|
||||
* The updater fetches each row of all TCA registered tables and
|
||||
* visits the client classes who may modify the row content.
|
||||
*
|
||||
* The updater remembers for each class if it run through, so the updater
|
||||
* will be shown again if a new updater class is registered that has not
|
||||
* been run yet.
|
||||
*
|
||||
* A start position pointer is stored in the registry that is updated during
|
||||
* the run process, so if for instance the PHP process runs into a timeout,
|
||||
* the job can restart at the position it stopped.
|
||||
*
|
||||
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
|
||||
*/
|
||||
#[UpgradeWizard('databaseRowsUpdateWizard')]
|
||||
class DatabaseRowsUpdateWizard implements UpgradeWizardInterface, RepeatableInterface
|
||||
{
|
||||
/**
|
||||
* @var array Single classes that may update rows
|
||||
* @todo No RowUpdater remaining allowing us to move the registration to attribute/interface based locator.
|
||||
*/
|
||||
protected $rowUpdater = [];
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @return string[]
|
||||
*/
|
||||
public function getAvailableRowUpdater(): array
|
||||
{
|
||||
return $this->rowUpdater;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string Title of this updater
|
||||
*/
|
||||
public function getTitle(): string
|
||||
{
|
||||
return 'Execute database migrations on single rows';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string Longer description of this updater
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public function getDescription(): string
|
||||
{
|
||||
$rowUpdaterNotExecuted = $this->getRowUpdatersToExecute();
|
||||
$description = 'Row updaters that have not been executed:';
|
||||
foreach ($rowUpdaterNotExecuted as $rowUpdateClassName) {
|
||||
$rowUpdater = GeneralUtility::makeInstance($rowUpdateClassName);
|
||||
if (!$rowUpdater instanceof RowUpdaterInterface) {
|
||||
throw new \RuntimeException(
|
||||
'Row updater must implement RowUpdaterInterface',
|
||||
1484066647
|
||||
);
|
||||
}
|
||||
$description .= LF . $rowUpdater->getTitle();
|
||||
}
|
||||
return $description;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool True if at least one row updater is not marked done
|
||||
*/
|
||||
public function updateNecessary(): bool
|
||||
{
|
||||
return !empty($this->getRowUpdatersToExecute());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[] All new fields and tables must exist
|
||||
*/
|
||||
public function getPrerequisites(): array
|
||||
{
|
||||
return [
|
||||
DatabaseUpdatedPrerequisite::class,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs the configuration update.
|
||||
*
|
||||
* @throws \Doctrine\DBAL\ConnectionException
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function executeUpdate(): bool
|
||||
{
|
||||
$registry = GeneralUtility::makeInstance(Registry::class);
|
||||
|
||||
// If rows from the target table that is updated and the sys_registry table are on the
|
||||
// same connection, the row update statement and sys_registry position update will be
|
||||
// handled in a transaction to have an atomic operation in case of errors during execution.
|
||||
$connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
|
||||
$connectionForSysRegistry = $connectionPool->getConnectionForTable('sys_registry');
|
||||
|
||||
/** @var RowUpdaterInterface[] $rowUpdaterInstances */
|
||||
$rowUpdaterInstances = [];
|
||||
// Single row updater instances are created only once for this method giving
|
||||
// them a chance to set up local properties during hasPotentialUpdateForTable()
|
||||
// and using that in updateTableRow()
|
||||
foreach ($this->getRowUpdatersToExecute() as $rowUpdater) {
|
||||
$rowUpdaterInstance = GeneralUtility::makeInstance($rowUpdater);
|
||||
if (!$rowUpdaterInstance instanceof RowUpdaterInterface) {
|
||||
throw new \RuntimeException(
|
||||
'Row updater must implement RowUpdaterInterface',
|
||||
1484071612
|
||||
);
|
||||
}
|
||||
$rowUpdaterInstances[] = $rowUpdaterInstance;
|
||||
}
|
||||
|
||||
// Scope of the row updater is to update all rows that have TCA,
|
||||
// our list of tables is just the list of loaded TCA tables.
|
||||
/** @var string[] $listOfAllTables */
|
||||
$listOfAllTables = array_keys($GLOBALS['TCA']);
|
||||
|
||||
// In case the PHP ended for whatever reason, fetch the last position from registry
|
||||
// and throw away all tables before that start point.
|
||||
sort($listOfAllTables);
|
||||
reset($listOfAllTables);
|
||||
$firstTable = current($listOfAllTables) ?: '';
|
||||
$startPosition = $this->getStartPosition($firstTable);
|
||||
foreach ($listOfAllTables as $key => $table) {
|
||||
if ($table === $startPosition['table']) {
|
||||
break;
|
||||
}
|
||||
unset($listOfAllTables[$key]);
|
||||
}
|
||||
|
||||
// Ask each row updater if it potentially has field updates for rows of a table
|
||||
$tableToUpdaterList = [];
|
||||
foreach ($listOfAllTables as $table) {
|
||||
foreach ($rowUpdaterInstances as $updater) {
|
||||
if ($updater->hasPotentialUpdateForTable($table)) {
|
||||
$tableToUpdaterList[$table] ??= [];
|
||||
$tableToUpdaterList[$table][] = $updater;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Iterate through all rows of all tables that have potential row updaters attached,
|
||||
// feed each single row to each updater and finally update each row in database if
|
||||
// a row updater changed a fields
|
||||
foreach ($tableToUpdaterList as $table => $updaters) {
|
||||
/** @var RowUpdaterInterface[] $updaters */
|
||||
$connectionForTable = $connectionPool->getConnectionForTable($table);
|
||||
$queryBuilder = $connectionPool->getQueryBuilderForTable($table);
|
||||
$queryBuilder->getRestrictions()->removeAll();
|
||||
$queryBuilder->select('*')
|
||||
->from($table)
|
||||
->orderBy('uid');
|
||||
if ($table === $startPosition['table']) {
|
||||
$queryBuilder->where(
|
||||
$queryBuilder->expr()->gt('uid', $queryBuilder->createNamedParameter($startPosition['uid']))
|
||||
);
|
||||
}
|
||||
$statement = $queryBuilder->executeQuery();
|
||||
$rowCountWithoutUpdate = 0;
|
||||
while ($row = $statement->fetchAssociative()) {
|
||||
$rowBefore = $row;
|
||||
foreach ($updaters as $updater) {
|
||||
$row = $updater->updateTableRow($table, $row);
|
||||
}
|
||||
$updatedFields = array_diff_assoc($row, $rowBefore);
|
||||
if (empty($updatedFields)) {
|
||||
// Updaters changed no field of that row
|
||||
$rowCountWithoutUpdate++;
|
||||
if ($rowCountWithoutUpdate >= 200) {
|
||||
// Update startPosition if there were many rows without data change
|
||||
$startPosition = [
|
||||
'table' => $table,
|
||||
'uid' => $row['uid'],
|
||||
];
|
||||
$registry->set('installUpdateRows', 'rowUpdatePosition', $startPosition);
|
||||
$rowCountWithoutUpdate = 0;
|
||||
}
|
||||
} else {
|
||||
$rowCountWithoutUpdate = 0;
|
||||
$startPosition = [
|
||||
'table' => $table,
|
||||
'uid' => $rowBefore['uid'],
|
||||
];
|
||||
if ($connectionForSysRegistry === $connectionForTable) {
|
||||
// Target table and sys_registry table are on the same connection, use a transaction
|
||||
$connectionForTable->beginTransaction();
|
||||
try {
|
||||
$this->updateOrDeleteRow(
|
||||
$connectionForTable,
|
||||
$connectionForTable,
|
||||
$table,
|
||||
(int)$rowBefore['uid'],
|
||||
$updatedFields,
|
||||
$startPosition
|
||||
);
|
||||
$connectionForTable->commit();
|
||||
} catch (\Exception $up) {
|
||||
$connectionForTable->rollBack();
|
||||
throw $up;
|
||||
}
|
||||
} else {
|
||||
// Different connections for table and sys_registry.
|
||||
// So, execute two distinct queries and hope for the best.
|
||||
$this->updateOrDeleteRow(
|
||||
$connectionForTable,
|
||||
$connectionForSysRegistry,
|
||||
$table,
|
||||
(int)$rowBefore['uid'],
|
||||
$updatedFields,
|
||||
$startPosition
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ready with updates, remove position information from sys_registry
|
||||
$registry->remove('installUpdateRows', 'rowUpdatePosition');
|
||||
// Mark row updaters that were executed as done
|
||||
foreach ($rowUpdaterInstances as $updater) {
|
||||
$this->setRowUpdaterExecuted($updater);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an array of class names that are not yet marked as done.
|
||||
*
|
||||
* @return array Class names
|
||||
*/
|
||||
protected function getRowUpdatersToExecute(): array
|
||||
{
|
||||
$doneRowUpdater = GeneralUtility::makeInstance(Registry::class)->get('installUpdateRows', 'rowUpdatersDone', []);
|
||||
return array_diff($this->rowUpdater, $doneRowUpdater);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a single updater as done
|
||||
*/
|
||||
protected function setRowUpdaterExecuted(RowUpdaterInterface $updater)
|
||||
{
|
||||
$registry = GeneralUtility::makeInstance(Registry::class);
|
||||
$doneRowUpdater = $registry->get('installUpdateRows', 'rowUpdatersDone', []);
|
||||
$doneRowUpdater[] = get_class($updater);
|
||||
$registry->set('installUpdateRows', 'rowUpdatersDone', $doneRowUpdater);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an array with table / uid combination that specifies the start position the
|
||||
* update row process should start with.
|
||||
*
|
||||
* @param string $firstTable Table name of the first TCA in case the start position needs to be initialized
|
||||
* @return array New start position
|
||||
*/
|
||||
protected function getStartPosition(string $firstTable): array
|
||||
{
|
||||
$registry = GeneralUtility::makeInstance(Registry::class);
|
||||
$startPosition = $registry->get('installUpdateRows', 'rowUpdatePosition', []);
|
||||
if (empty($startPosition)) {
|
||||
$startPosition = [
|
||||
'table' => $firstTable,
|
||||
'uid' => 0,
|
||||
];
|
||||
$registry->set('installUpdateRows', 'rowUpdatePosition', $startPosition);
|
||||
}
|
||||
return $startPosition;
|
||||
}
|
||||
|
||||
protected function updateOrDeleteRow(Connection $connectionForTable, Connection $connectionForSysRegistry, string $table, int $uid, array $updatedFields, array $startPosition): void
|
||||
{
|
||||
$deleteField = $GLOBALS['TCA'][$table]['ctrl']['delete'] ?? null;
|
||||
if ($deleteField === null && isset($updatedFields['deleted']) && $updatedFields['deleted'] === 1) {
|
||||
$connectionForTable->delete(
|
||||
$table,
|
||||
[
|
||||
'uid' => $uid,
|
||||
]
|
||||
);
|
||||
} else {
|
||||
$connectionForTable->update(
|
||||
$table,
|
||||
$updatedFields,
|
||||
[
|
||||
'uid' => $uid,
|
||||
]
|
||||
);
|
||||
}
|
||||
$connectionForSysRegistry->update(
|
||||
'sys_registry',
|
||||
[
|
||||
'entry_value' => serialize($startPosition),
|
||||
],
|
||||
[
|
||||
'entry_namespace' => 'installUpdateRows',
|
||||
'entry_key' => 'rowUpdatePosition',
|
||||
],
|
||||
[
|
||||
// Needs to be declared LOB, so MSSQL can handle the conversion from string (nvarchar) to blob (varbinary)
|
||||
'entry_value' => Connection::PARAM_LOB,
|
||||
'entry_namespace' => Connection::PARAM_STR,
|
||||
'entry_key' => Connection::PARAM_STR,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user