‪TYPO3CMS  ‪main
DatabaseRowsUpdateWizard.php
Go to the documentation of this file.
1 <?php
2 
3 declare(strict_types=1);
4 
5 /*
6  * This file is part of the TYPO3 CMS project.
7  *
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.
11  *
12  * For the full copyright and license information, please read the
13  * LICENSE.txt file that was distributed with this source code.
14  *
15  * The TYPO3 project - inspiring people to share!
16  */
17 
19 
27 
46 #[UpgradeWizard('databaseRowsUpdateWizard')]
48 {
52  protected ‪$rowUpdater = [
53  SysRedirectRootPageMoveMigration::class,
54  ];
55 
60  public function ‪getAvailableRowUpdater(): array
61  {
62  return ‪$this->rowUpdater;
63  }
64 
68  public function ‪getTitle(): string
69  {
70  return 'Execute database migrations on single rows';
71  }
72 
77  public function ‪getDescription(): string
78  {
79  $rowUpdaterNotExecuted = $this->‪getRowUpdatersToExecute();
80  $description = 'Row updaters that have not been executed:';
81  foreach ($rowUpdaterNotExecuted as $rowUpdateClassName) {
82  ‪$rowUpdater = GeneralUtility::makeInstance($rowUpdateClassName);
83  if (!‪$rowUpdater instanceof ‪RowUpdaterInterface) {
84  throw new \RuntimeException(
85  'Row updater must implement RowUpdaterInterface',
86  1484066647
87  );
88  }
89  $description .= LF . ‪$rowUpdater->getTitle();
90  }
91  return $description;
92  }
93 
97  public function ‪updateNecessary(): bool
98  {
99  return !empty($this->‪getRowUpdatersToExecute());
100  }
101 
105  public function ‪getPrerequisites(): array
106  {
107  return [
108  DatabaseUpdatedPrerequisite::class,
109  ];
110  }
111 
118  public function ‪executeUpdate(): bool
119  {
120  $registry = GeneralUtility::makeInstance(Registry::class);
121 
122  // If rows from the target table that is updated and the sys_registry table are on the
123  // same connection, the row update statement and sys_registry position update will be
124  // handled in a transaction to have an atomic operation in case of errors during execution.
125  $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
126  $connectionForSysRegistry = $connectionPool->getConnectionForTable('sys_registry');
127 
129  $rowUpdaterInstances = [];
130  // Single row updater instances are created only once for this method giving
131  // them a chance to set up local properties during hasPotentialUpdateForTable()
132  // and using that in updateTableRow()
133  foreach ($this->‪getRowUpdatersToExecute() as ‪$rowUpdater) {
134  $rowUpdaterInstance = GeneralUtility::makeInstance(‪$rowUpdater);
135  if (!$rowUpdaterInstance instanceof ‪RowUpdaterInterface) {
136  throw new \RuntimeException(
137  'Row updater must implement RowUpdaterInterface',
138  1484071612
139  );
140  }
141  $rowUpdaterInstances[] = $rowUpdaterInstance;
142  }
143 
144  // Scope of the row updater is to update all rows that have TCA,
145  // our list of tables is just the list of loaded TCA tables.
147  $listOfAllTables = array_keys(‪$GLOBALS['TCA']);
148 
149  // In case the PHP ended for whatever reason, fetch the last position from registry
150  // and throw away all tables before that start point.
151  sort($listOfAllTables);
152  reset($listOfAllTables);
153  $firstTable = current($listOfAllTables) ?: '';
154  $startPosition = $this->‪getStartPosition($firstTable);
155  foreach ($listOfAllTables as $key => $table) {
156  if ($table === $startPosition['table']) {
157  break;
158  }
159  unset($listOfAllTables[$key]);
160  }
161 
162  // Ask each row updater if it potentially has field updates for rows of a table
163  $tableToUpdaterList = [];
164  foreach ($listOfAllTables as $table) {
165  foreach ($rowUpdaterInstances as $updater) {
166  if ($updater->hasPotentialUpdateForTable($table)) {
167  if (!isset($tableToUpdaterList[$table]) || !is_array($tableToUpdaterList[$table])) {
168  $tableToUpdaterList[$table] = [];
169  }
170  $tableToUpdaterList[$table][] = $updater;
171  }
172  }
173  }
174 
175  // Iterate through all rows of all tables that have potential row updaters attached,
176  // feed each single row to each updater and finally update each row in database if
177  // a row updater changed a fields
178  foreach ($tableToUpdaterList as $table => $updaters) {
180  $connectionForTable = $connectionPool->getConnectionForTable($table);
181  $queryBuilder = $connectionPool->getQueryBuilderForTable($table);
182  $queryBuilder->getRestrictions()->removeAll();
183  $queryBuilder->select('*')
184  ->from($table)
185  ->orderBy('uid');
186  if ($table === $startPosition['table']) {
187  $queryBuilder->where(
188  $queryBuilder->expr()->gt('uid', $queryBuilder->createNamedParameter($startPosition['uid']))
189  );
190  }
191  $statement = $queryBuilder->executeQuery();
192  $rowCountWithoutUpdate = 0;
193  while ($row = $statement->fetchAssociative()) {
194  $rowBefore = $row;
195  foreach ($updaters as $updater) {
196  $row = $updater->updateTableRow($table, $row);
197  }
198  $updatedFields = array_diff_assoc($row, $rowBefore);
199  if (empty($updatedFields)) {
200  // Updaters changed no field of that row
201  $rowCountWithoutUpdate++;
202  if ($rowCountWithoutUpdate >= 200) {
203  // Update startPosition if there were many rows without data change
204  $startPosition = [
205  'table' => $table,
206  'uid' => $row['uid'],
207  ];
208  $registry->set('installUpdateRows', 'rowUpdatePosition', $startPosition);
209  $rowCountWithoutUpdate = 0;
210  }
211  } else {
212  $rowCountWithoutUpdate = 0;
213  $startPosition = [
214  'table' => $table,
215  'uid' => $rowBefore['uid'],
216  ];
217  if ($connectionForSysRegistry === $connectionForTable) {
218  // Target table and sys_registry table are on the same connection, use a transaction
219  $connectionForTable->beginTransaction();
220  try {
221  $this->‪updateOrDeleteRow(
222  $connectionForTable,
223  $connectionForTable,
224  $table,
225  (int)$rowBefore['uid'],
226  $updatedFields,
227  $startPosition
228  );
229  $connectionForTable->commit();
230  } catch (\‪Exception $up) {
231  $connectionForTable->rollBack();
232  throw $up;
233  }
234  } else {
235  // Different connections for table and sys_registry.
236  // So, execute two distinct queries and hope for the best.
237  $this->‪updateOrDeleteRow(
238  $connectionForTable,
239  $connectionForSysRegistry,
240  $table,
241  (int)$rowBefore['uid'],
242  $updatedFields,
243  $startPosition
244  );
245  }
246  }
247  }
248  }
249 
250  // Ready with updates, remove position information from sys_registry
251  $registry->remove('installUpdateRows', 'rowUpdatePosition');
252  // Mark row updaters that were executed as done
253  foreach ($rowUpdaterInstances as $updater) {
254  $this->‪setRowUpdaterExecuted($updater);
255  }
256 
257  return true;
258  }
259 
265  protected function ‪getRowUpdatersToExecute(): array
266  {
267  $doneRowUpdater = GeneralUtility::makeInstance(Registry::class)->get('installUpdateRows', 'rowUpdatersDone', []);
268  return array_diff($this->rowUpdater, $doneRowUpdater);
269  }
270 
274  protected function ‪setRowUpdaterExecuted(‪RowUpdaterInterface $updater)
275  {
276  $registry = GeneralUtility::makeInstance(Registry::class);
277  $doneRowUpdater = $registry->get('installUpdateRows', 'rowUpdatersDone', []);
278  $doneRowUpdater[] = get_class($updater);
279  $registry->set('installUpdateRows', 'rowUpdatersDone', $doneRowUpdater);
280  }
281 
289  protected function ‪getStartPosition(string $firstTable): array
290  {
291  $registry = GeneralUtility::makeInstance(Registry::class);
292  $startPosition = $registry->get('installUpdateRows', 'rowUpdatePosition', []);
293  if (empty($startPosition)) {
294  $startPosition = [
295  'table' => $firstTable,
296  'uid' => 0,
297  ];
298  $registry->set('installUpdateRows', 'rowUpdatePosition', $startPosition);
299  }
300  return $startPosition;
301  }
302 
303  protected function ‪updateOrDeleteRow(‪Connection $connectionForTable, ‪Connection $connectionForSysRegistry, string $table, int ‪$uid, array $updatedFields, array $startPosition): void
304  {
305  $deleteField = ‪$GLOBALS['TCA'][$table]['ctrl']['delete'] ?? null;
306  if ($deleteField === null && isset($updatedFields['deleted']) && $updatedFields['deleted'] === 1) {
307  $connectionForTable->‪delete(
308  $table,
309  [
310  'uid' => ‪$uid,
311  ]
312  );
313  } else {
314  $connectionForTable->‪update(
315  $table,
316  $updatedFields,
317  [
318  'uid' => ‪$uid,
319  ]
320  );
321  }
322  $connectionForSysRegistry->‪update(
323  'sys_registry',
324  [
325  'entry_value' => serialize($startPosition),
326  ],
327  [
328  'entry_namespace' => 'installUpdateRows',
329  'entry_key' => 'rowUpdatePosition',
330  ],
331  [
332  // Needs to be declared LOB, so MSSQL can handle the conversion from string (nvarchar) to blob (varbinary)
333  'entry_value' => ‪Connection::PARAM_LOB,
334  'entry_namespace' => ‪Connection::PARAM_STR,
335  'entry_key' => ‪Connection::PARAM_STR,
336  ]
337  );
338  }
339 }
‪TYPO3\CMS\Install\Updates\DatabaseRowsUpdateWizard\getAvailableRowUpdater
‪string[] getAvailableRowUpdater()
Definition: DatabaseRowsUpdateWizard.php:59
‪TYPO3\CMS\Install\Updates\RepeatableInterface
Definition: RepeatableInterface.php:25
‪TYPO3\CMS\Install\Updates\RowUpdater\RowUpdaterInterface
Definition: RowUpdaterInterface.php:24
‪TYPO3\CMS\Install\Updates\DatabaseRowsUpdateWizard\getDescription
‪string getDescription()
Definition: DatabaseRowsUpdateWizard.php:76
‪TYPO3\CMS\Core\Registry
Definition: Registry.php:33
‪TYPO3\CMS\Install\Attribute\UpgradeWizard
Definition: UpgradeWizard.php:25
‪TYPO3\CMS\Install\Updates\DatabaseRowsUpdateWizard\getStartPosition
‪array getStartPosition(string $firstTable)
Definition: DatabaseRowsUpdateWizard.php:288
‪TYPO3\CMS\Install\Updates\DatabaseRowsUpdateWizard\updateNecessary
‪bool updateNecessary()
Definition: DatabaseRowsUpdateWizard.php:96
‪TYPO3\CMS\Core\Database\Connection\PARAM_STR
‪const PARAM_STR
Definition: Connection.php:57
‪TYPO3\CMS\Install\Updates
Definition: LegacyClassesForIde.php:22
‪TYPO3\CMS\Install\Updates\DatabaseRowsUpdateWizard
Definition: DatabaseRowsUpdateWizard.php:48
‪TYPO3\CMS\Install\Exception
Definition: Exception.php:23
‪TYPO3\CMS\Install\Updates\DatabaseRowsUpdateWizard\$rowUpdater
‪array $rowUpdater
Definition: DatabaseRowsUpdateWizard.php:51
‪TYPO3\CMS\Install\Updates\RowUpdater\SysRedirectRootPageMoveMigration
Definition: SysRedirectRootPageMoveMigration.php:31
‪TYPO3\CMS\Install\Updates\DatabaseRowsUpdateWizard\getTitle
‪string getTitle()
Definition: DatabaseRowsUpdateWizard.php:67
‪TYPO3\CMS\Install\Updates\DatabaseRowsUpdateWizard\getRowUpdatersToExecute
‪array getRowUpdatersToExecute()
Definition: DatabaseRowsUpdateWizard.php:264
‪TYPO3\CMS\Install\Updates\DatabaseRowsUpdateWizard\setRowUpdaterExecuted
‪setRowUpdaterExecuted(RowUpdaterInterface $updater)
Definition: DatabaseRowsUpdateWizard.php:273
‪TYPO3\CMS\Install\Updates\DatabaseRowsUpdateWizard\executeUpdate
‪executeUpdate()
Definition: DatabaseRowsUpdateWizard.php:117
‪TYPO3\CMS\Install\Updates\DatabaseRowsUpdateWizard\getPrerequisites
‪string[] getPrerequisites()
Definition: DatabaseRowsUpdateWizard.php:104
‪TYPO3\CMS\Core\Database\Connection
Definition: Connection.php:41
‪TYPO3\CMS\Install\Updates\UpgradeWizardInterface
Definition: UpgradeWizardInterface.php:24
‪TYPO3\CMS\Core\Database\Connection\delete
‪int delete(string $tableName, array $identifier=[], array $types=[])
Definition: Connection.php:290
‪TYPO3\CMS\Webhooks\Message\$uid
‪identifier readonly int $uid
Definition: PageModificationMessage.php:35
‪$GLOBALS
‪$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['adminpanel']['modules']
Definition: ext_localconf.php:25
‪TYPO3\CMS\Install\Updates\DatabaseRowsUpdateWizard\updateOrDeleteRow
‪updateOrDeleteRow(Connection $connectionForTable, Connection $connectionForSysRegistry, string $table, int $uid, array $updatedFields, array $startPosition)
Definition: DatabaseRowsUpdateWizard.php:302
‪TYPO3\CMS\Core\Database\ConnectionPool
Definition: ConnectionPool.php:46
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:52
‪TYPO3\CMS\Core\Database\Connection\update
‪int update(string $tableName, array $data, array $identifier=[], array $types=[])
Definition: Connection.php:270
‪TYPO3\CMS\Core\Database\Connection\PARAM_LOB
‪const PARAM_LOB
Definition: Connection.php:62