‪TYPO3CMS  ‪main
SchemaMigrator.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\Exception as DBALException;
21 use Doctrine\DBAL\Schema\Column;
22 use Doctrine\DBAL\Schema\Index;
23 use Doctrine\DBAL\Schema\SchemaDiff;
24 use Doctrine\DBAL\Schema\SchemaException;
25 use Doctrine\DBAL\Schema\Table;
30 
38 {
39  public function ‪__construct(
40  private readonly ‪ConnectionPool $connectionPool,
41  private readonly ‪Parser ‪$parser,
42  private readonly ‪DefaultTcaSchema $defaultTcaSchema,
43  ) {}
44 
58  public function getUpdateSuggestions(array $statements, bool $remove = false): array
59  {
60  $tables = $this->‪parseCreateTableStatements($statements);
62  foreach ($this->connectionPool->getConnectionNames() as $connectionName) {
63  $connection = $this->connectionPool->getConnectionByName($connectionName);
64  $connectionMigrator = ConnectionMigrator::create($connectionName, $connection, $tables);
65  ‪$updateSuggestions[$connectionName] = $connectionMigrator->getUpdateSuggestions($remove);
66  }
68  }
69 
81  public function getSchemaDiffs(array $statements): array
82  {
83  $tables = $this->‪parseCreateTableStatements($statements);
85  foreach ($this->connectionPool->getConnectionNames() as $connectionName) {
86  $connection = $this->connectionPool->getConnectionByName($connectionName);
87  $connectionMigrator = ConnectionMigrator::create($connectionName, $connection, $tables);
88  ‪$schemaDiffs[$connectionName] = $connectionMigrator->getSchemaDiff();
89  }
91  }
92 
105  public function ‪migrate(array $statements, array $selectedStatements): array
106  {
107  ‪$result = [];
108  $updateSuggestionsPerConnection = array_replace_recursive(
109  $this->getUpdateSuggestions($statements),
110  $this->getUpdateSuggestions($statements, true)
111  );
112 
113  foreach ($updateSuggestionsPerConnection as $connectionName => ‪$updateSuggestions) {
114  unset(‪$updateSuggestions['tables_count'], ‪$updateSuggestions['change_currentValue']);
115  ‪$updateSuggestions = array_merge(...array_values(‪$updateSuggestions));
116  $statementsToExecute = array_intersect_key(‪$updateSuggestions, $selectedStatements);
117  if (count($statementsToExecute) === 0) {
118  continue;
119  }
120 
121  $connection = $this->connectionPool->getConnectionByName($connectionName);
122  foreach ($statementsToExecute as $hash => $statement) {
123  try {
124  $connection->executeStatement($statement);
125  } catch (DBALException $e) {
126  ‪$result[$hash] = $e->getPrevious()->getMessage();
127  }
128  }
129  }
130  ‪Bootstrap::createCache('database_schema')->flush();
131 
132  return ‪$result;
133  }
134 
147  public function install(array $statements, bool $createOnly = false): array
148  {
149  $tables = $this->‪parseCreateTableStatements($statements);
151  foreach ($this->connectionPool->getConnectionNames() as $connectionName) {
152  $connection = $this->connectionPool->getConnectionByName($connectionName);
153  $connectionMigrator = ConnectionMigrator::create($connectionName, $connection, $tables);
154  $lastResult = $connectionMigrator->install($createOnly);
155  ‪$result = array_merge(‪$result, $lastResult);
156  }
157  ‪Bootstrap::createCache('database_schema')->flush();
158 
159  return ‪$result;
160  }
161 
165  public function ‪importStaticData(array $statements, bool $truncate = false): array
166  {
167  ‪$result = [];
168  $insertStatements = [];
169 
170  foreach ($statements as $statement) {
171  // Only handle insert statements and extract the table at the same time. Extracting
172  // the table name is required to perform the inserts on the right connection.
173  if (preg_match('/^INSERT\s+INTO\s+`?(\w+)`?(.*)/i', $statement, $matches)) {
174  [, $tableName, $sqlFragment] = $matches;
175  $insertStatements[$tableName][] = sprintf(
176  'INSERT INTO %s %s',
177  $this->connectionPool->getConnectionForTable($tableName)->quoteIdentifier($tableName),
178  rtrim($sqlFragment, ';')
179  );
180  }
181  }
182 
183  foreach ($insertStatements as $tableName => $perTableStatements) {
184  $connection = $this->connectionPool->getConnectionForTable($tableName);
185 
186  if ($truncate) {
187  $connection->truncate($tableName);
188  }
189 
190  foreach ((array)$perTableStatements as $statement) {
191  try {
192  $connection->executeStatement($statement);
193  ‪$result[$statement] = '';
194  } catch (DBALException $e) {
195  ‪$result[$statement] = $e->getPrevious()->getMessage();
196  }
197  }
198  }
199 
200  return ‪$result;
201  }
202 
213  protected function ‪parseCreateTableStatements(array $statements): array
214  {
215  $tables = [];
216  foreach ($statements as $statement) {
217  // We need to keep multiple table definitions at this point so
218  // that Extensions can modify existing tables.
219  try {
220  $tables[] = $this->parser->parse($statement);
221  } catch (‪StatementException $statementException) {
222  // Enrich the error message with the full invalid statement
223  throw new ‪StatementException(
224  $statementException->getMessage() . ' in statement: ' . LF . $statement,
225  1476171315,
226  $statementException
227  );
228  }
229  }
230 
231  // Flatten the array of arrays by one level
232  $tables = array_merge(...$tables);
233 
234  // Ensure we have a table definition for all tables within TCA, add missing ones
235  // as "empty" tables without columns. This is needed for DefaultTcaSchema: It goes
236  // through TCA to add columns automatically, but needs a table definition of all
237  // TCA tables. We're not doing this in DefaultTcaSchema to not introduce a dependency
238  // to the Parser class in there, which we have here so conveniently already.
239  $tableNamesFromTca = array_keys(‪$GLOBALS['TCA']);
240  $tableNamesFromExtTables = [];
241  foreach ($tables as $table) {
242  $tableNamesFromExtTables[] = $table->getName();
243  }
244  $tableNamesFromExtTables = array_unique($tableNamesFromExtTables);
245  $missingTableNames = array_diff($tableNamesFromTca, $tableNamesFromExtTables);
246  foreach ($missingTableNames as $tableName) {
247  $createTableSql = 'CREATE TABLE ' . $tableName . '();';
248  $tables[] = $this->parser->parse($createTableSql)[0];
249  }
250 
251  // Add default TCA fields
252  $tables = $this->mergeTableDefinitions($tables);
253  $tables = $this->defaultTcaSchema->enrich($tables);
254  // Ensure the default TCA fields are ordered
255  foreach ($tables as $k => $table) {
256  $prioritizedColumnNames = $this->‪getPrioritizedFieldNames($table->getName());
257  // no TCA table
258  if (empty($prioritizedColumnNames)) {
259  continue;
260  }
261 
262  $prioritizedColumns = [];
263  $nonPrioritizedColumns = [];
264 
265  foreach ($table->getColumns() as $columnObject) {
266  if (in_array($columnObject->getName(), $prioritizedColumnNames, true)) {
267  $prioritizedColumns[] = $columnObject;
268  } else {
269  $nonPrioritizedColumns[] = $columnObject;
270  }
271  }
272 
273  $tables[$k] = new Table(
274  $table->getName(),
275  array_merge($prioritizedColumns, $nonPrioritizedColumns),
276  $table->getIndexes(),
277  [],
278  $table->getForeignKeys(),
279  $table->getOptions()
280  );
281  }
282 
283  return $tables;
284  }
285 
292  protected function ‪getPrioritizedFieldNames(string $tableName): array
293  {
294  if (!isset(‪$GLOBALS['TCA'][$tableName]['ctrl'])) {
295  return [];
296  }
297 
298  $prioritizedFieldNames = [
299  'uid',
300  'pid',
301  ];
302 
303  $tableDefinition = ‪$GLOBALS['TCA'][$tableName]['ctrl'];
304 
305  if (!empty($tableDefinition['crdate'])) {
306  $prioritizedFieldNames[] = $tableDefinition['crdate'];
307  }
308  if (!empty($tableDefinition['tstamp'])) {
309  $prioritizedFieldNames[] = $tableDefinition['tstamp'];
310  }
311  if (!empty($tableDefinition['delete'])) {
312  $prioritizedFieldNames[] = $tableDefinition['delete'];
313  }
314  if (!empty($tableDefinition['enablecolumns']['disabled'])) {
315  $prioritizedFieldNames[] = $tableDefinition['enablecolumns']['disabled'];
316  }
317  if (!empty($tableDefinition['enablecolumns']['starttime'])) {
318  $prioritizedFieldNames[] = $tableDefinition['enablecolumns']['starttime'];
319  }
320  if (!empty($tableDefinition['enablecolumns']['endtime'])) {
321  $prioritizedFieldNames[] = $tableDefinition['enablecolumns']['endtime'];
322  }
323  if (!empty($tableDefinition['enablecolumns']['fe_group'])) {
324  $prioritizedFieldNames[] = $tableDefinition['enablecolumns']['fe_group'];
325  }
326  if (!empty($tableDefinition['languageField'])) {
327  $prioritizedFieldNames[] = $tableDefinition['languageField'];
328  if (!empty($tableDefinition['transOrigPointerField'])) {
329  $prioritizedFieldNames[] = $tableDefinition['transOrigPointerField'];
330  $prioritizedFieldNames[] = 'l10n_state';
331  }
332  if (!empty($tableDefinition['translationSource'])) {
333  $prioritizedFieldNames[] = $tableDefinition['translationSource'];
334  }
335  if (!empty($tableDefinition['transOrigDiffSourceField'])) {
336  $prioritizedFieldNames[] = $tableDefinition['transOrigDiffSourceField'];
337  }
338  }
339  if (!empty($tableDefinition['sortby'])) {
340  $prioritizedFieldNames[] = $tableDefinition['sortby'];
341  }
342  if (!empty($tableDefinition['descriptionColumn'])) {
343  $prioritizedFieldNames[] = $tableDefinition['descriptionColumn'];
344  }
345  if (!empty($tableDefinition['editlock'])) {
346  $prioritizedFieldNames[] = $tableDefinition['editlock'];
347  }
348  if (!empty($tableDefinition['origUid'])) {
349  $prioritizedFieldNames[] = $tableDefinition['origUid'];
350  }
351  if (!empty($tableDefinition['versioningWS'])) {
352  $prioritizedFieldNames[] = 't3ver_wsid';
353  $prioritizedFieldNames[] = 't3ver_oid';
354  $prioritizedFieldNames[] = 't3ver_state';
355  $prioritizedFieldNames[] = 't3ver_stage';
356  }
357 
358  return $prioritizedFieldNames;
359  }
360 
369  private function mergeTableDefinitions(array $tables): array
370  {
371  ‪$return = [];
372  foreach ($tables as $table) {
373  $tableName = $this->‪trimIdentifierQuotes($table->getName());
374  if (!array_key_exists($tableName, ‪$return)) {
375  ‪$return[$tableName] = $table;
376  continue;
377  }
378 
379  // Merge multiple table definitions. Later definitions overrule identical
380  // columns, indexes and foreign_keys. Order of definitions is based on
381  // extension load order.
383  ‪$return[$tableName] = new Table(
384  $tableName,
385  $this->‪mergeColumns(...‪$currentTableDefinition->getColumns(), ...$table->getColumns()),
386  $this->mergeIndexes(...$currentTableDefinition->getIndexes(), ...$table->getIndexes()),
387  [],
388  array_merge(‪$currentTableDefinition->getForeignKeys(), $table->getForeignKeys()),
389  array_merge(‪$currentTableDefinition->getOptions(), $table->getOptions())
390  );
391  }
392 
393  return ‪$return;
394  }
395 
400  private function ‪mergeColumns(Column ...$columns): array
401  {
402  $mergedColumns = [];
403  foreach ($columns as $column) {
404  $mergedColumns[$column->getName()] = $column;
405  }
406  return array_values($mergedColumns);
407  }
408 
413  private function ‪mergeIndexes(Index ...$indexes): array
414  {
415  $mergedIndexes = [];
416  foreach ($indexes as $index) {
417  $mergedIndexes[$index->getName()] = $index;
418  }
419  return array_values($mergedIndexes);
420  }
421 
427  private function ‪trimIdentifierQuotes(string ‪$identifier): string
428  {
429  return str_replace(['`', '"', '[', ']'], '', ‪$identifier);
430  }
431 }
‪TYPO3\CMS\Core\Database\Schema\SchemaMigrator\parseCreateTableStatements
‪array< string, getUpdateSuggestions(array $statements, bool $remove=false):array { $tables=$this-> parseCreateTableStatements($statements)
‪TYPO3\CMS\Core\Database\Schema\Exception\StatementException
Definition: StatementException.php:24
‪TYPO3\CMS\Core\Database\Schema\SchemaMigrator\importStaticData
‪importStaticData(array $statements, bool $truncate=false)
Definition: SchemaMigrator.php:165
‪TYPO3\CMS\Core\Database\Schema\SchemaMigrator\parseCreateTableStatements
‪array< string, getSchemaDiffs(array $statements):array { $tables=$this-> parseCreateTableStatements($statements)
‪TYPO3\CMS\Core\Database\Schema\SchemaMigrator\trimIdentifierQuotes
‪trimIdentifierQuotes(string $identifier)
Definition: SchemaMigrator.php:427
‪$parser
‪$parser
Definition: annotationChecker.php:103
‪TYPO3\CMS\Core\Database\Schema
Definition: ColumnDiff.php:18
‪TYPO3\CMS\Core\Database\Schema\SchemaMigrator\$result
‪return $result
Definition: SchemaMigrator.php:159
‪TYPO3\CMS\Core\Database\Schema\SchemaMigrator\$updateSuggestions
‪foreach($this->connectionPool->getConnectionNames() as $connectionName) return $updateSuggestions
Definition: SchemaMigrator.php:62
‪TYPO3\CMS\Core\Database\Schema\Parser\Parser
Definition: Parser.php:75
‪TYPO3\CMS\Core\Database\Schema\SchemaMigrator
Definition: SchemaMigrator.php:38
‪TYPO3\CMS\Core\Database\Schema\SchemaMigrator\$schemaDiffs
‪foreach($this->connectionPool->getConnectionNames() as $connectionName) return $schemaDiffs
Definition: SchemaMigrator.php:85
‪TYPO3\CMS\Core\Database\Schema\DefaultTcaSchema
Definition: DefaultTcaSchema.php:43
‪TYPO3\CMS\Core\Database\Schema\SchemaMigrator\parseCreateTableStatements
‪array< string, install(array $statements, bool $createOnly=false):array { $tables=$this-> parseCreateTableStatements($statements)
‪TYPO3\CMS\Core\Database\Schema\SchemaMigrator\$updateSuggestions
‪$updateSuggestions
Definition: SchemaMigrator.php:61
‪TYPO3\CMS\Core\Database\Schema\SchemaMigrator\mergeColumns
‪Column[] mergeColumns(Column ... $columns)
Definition: SchemaMigrator.php:400
‪TYPO3\CMS\Core\Database\Schema\SchemaMigrator\getPrioritizedFieldNames
‪string[] getPrioritizedFieldNames(string $tableName)
Definition: SchemaMigrator.php:292
‪TYPO3\CMS\Core\Database\Schema\SchemaMigrator\$return
‪$return[$tableName]
Definition: SchemaMigrator.php:383
‪TYPO3\CMS\Core\Database\Schema\SchemaMigrator\__construct
‪__construct(private readonly ConnectionPool $connectionPool, private readonly Parser $parser, private readonly DefaultTcaSchema $defaultTcaSchema,)
Definition: SchemaMigrator.php:39
‪TYPO3\CMS\Core\Database\Schema\SchemaMigrator\migrate
‪migrate(array $statements, array $selectedStatements)
Definition: SchemaMigrator.php:105
‪TYPO3\CMS\Core\Core\Bootstrap\createCache
‪static createCache(string $identifier, bool $disableCaching=false)
Definition: Bootstrap.php:335
‪$GLOBALS
‪$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['adminpanel']['modules']
Definition: ext_localconf.php:25
‪TYPO3\CMS\Core\Database\Schema\SchemaMigrator\mergeIndexes
‪Index[] mergeIndexes(Index ... $indexes)
Definition: SchemaMigrator.php:413
‪TYPO3\CMS\Core\Database\Schema\SchemaMigrator\$result
‪$result
Definition: SchemaMigrator.php:150
‪TYPO3\CMS\Core\Core\Bootstrap
Definition: Bootstrap.php:62
‪TYPO3\CMS\Core\Database\Schema\SchemaMigrator\$schemaDiffs
‪$schemaDiffs
Definition: SchemaMigrator.php:84
‪TYPO3\CMS\Core\Database\Schema\SchemaMigrator\trimIdentifierQuotes
‪array< non-empty-string, mergeTableDefinitions(array $tables):array { $return=[];foreach( $tables as $table) { $tableName=$this-> trimIdentifierQuotes($table->getName())
‪TYPO3\CMS\Core\Database\ConnectionPool
Definition: ConnectionPool.php:46
‪TYPO3\CMS\Core\Database\Schema\SchemaMigrator\parseCreateTableStatements
‪Table[] parseCreateTableStatements(array $statements)
Definition: SchemaMigrator.php:213
‪TYPO3\CMS\Core\Database\Schema\SchemaMigrator\$return
‪return $return
Definition: SchemaMigrator.php:393
‪TYPO3\CMS\Core\Database\Schema\SchemaMigrator\$currentTableDefinition
‪if(!array_key_exists($tableName, $return)) $currentTableDefinition
Definition: SchemaMigrator.php:382
‪TYPO3\CMS\Webhooks\Message\$identifier
‪identifier readonly string $identifier
Definition: FileAddedMessage.php:37