1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Doctrine\DBAL\Migrations; |
4
|
|
|
|
5
|
|
|
use Doctrine\DBAL\Migrations\Configuration\Configuration; |
6
|
|
|
use Doctrine\DBAL\Migrations\Event\MigrationsVersionEventArgs; |
7
|
|
|
use Doctrine\DBAL\Migrations\Provider\LazySchemaDiffProvider; |
8
|
|
|
use Doctrine\DBAL\Migrations\Provider\SchemaDiffProvider; |
9
|
|
|
use Doctrine\DBAL\Migrations\Provider\SchemaDiffProviderInterface; |
10
|
|
|
use Doctrine\DBAL\Types\Type; |
11
|
|
|
|
12
|
|
|
/** |
13
|
|
|
* Class which wraps a migration version and allows execution of the |
14
|
|
|
* individual migration version up or down method. |
15
|
|
|
* |
16
|
|
|
* @license http://www.opensource.org/licenses/lgpl-license.php LGPL |
17
|
|
|
* @link www.doctrine-project.org |
18
|
|
|
* @since 2.0 |
19
|
|
|
* @author Jonathan H. Wage <[email protected]> |
20
|
|
|
*/ |
21
|
|
|
class Version |
22
|
|
|
{ |
23
|
|
|
const STATE_NONE = 0; |
24
|
|
|
const STATE_PRE = 1; |
25
|
|
|
const STATE_EXEC = 2; |
26
|
|
|
const STATE_POST = 3; |
27
|
|
|
|
28
|
|
|
const DIRECTION_UP = 'up'; |
29
|
|
|
const DIRECTION_DOWN = 'down'; |
30
|
|
|
|
31
|
|
|
/** |
32
|
|
|
* The Migrations Configuration instance for this migration |
33
|
|
|
* |
34
|
|
|
* @var Configuration |
35
|
|
|
*/ |
36
|
|
|
private $configuration; |
37
|
|
|
|
38
|
|
|
/** |
39
|
|
|
* The OutputWriter object instance used for outputting information |
40
|
|
|
* |
41
|
|
|
* @var OutputWriter |
42
|
|
|
*/ |
43
|
|
|
private $outputWriter; |
44
|
|
|
|
45
|
|
|
/** |
46
|
|
|
* The version in timestamp format (YYYYMMDDHHMMSS) |
47
|
|
|
* |
48
|
|
|
* @var string |
49
|
|
|
*/ |
50
|
|
|
private $version; |
51
|
|
|
|
52
|
|
|
/** |
53
|
|
|
* The migration instance for this version |
54
|
|
|
* |
55
|
|
|
* @var AbstractMigration |
56
|
|
|
*/ |
57
|
|
|
private $migration; |
58
|
|
|
|
59
|
|
|
/** |
60
|
|
|
* @var \Doctrine\DBAL\Connection |
61
|
|
|
*/ |
62
|
|
|
private $connection; |
63
|
|
|
|
64
|
|
|
/** |
65
|
|
|
* @var string |
66
|
|
|
*/ |
67
|
|
|
private $class; |
68
|
|
|
|
69
|
|
|
/** The array of collected SQL statements for this version */ |
70
|
|
|
private $sql = []; |
71
|
|
|
|
72
|
|
|
/** The array of collected parameters for SQL statements for this version */ |
73
|
|
|
private $params = []; |
74
|
|
|
|
75
|
|
|
/** The array of collected types for SQL statements for this version */ |
76
|
|
|
private $types = []; |
77
|
|
|
|
78
|
|
|
/** The time in seconds that this migration version took to execute */ |
79
|
|
|
private $time; |
80
|
|
|
|
81
|
|
|
/** |
82
|
|
|
* @var int |
83
|
|
|
*/ |
84
|
|
|
private $state = self::STATE_NONE; |
85
|
|
|
|
86
|
|
|
/** @var SchemaDiffProviderInterface */ |
87
|
|
|
private $schemaProvider; |
88
|
|
|
|
89
|
121 |
|
public function __construct(Configuration $configuration, $version, $class, SchemaDiffProviderInterface $schemaProvider = null) |
90
|
|
|
{ |
91
|
121 |
|
$this->configuration = $configuration; |
92
|
121 |
|
$this->outputWriter = $configuration->getOutputWriter(); |
93
|
121 |
|
$this->class = $class; |
94
|
121 |
|
$this->connection = $configuration->getConnection(); |
95
|
121 |
|
$this->migration = new $class($this); |
96
|
121 |
|
$this->version = $version; |
97
|
|
|
|
98
|
121 |
|
if ($schemaProvider !== null) { |
99
|
1 |
|
$this->schemaProvider = $schemaProvider; |
100
|
|
|
} |
101
|
121 |
|
if ($schemaProvider === null) { |
102
|
120 |
|
$schemaProvider = new SchemaDiffProvider( |
103
|
120 |
|
$this->connection->getSchemaManager(), |
104
|
120 |
|
$this->connection->getDatabasePlatform() |
105
|
|
|
); |
106
|
120 |
|
$this->schemaProvider = LazySchemaDiffProvider::fromDefaultProxyFactoryConfiguration($schemaProvider); |
107
|
|
|
} |
108
|
121 |
|
} |
109
|
|
|
|
110
|
|
|
/** |
111
|
|
|
* Returns the string version in the format YYYYMMDDHHMMSS |
112
|
|
|
* |
113
|
|
|
* @return string $version |
114
|
|
|
*/ |
115
|
78 |
|
public function getVersion() |
116
|
|
|
{ |
117
|
78 |
|
return $this->version; |
118
|
|
|
} |
119
|
|
|
|
120
|
|
|
/** |
121
|
|
|
* Returns the Migrations Configuration object instance |
122
|
|
|
* |
123
|
|
|
* @return Configuration $configuration |
124
|
|
|
*/ |
125
|
117 |
|
public function getConfiguration() |
126
|
|
|
{ |
127
|
117 |
|
return $this->configuration; |
128
|
|
|
} |
129
|
|
|
|
130
|
|
|
/** |
131
|
|
|
* Check if this version has been migrated or not. |
132
|
|
|
* |
133
|
|
|
* @return boolean |
134
|
|
|
*/ |
135
|
15 |
|
public function isMigrated() |
136
|
|
|
{ |
137
|
15 |
|
return $this->configuration->hasVersionMigrated($this); |
138
|
|
|
} |
139
|
|
|
|
140
|
43 |
|
public function markMigrated() |
141
|
|
|
{ |
142
|
43 |
|
$this->markVersion('up'); |
143
|
43 |
|
} |
144
|
|
|
|
145
|
43 |
|
private function markVersion($direction) |
146
|
|
|
{ |
147
|
43 |
|
$action = $direction === 'up' ? 'insert' : 'delete'; |
148
|
|
|
|
149
|
43 |
|
$this->configuration->createMigrationTable(); |
150
|
43 |
|
$this->connection->$action( |
151
|
43 |
|
$this->configuration->getMigrationsTableName(), |
152
|
43 |
|
[$this->configuration->getMigrationsColumnName() => $this->version] |
153
|
|
|
); |
154
|
43 |
|
} |
155
|
|
|
|
156
|
9 |
|
public function markNotMigrated() |
157
|
|
|
{ |
158
|
9 |
|
$this->markVersion('down'); |
159
|
9 |
|
} |
160
|
|
|
|
161
|
|
|
/** |
162
|
|
|
* Add some SQL queries to this versions migration |
163
|
|
|
* |
164
|
|
|
* @param array|string $sql |
165
|
|
|
* @param array $params |
166
|
|
|
* @param array $types |
167
|
|
|
*/ |
168
|
49 |
|
public function addSql($sql, array $params = [], array $types = []) |
169
|
|
|
{ |
170
|
49 |
|
if (is_array($sql)) { |
171
|
48 |
|
foreach ($sql as $key => $query) { |
172
|
14 |
|
$this->sql[] = $query; |
173
|
14 |
|
if ( ! empty($params[$key])) { |
174
|
1 |
|
$queryTypes = $types[$key] ?? []; |
175
|
48 |
|
$this->addQueryParams($params[$key], $queryTypes); |
176
|
|
|
} |
177
|
|
|
} |
178
|
|
|
} else { |
179
|
30 |
|
$this->sql[] = $sql; |
180
|
30 |
|
if ( ! empty($params)) { |
181
|
18 |
|
$this->addQueryParams($params, $types); |
182
|
|
|
} |
183
|
|
|
} |
184
|
49 |
|
} |
185
|
|
|
|
186
|
|
|
/** |
187
|
|
|
* @param mixed[] $params Array of prepared statement parameters |
188
|
|
|
* @param string[] $types Array of the types of each statement parameters |
189
|
|
|
*/ |
190
|
19 |
|
private function addQueryParams($params, $types) |
191
|
|
|
{ |
192
|
19 |
|
$index = count($this->sql) - 1; |
193
|
19 |
|
$this->params[$index] = $params; |
194
|
19 |
|
$this->types[$index] = $types; |
195
|
19 |
|
} |
196
|
|
|
|
197
|
|
|
/** |
198
|
|
|
* Write a migration SQL file to the given path |
199
|
|
|
* |
200
|
|
|
* @param string $path The path to write the migration SQL file. |
201
|
|
|
* @param string $direction The direction to execute. |
202
|
|
|
* |
203
|
|
|
* @return boolean $written |
204
|
|
|
* @throws MigrationException |
205
|
|
|
*/ |
206
|
9 |
|
public function writeSqlFile($path, $direction = self::DIRECTION_UP) |
207
|
|
|
{ |
208
|
9 |
|
$queries = $this->execute($direction, true); |
209
|
|
|
|
210
|
9 |
|
if ( ! empty($this->params)) { |
211
|
1 |
|
throw MigrationException::migrationNotConvertibleToSql($this->class); |
212
|
|
|
} |
213
|
|
|
|
214
|
8 |
|
$this->outputWriter->write("\n-- Version " . $this->version . "\n"); |
215
|
|
|
|
216
|
8 |
|
$sqlQueries = [$this->version => $queries]; |
217
|
|
|
|
218
|
|
|
/* |
219
|
|
|
* Since the configuration object changes during the creation we cannot inject things |
220
|
|
|
* properly, so I had to violate LoD here (so please, let's find a way to solve it on v2). |
221
|
|
|
*/ |
222
|
8 |
|
return $this->configuration->getQueryWriter() |
223
|
8 |
|
->write($path, $direction, $sqlQueries); |
224
|
|
|
} |
225
|
|
|
|
226
|
|
|
/** |
227
|
|
|
* @return AbstractMigration |
228
|
|
|
*/ |
229
|
11 |
|
public function getMigration() |
230
|
|
|
{ |
231
|
11 |
|
return $this->migration; |
232
|
|
|
} |
233
|
|
|
|
234
|
|
|
/** |
235
|
|
|
* Execute this migration version up or down and and return the SQL. |
236
|
|
|
* We are only allowing the addSql call and the schema modification to take effect in the up and down call. |
237
|
|
|
* This is necessary to ensure that the migration is revertable. |
238
|
|
|
* The schema is passed to the pre and post method only to be able to test the presence of some table, And the |
239
|
|
|
* connection that can get used trough it allow for the test of the presence of records. |
240
|
|
|
* |
241
|
|
|
* @param string $direction The direction to execute the migration. |
242
|
|
|
* @param boolean $dryRun Whether to not actually execute the migration SQL and just do a dry run. |
243
|
|
|
* @param boolean $timeAllQueries Measuring or not the execution time of each SQL query. |
244
|
|
|
* |
245
|
|
|
* @return array $sql |
246
|
|
|
* |
247
|
|
|
* @throws \Exception when migration fails |
248
|
|
|
*/ |
249
|
50 |
|
public function execute($direction, $dryRun = false, $timeAllQueries = false) |
250
|
|
|
{ |
251
|
50 |
|
$this->dispatchEvent(Events::onMigrationsVersionExecuting, $direction, $dryRun); |
252
|
|
|
|
253
|
50 |
|
$this->sql = []; |
254
|
|
|
|
255
|
50 |
|
$transaction = $this->migration->isTransactional(); |
256
|
50 |
|
if ($transaction) { |
257
|
|
|
//only start transaction if in transactional mode |
258
|
50 |
|
$this->connection->beginTransaction(); |
259
|
|
|
} |
260
|
|
|
|
261
|
|
|
try { |
262
|
50 |
|
$migrationStart = microtime(true); |
263
|
|
|
|
264
|
50 |
|
$this->state = self::STATE_PRE; |
265
|
50 |
|
$fromSchema = $this->schemaProvider->createFromSchema(); |
266
|
|
|
|
267
|
50 |
|
$this->migration->{'pre' . ucfirst($direction)}($fromSchema); |
268
|
|
|
|
269
|
48 |
|
if ($direction === self::DIRECTION_UP) { |
270
|
46 |
|
$this->outputWriter->write("\n" . sprintf(' <info>++</info> migrating <comment>%s</comment>', $this->version) . "\n"); |
271
|
|
|
} else { |
272
|
8 |
|
$this->outputWriter->write("\n" . sprintf(' <info>--</info> reverting <comment>%s</comment>', $this->version) . "\n"); |
273
|
|
|
} |
274
|
|
|
|
275
|
48 |
|
$this->state = self::STATE_EXEC; |
276
|
|
|
|
277
|
48 |
|
$toSchema = $this->schemaProvider->createToSchema($fromSchema); |
278
|
48 |
|
$this->migration->$direction($toSchema); |
279
|
|
|
|
280
|
47 |
|
$this->addSql($this->schemaProvider->getSqlDiffToMigrate($fromSchema, $toSchema)); |
281
|
|
|
|
282
|
47 |
|
$this->executeRegisteredSql($dryRun, $timeAllQueries); |
283
|
|
|
|
284
|
47 |
|
$this->state = self::STATE_POST; |
285
|
47 |
|
$this->migration->{'post' . ucfirst($direction)}($toSchema); |
286
|
|
|
|
287
|
47 |
|
if ( ! $dryRun) { |
288
|
30 |
|
if ($direction === self::DIRECTION_UP) { |
289
|
30 |
|
$this->markMigrated(); |
290
|
|
|
} else { |
291
|
6 |
|
$this->markNotMigrated(); |
292
|
|
|
} |
293
|
|
|
} |
294
|
|
|
|
295
|
47 |
|
$migrationEnd = microtime(true); |
296
|
47 |
|
$this->time = round($migrationEnd - $migrationStart, 2); |
297
|
47 |
|
if ($direction === self::DIRECTION_UP) { |
298
|
45 |
|
$this->outputWriter->write(sprintf("\n <info>++</info> migrated (%ss)", $this->time)); |
299
|
|
|
} else { |
300
|
8 |
|
$this->outputWriter->write(sprintf("\n <info>--</info> reverted (%ss)", $this->time)); |
301
|
|
|
} |
302
|
|
|
|
303
|
47 |
|
if ($transaction) { |
304
|
|
|
//commit only if running in transactional mode |
305
|
47 |
|
$this->connection->commit(); |
306
|
|
|
} |
307
|
|
|
|
308
|
47 |
|
$this->state = self::STATE_NONE; |
309
|
|
|
|
310
|
47 |
|
$this->dispatchEvent(Events::onMigrationsVersionExecuted, $direction, $dryRun); |
311
|
|
|
|
312
|
47 |
|
return $this->sql; |
313
|
7 |
|
} catch (SkipMigrationException $e) { |
314
|
6 |
|
if ($transaction) { |
315
|
|
|
//only rollback transaction if in transactional mode |
316
|
6 |
|
$this->connection->rollBack(); |
317
|
|
|
} |
318
|
|
|
|
319
|
6 |
|
if ($dryRun === false) { |
320
|
|
|
// now mark it as migrated |
321
|
5 |
|
if ($direction === self::DIRECTION_UP) { |
322
|
5 |
|
$this->markMigrated(); |
323
|
|
|
} else { |
324
|
1 |
|
$this->markNotMigrated(); |
325
|
|
|
} |
326
|
|
|
} |
327
|
|
|
|
328
|
6 |
|
$this->outputWriter->write(sprintf("\n <info>SS</info> skipped (Reason: %s)", $e->getMessage())); |
329
|
|
|
|
330
|
6 |
|
$this->state = self::STATE_NONE; |
331
|
|
|
|
332
|
6 |
|
$this->dispatchEvent(Events::onMigrationsVersionSkipped, $direction, $dryRun); |
333
|
|
|
|
334
|
6 |
|
return []; |
335
|
1 |
|
} catch (\Exception $e) { |
336
|
1 |
|
$this->outputWriter->write(sprintf( |
337
|
1 |
|
'<error>Migration %s failed during %s. Error %s</error>', |
338
|
1 |
|
$this->version, |
339
|
1 |
|
$this->getExecutionState(), |
340
|
1 |
|
$e->getMessage() |
341
|
|
|
)); |
342
|
|
|
|
343
|
1 |
|
if ($transaction) { |
344
|
|
|
//only rollback transaction if in transactional mode |
345
|
1 |
|
$this->connection->rollBack(); |
346
|
|
|
} |
347
|
|
|
|
348
|
1 |
|
$this->state = self::STATE_NONE; |
349
|
|
|
|
350
|
1 |
|
throw $e; |
351
|
|
|
} |
352
|
|
|
} |
353
|
|
|
|
354
|
8 |
|
public function getExecutionState() |
355
|
|
|
{ |
356
|
8 |
|
switch ($this->state) { |
357
|
8 |
|
case self::STATE_PRE: |
|
|
|
|
358
|
1 |
|
return 'Pre-Checks'; |
359
|
7 |
|
case self::STATE_POST: |
|
|
|
|
360
|
1 |
|
return 'Post-Checks'; |
361
|
6 |
|
case self::STATE_EXEC: |
362
|
2 |
|
return 'Execution'; |
363
|
|
|
default: |
364
|
4 |
|
return 'No State'; |
365
|
|
|
} |
366
|
|
|
} |
367
|
|
|
|
368
|
21 |
|
private function outputQueryTime($queryStart, $timeAllQueries = false) |
369
|
|
|
{ |
370
|
21 |
|
if ($timeAllQueries !== false) { |
371
|
1 |
|
$queryEnd = microtime(true); |
372
|
1 |
|
$queryTime = round($queryEnd - $queryStart, 4); |
373
|
|
|
|
374
|
1 |
|
$this->outputWriter->write(sprintf(" <info>%ss</info>", $queryTime)); |
375
|
|
|
} |
376
|
21 |
|
} |
377
|
|
|
|
378
|
|
|
/** |
379
|
|
|
* Returns the time this migration version took to execute |
380
|
|
|
* |
381
|
|
|
* @return integer $time The time this migration version took to execute |
382
|
|
|
*/ |
383
|
26 |
|
public function getTime() |
384
|
|
|
{ |
385
|
26 |
|
return $this->time; |
386
|
|
|
} |
387
|
|
|
|
388
|
2 |
|
public function __toString() |
389
|
|
|
{ |
390
|
2 |
|
return $this->version; |
391
|
|
|
} |
392
|
|
|
|
393
|
47 |
|
private function executeRegisteredSql($dryRun = false, $timeAllQueries = false) |
394
|
|
|
{ |
395
|
47 |
|
if ( ! $dryRun) { |
396
|
30 |
|
if ( ! empty($this->sql)) { |
397
|
19 |
|
foreach ($this->sql as $key => $query) { |
398
|
19 |
|
$queryStart = microtime(true); |
399
|
|
|
|
400
|
19 |
|
$this->outputSqlQuery($key, $query); |
401
|
19 |
|
if ( ! isset($this->params[$key])) { |
402
|
18 |
|
$this->connection->executeQuery($query); |
403
|
|
|
} else { |
404
|
8 |
|
$this->connection->executeQuery($query, $this->params[$key], $this->types[$key]); |
405
|
|
|
} |
406
|
|
|
|
407
|
19 |
|
$this->outputQueryTime($queryStart, $timeAllQueries); |
408
|
|
|
} |
409
|
|
|
} else { |
410
|
11 |
|
$this->outputWriter->write(sprintf( |
411
|
11 |
|
'<error>Migration %s was executed but did not result in any SQL statements.</error>', |
412
|
30 |
|
$this->version |
413
|
|
|
)); |
414
|
|
|
} |
415
|
|
|
} else { |
416
|
17 |
|
foreach ($this->sql as $idx => $query) { |
417
|
17 |
|
$this->outputSqlQuery($idx, $query); |
418
|
|
|
} |
419
|
|
|
} |
420
|
47 |
|
} |
421
|
|
|
|
422
|
|
|
/** |
423
|
|
|
* Outputs a SQL query via the `OutputWriter`. |
424
|
|
|
* |
425
|
|
|
* @param int $idx The SQL query index. Used to look up params. |
426
|
|
|
* @param string $query the query to output |
427
|
|
|
* @return void |
428
|
|
|
*/ |
429
|
36 |
|
private function outputSqlQuery($idx, $query) |
430
|
|
|
{ |
431
|
36 |
|
$params = $this->formatParamsForOutput( |
432
|
36 |
|
$this->params[$idx] ?? [], |
433
|
36 |
|
$this->types[$idx] ?? [] |
434
|
|
|
); |
435
|
|
|
|
436
|
36 |
|
$this->outputWriter->write(rtrim(sprintf( |
437
|
36 |
|
' <comment>-></comment> %s %s', |
438
|
36 |
|
$query, |
439
|
36 |
|
$params |
440
|
|
|
))); |
441
|
36 |
|
} |
442
|
|
|
|
443
|
|
|
/** |
444
|
|
|
* Formats a set of sql parameters for output with dry run. |
445
|
|
|
* |
446
|
|
|
* @param array $params The query parameters |
447
|
|
|
* @param array $types The types of the query params. Default type is a string |
448
|
|
|
* @return string|null a string of the parameters present. |
449
|
|
|
*/ |
450
|
36 |
|
private function formatParamsForOutput(array $params, array $types) |
451
|
|
|
{ |
452
|
36 |
|
if (empty($params)) { |
453
|
26 |
|
return ''; |
454
|
|
|
} |
455
|
|
|
|
456
|
18 |
|
$out = []; |
457
|
18 |
|
foreach ($params as $key => $value) { |
458
|
18 |
|
$type = $types[$key] ?? 'string'; |
459
|
18 |
|
$outval = '[' . $this->formatParameter($value, $type) . ']'; |
460
|
18 |
|
$out[] = is_string($key) ? sprintf(':%s => %s', $key, $outval) : $outval; |
461
|
|
|
} |
462
|
|
|
|
463
|
18 |
|
return sprintf('with parameters (%s)', implode(', ', $out)); |
464
|
|
|
} |
465
|
|
|
|
466
|
50 |
|
private function dispatchEvent($eventName, $direction, $dryRun) |
467
|
|
|
{ |
468
|
50 |
|
$this->configuration->dispatchEvent($eventName, new MigrationsVersionEventArgs( |
469
|
50 |
|
$this, |
470
|
50 |
|
$this->configuration, |
471
|
50 |
|
$direction, |
472
|
50 |
|
$dryRun |
473
|
|
|
)); |
474
|
50 |
|
} |
475
|
|
|
|
476
|
18 |
|
private function formatParameter($value, string $type) : string |
477
|
|
|
{ |
478
|
18 |
|
if (Type::hasType($type)) { |
479
|
13 |
|
return Type::getType($type)->convertToDatabaseValue( |
480
|
13 |
|
$value, |
481
|
13 |
|
$this->connection->getDatabasePlatform() |
482
|
|
|
); |
483
|
|
|
} |
484
|
|
|
|
485
|
5 |
|
return $this->parameterToString($value); |
486
|
|
|
} |
487
|
|
|
|
488
|
5 |
|
private function parameterToString($value) : string |
489
|
|
|
{ |
490
|
5 |
|
if (is_array($value)) { |
491
|
3 |
|
return implode(', ', array_map([$this, 'parameterToString'], $value)); |
492
|
|
|
} |
493
|
|
|
|
494
|
5 |
|
if (is_int($value) || is_string($value)) { |
495
|
3 |
|
return (string) $value; |
496
|
|
|
} |
497
|
|
|
|
498
|
2 |
|
if (is_bool($value)) { |
499
|
1 |
|
return $value === true ? 'true' : 'false'; |
500
|
|
|
} |
501
|
|
|
|
502
|
1 |
|
return '?'; |
503
|
|
|
} |
504
|
|
|
} |
505
|
|
|
|
As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next
break
.There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.
To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.