[TASK] Migrate DatabaseRowsUpdateWizard to UpgradeWizardInterface
[Packages/TYPO3.CMS.git] / typo3 / sysext / install / Classes / Updates / DatabaseRowsUpdateWizard.php
1 <?php
2 declare(strict_types = 1);
3 namespace TYPO3\CMS\Install\Updates;
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
18 use TYPO3\CMS\Core\Database\ConnectionPool;
19 use TYPO3\CMS\Core\Registry;
20 use TYPO3\CMS\Core\Utility\GeneralUtility;
21 use TYPO3\CMS\Install\Updates\RowUpdater\ImageCropUpdater;
22 use TYPO3\CMS\Install\Updates\RowUpdater\L10nModeUpdater;
23 use TYPO3\CMS\Install\Updates\RowUpdater\RowUpdaterInterface;
24 use TYPO3\CMS\Install\Updates\RowUpdater\RteLinkSyntaxUpdater;
25
26 /**
27 * This is a generic updater to migrate content of TCA rows.
28 *
29 * Multiple classes implementing interface "RowUpdateInterface" can be
30 * registered here, each for a specific update purpose.
31 *
32 * The updater fetches each row of all TCA registered tables and
33 * visits the client classes who may modify the row content.
34 *
35 * The updater remembers for each class if it run through, so the updater
36 * will be shown again if a new updater class is registered that has not
37 * been run yet.
38 *
39 * A start position pointer is stored in the registry that is updated during
40 * the run process, so if for instance the PHP process runs into a timeout,
41 * the job can restart at the position it stopped.
42 */
43 class DatabaseRowsUpdateWizard implements UpgradeWizardInterface, RepeatableInterface
44 {
45 /**
46 * @var array Single classes that may update rows
47 */
48 protected $rowUpdater = [
49 L10nModeUpdater::class,
50 ImageCropUpdater::class,
51 RteLinkSyntaxUpdater::class,
52 ];
53
54 /**
55 * @return string Unique identifier of this updater
56 */
57 public function getIdentifier(): string
58 {
59 return 'databaseRowsUpdateWizard';
60 }
61
62 /**
63 * @return string Title of this updater
64 */
65 public function getTitle(): string
66 {
67 return 'Execute database migrations on single rows';
68 }
69
70 /**
71 * @return string Longer description of this updater
72 */
73 public function getDescription(): string
74 {
75 $rowUpdaterNotExecuted = $this->getRowUpdatersToExecute();
76 $description = 'Row updaters that have not been executed:';
77 foreach ($rowUpdaterNotExecuted as $rowUpdateClassName) {
78 $rowUpdater = GeneralUtility::makeInstance($rowUpdateClassName);
79 if (!$rowUpdater instanceof RowUpdaterInterface) {
80 throw new \RuntimeException(
81 'Row updater must implement RowUpdaterInterface',
82 1484066647
83 );
84 }
85 $description .= LF . $rowUpdater->getTitle();
86 }
87 return $description;
88 }
89
90 /**
91 * @return bool True if at least one row updater is not marked done
92 */
93 public function updateNecessary(): bool
94 {
95 return !empty($this->getRowUpdatersToExecute());
96 }
97
98 /**
99 * @return string[] All new fields and tables must exist
100 */
101 public function getPrerequisites(): array
102 {
103 return [
104 DatabaseUpdatedPrerequisite::class
105 ];
106 }
107
108 /**
109 * Performs the configuration update.
110 *
111 * @return bool
112 * @throws \Doctrine\DBAL\ConnectionException
113 * @throws \Exception
114 */
115 public function executeUpdate(): bool
116 {
117 $registry = GeneralUtility::makeInstance(Registry::class);
118
119 // If rows from the target table that is updated and the sys_registry table are on the
120 // same connection, the row update statement and sys_registry position update will be
121 // handled in a transaction to have an atomic operation in case of errors during execution.
122 $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
123 $connectionForSysRegistry = $connectionPool->getConnectionForTable('sys_registry');
124
125 /** @var RowUpdaterInterface[] $rowUpdaterInstances */
126 $rowUpdaterInstances = [];
127 // Single row updater instances are created only once for this method giving
128 // them a chance to set up local properties during hasPotentialUpdateForTable()
129 // and using that in updateTableRow()
130 foreach ($this->getRowUpdatersToExecute() as $rowUpdater) {
131 $rowUpdaterInstance = GeneralUtility::makeInstance($rowUpdater);
132 if (!$rowUpdaterInstance instanceof RowUpdaterInterface) {
133 throw new \RuntimeException(
134 'Row updater must implement RowUpdaterInterface',
135 1484071612
136 );
137 }
138 $rowUpdaterInstances[] = $rowUpdaterInstance;
139 }
140
141 // Scope of the row updater is to update all rows that have TCA,
142 // our list of tables is just the list of loaded TCA tables.
143 $listOfAllTables = array_keys($GLOBALS['TCA']);
144
145 // In case the PHP ended for whatever reason, fetch the last position from registry
146 // and throw away all tables before that start point.
147 sort($listOfAllTables);
148 reset($listOfAllTables);
149 $firstTable = current($listOfAllTables);
150 $startPosition = $this->getStartPosition($firstTable);
151 foreach ($listOfAllTables as $key => $table) {
152 if ($table === $startPosition['table']) {
153 break;
154 }
155 unset($listOfAllTables[$key]);
156 }
157
158 // Ask each row updater if it potentially has field updates for rows of a table
159 $tableToUpdaterList = [];
160 foreach ($listOfAllTables as $table) {
161 foreach ($rowUpdaterInstances as $updater) {
162 if ($updater->hasPotentialUpdateForTable($table)) {
163 if (!is_array($tableToUpdaterList[$table])) {
164 $tableToUpdaterList[$table] = [];
165 }
166 $tableToUpdaterList[$table][] = $updater;
167 }
168 }
169 }
170
171 // Iterate through all rows of all tables that have potential row updaters attached,
172 // feed each single row to each updater and finally update each row in database if
173 // a row updater changed a fields
174 foreach ($tableToUpdaterList as $table => $updaters) {
175 /** @var RowUpdaterInterface[] $updaters */
176 $connectionForTable = $connectionPool->getConnectionForTable($table);
177 $queryBuilder = $connectionPool->getQueryBuilderForTable($table);
178 $queryBuilder->getRestrictions()->removeAll();
179 $queryBuilder->select('*')
180 ->from($table)
181 ->orderBy('uid');
182 if ($table === $startPosition['table']) {
183 $queryBuilder->where(
184 $queryBuilder->expr()->gt('uid', $queryBuilder->createNamedParameter($startPosition['uid']))
185 );
186 }
187 $statement = $queryBuilder->execute();
188 $rowCountWithoutUpdate = 0;
189 while ($row = $rowBefore = $statement->fetch()) {
190 foreach ($updaters as $updater) {
191 $row = $updater->updateTableRow($table, $row);
192 }
193 $updatedFields = array_diff_assoc($row, $rowBefore);
194 if (empty($updatedFields)) {
195 // Updaters changed no field of that row
196 $rowCountWithoutUpdate++;
197 if ($rowCountWithoutUpdate >= 200) {
198 // Update startPosition if there were many rows without data change
199 $startPosition = [
200 'table' => $table,
201 'uid' => $row['uid'],
202 ];
203 $registry->set('installUpdateRows', 'rowUpdatePosition', $startPosition);
204 $rowCountWithoutUpdate = 0;
205 }
206 } else {
207 $rowCountWithoutUpdate = 0;
208 $startPosition = [
209 'table' => $table,
210 'uid' => $rowBefore['uid'],
211 ];
212 if ($connectionForSysRegistry === $connectionForTable) {
213 // Target table and sys_registry table are on the same connection, use a transaction
214 $connectionForTable->beginTransaction();
215 try {
216 $connectionForTable->update(
217 $table,
218 $updatedFields,
219 [
220 'uid' => $rowBefore['uid'],
221 ]
222 );
223 $connectionForTable->update(
224 'sys_registry',
225 [
226 'entry_value' => serialize($startPosition),
227 ],
228 [
229 'entry_namespace' => 'installUpdateRows',
230 'entry_key' => 'rowUpdatePosition',
231 ]
232 );
233 $connectionForTable->commit();
234 } catch (\Exception $up) {
235 $connectionForTable->rollBack();
236 throw $up;
237 }
238 } else {
239 // Different connections for table and sys_registry -> execute two
240 // distinct queries and hope for the best.
241 $connectionForTable->update(
242 $table,
243 $updatedFields,
244 [
245 'uid' => $rowBefore['uid'],
246 ]
247 );
248 $connectionForSysRegistry->update(
249 'sys_registry',
250 [
251 'entry_value' => serialize($startPosition),
252 ],
253 [
254 'entry_namespace' => 'installUpdateRows',
255 'entry_key' => 'rowUpdatePosition',
256 ]
257 );
258 }
259 }
260 }
261 }
262
263 // Ready with updates, remove position information from sys_registry
264 $registry->remove('installUpdateRows', 'rowUpdatePosition');
265 // Mark row updaters that were executed as done
266 foreach ($rowUpdaterInstances as $updater) {
267 $this->setRowUpdaterExecuted($updater);
268 }
269
270 return true;
271 }
272
273 /**
274 * Return an array of class names that are not yet marked as done.
275 *
276 * @return array Class names
277 */
278 protected function getRowUpdatersToExecute(): array
279 {
280 $doneRowUpdater = GeneralUtility::makeInstance(Registry::class)->get('installUpdateRows', 'rowUpdatersDone', []);
281 return array_diff($this->rowUpdater, $doneRowUpdater);
282 }
283
284 /**
285 * Mark a single updater as done
286 *
287 * @param RowUpdaterInterface $updater
288 */
289 protected function setRowUpdaterExecuted(RowUpdaterInterface $updater)
290 {
291 $registry = GeneralUtility::makeInstance(Registry::class);
292 $doneRowUpdater = $registry->get('installUpdateRows', 'rowUpdatersDone', []);
293 $doneRowUpdater[] = get_class($updater);
294 $registry->set('installUpdateRows', 'rowUpdatersDone', $doneRowUpdater);
295 }
296
297 /**
298 * Return an array with table / uid combination that specifies the start position the
299 * update row process should start with.
300 *
301 * @param string $firstTable Table name of the first TCA in case the start position needs to be initialized
302 * @return array New start position
303 */
304 protected function getStartPosition(string $firstTable): array
305 {
306 $registry = GeneralUtility::makeInstance(Registry::class);
307 $startPosition = $registry->get('installUpdateRows', 'rowUpdatePosition', []);
308 if (empty($startPosition)) {
309 $startPosition = [
310 'table' => $firstTable,
311 'uid' => 0,
312 ];
313 $registry->set('installUpdateRows', 'rowUpdatePosition', $startPosition);
314 }
315 return $startPosition;
316 }
317 }