Completed
Push — master ( ac3b8b...3c857d )
by Gaetano
18:21
created

Migration::deleteMigrations()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
c 0
b 0
f 0
rs 10
cc 1
eloc 2
nc 1
nop 0
1
<?php
2
3
namespace Kaliop\eZMigrationBundle\Core\StorageHandler\Database;
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 as APIMigration;
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 Migration extends TableStorage implements StorageHandlerInterface
20
{
21
    protected $fieldList = 'migration, md5, path, execution_date, status, execution_error';
22
23
    /**
24
     * @param DatabaseHandler $dbHandler
25
     * @param string $tableName
26
     */
27
    public function __construct(DatabaseHandler $dbHandler, $tableName = 'kaliop_migrations')
28
    {
29
        parent::__construct($dbHandler, $tableName);
30
    }
31
32
    /**
33
     * @param int $limit
34
     * @param int $offset
35
     * @return MigrationCollection
36
     */
37
    public function loadMigrations($limit = null, $offset = null)
38
    {
39
        return $this->loadMigrationsInner(null, $limit, $offset);
40
    }
41
42
    /**
43
     * @param int $status
44
     * @param int $limit
45
     * @param int $offset
46
     * @return MigrationCollection
47
     */
48
    public function loadMigrationsByStatus($status, $limit = null, $offset = null)
49
    {
50
        return $this->loadMigrationsInner($status, $limit, $offset);
51
    }
52
53
    /**
54
     * @param int $status
55
     * @param int $limit
56
     * @param int $offset
57
     * @return MigrationCollection
58
     */
59
    protected function loadMigrationsInner($status = null, $limit = null, $offset = null)
60
    {
61
        $this->createTableIfNeeded();
62
63
        /** @var \eZ\Publish\Core\Persistence\Database\SelectQuery $q */
64
        $q = $this->dbHandler->createSelectQuery();
65
        $q->select($this->fieldList)
0 ignored issues
show
Unused Code introduced by
The call to SelectQuery::select() has too many arguments starting with $this->fieldList.

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 @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
66
            ->from($this->tableName)
0 ignored issues
show
Unused Code introduced by
The call to SelectQuery::from() has too many arguments starting with $this->tableName.

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 @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
67
            ->orderBy('migration', SelectQuery::ASC);
68
        if ($status !== null) {
69
            $q->where($q->expr->eq('status', $q->bindValue($status)));
0 ignored issues
show
Bug introduced by
Accessing expr on the interface eZ\Publish\Core\Persistence\Database\SelectQuery suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
Unused Code introduced by
The call to SelectQuery::where() has too many arguments starting with $q->expr->eq('status', $q->bindValue($status)).

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 @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
70
        }
71
        if ($limit > 0 || $offset > 0) {
72
            if ($limit <= 0) {
73
                $limit = null;
74
            }
75
            if ($offset == 0) {
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing $offset of type integer|null to 0; this is ambiguous as not only 0 == 0 is true, but null == 0 is true, too. Consider using a strict comparison ===.
Loading history...
76
                $offset = null;
77
            }
78
            $q->limit($limit, $offset);
79
        }
80
        $stmt = $q->prepare();
81
        $stmt->execute();
82
        $results = $stmt->fetchAll();
83
84
        $migrations = array();
85
        foreach ($results as $result) {
86
            $migrations[$result['migration']] = $this->arrayToMigration($result);
87
        }
88
89
        return new MigrationCollection($migrations);
90
    }
91
92
    /**
93
     * @param string $migrationName
94
     * @return APIMigration|null
95
     */
96 View Code Duplication
    public function loadMigration($migrationName)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
97
    {
98
        $this->createTableIfNeeded();
99
100
        /** @var \eZ\Publish\Core\Persistence\Database\SelectQuery $q */
101
        $q = $this->dbHandler->createSelectQuery();
102
        $q->select($this->fieldList)
0 ignored issues
show
Unused Code introduced by
The call to SelectQuery::select() has too many arguments starting with $this->fieldList.

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 @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
103
            ->from($this->tableName)
0 ignored issues
show
Unused Code introduced by
The call to SelectQuery::from() has too many arguments starting with $this->tableName.

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 @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
104
            ->where($q->expr->eq('migration', $q->bindValue($migrationName)));
0 ignored issues
show
Bug introduced by
Accessing expr on the interface eZ\Publish\Core\Persistence\Database\SelectQuery suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
Unused Code introduced by
The call to SelectQuery::where() has too many arguments starting with $q->expr->eq('migration'...dValue($migrationName)).

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 @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
105
        $stmt = $q->prepare();
106
        $stmt->execute();
107
        $result = $stmt->fetch(\PDO::FETCH_ASSOC);
108
109
        if (is_array($result) && !empty($result)) {
110
            return $this->arrayToMigration($result);
111
        }
112
113
        return null;
114
    }
115
116
    /**
117
     * Creates and stores a new migration (leaving it in TODO status)
118
     * @param MigrationDefinition $migrationDefinition
119
     * @return APIMigration
120
     * @throws \Exception If the migration exists already (we rely on the PK for that)
121
     */
122
    public function addMigration(MigrationDefinition $migrationDefinition)
123
    {
124
        $this->createTableIfNeeded();
125
126
        $conn = $this->getConnection();
127
128
        $migration = new APIMigration(
129
            $migrationDefinition->name,
130
            md5($migrationDefinition->rawDefinition),
131
            $migrationDefinition->path,
132
            null,
133
            APIMigration::STATUS_TODO
134
        );
135
        try {
136
            $conn->insert($this->tableName, $this->migrationToArray($migration));
137
        } catch (UniqueConstraintViolationException $e) {
138
            throw new \Exception("Migration '{$migrationDefinition->name}' already exists");
139
        }
140
141
        return $migration;
142
    }
143
144
    /**
145
     * Starts a migration, given its definition: stores its status in the db, returns the Migration object
146
     *
147
     * @param MigrationDefinition $migrationDefinition
148
     * @return APIMigration
149
     * @throws \Exception if migration was already executing or already done
150
     * @todo add a parameter to allow re-execution of already-done migrations
151
     */
152
    public function startMigration(MigrationDefinition $migrationDefinition)
153
    {
154
        return $this->createMigration($migrationDefinition, APIMigration::STATUS_STARTED, 'started');
155
    }
156
157
    /**
158
     * Stops a migration by storing it in the db. Migration status can not be 'started'
159
     *
160
     * NB: if this call happens within another DB transaction which has already been flagged for rollback, the result
161
     * will be that a RuntimeException is thrown, as Doctrine does not allow to call commit() after rollback().
162
     * One way to fix the problem would be not to use a transaction and select-for-update here, but since that is the
163
     * best way to insure atomic updates, I am loath to remove it.
164
     * A known workaround is to call the Doctrine Connection method setNestTransactionsWithSavepoints(true); this can
165
     * be achieved as simply as setting the parameter 'use_savepoints' in the doctrine connection configuration.
166
     *
167
     * @param APIMigration $migration
168
     * @param bool $force When true, the migration will be updated even if it was not in 'started' status
169
     * @throws \Exception If the migration was not started (unless $force=true)
170
     */
171
    public function endMigration(APIMigration $migration, $force = false)
172
    {
173
        if ($migration->status == APIMigration::STATUS_STARTED) {
174
            throw new \Exception("Migration '{$migration->name}' can not be ended as its status is 'started'...");
175
        }
176
177
        $this->createTableIfNeeded();
178
179
        // select for update
180
181
        // annoyingly enough, neither Doctrine nor EZP provide built in support for 'FOR UPDATE' in their query builders...
182
        // at least the doctrine one allows us to still use parameter binding when we add our sql particle
183
        $conn = $this->getConnection();
184
185
        $qb = $conn->createQueryBuilder();
186
        $qb->select('*')
187
            ->from($this->tableName, 'm')
188
            ->where('migration = ?');
189
        $sql = $qb->getSQL() . ' FOR UPDATE';
190
191
        $conn->beginTransaction();
192
193
        $stmt = $conn->executeQuery($sql, array($migration->name));
194
        $existingMigrationData = $stmt->fetch(\PDO::FETCH_ASSOC);
195
196
        // fail if it was not executing
197
198
        if (!is_array($existingMigrationData)) {
199
            // commit to release the lock
200
            $conn->commit();
201
            throw new \Exception("Migration '{$migration->name}' can not be ended as it is not found");
202
        }
203
204
        if (($existingMigrationData['status'] != APIMigration::STATUS_STARTED) && !$force) {
205
            // commit to release the lock
206
            $conn->commit();
207
            throw new \Exception("Migration '{$migration->name}' can not be ended as it is not executing");
208
        }
209
210
        $conn->update(
211
            $this->tableName,
212
            array(
213
                'status' => $migration->status,
214
                /// @todo use mb_substr (if all dbs we support count col length not in bytes but in chars...)
215
                'execution_error' => substr($migration->executionError, 0, 4000),
216
                'execution_date' => $migration->executionDate
217
            ),
218
            array('migration' => $migration->name)
219
        );
220
221
        $conn->commit();
222
    }
223
224
    /**
225
     * Removes a Migration from the table - regardless of its state!
226
     *
227
     * @param APIMigration $migration
228
     */
229
    public function deleteMigration(APIMigration $migration)
230
    {
231
        $this->createTableIfNeeded();
232
        $conn = $this->getConnection();
233
        $conn->delete($this->tableName, array('migration' => $migration->name));
234
    }
235
236
    /**
237
     * Skips a migration by storing it in the db. Migration status can not be 'started'
238
     *
239
     * @param MigrationDefinition $migrationDefinition
240
     * @return APIMigration
241
     * @throws \Exception If the migration was already executed or executing
242
     */
243
    public function skipMigration(MigrationDefinition $migrationDefinition)
244
    {
245
        return $this->createMigration($migrationDefinition, APIMigration::STATUS_SKIPPED, 'skipped');
246
    }
247
248
    /**
249
     * @param MigrationDefinition $migrationDefinition
250
     * @param int $status
251
     * @param string $action
252
     * @return APIMigration
253
     * @throws \Exception
254
     */
255
    protected function createMigration(MigrationDefinition $migrationDefinition, $status, $action)
256
    {
257
        $this->createTableIfNeeded();
258
259
        // select for update
260
261
        // annoyingly enough, neither Doctrine nor EZP provide built in support for 'FOR UPDATE' in their query builders...
262
        // at least the doctrine one allows us to still use parameter binding when we add our sql particle
263
        $conn = $this->getConnection();
264
265
        $qb = $conn->createQueryBuilder();
266
        $qb->select('*')
267
            ->from($this->tableName, 'm')
268
            ->where('migration = ?');
269
        $sql = $qb->getSQL() . ' FOR UPDATE';
270
271
        $conn->beginTransaction();
272
273
        $stmt = $conn->executeQuery($sql, array($migrationDefinition->name));
274
        $existingMigrationData = $stmt->fetch(\PDO::FETCH_ASSOC);
275
276
        if (is_array($existingMigrationData)) {
277
            // migration exists
278
279
            // fail if it was already executing or already done
280 View Code Duplication
            if ($existingMigrationData['status'] == APIMigration::STATUS_STARTED) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
281
                // commit to release the lock
282
                $conn->commit();
283
                throw new \Exception("Migration '{$migrationDefinition->name}' can not be $action as it is already executing");
284
            }
285 View Code Duplication
            if ($existingMigrationData['status'] == APIMigration::STATUS_DONE) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
286
                // commit to release the lock
287
                $conn->commit();
288
                throw new \Exception("Migration '{$migrationDefinition->name}' can not be $action as it was already executed");
289
            }
290 View Code Duplication
            if ($existingMigrationData['status'] == APIMigration::STATUS_SKIPPED) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
291
                // commit to release the lock
292
                $conn->commit();
293
                throw new \Exception("Migration '{$migrationDefinition->name}' can not be $action as it was already skipped");
294
            }
295
296
            // do not set migration start date if we are skipping it
297
            $migration = new APIMigration(
298
                $migrationDefinition->name,
299
                md5($migrationDefinition->rawDefinition),
300
                $migrationDefinition->path,
301
                ($status == APIMigration::STATUS_SKIPPED ? null : time()),
302
                $status
303
            );
304
            $conn->update(
305
                $this->tableName,
306
                array(
307
                    'execution_date' => $migration->executionDate,
308
                    'status' => $status,
309
                    'execution_error' => null
310
                ),
311
                array('migration' => $migrationDefinition->name)
312
            );
313
            $conn->commit();
314
315
        } else {
316
            // migration did not exist. Create it!
317
318
            // commit immediately, to release the lock and avoid deadlocks
319
            $conn->commit();
320
321
            $migration = new APIMigration(
322
                $migrationDefinition->name,
323
                md5($migrationDefinition->rawDefinition),
324
                $migrationDefinition->path,
325
                ($status == APIMigration::STATUS_SKIPPED ? null : time()),
326
                $status
327
            );
328
            $conn->insert($this->tableName, $this->migrationToArray($migration));
329
        }
330
331
        return $migration;
332
    }
333
334
    public function resumeMigration(APIMigration $migration)
335
    {
336
        $this->createTableIfNeeded();
337
338
        // select for update
339
340
        // annoyingly enough, neither Doctrine nor EZP provide built in support for 'FOR UPDATE' in their query builders...
341
        // at least the doctrine one allows us to still use parameter binding when we add our sql particle
342
        $conn = $this->getConnection();
343
344
        $qb = $conn->createQueryBuilder();
345
        $qb->select('*')
346
            ->from($this->tableName, 'm')
347
            ->where('migration = ?');
348
        $sql = $qb->getSQL() . ' FOR UPDATE';
349
350
        $conn->beginTransaction();
351
352
        $stmt = $conn->executeQuery($sql, array($migration->name));
353
        $existingMigrationData = $stmt->fetch(\PDO::FETCH_ASSOC);
354
355
        if (!is_array($existingMigrationData)) {
356
            // commit immediately, to release the lock and avoid deadlocks
357
            $conn->commit();
358
            throw new \Exception("Migration '{$migration->name}' can not be resumed as it is not found");
359
        }
360
361
        // migration exists
362
363
        // fail if it was not suspended
364
        if ($existingMigrationData['status'] != APIMigration::STATUS_SUSPENDED) {
365
            // commit to release the lock
366
            $conn->commit();
367
            throw new \Exception("Migration '{$migration->name}' can not be resumed as it is not suspended");
368
        }
369
370
        $migration = new APIMigration(
371
            $migration->name,
372
            $migration->md5,
373
            $migration->path,
374
            time(),
375
            APIMigration::STATUS_STARTED
376
        );
377
378
        $conn->update(
379
            $this->tableName,
380
            array(
381
                'execution_date' => $migration->executionDate,
382
                'status' => APIMigration::STATUS_STARTED,
383
                'execution_error' => null
384
            ),
385
            array('migration' => $migration->name)
386
        );
387
        $conn->commit();
388
389
        return $migration;
390
    }
391
392
    /**
393
     * Removes all migration from storage (regardless of their status)
394
     */
395
    public function deleteMigrations()
396
    {
397
        $this->drop();
398
    }
399
400
    public function createTable()
401
    {
402
        /** @var \Doctrine\DBAL\Schema\AbstractSchemaManager $sm */
403
        $sm = $this->getConnection()->getSchemaManager();
404
        $dbPlatform = $sm->getDatabasePlatform();
405
406
        $schema = new Schema();
407
408
        $t = $schema->createTable($this->tableName);
409
        $t->addColumn('migration', 'string', array('length' => 255));
410
        $t->addColumn('path', 'string', array('length' => 4000));
411
        $t->addColumn('md5', 'string', array('length' => 32));
412
        $t->addColumn('execution_date', 'integer', array('notnull' => false));
413
        $t->addColumn('status', 'integer', array('default ' => APIMigration::STATUS_TODO));
414
        $t->addColumn('execution_error', 'string', array('length' => 4000, 'notnull' => false));
415
        $t->setPrimaryKey(array('migration'));
416
        // in case users want to look up migrations by their full path
417
        // NB: disabled for the moment, as it causes problems on some versions of mysql which limit index length to 767 bytes,
418
        // and 767 bytes can be either 255 chars or 191 chars depending on charset utf8 or utf8mb4...
419
        //$t->addIndex(array('path'));
0 ignored issues
show
Unused Code Comprehensibility introduced by
90% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
420
421
        foreach ($schema->toSql($dbPlatform) as $sql) {
422
            $this->dbHandler->exec($sql);
423
        }
424
    }
425
426
    protected function migrationToArray(APIMigration $migration)
427
    {
428
        return array(
429
            'migration' => $migration->name,
430
            'md5' => $migration->md5,
431
            'path' => $migration->path,
432
            'execution_date' => $migration->executionDate,
433
            'status' => $migration->status,
434
            'execution_error' => $migration->executionError
435
        );
436
    }
437
438
    protected function arrayToMigration(array $data)
439
    {
440
        return new APIMigration(
441
            $data['migration'],
442
            $data['md5'],
443
            $data['path'],
444
            $data['execution_date'],
445
            $data['status'],
446
            $data['execution_error']
447
        );
448
    }
449
}
450