‪TYPO3CMS  10.4
SeparateSysHistoryFromSysLogUpdate.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 
25 
31 {
32 
34  private const ‪BATCH_SIZE = 100;
35 
37  private const ‪MOVE_DATA = 0;
38 
40  private const ‪UPDATE_HISTORY = 1;
41 
45  public function ‪getIdentifier(): string
46  {
47  return 'separateSysHistoryFromLog';
48  }
49 
53  public function ‪getTitle(): string
54  {
55  return 'Migrates existing sys_log entries into sys_history';
56  }
57 
61  public function ‪getDescription(): string
62  {
63  return 'The history of changes of a record is now solely stored within sys_history.'
64  . ' Previous data within sys_log needs to be migrated into sys_history now.';
65  }
66 
72  public function ‪updateNecessary(): bool
73  {
74  // sys_log field has been removed, no need to do something.
75  if (!$this->‪checkIfFieldInTableExists('sys_history', 'sys_log_uid')) {
76  return false;
77  }
78 
79  // Check if there is data to migrate
80  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
81  ->getQueryBuilderForTable('sys_history');
82  $queryBuilder->getRestrictions()->removeAll();
83  $count = $queryBuilder->count('*')
84  ->from('sys_history')
85  ->where($queryBuilder->expr()->neq('sys_log_uid', 0))
86  ->execute()
87  ->fetchColumn(0);
88 
89  return $count > 0;
90  }
91 
95  public function ‪getPrerequisites(): array
96  {
97  return [
98  DatabaseUpdatedPrerequisite::class
99  ];
100  }
101 
110  public function ‪executeUpdate(): bool
111  {
112  // If rows from the target table that is updated and the sys_registry table are on the
113  // same connection, the update statement and sys_registry position update will be
114  // handled in a transaction to have an atomic operation in case of errors during execution.
115  $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
116  $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('sys_history');
117  $connectionForSysRegistry = $connectionPool->getConnectionForTable('sys_registry');
118 
119  // In case the PHP ended for whatever reason, fetch the last position from registry
120  // and only execute the phase(s) that has/have not been executed yet
121  $startPositionAndPhase = $this->‪getStartPositionAndPhase();
122 
123  if ($startPositionAndPhase['phase'] === self::MOVE_DATA) {
124  $startPositionAndPhase = $this->‪moveDataFromSysLogToSysHistory(
125  $connection,
126  $connectionForSysRegistry,
127  $startPositionAndPhase
128  );
129  }
130 
131  if ($startPositionAndPhase['phase'] === self::UPDATE_HISTORY) {
133  $connectionForSysRegistry,
134  $startPositionAndPhase
135  );
136  }
137 
138  return true;
139  }
140 
150  ‪Connection $connection,
151  ‪Connection $connectionForSysRegistry,
152  array $startPositionAndPhase
153  ): array {
154  do {
155  $processedRows = 0;
156 
157  // update "modify" statements (= decoupling)
158  $queryBuilder = $connection->‪createQueryBuilder();
159  $rows = $queryBuilder->‪select('sys_history.uid AS history_uid', 'sys_history.history_data', 'sys_log.*')
160  ->‪from('sys_history')
161  ->‪leftJoin(
162  'sys_history',
163  'sys_log',
164  'sys_log',
165  $queryBuilder->expr()->eq('sys_history.sys_log_uid', $queryBuilder->quoteIdentifier('sys_log.uid'))
166  )
167  ->‪where($queryBuilder->expr()->gt('sys_history.uid', $queryBuilder->createNamedParameter($startPositionAndPhase['uid'])))
168  ->‪setMaxResults(self::BATCH_SIZE)
169  ->‪orderBy('sys_history.uid', 'ASC')
170  ->‪execute()
171  ->fetchAll();
172 
173  foreach ($rows as $row) {
174  $logData = $this->‪unserializeToArray((string)($row['log_data'] ?? ''));
175  $historyData = $this->‪unserializeToArray((string)($row['history_data'] ?? ''));
176  $updateData = [
177  'actiontype' => ‪RecordHistoryStore::ACTION_MODIFY,
178  'usertype' => 'BE',
179  'userid' => $row['userid'],
180  'sys_log_uid' => 0,
181  'history_data' => json_encode($historyData),
182  'originaluserid' => empty($logData['originalUser']) ? null : $logData['originalUser']
183  ];
184 
185  if ($connection === $connectionForSysRegistry) {
186  // sys_history and sys_registry tables are on the same connection, use a transaction
187  $connection->beginTransaction();
188  try {
189  $startPositionAndPhase = $this->‪updateTablesAndTrackProgress(
190  $connection,
191  $connection,
192  $updateData,
193  $logData,
194  $row
195  );
196  $connection->commit();
197  } catch (\‪Exception $up) {
198  $connection->rollBack();
199  throw ($up);
200  }
201  } else {
202  // Different connections for sys_history and sys_registry -> execute two
203  // distinct queries and hope for the best.
204  $startPositionAndPhase = $this->‪updateTablesAndTrackProgress(
205  $connection,
206  $connectionForSysRegistry,
207  $updateData,
208  $logData,
209  $row
210  );
211  }
212 
213  $processedRows++;
214  }
215  // repeat until a resultset smaller than the batch size was processed
216  } while ($processedRows === self::BATCH_SIZE);
217 
218  // phase 0 is finished
219  $registry = GeneralUtility::makeInstance(Registry::class);
220  $startPositionAndPhase = [
221  'phase' => ‪self::UPDATE_HISTORY,
222  'uid' => 0,
223  ];
224  $registry->set('installSeparateHistoryFromSysLog', 'phaseAndPosition', $startPositionAndPhase);
225 
226  return $startPositionAndPhase;
227  }
228 
242  ‪Connection $connection,
243  ‪Connection $connectionForSysRegistry,
244  array $updateData,
245  array $logData,
246  array $row
247  ): array {
248  $connection->‪update(
249  'sys_history',
250  $updateData,
251  ['uid' => (int)$row['history_uid']],
252  ['uid' => ‪Connection::PARAM_INT]
253  );
254 
255  // Store information about history entry in sys_log table
256  $logData['history'] = $row['history_uid'];
257  $connection->‪update(
258  'sys_log',
259  ['log_data' => serialize($logData)],
260  ['uid' => (int)$row['uid']],
261  ['uid' => ‪Connection::PARAM_INT]
262  );
263  $startPositionAndPhase = [
264  'phase' => ‪self::MOVE_DATA,
265  'uid' => $row['history_uid'],
266  ];
267  $connectionForSysRegistry->‪update(
268  'sys_registry',
269  [
270  'entry_value' => serialize($startPositionAndPhase)
271  ],
272  [
273  'entry_namespace' => 'installSeparateHistoryFromSysLog',
274  'entry_key' => 'phaseAndPosition',
275  ]
276  );
277 
278  return $startPositionAndPhase;
279  }
280 
288  ‪Connection $connectionForSysRegistry,
289  array $startPositionAndPhase
290  ) {
291  do {
292  $processedRows = 0;
293 
294  // Add insert/delete calls
295  $logQueryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_log');
296  $result = $logQueryBuilder->select('uid', 'userid', 'action', 'tstamp', 'log_data', 'tablename', 'recuid')
297  ->from('sys_log')
298  ->where(
299  $logQueryBuilder->expr()->eq('type', $logQueryBuilder->createNamedParameter(1, \PDO::PARAM_INT)),
300  $logQueryBuilder->expr()->orX(
301  $logQueryBuilder->expr()->eq('action', $logQueryBuilder->createNamedParameter(1, \PDO::PARAM_INT)),
302  $logQueryBuilder->expr()->eq('action', $logQueryBuilder->createNamedParameter(3, \PDO::PARAM_INT))
303  )
304  )
305  ->andWhere(
306  $logQueryBuilder->expr()->gt('uid', $logQueryBuilder->createNamedParameter($startPositionAndPhase['uid']))
307  )
308  ->orderBy('uid', 'ASC')
309  ->setMaxResults(self::BATCH_SIZE)
310  ->execute();
311 
312  foreach ($result as $row) {
313  $logData = $this->‪unserializeToArray((string)($row['log_data'] ?? ''));
314  $store = GeneralUtility::makeInstance(
315  RecordHistoryStore::class,
317  $row['userid'],
318  (empty($logData['originalUser']) ? null : $logData['originalUser']),
319  $row['tstamp']
320  );
321 
322  switch ($row['action']) {
323  // Insert
324  case 1:
325  $store->addRecord($row['tablename'], (int)$row['recuid'], $logData);
326  break;
327  // Delete
328  case 3:
329  $store->deleteRecord($row['tablename'], (int)$row['recuid']);
330  break;
331  }
332 
333  $startPositionAndPhase = [
334  'phase' => ‪self::UPDATE_HISTORY,
335  'uid' => $row['uid'],
336  ];
337  $connectionForSysRegistry->‪update(
338  'sys_registry',
339  [
340  'entry_value' => serialize($startPositionAndPhase)
341  ],
342  [
343  'entry_namespace' => 'installSeparateHistoryFromSysLog',
344  'entry_key' => 'phaseAndPosition',
345  ]
346  );
347 
348  $processedRows++;
349  }
350  // repeat until a result set smaller than the batch size was processed
351  } while ($processedRows === self::BATCH_SIZE);
352  }
353 
361  protected function ‪checkIfFieldInTableExists($table, $fieldName): bool
362  {
363  $tableColumns = GeneralUtility::makeInstance(ConnectionPool::class)
364  ->getConnectionForTable($table)
365  ->getSchemaManager()
366  ->listTableColumns($table);
367  return isset($tableColumns[$fieldName]);
368  }
369 
376  protected function ‪getStartPositionAndPhase(): array
377  {
378  $registry = GeneralUtility::makeInstance(Registry::class);
379  $startPosition = $registry->get('installSeparateHistoryFromSysLog', 'phaseAndPosition', []);
380  if (empty($startPosition)) {
381  $startPosition = [
382  'phase' => ‪self::MOVE_DATA,
383  'uid' => 0,
384  ];
385  $registry->set('installSeparateHistoryFromSysLog', 'phaseAndPosition', $startPosition);
386  }
387 
388  return $startPosition;
389  }
390 
391  protected function ‪unserializeToArray(string $serialized): array
392  {
393  $unserialized = @unserialize($serialized, ['allowed_classes' => false]);
394  return is_array($unserialized) ? $unserialized : [];
395  }
396 }
‪TYPO3\CMS\Core\DataHandling\History\RecordHistoryStore\USER_BACKEND
‪const USER_BACKEND
Definition: RecordHistoryStore.php:38
‪TYPO3\CMS\Core\Database\Connection\PARAM_INT
‪const PARAM_INT
Definition: Connection.php:47
‪TYPO3\CMS\Install\Updates\RepeatableInterface
Definition: RepeatableInterface.php:26
‪TYPO3\CMS\Core\Database\Query\QueryBuilder\select
‪QueryBuilder select(string ... $selects)
Definition: QueryBuilder.php:416
‪TYPO3\CMS\Install\Updates\SeparateSysHistoryFromSysLogUpdate\keepHistoryForInsertAndDeleteActions
‪keepHistoryForInsertAndDeleteActions(Connection $connectionForSysRegistry, array $startPositionAndPhase)
Definition: SeparateSysHistoryFromSysLogUpdate.php:287
‪TYPO3\CMS\Install\Updates\SeparateSysHistoryFromSysLogUpdate\moveDataFromSysLogToSysHistory
‪array moveDataFromSysLogToSysHistory(Connection $connection, Connection $connectionForSysRegistry, array $startPositionAndPhase)
Definition: SeparateSysHistoryFromSysLogUpdate.php:149
‪TYPO3\CMS\Install\Updates\SeparateSysHistoryFromSysLogUpdate\checkIfFieldInTableExists
‪bool checkIfFieldInTableExists($table, $fieldName)
Definition: SeparateSysHistoryFromSysLogUpdate.php:361
‪TYPO3\CMS\Core\Registry
Definition: Registry.php:33
‪TYPO3\CMS\Install\Updates\SeparateSysHistoryFromSysLogUpdate\getPrerequisites
‪string[] getPrerequisites()
Definition: SeparateSysHistoryFromSysLogUpdate.php:95
‪TYPO3\CMS\Install\Updates\SeparateSysHistoryFromSysLogUpdate\BATCH_SIZE
‪const BATCH_SIZE
Definition: SeparateSysHistoryFromSysLogUpdate.php:34
‪TYPO3\CMS\Core\Database\Query\QueryBuilder\orderBy
‪QueryBuilder orderBy(string $fieldName, string $order=null)
Definition: QueryBuilder.php:841
‪TYPO3\CMS\Core\DataHandling\History\RecordHistoryStore\ACTION_MODIFY
‪const ACTION_MODIFY
Definition: RecordHistoryStore.php:33
‪TYPO3\CMS\Install\Updates\SeparateSysHistoryFromSysLogUpdate\updateNecessary
‪bool updateNecessary()
Definition: SeparateSysHistoryFromSysLogUpdate.php:72
‪TYPO3\CMS\Core\Database\Connection\update
‪int update($tableName, array $data, array $identifier, array $types=[])
Definition: Connection.php:302
‪TYPO3\CMS\Install\Updates\SeparateSysHistoryFromSysLogUpdate\getDescription
‪string getDescription()
Definition: SeparateSysHistoryFromSysLogUpdate.php:61
‪TYPO3\CMS\Install\Updates\SeparateSysHistoryFromSysLogUpdate\MOVE_DATA
‪const MOVE_DATA
Definition: SeparateSysHistoryFromSysLogUpdate.php:37
‪TYPO3\CMS\Install\Updates
Definition: AbstractDownloadExtensionUpdate.php:16
‪TYPO3\CMS\Core\DataHandling\History\RecordHistoryStore
Definition: RecordHistoryStore.php:31
‪TYPO3\CMS\Core\Database\Query\QueryBuilder\from
‪QueryBuilder from(string $from, string $alias=null)
Definition: QueryBuilder.php:531
‪TYPO3\CMS\Install\Exception
Definition: Exception.php:24
‪TYPO3\CMS\Core\Database\Query\QueryBuilder\execute
‪Statement Doctrine DBAL ForwardCompatibility Result Doctrine DBAL Driver ResultStatement int execute()
Definition: QueryBuilder.php:204
‪TYPO3\CMS\Install\Updates\SeparateSysHistoryFromSysLogUpdate
Definition: SeparateSysHistoryFromSysLogUpdate.php:31
‪TYPO3\CMS\Core\Database\Query\QueryBuilder\setMaxResults
‪QueryBuilder setMaxResults(int $maxResults)
Definition: QueryBuilder.php:355
‪TYPO3\CMS\Install\Updates\SeparateSysHistoryFromSysLogUpdate\updateTablesAndTrackProgress
‪array updateTablesAndTrackProgress(Connection $connection, Connection $connectionForSysRegistry, array $updateData, array $logData, array $row)
Definition: SeparateSysHistoryFromSysLogUpdate.php:241
‪TYPO3\CMS\Core\Database\Connection
Definition: Connection.php:36
‪TYPO3\CMS\Install\Updates\SeparateSysHistoryFromSysLogUpdate\unserializeToArray
‪unserializeToArray(string $serialized)
Definition: SeparateSysHistoryFromSysLogUpdate.php:391
‪TYPO3\CMS\Install\Updates\UpgradeWizardInterface
Definition: UpgradeWizardInterface.php:24
‪TYPO3\CMS\Core\Database\Connection\createQueryBuilder
‪TYPO3 CMS Core Database Query QueryBuilder createQueryBuilder()
Definition: Connection.php:117
‪TYPO3\CMS\Install\Updates\SeparateSysHistoryFromSysLogUpdate\executeUpdate
‪bool executeUpdate()
Definition: SeparateSysHistoryFromSysLogUpdate.php:110
‪TYPO3\CMS\Core\Database\ConnectionPool
Definition: ConnectionPool.php:46
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:46
‪TYPO3\CMS\Core\Database\Query\QueryBuilder\where
‪QueryBuilder where(... $predicates)
Definition: QueryBuilder.php:677
‪TYPO3\CMS\Install\Updates\SeparateSysHistoryFromSysLogUpdate\UPDATE_HISTORY
‪const UPDATE_HISTORY
Definition: SeparateSysHistoryFromSysLogUpdate.php:40
‪TYPO3\CMS\Core\Database\Query\QueryBuilder\leftJoin
‪QueryBuilder leftJoin(string $fromAlias, string $join, string $alias, string $condition=null)
Definition: QueryBuilder.php:595
‪TYPO3\CMS\Install\Updates\SeparateSysHistoryFromSysLogUpdate\getTitle
‪string getTitle()
Definition: SeparateSysHistoryFromSysLogUpdate.php:53
‪TYPO3\CMS\Install\Updates\SeparateSysHistoryFromSysLogUpdate\getIdentifier
‪string getIdentifier()
Definition: SeparateSysHistoryFromSysLogUpdate.php:45
‪TYPO3\CMS\Install\Updates\SeparateSysHistoryFromSysLogUpdate\getStartPositionAndPhase
‪array getStartPositionAndPhase()
Definition: SeparateSysHistoryFromSysLogUpdate.php:376