Completed
Push — master ( 59250f...2c6719 )
by Gaetano
10:09
created

Database::migrationToArray()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 11
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

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