Completed
Branch master (e35419)
by Gaetano
06:40
created

Migration::resumeMigration()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 56
Code Lines 31

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 0
Metric Value
cc 3
eloc 31
nc 3
nop 1
dl 0
loc 56
ccs 0
cts 30
cp 0
crap 12
rs 9.424
c 0
b 0
f 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace Kaliop\eZMigrationBundle\Core\StorageHandler\Database;
4
5
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
6
use Doctrine\DBAL\Schema\Schema;
7
use eZ\Publish\Core\Persistence\Database\DatabaseHandler;
8
use eZ\Publish\Core\Persistence\Database\SelectQuery;
9
use Kaliop\eZMigrationBundle\API\StorageHandlerInterface;
10
use Kaliop\eZMigrationBundle\API\Collection\MigrationCollection;
11
use Kaliop\eZMigrationBundle\API\Value\Migration as APIMigration;
12
use Kaliop\eZMigrationBundle\API\Value\MigrationDefinition;
13
14
use Kaliop\eZMigrationBundle\API\ConfigResolverInterface;
15
16
/**
17
 * Database-backed storage for info on executed migrations
18
 *
19
 * @todo replace all usage of the ezcdb api with the doctrine dbal one, so that we only depend on one
20
 */
21
class Migration extends TableStorage implements StorageHandlerInterface
22
{
23
    protected $fieldList = 'migration, md5, path, execution_date, status, execution_error';
24
25
    /**
26
     * @param DatabaseHandler $dbHandler
27
     * @param string $tableNameParameter
28
     * @param ConfigResolverInterface $configResolver
29
     * @throws \Exception
30
     */
31 80
    public function __construct(DatabaseHandler $dbHandler, $tableNameParameter = 'kaliop_migrations', ConfigResolverInterface $configResolver = null)
32
    {
33 80
        parent::__construct($dbHandler, $tableNameParameter, $configResolver);
34 80
    }
35
36
    /**
37
     * @param int $limit
38
     * @param int $offset
39
     * @return MigrationCollection
40
     */
41 79
    public function loadMigrations($limit = null, $offset = null)
42
    {
43 79
        return $this->loadMigrationsInner(null, $limit, $offset);
44
    }
45
46
    /**
47
     * @param int $status
48
     * @param int $limit
49
     * @param int $offset
50
     * @return MigrationCollection
51
     */
52 1
    public function loadMigrationsByStatus($status, $limit = null, $offset = null)
53
    {
54 1
        return $this->loadMigrationsInner($status, $limit, $offset);
55
    }
56
57
    /**
58
     * @param int $status
59
     * @param int $limit
60
     * @param int $offset
61
     * @return MigrationCollection
62
     */
63 79
    protected function loadMigrationsInner($status = null, $limit = null, $offset = null)
64
    {
65 79
        $this->createTableIfNeeded();
66
67
        /** @var \eZ\Publish\Core\Persistence\Database\SelectQuery $q */
68 79
        $q = $this->dbHandler->createSelectQuery();
69 79
        $q->select($this->fieldList)
0 ignored issues
show
Unused Code introduced by
The call to eZ\Publish\Core\Persiste...e\SelectQuery::select() has too many arguments starting with $this->fieldList. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

69
        $q->/** @scrutinizer ignore-call */ 
70
            select($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. Please note the @ignore annotation hint above.

Loading history...
70 79
            ->from($this->tableName)
0 ignored issues
show
Unused Code introduced by
The call to eZ\Publish\Core\Persiste...ase\SelectQuery::from() has too many arguments starting with $this->tableName. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

70
            ->/** @scrutinizer ignore-call */ from($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. Please note the @ignore annotation hint above.

Loading history...
71 79
            ->orderBy('migration', SelectQuery::ASC);
72 79
        if ($status !== null) {
73 1
            $q->where($q->expr->eq('status', $q->bindValue($status)));
0 ignored issues
show
Unused Code introduced by
The call to eZ\Publish\Core\Persiste...se\SelectQuery::where() has too many arguments starting with $q->expr->eq('status', $q->bindValue($status)). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

73
            $q->/** @scrutinizer ignore-call */ 
74
                where($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. Please note the @ignore annotation hint above.

Loading history...
74
        }
75 79
        if ($limit > 0 || $offset > 0) {
76 1
            if ($limit <= 0) {
77
                $limit = null;
78
            }
79 1
            if ($offset == 0) {
0 ignored issues
show
Bug Best Practice 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...
80 1
                $offset = null;
81
            }
82 1
            $q->limit($limit, $offset);
83
        }
84 79
        $stmt = $q->prepare();
85 79
        $stmt->execute();
86 79
        $results = $stmt->fetchAll();
87
88 79
        $migrations = array();
89 79
        foreach ($results as $result) {
90 48
            $migrations[$result['migration']] = $this->arrayToMigration($result);
91
        }
92
93 79
        return new MigrationCollection($migrations);
94
    }
95
96
    /**
97
     * @param string $migrationName
98
     * @return APIMigration|null
99
     */
100 49
    public function loadMigration($migrationName)
101
    {
102 49
        $this->createTableIfNeeded();
103
104
        /** @var \eZ\Publish\Core\Persistence\Database\SelectQuery $q */
105 49
        $q = $this->dbHandler->createSelectQuery();
106 49
        $q->select($this->fieldList)
0 ignored issues
show
Unused Code introduced by
The call to eZ\Publish\Core\Persiste...e\SelectQuery::select() has too many arguments starting with $this->fieldList. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

106
        $q->/** @scrutinizer ignore-call */ 
107
            select($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. Please note the @ignore annotation hint above.

Loading history...
107 49
            ->from($this->tableName)
0 ignored issues
show
Unused Code introduced by
The call to eZ\Publish\Core\Persiste...ase\SelectQuery::from() has too many arguments starting with $this->tableName. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

107
            ->/** @scrutinizer ignore-call */ from($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. Please note the @ignore annotation hint above.

Loading history...
108 49
            ->where($q->expr->eq('migration', $q->bindValue($migrationName)));
0 ignored issues
show
Unused Code introduced by
The call to eZ\Publish\Core\Persiste...se\SelectQuery::where() has too many arguments starting with $q->expr->eq('migration'...dValue($migrationName)). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

108
            ->/** @scrutinizer ignore-call */ where($q->expr->eq('migration', $q->bindValue($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. Please note the @ignore annotation hint above.

Loading history...
109 49
        $stmt = $q->prepare();
110 49
        $stmt->execute();
111 49
        $result = $stmt->fetch(\PDO::FETCH_ASSOC);
112
113 49
        if (is_array($result) && !empty($result)) {
114 48
            return $this->arrayToMigration($result);
115
        }
116
117 47
        return null;
118
    }
119
120
    /**
121
     * Creates and stores a new migration (leaving it in TODO status)
122
     * @param MigrationDefinition $migrationDefinition
123
     * @return APIMigration
124
     * @throws \Exception If the migration exists already (we rely on the PK for that)
125
     */
126 48
    public function addMigration(MigrationDefinition $migrationDefinition)
127
    {
128 48
        $this->createTableIfNeeded();
129
130 48
        $conn = $this->getConnection();
131
132 48
        $migration = new APIMigration(
133 48
            $migrationDefinition->name,
134 48
            md5($migrationDefinition->rawDefinition),
135 48
            $migrationDefinition->path,
136 48
            null,
137 48
            APIMigration::STATUS_TODO
138
        );
139
        try {
140 48
            $conn->insert($this->tableName, $this->migrationToArray($migration));
141
        } catch (UniqueConstraintViolationException $e) {
142
            throw new \Exception("Migration '{$migrationDefinition->name}' already exists");
143
        }
144
145 48
        return $migration;
146
    }
147
148
    /**
149
     * Starts a migration, given its definition: stores its status in the db, returns the Migration object
150
     *
151
     * @param MigrationDefinition $migrationDefinition
152
     * @param bool $force when true, starts migrations even if they already exist in DONE, SKIPPED status
153
     * @return APIMigration
154
     * @throws \Exception if migration was already executing or already done
155
     * @todo add a parameter to allow re-execution of already-done migrations
156
     */
157 40
    public function startMigration(MigrationDefinition $migrationDefinition, $force = false)
158
    {
159 40
        return $this->createMigration($migrationDefinition, APIMigration::STATUS_STARTED, 'started', $force);
160
    }
161
162
    /**
163
     * Stops a migration by storing it in the db. Migration status can not be 'started'
164
     *
165
     * NB: if this call happens within another DB transaction which has already been flagged for rollback, the result
166
     * will be that a RuntimeException is thrown, as Doctrine does not allow to call commit() after rollback().
167
     * One way to fix the problem would be not to use a transaction and select-for-update here, but since that is the
168
     * best way to insure atomic updates, I am loath to remove it.
169
     * A known workaround is to call the Doctrine Connection method setNestTransactionsWithSavepoints(true); this can
170
     * be achieved as simply as setting the parameter 'use_savepoints' in the doctrine connection configuration.
171
     *
172
     * @param APIMigration $migration
173
     * @param bool $force When true, the migration will be updated even if it was not in 'started' status
174
     * @throws \Exception If the migration was not started (unless $force=true)
175
     */
176 40
    public function endMigration(APIMigration $migration, $force = false)
177
    {
178 40
        if ($migration->status == APIMigration::STATUS_STARTED) {
179
            throw new \Exception($this->getEntityName($migration)." '{$migration->name}' can not be ended as its status is 'started'...");
180
        }
181
182 40
        $this->createTableIfNeeded();
183
184
        // select for update
185
186
        // annoyingly enough, neither Doctrine nor EZP provide built in support for 'FOR UPDATE' in their query builders...
187
        // at least the doctrine one allows us to still use parameter binding when we add our sql particle
188 40
        $conn = $this->getConnection();
189
190 40
        $qb = $conn->createQueryBuilder();
191 40
        $qb->select('*')
192 40
            ->from($this->tableName, 'm')
193 40
            ->where('migration = ?');
194 40
        $sql = $qb->getSQL() . ' FOR UPDATE';
195
196 40
        $conn->beginTransaction();
197
198 40
        $stmt = $conn->executeQuery($sql, array($migration->name));
199 40
        $existingMigrationData = $stmt->fetch(\PDO::FETCH_ASSOC);
200
201
        // fail if it was not executing
202
203 40
        if (!is_array($existingMigrationData)) {
204
            // commit to release the lock
205
            $conn->commit();
206
            throw new \Exception($this->getEntityName($migration)." '{$migration->name}' can not be ended as it is not found");
207
        }
208
209 40
        if (($existingMigrationData['status'] != APIMigration::STATUS_STARTED) && !$force) {
210
            // commit to release the lock
211
            $conn->commit();
212
            throw new \Exception($this->getEntityName($migration)." '{$migration->name}' can not be ended as it is not executing");
213
        }
214
215 40
        $conn->update(
216 40
            $this->tableName,
217
            array(
218 40
                'status' => $migration->status,
219
                /// @todo use mb_substr (if all dbs we support count col length not in bytes but in chars...)
220 40
                'execution_error' => substr($migration->executionError, 0, 4000),
221 40
                'execution_date' => $migration->executionDate
222
            ),
223 40
            array('migration' => $migration->name)
224
        );
225
226 40
        $conn->commit();
227 40
    }
228
229
    /**
230
     * Removes a Migration from the table - regardless of its state!
231
     *
232
     * @param APIMigration $migration
233
     */
234 46
    public function deleteMigration(APIMigration $migration)
235
    {
236 46
        $this->createTableIfNeeded();
237 46
        $conn = $this->getConnection();
238 46
        $conn->delete($this->tableName, array('migration' => $migration->name));
239 46
    }
240
241
    /**
242
     * Skips a migration by storing it in the db. Migration status can not be 'started'
243
     *
244
     * @param MigrationDefinition $migrationDefinition
245
     * @return APIMigration
246
     * @throws \Exception If the migration was already executed or executing
247
     */
248
    public function skipMigration(MigrationDefinition $migrationDefinition)
249
    {
250
        return $this->createMigration($migrationDefinition, APIMigration::STATUS_SKIPPED, 'skipped');
251
    }
252
253
    /**
254
     * @param MigrationDefinition $migrationDefinition
255
     * @param int $status
256
     * @param string $action
257
     * @param bool $force
258
     * @return APIMigration
259
     * @throws \Exception
260
     */
261 40
    protected function createMigration(MigrationDefinition $migrationDefinition, $status, $action, $force = false)
262
    {
263 40
        $this->createTableIfNeeded();
264
265
        // select for update
266
267
        // annoyingly enough, neither Doctrine nor EZP provide built in support for 'FOR UPDATE' in their query builders...
268
        // at least the doctrine one allows us to still use parameter binding when we add our sql particle
269 40
        $conn = $this->getConnection();
270
271 40
        $qb = $conn->createQueryBuilder();
272 40
        $qb->select('*')
273 40
            ->from($this->tableName, 'm')
274 40
            ->where('migration = ?');
275 40
        $sql = $qb->getSQL() . ' FOR UPDATE';
276
277 40
        $conn->beginTransaction();
278
279 40
        $stmt = $conn->executeQuery($sql, array($migrationDefinition->name));
280 40
        $existingMigrationData = $stmt->fetch(\PDO::FETCH_ASSOC);
281
282 40
        if (is_array($existingMigrationData)) {
283
            // migration exists
284
285
            // fail if it was already executing
286 38
            if ($existingMigrationData['status'] == APIMigration::STATUS_STARTED) {
287
                // commit to release the lock
288
                $conn->commit();
289
                throw new \Exception("Migration '{$migrationDefinition->name}' can not be $action as it is already executing");
290
            }
291
            // fail if it was already already done, unless in 'force' mode
292 38
            if (!$force) {
293 38
                if ($existingMigrationData['status'] == APIMigration::STATUS_DONE) {
294
                    // commit to release the lock
295
                    $conn->commit();
296
                    throw new \Exception("Migration '{$migrationDefinition->name}' can not be $action as it was already executed");
297
                }
298 38
                if ($existingMigrationData['status'] == APIMigration::STATUS_SKIPPED) {
299
                    // commit to release the lock
300
                    $conn->commit();
301
                    throw new \Exception("Migration '{$migrationDefinition->name}' can not be $action as it was already skipped");
302
                }
303
            }
304
305
            // do not set migration start date if we are skipping it
306 38
            $migration = new APIMigration(
307 38
                $migrationDefinition->name,
308 38
                md5($migrationDefinition->rawDefinition),
309 38
                $migrationDefinition->path,
310 38
                ($status == APIMigration::STATUS_SKIPPED ? null : time()),
311 38
                $status
312
            );
313 38
            $conn->update(
314 38
                $this->tableName,
315
                array(
316 38
                    'execution_date' => $migration->executionDate,
317 38
                    'status' => $status,
318
                    'execution_error' => null
319
                ),
320 38
                array('migration' => $migrationDefinition->name)
321
            );
322 38
            $conn->commit();
323
324
        } else {
325
            // migration did not exist. Create it!
326
327
            // commit immediately, to release the lock and avoid deadlocks
328 2
            $conn->commit();
329
330 2
            $migration = new APIMigration(
331 2
                $migrationDefinition->name,
332 2
                md5($migrationDefinition->rawDefinition),
333 2
                $migrationDefinition->path,
334 2
                ($status == APIMigration::STATUS_SKIPPED ? null : time()),
335 2
                $status
336
            );
337 2
            $conn->insert($this->tableName, $this->migrationToArray($migration));
338
        }
339
340 40
        return $migration;
341
    }
342
343
    public function resumeMigration(APIMigration $migration)
344
    {
345
        $this->createTableIfNeeded();
346
347
        // select for update
348
349
        // annoyingly enough, neither Doctrine nor EZP provide built in support for 'FOR UPDATE' in their query builders...
350
        // at least the doctrine one allows us to still use parameter binding when we add our sql particle
351
        $conn = $this->getConnection();
352
353
        $qb = $conn->createQueryBuilder();
354
        $qb->select('*')
355
            ->from($this->tableName, 'm')
356
            ->where('migration = ?');
357
        $sql = $qb->getSQL() . ' FOR UPDATE';
358
359
        $conn->beginTransaction();
360
361
        $stmt = $conn->executeQuery($sql, array($migration->name));
362
        $existingMigrationData = $stmt->fetch(\PDO::FETCH_ASSOC);
363
364
        if (!is_array($existingMigrationData)) {
365
            // commit immediately, to release the lock and avoid deadlocks
366
            $conn->commit();
367
            throw new \Exception($this->getEntityName($migration)." '{$migration->name}' can not be resumed as it is not found");
368
        }
369
370
        // migration exists
371
372
        // fail if it was not suspended
373
        if ($existingMigrationData['status'] != APIMigration::STATUS_SUSPENDED) {
374
            // commit to release the lock
375
            $conn->commit();
376
            throw new \Exception($this->getEntityName($migration)." '{$migration->name}' can not be resumed as it is not suspended");
377
        }
378
379
        $migration = new APIMigration(
380
            $migration->name,
381
            $migration->md5,
382
            $migration->path,
383
            time(),
384
            APIMigration::STATUS_STARTED
385
        );
386
387
        $conn->update(
388
            $this->tableName,
389
            array(
390
                'execution_date' => $migration->executionDate,
391
                'status' => APIMigration::STATUS_STARTED,
392
                'execution_error' => null
393
            ),
394
            array('migration' => $migration->name)
395
        );
396
        $conn->commit();
397
398
        return $migration;
399
    }
400
401
    /**
402
     * Removes all migration from storage (regardless of their status)
403
     */
404
    public function deleteMigrations()
405
    {
406
        $this->drop();
407
    }
408
409 1
    public function createTable()
410
    {
411
        /** @var \Doctrine\DBAL\Schema\AbstractSchemaManager $sm */
412 1
        $sm = $this->getConnection()->getSchemaManager();
413 1
        $dbPlatform = $sm->getDatabasePlatform();
414
415 1
        $schema = new Schema();
416
417 1
        $t = $schema->createTable($this->tableName);
418 1
        $t->addColumn('migration', 'string', array('length' => 255));
419 1
        $t->addColumn('path', 'string', array('length' => 4000));
420 1
        $t->addColumn('md5', 'string', array('length' => 32));
421 1
        $t->addColumn('execution_date', 'integer', array('notnull' => false));
422 1
        $t->addColumn('status', 'integer', array('default ' => APIMigration::STATUS_TODO));
423 1
        $t->addColumn('execution_error', 'string', array('length' => 4000, 'notnull' => false));
424 1
        $t->setPrimaryKey(array('migration'));
425
        // in case users want to look up migrations by their full path
426
        // NB: disabled for the moment, as it causes problems on some versions of mysql which limit index length to 767 bytes,
427
        // and 767 bytes can be either 255 chars or 191 chars depending on charset utf8 or utf8mb4...
428
        //$t->addIndex(array('path'));
429
430 1
        foreach ($schema->toSql($dbPlatform) as $sql) {
431 1
            $this->dbHandler->exec($sql);
432
        }
433 1
    }
434
435 49
    protected function migrationToArray(APIMigration $migration)
436
    {
437
        return array(
438 49
            'migration' => $migration->name,
439 49
            'md5' => $migration->md5,
440 49
            'path' => $migration->path,
441 49
            'execution_date' => $migration->executionDate,
442 49
            'status' => $migration->status,
443 49
            'execution_error' => $migration->executionError
444
        );
445
    }
446
447 49
    protected function arrayToMigration(array $data)
448
    {
449 49
        return new APIMigration(
450 49
            $data['migration'],
451 49
            $data['md5'],
452 49
            $data['path'],
453 49
            $data['execution_date'],
454 49
            $data['status'],
455 49
            $data['execution_error']
456
        );
457
    }
458
459
    protected function getEntityName($migration)
460
    {
461
        return end(explode('\\', get_class($migration)));
462
    }
463
}
464