TYPO3 CMS  TYPO3_8-7
DatabaseRowsUpdateWizard.php
Go to the documentation of this file.
1 <?php
2 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 
25 
44 {
48  protected $title = 'Execute database migrations on single rows';
49 
53  protected $rowUpdater = [
54  L10nModeUpdater::class,
55  ImageCropUpdater::class,
56  RteLinkSyntaxUpdater::class,
57  ];
58 
66  public function checkForUpdate(&$description)
67  {
68  $updateNeeded = false;
69  $rowUpdaterNotExecuted = $this->getRowUpdatersToExecute();
70  if (!empty($rowUpdaterNotExecuted)) {
71  $updateNeeded = true;
72  }
73  if (!$updateNeeded) {
74  return false;
75  }
76 
77  $description = 'Some row updaters have not been executed:';
78  foreach ($rowUpdaterNotExecuted as $rowUpdateClassName) {
79  $rowUpdater = GeneralUtility::makeInstance($rowUpdateClassName);
80  if (!$rowUpdater instanceof RowUpdaterInterface) {
81  throw new \RuntimeException(
82  'Row updater must implement RowUpdaterInterface',
83  1484066647
84  );
85  }
86  $description .= '<br />' . htmlspecialchars($rowUpdater->getTitle());
87  }
88 
89  return $updateNeeded;
90  }
91 
101  public function performUpdate(array &$databaseQueries, &$customMessage)
102  {
103  $registry = GeneralUtility::makeInstance(Registry::class);
104 
105  // If rows from the target table that is updated and the sys_registry table are on the
106  // same connection, the row update statement and sys_registry position update will be
107  // handled in a transaction to have an atomic operation in case of errors during execution.
108  $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
109  $connectionForSysRegistry = $connectionPool->getConnectionForTable('sys_registry');
110 
112  $rowUpdaterInstances = [];
113  // Single row updater instances are created only once for this method giving
114  // them a chance to set up local properties during hasPotentialUpdateForTable()
115  // and using that in updateTableRow()
116  foreach ($this->getRowUpdatersToExecute() as $rowUpdater) {
117  $rowUpdaterInstance = GeneralUtility::makeInstance($rowUpdater);
118  if (!$rowUpdaterInstance instanceof RowUpdaterInterface) {
119  throw new \RuntimeException(
120  'Row updater must implement RowUpdaterInterface',
121  1484071612
122  );
123  }
124  $rowUpdaterInstances[] = $rowUpdaterInstance;
125  }
126 
127  // Scope of the row updater is to update all rows that have TCA,
128  // our list of tables is just the list of loaded TCA tables.
129  $listOfAllTables = array_keys($GLOBALS['TCA']);
130 
131  // In case the PHP ended for whatever reason, fetch the last position from registry
132  // and throw away all tables before that start point.
133  sort($listOfAllTables);
134  reset($listOfAllTables);
135  $firstTable = current($listOfAllTables);
136  $startPosition = $this->getStartPosition($firstTable);
137  foreach ($listOfAllTables as $key => $table) {
138  if ($table === $startPosition['table']) {
139  break;
140  }
141  unset($listOfAllTables[$key]);
142  }
143 
144  // Ask each row updater if it potentially has field updates for rows of a table
145  $tableToUpdaterList = [];
146  foreach ($listOfAllTables as $table) {
147  foreach ($rowUpdaterInstances as $updater) {
148  if ($updater->hasPotentialUpdateForTable($table)) {
149  if (!is_array($tableToUpdaterList[$table])) {
150  $tableToUpdaterList[$table] = [];
151  }
152  $tableToUpdaterList[$table][] = $updater;
153  }
154  }
155  }
156 
157  // Iterate through all rows of all tables that have potential row updaters attached,
158  // feed each single row to each updater and finally update each row in database if
159  // a row updater changed a fields
160  foreach ($tableToUpdaterList as $table => $updaters) {
162  $connectionForTable = $connectionPool->getConnectionForTable($table);
163  $queryBuilder = $connectionPool->getQueryBuilderForTable($table);
164  $queryBuilder->getRestrictions()->removeAll();
165  $queryBuilder->select('*')
166  ->from($table)
167  ->orderBy('uid');
168  if ($table === $startPosition['table']) {
169  $queryBuilder->where(
170  $queryBuilder->expr()->gt('uid', $queryBuilder->createNamedParameter($startPosition['uid']))
171  );
172  }
173  $statement = $queryBuilder->execute();
174  $rowCountWithoutUpdate = 0;
175  while ($row = $rowBefore = $statement->fetch()) {
176  foreach ($updaters as $updater) {
177  $row = $updater->updateTableRow($table, $row);
178  }
179  $updatedFields = array_diff_assoc($row, $rowBefore);
180  if (empty($updatedFields)) {
181  // Updaters changed no field of that row
182  $rowCountWithoutUpdate ++;
183  if ($rowCountWithoutUpdate >= 200) {
184  // Update startPosition if there were many rows without data change
185  $startPosition = [
186  'table' => $table,
187  'uid' => $row['uid'],
188  ];
189  $registry->set('installUpdateRows', 'rowUpdatePosition', $startPosition);
190  $rowCountWithoutUpdate = 0;
191  }
192  } else {
193  $rowCountWithoutUpdate = 0;
194  $startPosition = [
195  'table' => $table,
196  'uid' => $rowBefore['uid'],
197  ];
198  if ($connectionForSysRegistry === $connectionForTable) {
199  // Target table and sys_registry table are on the same connection, use a transaction
200  $connectionForTable->beginTransaction();
201  try {
202  $connectionForTable->update(
203  $table,
204  $updatedFields,
205  [
206  'uid' => $rowBefore['uid'],
207  ]
208  );
209  $connectionForTable->update(
210  'sys_registry',
211  [
212  'entry_value' => serialize($startPosition),
213  ],
214  [
215  'entry_namespace' => 'installUpdateRows',
216  'entry_key' => 'rowUpdatePosition',
217  ]
218  );
219  $connectionForTable->commit();
220  } catch (\Exception $up) {
221  $connectionForTable->rollBack();
222  throw $up;
223  }
224  } else {
225  // Different connections for table and sys_registry -> execute two
226  // distinct queries and hope for the best.
227  $connectionForTable->update(
228  $table,
229  $updatedFields,
230  [
231  'uid' => $rowBefore['uid'],
232  ]
233  );
234  $connectionForSysRegistry->update(
235  'sys_registry',
236  [
237  'entry_value' => serialize($startPosition),
238  ],
239  [
240  'entry_namespace' => 'installUpdateRows',
241  'entry_key' => 'rowUpdatePosition',
242  ]
243  );
244  }
245  }
246  }
247  }
248 
249  // Ready with updates, remove position information from sys_registry
250  $registry->remove('installUpdateRows', 'rowUpdatePosition');
251  // Mark row updaters that were executed as done
252  foreach ($rowUpdaterInstances as $updater) {
253  $this->setRowUpdaterExecuted($updater);
254  }
255 
256  return true;
257  }
258 
264  protected function getRowUpdatersToExecute(): array
265  {
266  $doneRowUpdater = GeneralUtility::makeInstance(Registry::class)->get('installUpdateRows', 'rowUpdatersDone', []);
267  return array_diff($this->rowUpdater, $doneRowUpdater);
268  }
269 
275  protected function setRowUpdaterExecuted(RowUpdaterInterface $updater)
276  {
277  $registry = GeneralUtility::makeInstance(Registry::class);
278  $doneRowUpdater = $registry->get('installUpdateRows', 'rowUpdatersDone', []);
279  $doneRowUpdater[] = get_class($updater);
280  $registry->set('installUpdateRows', 'rowUpdatersDone', $doneRowUpdater);
281  }
282 
290  protected function getStartPosition(string $firstTable): array
291  {
292  $registry = GeneralUtility::makeInstance(Registry::class);
293  $startPosition = $registry->get('installUpdateRows', 'rowUpdatePosition', []);
294  if (empty($startPosition)) {
295  $startPosition = [
296  'table' => $firstTable,
297  'uid' => 0,
298  ];
299  $registry->set('installUpdateRows', 'rowUpdatePosition', $startPosition);
300  }
301  return $startPosition;
302  }
303 }
performUpdate(array &$dbQueries, &$customMessage)
static makeInstance($className,... $constructorArguments)
if(TYPO3_MODE==='BE') $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tsfebeuserauth.php']['frontendEditingController']['default']