2 declare(strict_types
=1);
3 namespace TYPO3\CMS\Install\Updates
;
6 * This file is part of the TYPO3 CMS project.
8 * It is free software; you can redistribute it and/or modify it under
9 * the terms of the GNU General Public License, either version 2
10 * of the License, or any later version.
12 * For the full copyright and license information, please read the
13 * LICENSE.txt file that was distributed with this source code.
15 * The TYPO3 project - inspiring people to share!
17 use TYPO3\CMS\Core\Database\ConnectionPool
;
18 use TYPO3\CMS\Core\Registry
;
19 use TYPO3\CMS\Core\Utility\GeneralUtility
;
20 use TYPO3\CMS\Install\Updates\RowUpdater\RowUpdaterInterface
;
23 * This is a generic updater to migrate content of TCA rows.
25 * Multiple classes implementing interface "RowUpdateInterface" can be
26 * registered here, each for a specific update purpose,
28 * The updater fetches each row of all TCA registered tables and
29 * visits the client classes who may modify the row content.
31 * The updater remembers for each class if it run through, so the updater
32 * will be shown again if a new updater class is registered that has not
35 * A start position pointer is stored in the registry that is updated during
36 * the run process, so if for instance the PHP process runs into a timeout,
37 * the job can restart at the position it stopped again.
39 class DatabaseRowsUpdateWizard
extends AbstractUpdate
42 * @var string Title of this updater
44 protected $title = 'Execute database migrations on single rows';
47 * @var array Single classes that may update rows
49 protected $rowUpdater = [
53 * Checks if an update is needed by looking up in registry if all
54 * registered update row classes are marked as done or not.
56 * @param string &$description The description for the update
57 * @return bool Whether an update is needed (TRUE) or not (FALSE)
59 public function checkForUpdate(&$description)
61 $updateNeeded = false;
62 $rowUpdaterNotExecuted = $this->getRowUpdatersToExecute();
63 if (!empty($rowUpdaterNotExecuted)) {
70 $description = 'Some row updates have not been run yet:';
71 foreach ($rowUpdaterNotExecuted as $rowUpdateClassName) {
72 $rowUpdater = GeneralUtility
::makeInstance($rowUpdateClassName);
73 if (!$rowUpdater instanceof RowUpdaterInterface
) {
74 throw new \
RuntimeException(
75 'Row updater must implement RowUpdaterInterface',
79 $description .= '<br />' . htmlspecialchars($rowUpdater->getTitle());
86 * Performs the configuration update.
88 * @param array &$databaseQueries Queries done in this update
89 * @param mixed &$customMessages Custom messages
91 * @throws \Doctrine\DBAL\ConnectionException
94 public function performUpdate(array &$databaseQueries, &$customMessages)
96 $registry = GeneralUtility
::makeInstance(Registry
::class);
98 // If rows from the target table that is updated and the sys_registry table are on the
99 // same connection, the row update statement and sys_registry position update will be
100 // handled in a transaction to have an atomic operation in case of errors during execution.
101 $connectionPool = GeneralUtility
::makeInstance(ConnectionPool
::class);
102 $connectionForSysRegistry = $connectionPool->getConnectionForTable('sys_registry');
104 /** @var RowUpdaterInterface[] $rowUpdaterInstances */
105 $rowUpdaterInstances = [];
106 // Single row updater instances are created only once for this method giving
107 // them a chance to set up local properties during hasPotentialUpdateForTable()
108 // and using that in updateTableRow()
109 foreach ($this->getRowUpdatersToExecute() as $rowUpdater) {
110 $rowUpdaterInstance = GeneralUtility
::makeInstance($rowUpdater);
111 if (!$rowUpdaterInstance instanceof RowUpdaterInterface
) {
112 throw new \
RuntimeException(
113 'Row updater must implement RowUpdaterInterface',
117 $rowUpdaterInstances[] = $rowUpdaterInstance;
120 // Scope of the row updater is to update all rows that have TCA,
121 // so our list of tables is just the list of loaded TCA tables.
122 $listOfAllTables = array_keys($GLOBALS['TCA']);
125 $listOfAllTables = [ 'tx_styleguide_staticdata' ];
127 // In case the PHP ended for whatever reason, fetch the last position from registry
128 // and throw away all tables before the first table from registry table name.
129 sort($listOfAllTables);
130 reset($listOfAllTables);
131 $firstTable = current($listOfAllTables);
132 $startPosition = $this->getStartPosition($firstTable);
133 foreach ($listOfAllTables as $table) {
134 if ($table === $startPosition['table']) {
137 unset($listOfAllTables[$table]);
141 // Ask each row updater if it potentially has field updates for rows of each table
142 $tableToUpdaterList = [];
143 foreach ($listOfAllTables as $table) {
144 foreach ($rowUpdaterInstances as $updater) {
145 if ($updater->hasPotentialUpdateForTable($table)) {
146 if (!is_array($tableToUpdaterList[$table])) {
147 $tableToUpdaterList[$table] = [];
149 $tableToUpdaterList[$table][] = $updater;
154 // Iterate through all rows of all table that have potential row updaters attached,
155 // feed them each single row to each updater and finally update each row
156 foreach ($tableToUpdaterList as $table => $updaters) {
157 /** @var RowUpdaterInterface[] $updaters */
158 $connectionForTable = $connectionPool->getConnectionForTable($table);
159 $queryBuilder = $connectionPool->getQueryBuilderForTable($table);
160 $queryBuilder->getRestrictions()->removeAll();
161 $queryBuilder->select('*')
164 if ($table === $startPosition['table']) {
165 $queryBuilder->where(
166 $queryBuilder->expr()->gt('uid', $queryBuilder->createNamedParameter($startPosition['uid']))
169 $statement = $queryBuilder->execute();
170 $rowCountWithoutUpdate = 0;
171 while ($row = $rowBefore = $statement->fetch()) {
172 foreach ($updaters as $updater) {
173 $row = $updater->updateTableRow($table, $row);
175 $updatedFields = array_diff_assoc($row, $rowBefore);
176 if (empty($updatedFields)) {
177 // Updaters changed no field of that row
178 $rowCountWithoutUpdate ++
;
179 if ($rowCountWithoutUpdate >= 200) {
180 // Update startPosition every number rows without update
183 'uid' => $row['uid'],
185 $registry->set('installUpdateRows', 'rowUpdatePosition', $startPosition);
186 $rowCountWithoutUpdate = 0;
189 $rowCountWithoutUpdate = 0;
192 'uid' => $rowBefore['uid'],
194 if ($connectionForSysRegistry === $connectionForTable) {
195 // Target table and sys_registry table are on the same connection, use a transaction
196 $connectionForTable->beginTransaction();
198 $connectionForTable->update(
202 'uid' => $rowBefore['uid'],
205 $connectionForTable->update(
208 'entry_value' => serialize($startPosition),
211 'entry_namespace' => 'installUpdateRows',
212 'entry_key' => 'rowUpdatePosition',
215 $connectionForTable->commit();
216 } catch (\Exception
$up) {
217 $connectionForTable->rollBack();
221 // Different connections for table and sys_registry -> execute two
222 // distinct queries and hope for the best.
223 $connectionForTable->update(
227 'uid' => $rowBefore['uid'],
230 $connectionForSysRegistry->update(
233 'entry_value' => serialize($startPosition),
236 'entry_namespace' => 'installUpdateRows',
237 'entry_key' => 'rowUpdatePosition',
245 // Ready with updates, remove position information from sys_registry
246 $registry->remove('installUpdateRows', 'rowUpdatePosition');
247 // Mark those row updaters as done
248 foreach ($rowUpdaterInstances as $updater) {
249 $this->setRowUpdaterExecuted($updater);
256 * Return an array of class names that are not yet marked as done.
258 * @return array Class names
260 protected function getRowUpdatersToExecute(): array
262 $doneRowUpdater = GeneralUtility
::makeInstance(Registry
::class)->get('installUpdateRows', 'rowUpdatersDone', []);
263 return array_diff($this->rowUpdater
, $doneRowUpdater);
267 * Mark a single updater as done
269 * @param RowUpdaterInterface $updater
271 protected function setRowUpdaterExecuted(RowUpdaterInterface
$updater)
273 $registry = GeneralUtility
::makeInstance(Registry
::class);
274 $doneRowUpdater = $registry->get('installUpdateRows', 'rowUpdatersDone', []);
275 $doneRowUpdater[] = get_class($updater);
276 $registry->set('installUpdateRows', 'rowUpdatersDone', $doneRowUpdater);
280 * Return an array with table / uid combination that specifies the start position the
281 * update row process should start with.
283 * @param string $firstTable Table name of the first TCA in case the start position needs to be initialized
284 * @return array New start position
286 protected function getStartPosition(string $firstTable): array
288 $registry = GeneralUtility
::makeInstance(Registry
::class);
289 $startPosition = $registry->get('installUpdateRows', 'rowUpdaterPosition', []);
290 if (empty($startPosition)) {
292 'table' => $firstTable,
295 $registry->set('installUpdateRows', 'rowUpdatePosition', $startPosition);
297 return $startPosition;