|
1
|
|
|
<?php |
|
2
|
|
|
|
|
3
|
|
|
namespace Kaliop\eZMigrationBundle\Core\StorageHandler; |
|
4
|
|
|
|
|
5
|
|
|
use Kaliop\eZMigrationBundle\API\StorageHandlerInterface; |
|
6
|
|
|
use Kaliop\eZMigrationBundle\API\Collection\MigrationCollection; |
|
7
|
|
|
use eZ\Publish\Core\Persistence\Database\DatabaseHandler; |
|
8
|
|
|
use Doctrine\DBAL\Schema\Schema; |
|
9
|
|
|
use eZ\Publish\Core\Persistence\Database\SelectQuery; |
|
10
|
|
|
use Kaliop\eZMigrationBundle\API\Value\Migration; |
|
11
|
|
|
use Kaliop\eZMigrationBundle\API\Value\MigrationDefinition; |
|
12
|
|
|
use Doctrine\DBAL\Exception\UniqueConstraintViolationException; |
|
13
|
|
|
|
|
14
|
|
|
/** |
|
15
|
|
|
* Database-backed storage for info on executed migrations |
|
16
|
|
|
* |
|
17
|
|
|
* @todo replace all usage of the ezcdb api with the doctrine dbal one, so that we only depend on one |
|
18
|
|
|
*/ |
|
19
|
|
|
class Database implements StorageHandlerInterface |
|
20
|
|
|
{ |
|
21
|
|
|
/** |
|
22
|
|
|
* Flag to indicate that the migration version table has been created |
|
23
|
|
|
* |
|
24
|
|
|
* @var boolean |
|
25
|
|
|
*/ |
|
26
|
|
|
private $migrationsTableExists = false; |
|
27
|
|
|
|
|
28
|
|
|
/** |
|
29
|
|
|
* Name of the database table where installed migration versions are tracked. |
|
30
|
|
|
* @var string |
|
31
|
|
|
* |
|
32
|
|
|
* @todo add setter/getter, as we need to clear versionTableExists when switching this |
|
33
|
|
|
*/ |
|
34
|
|
|
private $migrationsTableName; |
|
35
|
|
|
|
|
36
|
|
|
/** |
|
37
|
|
|
* @var DatabaseHandler $connection |
|
38
|
|
|
*/ |
|
39
|
|
|
protected $dbHandler; |
|
40
|
|
|
|
|
41
|
|
|
/** |
|
42
|
|
|
* @param DatabaseHandler $dbHandler |
|
43
|
|
|
* @param string $migrationsTableName |
|
44
|
|
|
*/ |
|
45
|
|
|
public function __construct(DatabaseHandler $dbHandler, $migrationsTableName = 'kaliop_migrations') |
|
46
|
|
|
{ |
|
47
|
|
|
$this->dbHandler = $dbHandler; |
|
48
|
|
|
$this->migrationsTableName = $migrationsTableName; |
|
49
|
|
|
} |
|
50
|
|
|
|
|
51
|
|
|
/** |
|
52
|
|
|
* @return MigrationCollection |
|
53
|
|
|
* @todo add support offset, limit |
|
54
|
|
|
*/ |
|
55
|
|
|
public function loadMigrations() |
|
56
|
|
|
{ |
|
57
|
|
|
$this->createMigrationsTableIfNeeded(); |
|
58
|
|
|
|
|
59
|
|
|
/** @var \eZ\Publish\Core\Persistence\Database\SelectQuery $q */ |
|
60
|
|
|
$q = $this->dbHandler->createSelectQuery(); |
|
61
|
|
|
$q->select('migration, md5, path, execution_date, status, execution_error') |
|
|
|
|
|
|
62
|
|
|
->from($this->migrationsTableName) |
|
|
|
|
|
|
63
|
|
|
->orderBy('migration', SelectQuery::ASC); |
|
64
|
|
|
$stmt = $q->prepare(); |
|
65
|
|
|
$stmt->execute(); |
|
66
|
|
|
$results = $stmt->fetchAll(); |
|
67
|
|
|
|
|
68
|
|
|
$migrations = array(); |
|
69
|
|
|
foreach ($results as $result) { |
|
70
|
|
|
$migrations[$result['migration']] = $this->arrayToMigration($result); |
|
71
|
|
|
} |
|
72
|
|
|
|
|
73
|
|
|
return new MigrationCollection($migrations); |
|
74
|
|
|
} |
|
75
|
|
|
|
|
76
|
|
|
/** |
|
77
|
|
|
* @param string $migrationName |
|
78
|
|
|
* @return Migration|null |
|
79
|
|
|
*/ |
|
80
|
|
|
public function loadMigration($migrationName) |
|
81
|
|
|
{ |
|
82
|
|
|
/** @var \eZ\Publish\Core\Persistence\Database\SelectQuery $q */ |
|
83
|
|
|
$q = $this->dbHandler->createSelectQuery(); |
|
84
|
|
|
$q->select('migration, md5, path, execution_date, status, execution_error') |
|
|
|
|
|
|
85
|
|
|
->from($this->migrationsTableName) |
|
|
|
|
|
|
86
|
|
|
->where($q->expr->eq('migration', $q->bindValue($migrationName))); |
|
|
|
|
|
|
87
|
|
|
$stmt = $q->prepare(); |
|
88
|
|
|
$stmt->execute(); |
|
89
|
|
|
$result = $stmt->fetch(\PDO::FETCH_ASSOC); |
|
90
|
|
|
|
|
91
|
|
|
if (is_array($result) && !empty($result)) { |
|
92
|
|
|
return $this->arrayToMigration($result); |
|
93
|
|
|
} |
|
94
|
|
|
|
|
95
|
|
|
return null; |
|
96
|
|
|
} |
|
97
|
|
|
|
|
98
|
|
|
/** |
|
99
|
|
|
* Creates and stores a new migration (leaving it in TODO status) |
|
100
|
|
|
* @param MigrationDefinition $migrationDefinition |
|
101
|
|
|
* @return mixed |
|
102
|
|
|
* @throws \Exception If the migration exists already (we rely on the PK for that) |
|
103
|
|
|
*/ |
|
104
|
|
|
public function addMigration(MigrationDefinition $migrationDefinition) |
|
105
|
|
|
{ |
|
106
|
|
|
$this->createMigrationsTableIfNeeded(); |
|
107
|
|
|
|
|
108
|
|
|
$conn = $this->dbHandler->getConnection(); |
|
109
|
|
|
|
|
110
|
|
|
$migration = new Migration( |
|
111
|
|
|
$migrationDefinition->name, |
|
112
|
|
|
md5($migrationDefinition->rawDefinition), |
|
113
|
|
|
$migrationDefinition->path, |
|
114
|
|
|
null, |
|
115
|
|
|
Migration::STATUS_TODO |
|
116
|
|
|
); |
|
117
|
|
|
try { |
|
118
|
|
|
$conn->insert($this->migrationsTableName, $this->migrationToArray($migration)); |
|
119
|
|
|
} catch(UniqueConstraintViolationException $e) { |
|
120
|
|
|
throw new \Exception("Migration '{$migrationDefinition->name}' already exists"); |
|
121
|
|
|
} |
|
122
|
|
|
|
|
123
|
|
|
return $migration; |
|
124
|
|
|
} |
|
125
|
|
|
|
|
126
|
|
|
/** |
|
127
|
|
|
* Starts a migration, given its definition: stores its status in the db, returns the Migration object |
|
128
|
|
|
* |
|
129
|
|
|
* @param MigrationDefinition $migrationDefinition |
|
130
|
|
|
* @return Migration |
|
131
|
|
|
* @throws \Exception if migration was already executing or already done |
|
132
|
|
|
* @todo add a parameter to allow re-execution of already-done migrations |
|
133
|
|
|
*/ |
|
134
|
|
|
public function startMigration(MigrationDefinition $migrationDefinition) |
|
135
|
|
|
{ |
|
136
|
|
|
$this->createMigrationsTableIfNeeded(); |
|
137
|
|
|
|
|
138
|
|
|
// select for update |
|
139
|
|
|
|
|
140
|
|
|
// annoyingly enough, neither Doctrine nor EZP provide built in support for 'FOR UPDATE' in their query builders... |
|
141
|
|
|
// at least the doctrine one allows us to still use parameter binding when we add our sql pqrticle |
|
142
|
|
|
$conn = $this->dbHandler->getConnection(); |
|
143
|
|
|
|
|
144
|
|
|
$qb = $conn->createQueryBuilder(); |
|
145
|
|
|
$qb->select('*') |
|
146
|
|
|
->from($this->migrationsTableName) |
|
147
|
|
|
->where('migration = ?'); |
|
148
|
|
|
$sql = $qb->getSQL() . ' FOR UPDATE'; |
|
149
|
|
|
|
|
150
|
|
|
$conn->beginTransaction(); |
|
151
|
|
|
|
|
152
|
|
|
$stmt = $conn->executeQuery($sql, array($migrationDefinition->name)); |
|
153
|
|
|
$existingMigrationData = $stmt->fetch(\PDO::FETCH_ASSOC); |
|
154
|
|
|
|
|
155
|
|
|
if (is_array($existingMigrationData)) { |
|
156
|
|
|
// migration exists |
|
157
|
|
|
|
|
158
|
|
|
// fail if it was already executing or already done |
|
159
|
|
|
if ($existingMigrationData['status'] == Migration::STATUS_STARTED) { |
|
160
|
|
|
// commit to release the lock |
|
161
|
|
|
$conn->commit(); |
|
162
|
|
|
throw new \Exception("Migration '{$migrationDefinition->name}' can not be started as it is already executing"); |
|
163
|
|
|
} |
|
164
|
|
|
if ($existingMigrationData['status'] == Migration::STATUS_DONE) { |
|
165
|
|
|
// commit to release the lock |
|
166
|
|
|
$conn->commit(); |
|
167
|
|
|
throw new \Exception("Migration '{$migrationDefinition->name}' can not be started as it was already executed"); |
|
168
|
|
|
} |
|
169
|
|
|
|
|
170
|
|
|
$migration = new Migration( |
|
171
|
|
|
$migrationDefinition->name, |
|
172
|
|
|
md5($migrationDefinition->rawDefinition), |
|
173
|
|
|
$migrationDefinition->path, |
|
174
|
|
|
time(), |
|
175
|
|
|
Migration::STATUS_STARTED |
|
176
|
|
|
); |
|
177
|
|
|
$conn->update( |
|
178
|
|
|
$this->migrationsTableName, |
|
179
|
|
|
array( |
|
180
|
|
|
'execution_date' => $migration->executionDate, |
|
181
|
|
|
'status' => Migration::STATUS_STARTED, |
|
182
|
|
|
'execution_error' => null, |
|
183
|
|
|
), |
|
184
|
|
|
array('migration' => $migrationDefinition->name) |
|
185
|
|
|
); |
|
186
|
|
|
} else { |
|
187
|
|
|
// migration did not exist. Create it! |
|
188
|
|
|
|
|
189
|
|
|
$migration = new Migration( |
|
190
|
|
|
$migrationDefinition->name, |
|
191
|
|
|
md5($migrationDefinition->rawDefinition), |
|
192
|
|
|
$migrationDefinition->path, |
|
193
|
|
|
time(), |
|
194
|
|
|
Migration::STATUS_STARTED |
|
195
|
|
|
); |
|
196
|
|
|
$conn->insert($this->migrationsTableName, $this->migrationToArray($migration)); |
|
197
|
|
|
} |
|
198
|
|
|
|
|
199
|
|
|
$conn->commit(); |
|
200
|
|
|
return $migration; |
|
201
|
|
|
} |
|
202
|
|
|
|
|
203
|
|
|
/** |
|
204
|
|
|
* Stops a migration by storing it in the db. Migration status can not be 'started' |
|
205
|
|
|
* |
|
206
|
|
|
* @param Migration $migration |
|
207
|
|
|
* @throws \Exception |
|
208
|
|
|
*/ |
|
209
|
|
|
public function endMigration(Migration $migration) |
|
210
|
|
|
{ |
|
211
|
|
|
if ($migration->status == Migration::STATUS_STARTED) { |
|
212
|
|
|
throw new \Exception("Migration '{$migration->name}' can not be ended as its status is 'started'..."); |
|
213
|
|
|
} |
|
214
|
|
|
|
|
215
|
|
|
$this->createMigrationsTableIfNeeded(); |
|
216
|
|
|
|
|
217
|
|
|
// select for update |
|
218
|
|
|
|
|
219
|
|
|
// annoyingly enough, neither Doctrine nor EZP provide built in support for 'FOR UPDATE' in their query builders... |
|
220
|
|
|
// at least the doctrine one allows us to still use parameter binding when we add our sql pqrticle |
|
221
|
|
|
$conn = $this->dbHandler->getConnection(); |
|
222
|
|
|
|
|
223
|
|
|
$qb = $conn->createQueryBuilder(); |
|
224
|
|
|
$qb->select('*') |
|
225
|
|
|
->from($this->migrationsTableName) |
|
226
|
|
|
->where('migration = ?'); |
|
227
|
|
|
$sql = $qb->getSQL() . ' FOR UPDATE'; |
|
228
|
|
|
|
|
229
|
|
|
$conn->beginTransaction(); |
|
230
|
|
|
|
|
231
|
|
|
$stmt = $conn->executeQuery($sql, array($migration->name)); |
|
232
|
|
|
$existingMigrationData = $stmt->fetch(\PDO::FETCH_ASSOC); |
|
233
|
|
|
|
|
234
|
|
|
// fail if it was not executing |
|
235
|
|
|
|
|
236
|
|
|
if (!is_array($existingMigrationData)) { |
|
237
|
|
|
// commit to release the lock |
|
238
|
|
|
$conn->commit(); |
|
239
|
|
|
throw new \Exception("Migration '{$migration->name}' can not be ended as it is not found"); |
|
240
|
|
|
} |
|
241
|
|
|
|
|
242
|
|
|
if ($existingMigrationData['status'] != Migration::STATUS_STARTED) { |
|
243
|
|
|
// commit to release the lock |
|
244
|
|
|
$conn->commit(); |
|
245
|
|
|
throw new \Exception("Migration '{$migration->name}' can not be ended as it is not executing"); |
|
246
|
|
|
} |
|
247
|
|
|
|
|
248
|
|
|
$conn->update( |
|
249
|
|
|
$this->migrationsTableName, |
|
250
|
|
|
array( |
|
251
|
|
|
'status' => $migration->status, |
|
252
|
|
|
'execution_error' => $migration->executionError, |
|
253
|
|
|
'execution_date' => $migration->executionDate |
|
254
|
|
|
), |
|
255
|
|
|
array('migration' => $migration->name) |
|
256
|
|
|
); |
|
257
|
|
|
|
|
258
|
|
|
$conn->commit(); |
|
259
|
|
|
} |
|
260
|
|
|
|
|
261
|
|
|
/** |
|
262
|
|
|
* Removes a Migration from the table |
|
263
|
|
|
* @param Migration $migration |
|
264
|
|
|
*/ |
|
265
|
|
|
public function deleteMigration(Migration $migration) |
|
266
|
|
|
{ |
|
267
|
|
|
$this->createMigrationsTableIfNeeded(); |
|
268
|
|
|
$conn = $this->dbHandler->getConnection(); |
|
269
|
|
|
$conn->delete($this->migrationsTableName, array('migration' => $migration->name)); |
|
270
|
|
|
} |
|
271
|
|
|
|
|
272
|
|
|
/** |
|
273
|
|
|
* Check if the version db table exists and create it if not. |
|
274
|
|
|
* |
|
275
|
|
|
* @return bool true if table has been created, false if it was already there |
|
276
|
|
|
* |
|
277
|
|
|
* @todo add a 'force' flag to force table re-creation |
|
278
|
|
|
* @todo manage changes to table definition |
|
279
|
|
|
*/ |
|
280
|
|
|
public function createMigrationsTableIfNeeded() |
|
281
|
|
|
{ |
|
282
|
|
|
if ($this->migrationsTableExists) { |
|
283
|
|
|
return false; |
|
284
|
|
|
} |
|
285
|
|
|
|
|
286
|
|
|
if ($this->tableExist($this->migrationsTableName)) { |
|
287
|
|
|
$this->migrationsTableExists = true; |
|
288
|
|
|
return false; |
|
289
|
|
|
} |
|
290
|
|
|
|
|
291
|
|
|
$this->createMigrationsTable(); |
|
292
|
|
|
|
|
293
|
|
|
$this->migrationsTableExists = true; |
|
294
|
|
|
return true; |
|
295
|
|
|
} |
|
296
|
|
|
|
|
297
|
|
|
public function createMigrationsTable() |
|
298
|
|
|
{ |
|
299
|
|
|
/** @var \Doctrine\DBAL\Schema\AbstractSchemaManager $sm */ |
|
300
|
|
|
$sm = $this->dbHandler->getConnection()->getSchemaManager(); |
|
301
|
|
|
$dbPlatform = $sm->getDatabasePlatform(); |
|
302
|
|
|
|
|
303
|
|
|
$schema = new Schema(); |
|
304
|
|
|
|
|
305
|
|
|
$t = $schema->createTable($this->migrationsTableName); |
|
306
|
|
|
$t->addColumn('migration', 'string', array('length' => 255)); |
|
307
|
|
|
$t->addColumn('path', 'string', array('length' => 4000)); |
|
308
|
|
|
$t->addColumn('md5', 'string', array('length' => 32)); |
|
309
|
|
|
$t->addColumn('execution_date', 'integer', array('notnull' => false)); |
|
310
|
|
|
$t->addColumn('status', 'integer', array('default ' => Migration::STATUS_TODO)); |
|
311
|
|
|
$t->addColumn('execution_error', 'string', array('length' => 4000, 'notnull' => false)); |
|
312
|
|
|
$t->setPrimaryKey(array('migration')); |
|
313
|
|
|
|
|
314
|
|
|
foreach($schema->toSql($dbPlatform) as $sql) { |
|
315
|
|
|
$this->dbHandler->exec($sql); |
|
316
|
|
|
} |
|
317
|
|
|
} |
|
318
|
|
|
|
|
319
|
|
|
/** |
|
320
|
|
|
* Check if a table exists in the database |
|
321
|
|
|
* |
|
322
|
|
|
* @param string $tableName |
|
323
|
|
|
* @return bool |
|
324
|
|
|
*/ |
|
325
|
|
View Code Duplication |
protected function tableExist($tableName) |
|
|
|
|
|
|
326
|
|
|
{ |
|
327
|
|
|
/** @var \Doctrine\DBAL\Schema\AbstractSchemaManager $sm */ |
|
328
|
|
|
$sm = $this->dbHandler->getConnection()->getSchemaManager(); |
|
329
|
|
|
foreach($sm->listTables() as $table) { |
|
330
|
|
|
if ($table->getName() == $tableName) { |
|
331
|
|
|
return true; |
|
332
|
|
|
} |
|
333
|
|
|
} |
|
334
|
|
|
|
|
335
|
|
|
return false; |
|
336
|
|
|
} |
|
337
|
|
|
|
|
338
|
|
|
protected function migrationToArray(Migration $migration) |
|
339
|
|
|
{ |
|
340
|
|
|
return array( |
|
341
|
|
|
'migration' => $migration->name, |
|
342
|
|
|
'md5' => $migration->md5, |
|
343
|
|
|
'path' => $migration->path, |
|
344
|
|
|
'execution_date' => $migration->executionDate, |
|
345
|
|
|
'status' => $migration->status, |
|
346
|
|
|
'execution_error' => $migration->executionError |
|
347
|
|
|
); |
|
348
|
|
|
} |
|
349
|
|
|
|
|
350
|
|
|
protected function arrayToMigration(array $data) |
|
351
|
|
|
{ |
|
352
|
|
|
return new Migration( |
|
353
|
|
|
$data['migration'], |
|
354
|
|
|
$data['md5'], |
|
355
|
|
|
$data['path'], |
|
356
|
|
|
$data['execution_date'], |
|
357
|
|
|
$data['status'], |
|
358
|
|
|
$data['execution_error'] |
|
359
|
|
|
); |
|
360
|
|
|
} |
|
361
|
|
|
} |
This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.
If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.
In this case you can add the
@ignorePhpDoc annotation to the duplicate definition and it will be ignored.