Migration   B
last analyzed

Complexity

Total Complexity 48

Size/Duplication

Total Lines 389
Duplicated Lines 0 %

Importance

Changes 19
Bugs 3 Features 0
Metric Value
wmc 48
eloc 105
c 19
b 3
f 0
dl 0
loc 389
rs 8.5599

21 Methods

Rating   Name   Duplication   Size   Complexity  
A down() 0 3 1
A __construct() 0 8 3
B migrate() 0 25 7
A isDatabaseVersioned() 0 3 1
A getDatabaseClassName() 0 7 2
A createVersion() 0 3 1
A registerDatabase() 0 9 3
A canContinue() 0 9 4
A updateTableVersion() 0 3 1
A getDbCommand() 0 7 2
A getMigrationSql() 0 30 6
A update() 0 9 3
A up() 0 3 1
A getMigrationTable() 0 3 1
A addCallbackProgress() 0 3 1
A reset() 0 17 3
A getDbDriver() 0 3 1
A getCurrentVersion() 0 3 1
A getFileContent() 0 23 4
A getBaseSql() 0 3 1
A prepareEnvironment() 0 4 1

How to fix   Complexity   

Complex Class

Complex classes like Migration often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Migration, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace ByJG\DbMigration;
4
5
use ByJG\AnyDataset\Db\DbDriverInterface;
6
use ByJG\DbMigration\Database\DatabaseInterface;
7
use ByJG\DbMigration\Exception\DatabaseDoesNotRegistered;
8
use ByJG\DbMigration\Exception\DatabaseIsIncompleteException;
9
use ByJG\DbMigration\Exception\InvalidMigrationFile;
10
use InvalidArgumentException;
11
use Psr\Http\Message\UriInterface;
12
13
class Migration
14
{
15
    const VERSION_STATUS_UNKNOWN = "unknown";
16
    const VERSION_STATUS_PARTIAL = "partial";
17
    const VERSION_STATUS_COMPLETE = "complete";
18
19
    /**
20
     * @var UriInterface
21
     */
22
    protected $uri;
23
24
    /**
25
     * @var string
26
     */
27
    protected $folder;
28
29
    /**
30
     * @var DbDriverInterface
31
     */
32
    protected $dbDriver;
33
34
    /**
35
     * @var DatabaseInterface
36
     */
37
    protected $dbCommand;
38
39
    /**
40
     * @var Callable
41
     */
42
    protected $callableProgress;
43
44
    /**
45
     * @var array
46
     */
47
    protected static $databases = [];
48
    /**
49
     * @var string
50
     */
51
    private $migrationTable;
52
53
    /**
54
     * Migration constructor.
55
     *
56
     * @param UriInterface $uri
57
     * @param string $folder
58
     * @param bool $requiredBase Define if base.sql is required
59
     * @param string $migrationTable
60
     * @throws InvalidMigrationFile
61
     */
62
    public function __construct(UriInterface $uri, $folder, $requiredBase = true, $migrationTable = 'migration_version')
63
    {
64
        $this->uri = $uri;
65
        $this->folder = $folder;
66
        if ($requiredBase && !file_exists($this->folder . '/base.sql')) {
67
            throw new InvalidMigrationFile("Migration script '{$this->folder}/base.sql' not found");
68
        }
69
        $this->migrationTable = $migrationTable;
70
    }
71
72
    /**
73
     * @param $scheme
74
     * @param $className
75
     * @return $this
76
     */
77
    public static function registerDatabase($class)
78
    {
79
        if (!in_array(DatabaseInterface::class, class_implements($class))) {
80
            throw new InvalidArgumentException('Class not implements DatabaseInterface!');
81
        }
82
83
        $protocolList = $class::schema();
84
        foreach ((array)$protocolList as $item) {
85
            self::$databases[$item] = $class;
86
        }
87
    }
88
89
    /**
90
     * @return DbDriverInterface
91
     * @throws \ByJG\DbMigration\Exception\DatabaseDoesNotRegistered
92
     */
93
    public function getDbDriver()
94
    {
95
        return $this->getDbCommand()->getDbDriver();
96
    }
97
98
    /**
99
     * @return DatabaseInterface
100
     * @throws \ByJG\DbMigration\Exception\DatabaseDoesNotRegistered
101
     */
102
    public function getDbCommand()
103
    {
104
        if (is_null($this->dbCommand)) {
105
            $class = $this->getDatabaseClassName();
106
            $this->dbCommand = new $class($this->uri, $this->migrationTable);
107
        }
108
        return $this->dbCommand;
109
    }
110
111
    public function getMigrationTable()
112
    {
113
        return $this->migrationTable;
114
    }
115
116
    /**
117
     * @return mixed
118
     * @throws \ByJG\DbMigration\Exception\DatabaseDoesNotRegistered
119
     */
120
    protected function getDatabaseClassName()
121
    {
122
        if (isset(self::$databases[$this->uri->getScheme()])) {
123
            return self::$databases[$this->uri->getScheme()];
124
        }
125
        throw new DatabaseDoesNotRegistered(
126
            'Scheme "' . $this->uri->getScheme() . '" does not found. Did you registered it?'
127
        );
128
    }
129
130
    /**
131
     * Get the full path and name of the "base.sql" script
132
     *
133
     * @return string
134
     */
135
    public function getBaseSql()
136
    {
137
        return $this->folder . "/base.sql";
138
    }
139
140
    /**
141
     * Get the full path script based on the version
142
     *
143
     * @param $version
144
     * @param $increment
145
     * @return string
146
     * @throws \ByJG\DbMigration\Exception\InvalidMigrationFile
147
     */
148
    public function getMigrationSql($version, $increment)
149
    {
150
        // I could use the GLOB_BRACE but it is not supported on ALPINE distros.
151
        // So, I have to call multiple times to simulate the braces.
152
153
        if (intval($version) != $version) {
154
            throw new \InvalidArgumentException("Version '$version' should be a integer number");
155
        }
156
        $version = intval($version);
157
158
        $filePattern = $this->folder
159
            . "/migrations"
160
            . "/" . ($increment < 0 ? "down" : "up")
161
            . "/*.sql";
162
163
        $result = array_filter(glob($filePattern), function ($file) use ($version) {
164
            return preg_match("/^0*$version(-dev)?\.sql$/", basename($file));
165
        });
166
167
        // Valid values are 0 or 1
168
        if (count($result) > 1) {
169
            throw new InvalidMigrationFile("You have two files with the same version number '$version'");
170
        }
171
172
        foreach ($result as $file) {
173
            if (intval(basename($file)) === $version) {
174
                return $file;
175
            }
176
        }
177
        return null;
178
    }
179
180
    /**
181
     * Get the file contents and metainfo
182
     * @param $file
183
     * @return array
184
     */
185
    public function getFileContent($file)
186
    {
187
        $data = [
188
            "file" => $file,
189
            "description" => "no description provided. Pro tip: use `-- @description:` to define one.",
190
            "exists" => false,
191
            "checksum" => null,
192
            "content" => null,
193
        ];
194
        if (empty($file) || !file_exists($file)) {
195
            return $data;
196
        }
197
198
        $data["content"] = file_get_contents($file);
199
200
        if (preg_match("/--\s*@description:\s*(?<name>.*)/", $data["content"], $description)) {
201
            $data["description"] = $description["name"];
202
        }
203
204
        $data["exists"] = true;
205
        $data["checksum"] = sha1($data["content"]);
206
207
        return $data;
208
    }
209
210
    /**
211
     * Create the database it it does not exists. Does not use this methos in a production environment
212
     *
213
     * @throws \ByJG\DbMigration\Exception\DatabaseDoesNotRegistered
214
     */
215
    public function prepareEnvironment()
216
    {
217
        $class = $this->getDatabaseClassName();
218
        $class::prepareEnvironment($this->uri);
219
    }
220
221
    /**
222
     * Restore the database using the "base.sql" script and run all migration scripts
223
     * Note: the database must exists. If dont exist run the method prepareEnvironment
224
     *
225
     * @param int $upVersion
226
     * @throws \ByJG\DbMigration\Exception\DatabaseDoesNotRegistered
227
     * @throws \ByJG\DbMigration\Exception\DatabaseIsIncompleteException
228
     * @throws \ByJG\DbMigration\Exception\DatabaseNotVersionedException
229
     * @throws \ByJG\DbMigration\Exception\InvalidMigrationFile
230
     * @throws \ByJG\DbMigration\Exception\OldVersionSchemaException
231
     */
232
    public function reset($upVersion = null)
233
    {
234
        $fileInfo = $this->getFileContent($this->getBaseSql());
235
236
        if ($this->callableProgress) {
237
            call_user_func_array($this->callableProgress, ['reset', 0, $fileInfo]);
238
        }
239
        $this->getDbCommand()->dropDatabase();
240
        $this->getDbCommand()->createDatabase();
241
        $this->getDbCommand()->createVersion();
242
243
        if ($fileInfo["exists"]) {
244
            $this->getDbCommand()->executeSql($fileInfo["content"]);
245
        }
246
247
        $this->getDbCommand()->setVersion(0, Migration::VERSION_STATUS_COMPLETE);
248
        $this->up($upVersion);
249
    }
250
251
    /**
252
     * @throws \ByJG\DbMigration\Exception\DatabaseDoesNotRegistered
253
     */
254
    public function createVersion()
255
    {
256
        $this->getDbCommand()->createVersion();
257
    }
258
259
    /**
260
     * @throws \ByJG\DbMigration\Exception\DatabaseDoesNotRegistered
261
     */
262
    public function updateTableVersion()
263
    {
264
        $this->getDbCommand()->updateVersionTable();
265
    }
266
267
    /**
268
     * Get the current database version
269
     *
270
     * @return string[] The current 'version' and 'status' as an associative array
271
     * @throws \ByJG\DbMigration\Exception\DatabaseDoesNotRegistered
272
     * @throws \ByJG\DbMigration\Exception\DatabaseNotVersionedException
273
     * @throws \ByJG\DbMigration\Exception\OldVersionSchemaException
274
     */
275
    public function getCurrentVersion()
276
    {
277
        return $this->getDbCommand()->getVersion();
278
    }
279
280
    /**
281
     * @param $currentVersion
282
     * @param $upVersion
283
     * @param $increment
284
     * @return bool
285
     */
286
    protected function canContinue($currentVersion, $upVersion, $increment)
287
    {
288
        $existsUpVersion = ($upVersion !== null);
289
        $compareVersion =
290
            intval($currentVersion) < intval($upVersion)
291
                ? -1
292
                : (intval($currentVersion) > intval($upVersion) ? 1 : 0);
293
294
        return !($existsUpVersion && ($compareVersion === intval($increment)));
295
    }
296
297
    /**
298
     * Method for execute the migration.
299
     *
300
     * @param int $upVersion
301
     * @param int $increment Can accept 1 for UP or -1 for down
302
     * @param bool $force
303
     * @throws \ByJG\DbMigration\Exception\DatabaseDoesNotRegistered
304
     * @throws \ByJG\DbMigration\Exception\DatabaseIsIncompleteException
305
     * @throws \ByJG\DbMigration\Exception\DatabaseNotVersionedException
306
     * @throws \ByJG\DbMigration\Exception\InvalidMigrationFile
307
     * @throws \ByJG\DbMigration\Exception\OldVersionSchemaException
308
     */
309
    protected function migrate($upVersion, $increment, $force)
310
    {
311
        $versionInfo = $this->getCurrentVersion();
312
        $currentVersion = intval($versionInfo['version']) + $increment;
313
314
        if (strpos($versionInfo['status'], Migration::VERSION_STATUS_PARTIAL) !== false && !$force) {
315
            throw new DatabaseIsIncompleteException('Database was not fully updated. Use --force for migrate.');
316
        }
317
318
        while ($this->canContinue($currentVersion, $upVersion, $increment)
319
        ) {
320
            $fileInfo = $this->getFileContent($this->getMigrationSql($currentVersion, $increment));
321
322
            if (!$fileInfo["exists"]) {
323
                break;
324
            }
325
326
            if ($this->callableProgress) {
327
                call_user_func_array($this->callableProgress, ['migrate', $currentVersion, $fileInfo]);
328
            }
329
330
            $this->getDbCommand()->setVersion($currentVersion, Migration::VERSION_STATUS_PARTIAL . ' ' . ($increment>0 ? 'up' : 'down'));
331
            $this->getDbCommand()->executeSql($fileInfo["content"]);
332
            $this->getDbCommand()->setVersion($currentVersion, Migration::VERSION_STATUS_COMPLETE);
333
            $currentVersion = $currentVersion + $increment;
334
        }
335
    }
336
337
    /**
338
     * Run all scripts to up the database version from current up to latest version or the specified version.
339
     *
340
     * @param int $upVersion
341
     * @param bool $force
342
     * @throws \ByJG\DbMigration\Exception\DatabaseDoesNotRegistered
343
     * @throws \ByJG\DbMigration\Exception\DatabaseIsIncompleteException
344
     * @throws \ByJG\DbMigration\Exception\DatabaseNotVersionedException
345
     * @throws \ByJG\DbMigration\Exception\InvalidMigrationFile
346
     * @throws \ByJG\DbMigration\Exception\OldVersionSchemaException
347
     */
348
    public function up($upVersion = null, $force = false)
349
    {
350
        $this->migrate($upVersion, 1, $force);
351
    }
352
353
    /**
354
     * Run all scripts to up or down the database version from current up to latest version or the specified version.
355
     *
356
     * @param int $upVersion
357
     * @param bool $force
358
     * @throws \ByJG\DbMigration\Exception\DatabaseDoesNotRegistered
359
     * @throws \ByJG\DbMigration\Exception\DatabaseIsIncompleteException
360
     * @throws \ByJG\DbMigration\Exception\DatabaseNotVersionedException
361
     * @throws \ByJG\DbMigration\Exception\InvalidMigrationFile
362
     * @throws \ByJG\DbMigration\Exception\OldVersionSchemaException
363
     */
364
    public function update($upVersion = null, $force = false)
365
    {
366
        $versionInfo = $this->getCurrentVersion();
367
        $version = intval($versionInfo['version']);
368
        $increment = 1;
369
        if ($upVersion !== null && $upVersion < $version) {
370
            $increment = -1;
371
        }
372
        $this->migrate($upVersion, $increment, $force);
373
    }
374
375
    /**
376
     * Run all scripts to down the database version from current version up to the specified version.
377
     *
378
     * @param int $upVersion
379
     * @param bool $force
380
     * @throws \ByJG\DbMigration\Exception\DatabaseDoesNotRegistered
381
     * @throws \ByJG\DbMigration\Exception\DatabaseIsIncompleteException
382
     * @throws \ByJG\DbMigration\Exception\DatabaseNotVersionedException
383
     * @throws \ByJG\DbMigration\Exception\InvalidMigrationFile
384
     * @throws \ByJG\DbMigration\Exception\OldVersionSchemaException
385
     */
386
    public function down($upVersion, $force = false)
387
    {
388
        $this->migrate($upVersion, -1, $force);
389
    }
390
391
    /**
392
     * @param callable $callable
393
     */
394
    public function addCallbackProgress(callable $callable)
395
    {
396
        $this->callableProgress = $callable;
397
    }
398
399
    public function isDatabaseVersioned()
400
    {
401
        return $this->getDbCommand()->isDatabaseVersioned();
402
    }
403
}
404