‪TYPO3CMS  10.4
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 
20 use Doctrine\DBAL\Platforms\SQLServerPlatform;
27 
47 {
51  protected ‪$rowUpdater = [
52  WorkspaceVersionRecordsMigration::class,
53  ];
54 
59  public function ‪getAvailableRowUpdater(): array
60  {
61  return ‪$this->rowUpdater;
62  }
63 
67  public function ‪getIdentifier(): string
68  {
69  return 'databaseRowsUpdateWizard';
70  }
71 
75  public function ‪getTitle(): string
76  {
77  return 'Execute database migrations on single rows';
78  }
79 
84  public function ‪getDescription(): string
85  {
86  $rowUpdaterNotExecuted = $this->‪getRowUpdatersToExecute();
87  $description = 'Row updaters that have not been executed:';
88  foreach ($rowUpdaterNotExecuted as $rowUpdateClassName) {
89  ‪$rowUpdater = GeneralUtility::makeInstance($rowUpdateClassName);
90  if (!‪$rowUpdater instanceof ‪RowUpdaterInterface) {
91  throw new \RuntimeException(
92  'Row updater must implement RowUpdaterInterface',
93  1484066647
94  );
95  }
96  $description .= LF . ‪$rowUpdater->getTitle();
97  }
98  return $description;
99  }
100 
104  public function ‪updateNecessary(): bool
105  {
106  return !empty($this->‪getRowUpdatersToExecute());
107  }
108 
112  public function ‪getPrerequisites(): array
113  {
114  return [
115  DatabaseUpdatedPrerequisite::class
116  ];
117  }
118 
126  public function ‪executeUpdate(): bool
127  {
128  $registry = GeneralUtility::makeInstance(Registry::class);
129 
130  // If rows from the target table that is updated and the sys_registry table are on the
131  // same connection, the row update statement and sys_registry position update will be
132  // handled in a transaction to have an atomic operation in case of errors during execution.
133  $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
134  $connectionForSysRegistry = $connectionPool->getConnectionForTable('sys_registry');
135 
137  $rowUpdaterInstances = [];
138  // Single row updater instances are created only once for this method giving
139  // them a chance to set up local properties during hasPotentialUpdateForTable()
140  // and using that in updateTableRow()
141  foreach ($this->‪getRowUpdatersToExecute() as ‪$rowUpdater) {
142  $rowUpdaterInstance = GeneralUtility::makeInstance(‪$rowUpdater);
143  if (!$rowUpdaterInstance instanceof ‪RowUpdaterInterface) {
144  throw new \RuntimeException(
145  'Row updater must implement RowUpdaterInterface',
146  1484071612
147  );
148  }
149  $rowUpdaterInstances[] = $rowUpdaterInstance;
150  }
151 
152  // Scope of the row updater is to update all rows that have TCA,
153  // our list of tables is just the list of loaded TCA tables.
155  $listOfAllTables = array_keys(‪$GLOBALS['TCA']);
156 
157  // In case the PHP ended for whatever reason, fetch the last position from registry
158  // and throw away all tables before that start point.
159  sort($listOfAllTables);
160  reset($listOfAllTables);
161  $firstTable = current($listOfAllTables);
162  $startPosition = $this->‪getStartPosition($firstTable);
163  foreach ($listOfAllTables as $key => $table) {
164  if ($table === $startPosition['table']) {
165  break;
166  }
167  unset($listOfAllTables[$key]);
168  }
169 
170  // Ask each row updater if it potentially has field updates for rows of a table
171  $tableToUpdaterList = [];
172  foreach ($listOfAllTables as $table) {
173  foreach ($rowUpdaterInstances as $updater) {
174  if ($updater->hasPotentialUpdateForTable($table)) {
175  if (!is_array($tableToUpdaterList[$table])) {
176  $tableToUpdaterList[$table] = [];
177  }
178  $tableToUpdaterList[$table][] = $updater;
179  }
180  }
181  }
182 
183  // Iterate through all rows of all tables that have potential row updaters attached,
184  // feed each single row to each updater and finally update each row in database if
185  // a row updater changed a fields
186  foreach ($tableToUpdaterList as $table => $updaters) {
188  $connectionForTable = $connectionPool->getConnectionForTable($table);
189  $queryBuilder = $connectionPool->getQueryBuilderForTable($table);
190  $queryBuilder->getRestrictions()->removeAll();
191  $queryBuilder->select('*')
192  ->from($table)
193  ->orderBy('uid');
194  if ($table === $startPosition['table']) {
195  $queryBuilder->where(
196  $queryBuilder->expr()->gt('uid', $queryBuilder->createNamedParameter($startPosition['uid']))
197  );
198  }
199  $statement = $queryBuilder->execute();
200  $rowCountWithoutUpdate = 0;
201  while ($row = $rowBefore = $statement->fetch()) {
202  foreach ($updaters as $updater) {
203  $row = $updater->updateTableRow($table, $row);
204  }
205  $updatedFields = array_diff_assoc($row, $rowBefore);
206  if (empty($updatedFields)) {
207  // Updaters changed no field of that row
208  $rowCountWithoutUpdate++;
209  if ($rowCountWithoutUpdate >= 200) {
210  // Update startPosition if there were many rows without data change
211  $startPosition = [
212  'table' => $table,
213  'uid' => $row['uid'],
214  ];
215  $registry->set('installUpdateRows', 'rowUpdatePosition', $startPosition);
216  $rowCountWithoutUpdate = 0;
217  }
218  } else {
219  $rowCountWithoutUpdate = 0;
220  $startPosition = [
221  'table' => $table,
222  'uid' => $rowBefore['uid'],
223  ];
224  if ($connectionForSysRegistry === $connectionForTable
225  && !($connectionForSysRegistry->getDatabasePlatform() instanceof SQLServerPlatform)
226  ) {
227  // Target table and sys_registry table are on the same connection and not mssql, use a transaction
228  $connectionForTable->beginTransaction();
229  try {
230  $this->‪updateOrDeleteRow(
231  $connectionForTable,
232  $connectionForTable,
233  $table,
234  (int)$rowBefore['uid'],
235  $updatedFields,
236  $startPosition
237  );
238  $connectionForTable->commit();
239  } catch (\‪Exception $up) {
240  $connectionForTable->rollBack();
241  throw $up;
242  }
243  } else {
244  // Either different connections for table and sys_registry, or mssql.
245  // SqlServer can not run a transaction for a table if the same table is queried
246  // currently - our above ->fetch() main loop.
247  // So, execute two distinct queries and hope for the best.
248  $this->‪updateOrDeleteRow(
249  $connectionForTable,
250  $connectionForSysRegistry,
251  $table,
252  (int)$rowBefore['uid'],
253  $updatedFields,
254  $startPosition
255  );
256  }
257  }
258  }
259  }
260 
261  // Ready with updates, remove position information from sys_registry
262  $registry->remove('installUpdateRows', 'rowUpdatePosition');
263  // Mark row updaters that were executed as done
264  foreach ($rowUpdaterInstances as $updater) {
265  $this->‪setRowUpdaterExecuted($updater);
266  }
267 
268  return true;
269  }
270 
276  protected function ‪getRowUpdatersToExecute(): array
277  {
278  $doneRowUpdater = GeneralUtility::makeInstance(Registry::class)->get('installUpdateRows', 'rowUpdatersDone', []);
279  return array_diff($this->rowUpdater, $doneRowUpdater);
280  }
281 
287  protected function ‪setRowUpdaterExecuted(‪RowUpdaterInterface $updater)
288  {
289  $registry = GeneralUtility::makeInstance(Registry::class);
290  $doneRowUpdater = $registry->get('installUpdateRows', 'rowUpdatersDone', []);
291  $doneRowUpdater[] = get_class($updater);
292  $registry->set('installUpdateRows', 'rowUpdatersDone', $doneRowUpdater);
293  }
294 
302  protected function ‪getStartPosition(string $firstTable): array
303  {
304  $registry = GeneralUtility::makeInstance(Registry::class);
305  $startPosition = $registry->get('installUpdateRows', 'rowUpdatePosition', []);
306  if (empty($startPosition)) {
307  $startPosition = [
308  'table' => $firstTable,
309  'uid' => 0,
310  ];
311  $registry->set('installUpdateRows', 'rowUpdatePosition', $startPosition);
312  }
313  return $startPosition;
314  }
315 
324  protected function ‪updateOrDeleteRow(‪Connection $connectionForTable, ‪Connection $connectionForSysRegistry, string $table, int $uid, array $updatedFields, array $startPosition): void
325  {
326  $deleteField = ‪$GLOBALS['TCA'][$table]['ctrl']['delete'] ?? null;
327  if ($deleteField === null && $updatedFields['deleted'] === 1) {
328  $connectionForTable->‪delete(
329  $table,
330  [
331  'uid' => $uid,
332  ]
333  );
334  } else {
335  $connectionForTable->‪update(
336  $table,
337  $updatedFields,
338  [
339  'uid' => $uid,
340  ]
341  );
342  }
343  $connectionForSysRegistry->‪update(
344  'sys_registry',
345  [
346  'entry_value' => serialize($startPosition),
347  ],
348  [
349  'entry_namespace' => 'installUpdateRows',
350  'entry_key' => 'rowUpdatePosition',
351  ],
352  [
353  // Needs to be declared LOB, so MSSQL can handle the conversion from string (nvarchar) to blob (varbinary)
354  'entry_value' => \PDO::PARAM_LOB,
355  'entry_namespace' => \PDO::PARAM_STR,
356  'entry_key' => \PDO::PARAM_STR,
357  ]
358  );
359  }
360 }
‪TYPO3\CMS\Install\Updates\DatabaseRowsUpdateWizard\getAvailableRowUpdater
‪string[] getAvailableRowUpdater()
Definition: DatabaseRowsUpdateWizard.php:58
‪TYPO3\CMS\Install\Updates\RowUpdater\WorkspaceVersionRecordsMigration
Definition: WorkspaceVersionRecordsMigration.php:34
‪TYPO3\CMS\Install\Updates\RepeatableInterface
Definition: RepeatableInterface.php:26
‪TYPO3\CMS\Install\Updates\DatabaseRowsUpdateWizard\getIdentifier
‪string getIdentifier()
Definition: DatabaseRowsUpdateWizard.php:66
‪TYPO3\CMS\Install\Updates\RowUpdater\RowUpdaterInterface
Definition: RowUpdaterInterface.php:24
‪TYPO3\CMS\Install\Updates\DatabaseRowsUpdateWizard\getDescription
‪string getDescription()
Definition: DatabaseRowsUpdateWizard.php:83
‪TYPO3\CMS\Core\Registry
Definition: Registry.php:33
‪TYPO3\CMS\Install\Updates\DatabaseRowsUpdateWizard\getStartPosition
‪array getStartPosition(string $firstTable)
Definition: DatabaseRowsUpdateWizard.php:301
‪TYPO3\CMS\Install\Updates\DatabaseRowsUpdateWizard\updateNecessary
‪bool updateNecessary()
Definition: DatabaseRowsUpdateWizard.php:103
‪TYPO3\CMS\Core\Database\Connection\update
‪int update($tableName, array $data, array $identifier, array $types=[])
Definition: Connection.php:302
‪TYPO3\CMS\Core\Database\Connection\delete
‪int delete($tableName, array $identifier, array $types=[])
Definition: Connection.php:323
‪TYPO3\CMS\Install\Updates
Definition: AbstractDownloadExtensionUpdate.php:16
‪TYPO3\CMS\Install\Updates\DatabaseRowsUpdateWizard
Definition: DatabaseRowsUpdateWizard.php:47
‪TYPO3\CMS\Install\Exception
Definition: Exception.php:24
‪TYPO3\CMS\Install\Updates\DatabaseRowsUpdateWizard\$rowUpdater
‪array $rowUpdater
Definition: DatabaseRowsUpdateWizard.php:50
‪TYPO3\CMS\Install\Updates\DatabaseRowsUpdateWizard\getTitle
‪string getTitle()
Definition: DatabaseRowsUpdateWizard.php:74
‪TYPO3\CMS\Install\Updates\DatabaseRowsUpdateWizard\getRowUpdatersToExecute
‪array getRowUpdatersToExecute()
Definition: DatabaseRowsUpdateWizard.php:275
‪TYPO3\CMS\Install\Updates\DatabaseRowsUpdateWizard\setRowUpdaterExecuted
‪setRowUpdaterExecuted(RowUpdaterInterface $updater)
Definition: DatabaseRowsUpdateWizard.php:286
‪TYPO3\CMS\Install\Updates\DatabaseRowsUpdateWizard\executeUpdate
‪bool executeUpdate()
Definition: DatabaseRowsUpdateWizard.php:125
‪TYPO3\CMS\Install\Updates\DatabaseRowsUpdateWizard\getPrerequisites
‪string[] getPrerequisites()
Definition: DatabaseRowsUpdateWizard.php:111
‪TYPO3\CMS\Core\Database\Connection
Definition: Connection.php:36
‪TYPO3\CMS\Install\Updates\UpgradeWizardInterface
Definition: UpgradeWizardInterface.php:24
‪$GLOBALS
‪$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['adminpanel']['modules']
Definition: ext_localconf.php:5
‪TYPO3\CMS\Install\Updates\DatabaseRowsUpdateWizard\updateOrDeleteRow
‪updateOrDeleteRow(Connection $connectionForTable, Connection $connectionForSysRegistry, string $table, int $uid, array $updatedFields, array $startPosition)
Definition: DatabaseRowsUpdateWizard.php:323
‪TYPO3\CMS\Core\Database\ConnectionPool
Definition: ConnectionPool.php:46
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:46