‪TYPO3CMS  11.5
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\SQLServer2012Platform as SQLServerPlatform;
30 
50 {
54  protected ‪$rowUpdater = [
55  WorkspaceVersionRecordsMigration::class,
56  L18nDiffsourceToJsonMigration::class,
57  WorkspaceMovePlaceholderRemovalMigration::class,
58  WorkspaceNewPlaceholderRemovalMigration::class,
59  ];
60 
65  public function ‪getAvailableRowUpdater(): array
66  {
67  return ‪$this->rowUpdater;
68  }
69 
73  public function ‪getIdentifier(): string
74  {
75  return 'databaseRowsUpdateWizard';
76  }
77 
81  public function ‪getTitle(): string
82  {
83  return 'Execute database migrations on single rows';
84  }
85 
90  public function ‪getDescription(): string
91  {
92  $rowUpdaterNotExecuted = $this->‪getRowUpdatersToExecute();
93  $description = 'Row updaters that have not been executed:';
94  foreach ($rowUpdaterNotExecuted as $rowUpdateClassName) {
95  ‪$rowUpdater = GeneralUtility::makeInstance($rowUpdateClassName);
96  if (!‪$rowUpdater instanceof ‪RowUpdaterInterface) {
97  throw new \RuntimeException(
98  'Row updater must implement RowUpdaterInterface',
99  1484066647
100  );
101  }
102  $description .= LF . ‪$rowUpdater->getTitle();
103  }
104  return $description;
105  }
106 
110  public function ‪updateNecessary(): bool
111  {
112  return !empty($this->‪getRowUpdatersToExecute());
113  }
114 
118  public function ‪getPrerequisites(): array
119  {
120  return [
121  DatabaseUpdatedPrerequisite::class,
122  ];
123  }
124 
132  public function ‪executeUpdate(): bool
133  {
134  $registry = GeneralUtility::makeInstance(Registry::class);
135 
136  // If rows from the target table that is updated and the sys_registry table are on the
137  // same connection, the row update statement and sys_registry position update will be
138  // handled in a transaction to have an atomic operation in case of errors during execution.
139  $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
140  $connectionForSysRegistry = $connectionPool->getConnectionForTable('sys_registry');
141 
143  $rowUpdaterInstances = [];
144  // Single row updater instances are created only once for this method giving
145  // them a chance to set up local properties during hasPotentialUpdateForTable()
146  // and using that in updateTableRow()
147  foreach ($this->‪getRowUpdatersToExecute() as ‪$rowUpdater) {
148  $rowUpdaterInstance = GeneralUtility::makeInstance(‪$rowUpdater);
149  if (!$rowUpdaterInstance instanceof ‪RowUpdaterInterface) {
150  throw new \RuntimeException(
151  'Row updater must implement RowUpdaterInterface',
152  1484071612
153  );
154  }
155  $rowUpdaterInstances[] = $rowUpdaterInstance;
156  }
157 
158  // Scope of the row updater is to update all rows that have TCA,
159  // our list of tables is just the list of loaded TCA tables.
161  $listOfAllTables = array_keys(‪$GLOBALS['TCA']);
162 
163  // In case the PHP ended for whatever reason, fetch the last position from registry
164  // and throw away all tables before that start point.
165  sort($listOfAllTables);
166  reset($listOfAllTables);
167  $firstTable = current($listOfAllTables) ?: '';
168  $startPosition = $this->‪getStartPosition($firstTable);
169  foreach ($listOfAllTables as $key => $table) {
170  if ($table === $startPosition['table']) {
171  break;
172  }
173  unset($listOfAllTables[$key]);
174  }
175 
176  // Ask each row updater if it potentially has field updates for rows of a table
177  $tableToUpdaterList = [];
178  foreach ($listOfAllTables as $table) {
179  foreach ($rowUpdaterInstances as $updater) {
180  if ($updater->hasPotentialUpdateForTable($table)) {
181  if (!isset($tableToUpdaterList[$table]) || !is_array($tableToUpdaterList[$table])) {
182  $tableToUpdaterList[$table] = [];
183  }
184  $tableToUpdaterList[$table][] = $updater;
185  }
186  }
187  }
188 
189  // Iterate through all rows of all tables that have potential row updaters attached,
190  // feed each single row to each updater and finally update each row in database if
191  // a row updater changed a fields
192  foreach ($tableToUpdaterList as $table => $updaters) {
194  $connectionForTable = $connectionPool->getConnectionForTable($table);
195  $queryBuilder = $connectionPool->getQueryBuilderForTable($table);
196  $queryBuilder->getRestrictions()->removeAll();
197  $queryBuilder->select('*')
198  ->from($table)
199  ->orderBy('uid');
200  if ($table === $startPosition['table']) {
201  $queryBuilder->where(
202  $queryBuilder->expr()->gt('uid', $queryBuilder->createNamedParameter($startPosition['uid']))
203  );
204  }
205  $statement = $queryBuilder->executeQuery();
206  $rowCountWithoutUpdate = 0;
207  while ($row = $statement->fetchAssociative()) {
208  $rowBefore = $row;
209  foreach ($updaters as $updater) {
210  $row = $updater->updateTableRow($table, $row);
211  }
212  $updatedFields = array_diff_assoc($row, $rowBefore);
213  if (empty($updatedFields)) {
214  // Updaters changed no field of that row
215  $rowCountWithoutUpdate++;
216  if ($rowCountWithoutUpdate >= 200) {
217  // Update startPosition if there were many rows without data change
218  $startPosition = [
219  'table' => $table,
220  'uid' => $row['uid'],
221  ];
222  $registry->set('installUpdateRows', 'rowUpdatePosition', $startPosition);
223  $rowCountWithoutUpdate = 0;
224  }
225  } else {
226  $rowCountWithoutUpdate = 0;
227  $startPosition = [
228  'table' => $table,
229  'uid' => $rowBefore['uid'],
230  ];
231  if ($connectionForSysRegistry === $connectionForTable
232  && !($connectionForSysRegistry->getDatabasePlatform() instanceof SQLServerPlatform)
233  ) {
234  // Target table and sys_registry table are on the same connection and not mssql, use a transaction
235  $connectionForTable->beginTransaction();
236  try {
237  $this->‪updateOrDeleteRow(
238  $connectionForTable,
239  $connectionForTable,
240  $table,
241  (int)$rowBefore['uid'],
242  $updatedFields,
243  $startPosition
244  );
245  $connectionForTable->commit();
246  } catch (\‪Exception $up) {
247  $connectionForTable->rollBack();
248  throw $up;
249  }
250  } else {
251  // Either different connections for table and sys_registry, or mssql.
252  // SqlServer can not run a transaction for a table if the same table is queried
253  // currently - our above ->fetchAssociative() main loop.
254  // So, execute two distinct queries and hope for the best.
255  $this->‪updateOrDeleteRow(
256  $connectionForTable,
257  $connectionForSysRegistry,
258  $table,
259  (int)$rowBefore['uid'],
260  $updatedFields,
261  $startPosition
262  );
263  }
264  }
265  }
266  }
267 
268  // Ready with updates, remove position information from sys_registry
269  $registry->remove('installUpdateRows', 'rowUpdatePosition');
270  // Mark row updaters that were executed as done
271  foreach ($rowUpdaterInstances as $updater) {
272  $this->‪setRowUpdaterExecuted($updater);
273  }
274 
275  return true;
276  }
277 
283  protected function ‪getRowUpdatersToExecute(): array
284  {
285  $doneRowUpdater = GeneralUtility::makeInstance(Registry::class)->get('installUpdateRows', 'rowUpdatersDone', []);
286  return array_diff($this->rowUpdater, $doneRowUpdater);
287  }
288 
294  protected function ‪setRowUpdaterExecuted(‪RowUpdaterInterface $updater)
295  {
296  $registry = GeneralUtility::makeInstance(Registry::class);
297  $doneRowUpdater = $registry->get('installUpdateRows', 'rowUpdatersDone', []);
298  $doneRowUpdater[] = get_class($updater);
299  $registry->set('installUpdateRows', 'rowUpdatersDone', $doneRowUpdater);
300  }
301 
309  protected function ‪getStartPosition(string $firstTable): array
310  {
311  $registry = GeneralUtility::makeInstance(Registry::class);
312  $startPosition = $registry->get('installUpdateRows', 'rowUpdatePosition', []);
313  if (empty($startPosition)) {
314  $startPosition = [
315  'table' => $firstTable,
316  'uid' => 0,
317  ];
318  $registry->set('installUpdateRows', 'rowUpdatePosition', $startPosition);
319  }
320  return $startPosition;
321  }
322 
331  protected function ‪updateOrDeleteRow(‪Connection $connectionForTable, ‪Connection $connectionForSysRegistry, string $table, int $uid, array $updatedFields, array $startPosition): void
332  {
333  $deleteField = ‪$GLOBALS['TCA'][$table]['ctrl']['delete'] ?? null;
334  if ($deleteField === null && isset($updatedFields['deleted']) && $updatedFields['deleted'] === 1) {
335  $connectionForTable->‪delete(
336  $table,
337  [
338  'uid' => $uid,
339  ]
340  );
341  } else {
342  $connectionForTable->‪update(
343  $table,
344  $updatedFields,
345  [
346  'uid' => $uid,
347  ]
348  );
349  }
350  $connectionForSysRegistry->‪update(
351  'sys_registry',
352  [
353  'entry_value' => serialize($startPosition),
354  ],
355  [
356  'entry_namespace' => 'installUpdateRows',
357  'entry_key' => 'rowUpdatePosition',
358  ],
359  [
360  // Needs to be declared LOB, so MSSQL can handle the conversion from string (nvarchar) to blob (varbinary)
361  'entry_value' => ‪Connection::PARAM_LOB,
362  'entry_namespace' => ‪Connection::PARAM_STR,
363  'entry_key' => ‪Connection::PARAM_STR,
364  ]
365  );
366  }
367 }
‪TYPO3\CMS\Install\Updates\DatabaseRowsUpdateWizard\getAvailableRowUpdater
‪string[] getAvailableRowUpdater()
Definition: DatabaseRowsUpdateWizard.php:64
‪TYPO3\CMS\Install\Updates\RowUpdater\WorkspaceVersionRecordsMigration
Definition: WorkspaceVersionRecordsMigration.php:33
‪TYPO3\CMS\Install\Updates\RepeatableInterface
Definition: RepeatableInterface.php:25
‪TYPO3\CMS\Install\Updates\DatabaseRowsUpdateWizard\getIdentifier
‪string getIdentifier()
Definition: DatabaseRowsUpdateWizard.php:72
‪TYPO3\CMS\Install\Updates\RowUpdater\RowUpdaterInterface
Definition: RowUpdaterInterface.php:24
‪TYPO3\CMS\Install\Updates\DatabaseRowsUpdateWizard\getDescription
‪string getDescription()
Definition: DatabaseRowsUpdateWizard.php:89
‪TYPO3\CMS\Core\Registry
Definition: Registry.php:33
‪TYPO3\CMS\Install\Updates\RowUpdater\WorkspaceMovePlaceholderRemovalMigration
Definition: WorkspaceMovePlaceholderRemovalMigration.php:38
‪TYPO3\CMS\Install\Updates\DatabaseRowsUpdateWizard\getStartPosition
‪array getStartPosition(string $firstTable)
Definition: DatabaseRowsUpdateWizard.php:308
‪TYPO3\CMS\Install\Updates\DatabaseRowsUpdateWizard\updateNecessary
‪bool updateNecessary()
Definition: DatabaseRowsUpdateWizard.php:109
‪TYPO3\CMS\Core\Database\Connection\PARAM_STR
‪const PARAM_STR
Definition: Connection.php:54
‪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:50
‪TYPO3\CMS\Install\Updates\RowUpdater\WorkspaceNewPlaceholderRemovalMigration
Definition: WorkspaceNewPlaceholderRemovalMigration.php:44
‪TYPO3\CMS\Install\Exception
Definition: Exception.php:23
‪TYPO3\CMS\Install\Updates\DatabaseRowsUpdateWizard\$rowUpdater
‪array $rowUpdater
Definition: DatabaseRowsUpdateWizard.php:53
‪TYPO3\CMS\Install\Updates\DatabaseRowsUpdateWizard\getTitle
‪string getTitle()
Definition: DatabaseRowsUpdateWizard.php:80
‪TYPO3\CMS\Install\Updates\DatabaseRowsUpdateWizard\getRowUpdatersToExecute
‪array getRowUpdatersToExecute()
Definition: DatabaseRowsUpdateWizard.php:282
‪TYPO3\CMS\Install\Updates\DatabaseRowsUpdateWizard\setRowUpdaterExecuted
‪setRowUpdaterExecuted(RowUpdaterInterface $updater)
Definition: DatabaseRowsUpdateWizard.php:293
‪TYPO3\CMS\Install\Updates\DatabaseRowsUpdateWizard\executeUpdate
‪bool executeUpdate()
Definition: DatabaseRowsUpdateWizard.php:131
‪TYPO3\CMS\Install\Updates\RowUpdater\L18nDiffsourceToJsonMigration
Definition: L18nDiffsourceToJsonMigration.php:29
‪TYPO3\CMS\Install\Updates\DatabaseRowsUpdateWizard\getPrerequisites
‪string[] getPrerequisites()
Definition: DatabaseRowsUpdateWizard.php:117
‪TYPO3\CMS\Core\Database\Connection
Definition: Connection.php:38
‪TYPO3\CMS\Install\Updates\UpgradeWizardInterface
Definition: UpgradeWizardInterface.php:24
‪$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:330
‪TYPO3\CMS\Core\Database\ConnectionPool
Definition: ConnectionPool.php:46
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:50
‪TYPO3\CMS\Core\Database\Connection\PARAM_LOB
‪const PARAM_LOB
Definition: Connection.php:59