Completed
Push — master ( e5ee14...111d0a )
by Gaetano
07:17
created

Database::skipMigration()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 4
c 0
b 0
f 0
ccs 0
cts 0
cp 0
rs 10
cc 1
eloc 2
nc 1
nop 1
crap 2
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
     * @param Migration $migration
143
     * @param bool $force When true, the migration will be updated even if it was not in 'started' status
144 12
     * @throws \Exception If the migration was not started (unless $force=true)
145 12
     */
146 12
    public function endMigration(Migration $migration, $force=false)
147 12
    {
148 12
        if ($migration->status == Migration::STATUS_STARTED) {
149
            throw new \Exception("Migration '{$migration->name}' can not be ended as its status is 'started'...");
150 12
        }
151
152 12
        $this->createMigrationsTableIfNeeded();
153 12
154
        // select for update
155 12
156
        // annoyingly enough, neither Doctrine nor EZP provide built in support for 'FOR UPDATE' in their query builders...
157
        // at least the doctrine one allows us to still use parameter binding when we add our sql particle
158
        $conn = $this->dbHandler->getConnection();
159 12
160
        $qb = $conn->createQueryBuilder();
161
        $qb->select('*')
162
            ->from($this->migrationsTableName, 'm')
163
            ->where('migration = ?');
164 12
        $sql = $qb->getSQL() . ' FOR UPDATE';
165
166
        $conn->beginTransaction();
167
168
        $stmt = $conn->executeQuery($sql, array($migration->name));
169
        $existingMigrationData = $stmt->fetch(\PDO::FETCH_ASSOC);
170 12
171 12
        // fail if it was not executing
172 12
173 12
        if (!is_array($existingMigrationData)) {
174 12
            // commit to release the lock
175
            $conn->commit();
176 12
            throw new \Exception("Migration '{$migration->name}' can not be ended as it is not found");
177 12
        }
178 12
179
        if (($existingMigrationData['status'] != Migration::STATUS_STARTED) && !$force) {
180 12
            // commit to release the lock
181 12
            $conn->commit();
182 12
            throw new \Exception("Migration '{$migration->name}' can not be ended as it is not executing");
183 12
        }
184 12
185 12
        $conn->update(
186 12
            $this->migrationsTableName,
187
            array(
188
                'status' => $migration->status,
189
                'execution_error' => $migration->executionError,
190
                'execution_date' => $migration->executionDate
191
            ),
192
            array('migration' => $migration->name)
193
        );
194
195
        $conn->commit();
196
    }
197
198
    /**
199 12
     * Removes a Migration from the table - regardless of its state!
200 12
     * @param Migration $migration
201
     */
202
    public function deleteMigration(Migration $migration)
203
    {
204
        $this->createMigrationsTableIfNeeded();
205
        $conn = $this->dbHandler->getConnection();
206
        $conn->delete($this->migrationsTableName, array('migration' => $migration->name));
207
    }
208
209 12
   /**
210
     * Stops a migration by storing it in the db. Migration status can not be 'started'
211 12
     *
212
     * @param MigrationDefinition $migrationDefinition
213
     * @return Migration
214
     * @throws \Exception If the migration was already executed or executing
215 12
     */
216
    public function skipMigration(MigrationDefinition $migrationDefinition)
217
    {
218
        return $this->createMigration($migrationDefinition, Migration::STATUS_SKIPPED, 'skipped');
219
    }
220
221 12
    protected function createMigration(MigrationDefinition $migrationDefinition, $status, $action)
222
    {
223 12
        $this->createMigrationsTableIfNeeded();
224 12
225 12
        // select for update
226 12
227 12
        // annoyingly enough, neither Doctrine nor EZP provide built in support for 'FOR UPDATE' in their query builders...
228
        // at least the doctrine one allows us to still use parameter binding when we add our sql particle
229 12
        $conn = $this->dbHandler->getConnection();
230
231 12
        $qb = $conn->createQueryBuilder();
232 12
        $qb->select('*')
233
            ->from($this->migrationsTableName, 'm')
234
            ->where('migration = ?');
235
        $sql = $qb->getSQL() . ' FOR UPDATE';
236 12
237
        $conn->beginTransaction();
238
239
        $stmt = $conn->executeQuery($sql, array($migrationDefinition->name));
240
        $existingMigrationData = $stmt->fetch(\PDO::FETCH_ASSOC);
241
242 12
        if (is_array($existingMigrationData)) {
243
            // migration exists
244
245
            // fail if it was already executing or already done
246 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...
247
                // commit to release the lock
248 12
                $conn->commit();
249 12
                throw new \Exception("Migration '{$migrationDefinition->name}' can not be $action as it is already executing");
250
            }
251 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...
252 12
                // commit to release the lock
253 12
                $conn->commit();
254 12
                throw new \Exception("Migration '{$migrationDefinition->name}' can not be $action as it was already executed");
255 12
            }
256 12 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...
257
                // commit to release the lock
258 12
                $conn->commit();
259 12
                throw new \Exception("Migration '{$migrationDefinition->name}' can not be $action as it was already skipped");
260
            }
261
262
            // do not set migration start date if we are skipping it
263
            $migration = new Migration(
264
                $migrationDefinition->name,
265 19
                md5($migrationDefinition->rawDefinition),
266
                $migrationDefinition->path,
267 19
                ($status == Migration::STATUS_SKIPPED ? null: time()),
268 19
                $status
269 19
            );
270 19
            $conn->update(
271
                $this->migrationsTableName,
272
                array(
273
                    'execution_date' => $migration->executionDate,
274
                    'status' => $status,
275
                    'execution_error' => null
276
                ),
277
                array('migration' => $migrationDefinition->name)
278
            );
279
        } else {
280 20
            // migration did not exist. Create it!
281
282 20
            $migration = new Migration(
283 20
                $migrationDefinition->name,
284
                md5($migrationDefinition->rawDefinition),
285
                $migrationDefinition->path,
286 20
                ($status == Migration::STATUS_SKIPPED ? null: time()),
287 19
                $status
288 19
            );
289
            $conn->insert($this->migrationsTableName, $this->migrationToArray($migration));
290
        }
291 1
292
        $conn->commit();
293 1
        return $migration;
294 1
    }
295
296
    /**
297 1
     * Check if the version db table exists and create it if not.
298
     *
299
     * @return bool true if table has been created, false if it was already there
300 1
     *
301 1
     * @todo add a 'force' flag to force table re-creation
302
     * @todo manage changes to table definition
303 1
     */
304
    public function createMigrationsTableIfNeeded()
305 1
    {
306 1
        if ($this->migrationsTableExists) {
307 1
            return false;
308 1
        }
309 1
310 1
        if ($this->tableExist($this->migrationsTableName)) {
311 1
            $this->migrationsTableExists = true;
312 1
            return false;
313
        }
314
315
        $this->createMigrationsTable();
316
317
        $this->migrationsTableExists = true;
318 1
        return true;
319 1
    }
320 1
321 1
    public function createMigrationsTable()
322
    {
323
        /** @var \Doctrine\DBAL\Schema\AbstractSchemaManager $sm */
324
        $sm = $this->dbHandler->getConnection()->getSchemaManager();
325
        $dbPlatform = $sm->getDatabasePlatform();
326
327
        $schema = new Schema();
328
329 20
        $t = $schema->createTable($this->migrationsTableName);
330
        $t->addColumn('migration', 'string', array('length' => 255));
331
        $t->addColumn('path', 'string', array('length' => 4000));
332 20
        $t->addColumn('md5', 'string', array('length' => 32));
333 20
        $t->addColumn('execution_date', 'integer', array('notnull' => false));
334 20
        $t->addColumn('status', 'integer', array('default ' => Migration::STATUS_TODO));
335 19
        $t->addColumn('execution_error', 'string', array('length' => 4000, 'notnull' => false));
336
        $t->setPrimaryKey(array('migration'));
337 20
        // in case users want to look up migrations by their full path
338
        // NB: disabled for the moment, as it causes problems on some versions of mysql which limit index length to 767 bytes,
339 1
        // and 767 bytes can be either 255 chars or 191 chars depending on charset utf8 or utf8mb4...
340
        //$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...
341
342 19
        foreach($schema->toSql($dbPlatform) as $sql) {
343
            $this->dbHandler->exec($sql);
344
        }
345 19
    }
346 19
347 19
    /**
348 19
     * Check if a table exists in the database
349 19
     *
350 19
     * @param string $tableName
351 19
     * @return bool
352
     */
353 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...
354 19
    {
355
        /** @var \Doctrine\DBAL\Schema\AbstractSchemaManager $sm */
356 19
        $sm = $this->dbHandler->getConnection()->getSchemaManager();
357 19
        foreach($sm->listTables() as $table) {
358 19
            if ($table->getName() == $tableName) {
359 19
                return true;
360 19
            }
361 19
        }
362 19
363 19
        return false;
364
    }
365
366
    protected function migrationToArray(Migration $migration)
367
    {
368
        return array(
369
            'migration' => $migration->name,
370
            'md5' => $migration->md5,
371
            'path' => $migration->path,
372
            'execution_date' => $migration->executionDate,
373
            'status' => $migration->status,
374
            'execution_error' => $migration->executionError
375
        );
376
    }
377
378
    protected function arrayToMigration(array $data)
379
    {
380
        return new Migration(
381
            $data['migration'],
382
            $data['md5'],
383
            $data['path'],
384
            $data['execution_date'],
385
            $data['status'],
386
            $data['execution_error']
387
        );
388
    }
389
}