Completed
Push — master ( 966dae...2c3775 )
by max
02:11
created

Migration::checkCreateMigrationTable()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 17
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 17
rs 9.4285
cc 2
eloc 10
nc 2
nop 0
1
<?php
2
3
namespace T4web\Migrations\Service;
4
5
use T4web\Migrations\Migration\AbstractMigration;
6
use Zend\Db\Adapter\Adapter;
7
use Zend\Db\Adapter\AdapterAwareInterface;
8
use Zend\Db\Adapter\Exception\InvalidQueryException;
9
use Zend\Db\Sql\Ddl;
10
use Zend\ServiceManager\ServiceLocatorAwareInterface;
11
12
/**
13
 * Main migration logic
14
 */
15
class Migration
16
{
17
    protected $migrationsDir;
18
    protected $migrationsNamespace;
19
    protected $adapter;
20
    /**
21
     * @var \Zend\Db\Adapter\Driver\ConnectionInterface
22
     */
23
    protected $connection;
24
    protected $metadata;
25
    protected $migrationVersionTable;
26
    protected $outputWriter;
27
    protected $serviceLocator;
28
29
    /**
30
     * @return OutputWriter
31
     */
32
    public function getOutputWriter()
33
    {
34
        return $this->outputWriter;
35
    }
36
37
    /**
38
     * @param \Zend\Db\Adapter\Adapter $adapter
39
     * @param                          $metadata
40
     * @param array                    $config
41
     * @param                          $migrationVersionTable
42
     * @param OutputWriter             $writer
43
     * @param null                     $serviceLocator
44
     * @throws \Exception
45
     */
46
    public function __construct(
47
        Adapter $adapter,
48
        $metadata,
49
        array $config,
50
        $migrationVersionTable,
51
        OutputWriter $writer = null,
52
        $serviceLocator = null
53
    ) {
54
        $this->adapter = $adapter;
55
        $this->metadata = $metadata;
56
        $this->connection = $this->adapter->getDriver()->getConnection();
57
        $this->migrationsDir = $config['dir'];
58
        $this->migrationsNamespace = $config['namespace'];
59
        $this->migrationVersionTable = $migrationVersionTable;
60
        $this->outputWriter = $writer;
61
        $this->serviceLocator = $serviceLocator;
62
63
        if (is_null($this->migrationsDir)) {
64
            throw new \Exception('Migrations directory not set!');
65
        }
66
67
        if (is_null($this->migrationsNamespace)) {
68
            throw new \Exception('Unknown namespaces!');
69
        }
70
71
        if (!is_dir($this->migrationsDir)) {
72
            if (!mkdir($this->migrationsDir, 0775)) {
73
                throw new \Exception(sprintf('Failed to create migrations directory %s', $this->migrationsDir));
74
            }
75
        }
76
    }
77
78
    /**
79
     * @return int
80
     */
81
    public function getCurrentVersion()
82
    {
83
        return $this->migrationVersionTable->getCurrentVersion();
84
    }
85
86
    /**
87
     * @param int $version target migration version, if not set all not applied available migrations will be applied
88
     * @param bool $force force apply migration
89
     * @param bool $down rollback migration
90
     * @param bool $fake
91
     * @throws \Exception
92
     */
93
    public function migrate($version = null, $force = false, $down = false, $fake = false)
94
    {
95
        $migrations = $this->getMigrationClasses($force);
96
97
        if (!is_null($version) && !$this->hasMigrationVersions($migrations, $version)) {
98
            throw new \Exception(sprintf('Migration version %s is not found!', $version));
99
        }
100
101
        $currentMigrationVersion = $this->migrationVersionTable->getCurrentVersion();
102
        if (!is_null($version) && $version == $currentMigrationVersion && !$force) {
103
            throw new \Exception(sprintf('Migration version %s is current version!', $version));
104
        }
105
106
        if ($version && $force) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $version of type integer|null is loosely compared to true; this is ambiguous if the integer can be zero. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
107
            foreach ($migrations as $migration) {
108
                if ($migration['version'] == $version) {
109
                    // if existing migration is forced to apply - delete its information from migrated
110
                    // to avoid duplicate key error
111
                    if (!$down) {
112
                        $this->migrationVersionTable->delete($migration['version']);
113
                    }
114
                    $this->applyMigration($migration, $down, $fake);
115
                    break;
116
                }
117
            }
118
119
            return;
120
        }
121
122
        foreach ($this->getMigrationClasses() as $migration) {
123
            $this->applyMigration($migration, false, $fake);
124
        }
125
    }
126
127
    /**
128
     * @param \ArrayIterator $migrations
129
     * @return \ArrayIterator
130
     */
131
    public function sortMigrationsByVersionDesc(\ArrayIterator $migrations)
132
    {
133
        $sortedMigrations = clone $migrations;
134
135
        $sortedMigrations->uasort(
136 View Code Duplication
            function ($a, $b) {
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...
137
                if ($a['version'] == $b['version']) {
138
                    return 0;
139
                }
140
141
                return ($a['version'] > $b['version']) ? -1 : 1;
142
            }
143
        );
144
145
        return $sortedMigrations;
146
    }
147
148
    /**
149
     * Check migrations classes existence
150
     *
151
     * @param \ArrayIterator $migrations
152
     * @param int            $version
153
     * @return bool
154
     */
155
    public function hasMigrationVersions(\ArrayIterator $migrations, $version)
156
    {
157
        foreach ($migrations as $migration) {
158
            if ($migration['version'] == $version) {
159
                return true;
160
            }
161
        }
162
163
        return false;
164
    }
165
166
    /**
167
     * @param \ArrayIterator $migrations
168
     * @return int
169
     */
170
    public function getMaxMigrationVersion(\ArrayIterator $migrations)
171
    {
172
        $versions = [];
173
        foreach ($migrations as $migration) {
174
            $versions[] = $migration['version'];
175
        }
176
177
        sort($versions, SORT_NUMERIC);
178
        $versions = array_reverse($versions);
179
180
        return count($versions) > 0 ? $versions[0] : 0;
181
    }
182
183
    /**
184
     * @param bool $all
185
     * @return \ArrayIterator
186
     */
187
    public function getMigrationClasses($all = false)
188
    {
189
        $classes = new \ArrayIterator();
190
191
        $iterator = new \GlobIterator(
192
            sprintf('%s/Version_*.php', $this->migrationsDir),
193
            \FilesystemIterator::KEY_AS_FILENAME
194
        );
195
        foreach ($iterator as $item) {
196
            /** @var $item \SplFileInfo */
197
            if (preg_match('/(Version_(\d+))\.php/', $item->getFilename(), $matches)) {
198
                $applied = $this->migrationVersionTable->applied($matches[2]);
199
                if ($all || !$applied) {
200
                    $className = $this->migrationsNamespace . '\\' . $matches[1];
201
202
                    if (!class_exists($className)) { /** @noinspection PhpIncludeInspection */
203
                        require_once $this->migrationsDir . '/' . $item->getFilename();
204
                    }
205
206
                    if (class_exists($className)) {
207
                        $reflectionClass = new \ReflectionClass($className);
208
                        $reflectionDescription = new \ReflectionProperty($className, 'description');
209
210
                        if ($reflectionClass->implementsInterface('T4web\Migrations\Migration\MigrationInterface')) {
211
                            $classes->append(
212
                                [
213
                                    'version' => $matches[2],
214
                                    'class' => $className,
215
                                    'description' => $reflectionDescription->getValue(),
216
                                    'applied' => $applied,
217
                                ]
218
                            );
219
                        }
220
                    }
221
                }
222
            }
223
        }
224
225
        $classes->uasort(
226 View Code Duplication
            function ($a, $b) {
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...
227
                if ($a['version'] == $b['version']) {
228
                    return 0;
229
                }
230
231
                return ($a['version'] < $b['version']) ? -1 : 1;
232
            }
233
        );
234
235
        return $classes;
236
    }
237
238
    protected function applyMigration(array $migration, $down = false, $fake = false)
239
    {
240
        $this->connection->beginTransaction();
241
242
        try {
243
            /** @var $migrationObject AbstractMigration */
244
            $migrationObject = new $migration['class']($this->metadata, $this->outputWriter);
245
246
            if ($migrationObject instanceof ServiceLocatorAwareInterface) {
247
                if (is_null($this->serviceLocator)) {
248
                    throw new \RuntimeException(
249
                        sprintf(
250
                            'Migration class %s requires a ServiceLocator, but there is no instance available.',
251
                            get_class($migrationObject)
252
                        )
253
                    );
254
                }
255
256
                $migrationObject->setServiceLocator($this->serviceLocator);
257
            }
258
259
            if ($migrationObject instanceof AdapterAwareInterface) {
260
                if (is_null($this->adapter)) {
261
                    throw new \RuntimeException(
262
                        sprintf(
263
                            'Migration class %s requires an Adapter, but there is no instance available.',
264
                            get_class($migrationObject)
265
                        )
266
                    );
267
                }
268
269
                $migrationObject->setDbAdapter($this->adapter);
270
            }
271
272
            $this->outputWriter->writeLine(
273
                sprintf(
274
                    "%sExecute migration class %s %s",
275
                    $fake ? '[FAKE] ' : '',
276
                    $migration['class'],
277
                    $down ? 'down' : 'up'
278
                )
279
            );
280
281
            if (!$fake) {
282
                $sqlList = $down ? $migrationObject->getDownSql() : $migrationObject->getUpSql();
283
                foreach ($sqlList as $sql) {
284
                    $this->outputWriter->writeLine("Execute query:\n\n" . $sql);
285
                    $this->connection->execute($sql);
286
                }
287
            }
288
289
            if ($down) {
290
                $this->migrationVersionTable->delete($migration['version']);
291
            } else {
292
                $this->migrationVersionTable->save($migration['version']);
293
            }
294
            $this->connection->commit();
295
        } catch (InvalidQueryException $e) {
296
            $this->connection->rollback();
297
            $previousMessage = $e->getPrevious() ? $e->getPrevious()->getMessage() : null;
298
            $msg = sprintf(
299
                '%s: "%s"; File: %s; Line #%d',
300
                $e->getMessage(),
301
                $previousMessage,
302
                $e->getFile(),
303
                $e->getLine()
304
            );
305
            throw new \Exception($msg, $e->getCode(), $e);
306
        } catch (\Exception $e) {
307
            $this->connection->rollback();
308
            $msg = sprintf('%s; File: %s; Line #%d', $e->getMessage(), $e->getFile(), $e->getLine());
309
            throw new \Exception($msg, $e->getCode(), $e);
310
        }
311
    }
312
}
313