Completed
Push — master ( 8c6bbe...f0215b )
by Gaetano
10:40
created

Database::endMigration()   B

Complexity

Conditions 5
Paths 4

Size

Total Lines 51
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 21
CRAP Score 6.015

Importance

Changes 0
Metric Value
dl 0
loc 51
c 0
b 0
f 0
ccs 21
cts 32
cp 0.6563
rs 8.6588
cc 5
eloc 27
nc 4
nop 2
crap 6.015

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;
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 20
    public function __construct(DatabaseHandler $dbHandler, $migrationsTableName = 'kaliop_migrations')
46
    {
47 20
        $this->dbHandler = $dbHandler;
48 20
        $this->migrationsTableName = $migrationsTableName;
49 20
    }
50
51
    /**
52
     * @return MigrationCollection
53
     * @todo add support offset, limit
54
     */
55 20
    public function loadMigrations()
56
    {
57 20
        $this->createMigrationsTableIfNeeded();
58
59
        /** @var \eZ\Publish\Core\Persistence\Database\SelectQuery $q */
60 20
        $q = $this->dbHandler->createSelectQuery();
61 20
        $q->select('migration, md5, path, execution_date, status, execution_error')
0 ignored issues
show
Unused Code introduced by
The call to SelectQuery::select() has too many arguments starting with 'migration, md5, path, e...tatus, execution_error'.

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...
62 20
            ->from($this->migrationsTableName)
0 ignored issues
show
Unused Code introduced by
The call to SelectQuery::from() has too many arguments starting with $this->migrationsTableName.

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...
63 20
            ->orderBy('migration', SelectQuery::ASC);
64 20
        $stmt = $q->prepare();
65 20
        $stmt->execute();
66 20
        $results = $stmt->fetchAll();
67
68 20
        $migrations = array();
69 20
        foreach ($results as $result) {
70 19
            $migrations[$result['migration']] = $this->arrayToMigration($result);
71 20
        }
72
73 20
        return new MigrationCollection($migrations);
74
    }
75
76
    /**
77
     * @param string $migrationName
78
     * @return Migration|null
79
     */
80 19
    public function loadMigration($migrationName)
81
    {
82
        /** @var \eZ\Publish\Core\Persistence\Database\SelectQuery $q */
83 19
        $q = $this->dbHandler->createSelectQuery();
84 19
        $q->select('migration, md5, path, execution_date, status, execution_error')
0 ignored issues
show
Unused Code introduced by
The call to SelectQuery::select() has too many arguments starting with 'migration, md5, path, e...tatus, execution_error'.

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...
85 19
            ->from($this->migrationsTableName)
0 ignored issues
show
Unused Code introduced by
The call to SelectQuery::from() has too many arguments starting with $this->migrationsTableName.

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...
86 19
            ->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...
87 19
        $stmt = $q->prepare();
88 19
        $stmt->execute();
89 19
        $result = $stmt->fetch(\PDO::FETCH_ASSOC);
90
91 19
        if (is_array($result) && !empty($result)) {
92 19
            return $this->arrayToMigration($result);
93 1
        }
94
95 19
        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 19
    public function addMigration(MigrationDefinition $migrationDefinition)
105
    {
106 19
        $this->createMigrationsTableIfNeeded();
107
108 19
        $conn = $this->dbHandler->getConnection();
109
110 19
        $migration = new Migration(
111 19
            $migrationDefinition->name,
112 19
            md5($migrationDefinition->rawDefinition),
113 19
            $migrationDefinition->path,
114 19
            null,
115
            Migration::STATUS_TODO
116 19
        );
117
        try {
118 19
            $conn->insert($this->migrationsTableName, $this->migrationToArray($migration));
119 19
        } catch(UniqueConstraintViolationException $e) {
120
            throw new \Exception("Migration '{$migrationDefinition->name}' already exists");
121
        }
122
123 19
        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 12
    public function startMigration(MigrationDefinition $migrationDefinition)
135
    {
136 12
        return $this->createMigration($migrationDefinition, Migration::STATUS_STARTED, 'started');
137
    }
138
139
    /**
140
     * Stops a migration by storing it in the db. Migration status can not be 'started'
141
     *
142 12
     * NB: if this call happens within another DB transaction which has already been flagged for rollback, the result
143
     * will be that a RuntimeException is thrown, as Doctrine does not allow to call commit() after rollback().
144 12
     * One way to fix the problem would be not to use a transaction and select-for-update here, but since that is the
145 12
     * best way to insure atomic updates, I am loath to remove it.
146 12
     * A known workaround is to call the Doctrine Connection method setNestTransactionsWithSavepoints(true); this can
147 12
     * be achieved as simply as setting the parameter 'use_savepoints' in the doctrine connection configuration.
148 12
     *
149
     * @param Migration $migration
150 12
     * @param bool $force When true, the migration will be updated even if it was not in 'started' status
151
     * @throws \Exception If the migration was not started (unless $force=true)
152 12
     */
153 12
    public function endMigration(Migration $migration, $force=false)
154
    {
155 12
        if ($migration->status == Migration::STATUS_STARTED) {
156
            throw new \Exception("Migration '{$migration->name}' can not be ended as its status is 'started'...");
157
        }
158
159 12
        $this->createMigrationsTableIfNeeded();
160
161
        // select for update
162
163
        // annoyingly enough, neither Doctrine nor EZP provide built in support for 'FOR UPDATE' in their query builders...
164 12
        // at least the doctrine one allows us to still use parameter binding when we add our sql particle
165
        $conn = $this->dbHandler->getConnection();
166
167
        $qb = $conn->createQueryBuilder();
168
        $qb->select('*')
169
            ->from($this->migrationsTableName, 'm')
170 12
            ->where('migration = ?');
171 12
        $sql = $qb->getSQL() . ' FOR UPDATE';
172 12
173 12
        $conn->beginTransaction();
174 12
175
        $stmt = $conn->executeQuery($sql, array($migration->name));
176 12
        $existingMigrationData = $stmt->fetch(\PDO::FETCH_ASSOC);
177 12
178 12
        // fail if it was not executing
179
180 12
        if (!is_array($existingMigrationData)) {
181 12
            // commit to release the lock
182 12
            $conn->commit();
183 12
            throw new \Exception("Migration '{$migration->name}' can not be ended as it is not found");
184 12
        }
185 12
186 12
        if (($existingMigrationData['status'] != Migration::STATUS_STARTED) && !$force) {
187
            // commit to release the lock
188
            $conn->commit();
189
            throw new \Exception("Migration '{$migration->name}' can not be ended as it is not executing");
190
        }
191
192
        $conn->update(
193
            $this->migrationsTableName,
194
            array(
195
                'status' => $migration->status,
196
                'execution_error' => $migration->executionError,
197
                'execution_date' => $migration->executionDate
198
            ),
199 12
            array('migration' => $migration->name)
200 12
        );
201
202
        $conn->commit();
203
    }
204
205
    /**
206
     * Removes a Migration from the table - regardless of its state!
207
     *
208
     * @param Migration $migration
209 12
     */
210
    public function deleteMigration(Migration $migration)
211 12
    {
212
        $this->createMigrationsTableIfNeeded();
213
        $conn = $this->dbHandler->getConnection();
214
        $conn->delete($this->migrationsTableName, array('migration' => $migration->name));
215 12
    }
216
217
    /**
218
     * Skips a migration by storing it in the db. Migration status can not be 'started'
219
     *
220
     * @param MigrationDefinition $migrationDefinition
221 12
     * @return Migration
222
     * @throws \Exception If the migration was already executed or executing
223 12
     */
224 12
    public function skipMigration(MigrationDefinition $migrationDefinition)
225 12
    {
226 12
        return $this->createMigration($migrationDefinition, Migration::STATUS_SKIPPED, 'skipped');
227 12
    }
228
229 12
    protected function createMigration(MigrationDefinition $migrationDefinition, $status, $action)
230
    {
231 12
        $this->createMigrationsTableIfNeeded();
232 12
233
        // select for update
234
235
        // annoyingly enough, neither Doctrine nor EZP provide built in support for 'FOR UPDATE' in their query builders...
236 12
        // at least the doctrine one allows us to still use parameter binding when we add our sql particle
237
        $conn = $this->dbHandler->getConnection();
238
239
        $qb = $conn->createQueryBuilder();
240
        $qb->select('*')
241
            ->from($this->migrationsTableName, 'm')
242 12
            ->where('migration = ?');
243
        $sql = $qb->getSQL() . ' FOR UPDATE';
244
245
        $conn->beginTransaction();
246
247
        $stmt = $conn->executeQuery($sql, array($migrationDefinition->name));
248 12
        $existingMigrationData = $stmt->fetch(\PDO::FETCH_ASSOC);
249 12
250
        if (is_array($existingMigrationData)) {
251 12
            // migration exists
252 12
253 12
            // fail if it was already executing or already done
254 12 View Code Duplication
            if ($existingMigrationData['status'] == Migration::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...
255 12
                // commit to release the lock
256 12
                $conn->commit();
257
                throw new \Exception("Migration '{$migrationDefinition->name}' can not be $action as it is already executing");
258 12
            }
259 12 View Code Duplication
            if ($existingMigrationData['status'] == Migration::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...
260
                // commit to release the lock
261
                $conn->commit();
262
                throw new \Exception("Migration '{$migrationDefinition->name}' can not be $action as it was already executed");
263
            }
264 View Code Duplication
            if ($existingMigrationData['status'] == Migration::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...
265 19
                // commit to release the lock
266
                $conn->commit();
267 19
                throw new \Exception("Migration '{$migrationDefinition->name}' can not be $action as it was already skipped");
268 19
            }
269 19
270 19
            // do not set migration start date if we are skipping it
271
            $migration = new Migration(
272
                $migrationDefinition->name,
273
                md5($migrationDefinition->rawDefinition),
274
                $migrationDefinition->path,
275
                ($status == Migration::STATUS_SKIPPED ? null: time()),
276
                $status
277
            );
278
            $conn->update(
279
                $this->migrationsTableName,
280 20
                array(
281
                    'execution_date' => $migration->executionDate,
282 20
                    'status' => $status,
283 20
                    'execution_error' => null
284
                ),
285
                array('migration' => $migrationDefinition->name)
286 20
            );
287 19
        } else {
288 19
            // migration did not exist. Create it!
289
290
            $migration = new Migration(
291 1
                $migrationDefinition->name,
292
                md5($migrationDefinition->rawDefinition),
293 1
                $migrationDefinition->path,
294 1
                ($status == Migration::STATUS_SKIPPED ? null: time()),
295
                $status
296
            );
297 1
            $conn->insert($this->migrationsTableName, $this->migrationToArray($migration));
298
        }
299
300 1
        $conn->commit();
301 1
        return $migration;
302
    }
303 1
304
    /**
305 1
     * Removes all migration from storage (regardless of their status)
306 1
     */
307 1
    public function deleteMigrations()
308 1
    {
309 1
        if ($this->tableExist($this->migrationsTableName)) {
310 1
            $this->dbHandler->exec('DROP TABLE ' . $this->migrationsTableName);
311 1
        }
312 1
    }
313
314
    /**
315
     * Check if the version db table exists and create it if not.
316
     *
317
     * @return bool true if table has been created, false if it was already there
318 1
     *
319 1
     * @todo add a 'force' flag to force table re-creation
320 1
     * @todo manage changes to table definition
321 1
     */
322
    public function createMigrationsTableIfNeeded()
323
    {
324
        if ($this->migrationsTableExists) {
325
            return false;
326
        }
327
328
        if ($this->tableExist($this->migrationsTableName)) {
329 20
            $this->migrationsTableExists = true;
330
            return false;
331
        }
332 20
333 20
        $this->createMigrationsTable();
334 20
335 19
        $this->migrationsTableExists = true;
336
        return true;
337 20
    }
338
339 1
    public function createMigrationsTable()
340
    {
341
        /** @var \Doctrine\DBAL\Schema\AbstractSchemaManager $sm */
342 19
        $sm = $this->dbHandler->getConnection()->getSchemaManager();
343
        $dbPlatform = $sm->getDatabasePlatform();
344
345 19
        $schema = new Schema();
346 19
347 19
        $t = $schema->createTable($this->migrationsTableName);
348 19
        $t->addColumn('migration', 'string', array('length' => 255));
349 19
        $t->addColumn('path', 'string', array('length' => 4000));
350 19
        $t->addColumn('md5', 'string', array('length' => 32));
351 19
        $t->addColumn('execution_date', 'integer', array('notnull' => false));
352
        $t->addColumn('status', 'integer', array('default ' => Migration::STATUS_TODO));
353
        $t->addColumn('execution_error', 'string', array('length' => 4000, 'notnull' => false));
354 19
        $t->setPrimaryKey(array('migration'));
355
        // in case users want to look up migrations by their full path
356 19
        // NB: disabled for the moment, as it causes problems on some versions of mysql which limit index length to 767 bytes,
357 19
        // and 767 bytes can be either 255 chars or 191 chars depending on charset utf8 or utf8mb4...
358 19
        //$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...
359 19
360 19
        foreach($schema->toSql($dbPlatform) as $sql) {
361 19
            $this->dbHandler->exec($sql);
362 19
        }
363 19
    }
364
365
    /**
366
     * Check if a table exists in the database
367
     *
368
     * @param string $tableName
369
     * @return bool
370
     */
371 View Code Duplication
    protected function tableExist($tableName)
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...
372
    {
373
        /** @var \Doctrine\DBAL\Schema\AbstractSchemaManager $sm */
374
        $sm = $this->dbHandler->getConnection()->getSchemaManager();
375
        foreach($sm->listTables() as $table) {
376
            if ($table->getName() == $tableName) {
377
                return true;
378
            }
379
        }
380
381
        return false;
382
    }
383
384
    protected function migrationToArray(Migration $migration)
385
    {
386
        return array(
387
            'migration' => $migration->name,
388
            'md5' => $migration->md5,
389
            'path' => $migration->path,
390
            'execution_date' => $migration->executionDate,
391
            'status' => $migration->status,
392
            'execution_error' => $migration->executionError
393
        );
394
    }
395
396
    protected function arrayToMigration(array $data)
397
    {
398
        return new Migration(
399
            $data['migration'],
400
            $data['md5'],
401
            $data['path'],
402
            $data['execution_date'],
403
            $data['status'],
404
            $data['execution_error']
405
        );
406
    }
407
}