MigrationManager   A
last analyzed

Complexity

Total Complexity 34

Size/Duplication

Total Lines 312
Duplicated Lines 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 95
dl 0
loc 312
rs 9.68
c 2
b 0
f 0
wmc 34

11 Methods

Rating   Name   Duplication   Size   Complexity  
A generateMigration() 0 13 2
A prepareDownMigrations() 0 18 4
A getMigratedEntries() 0 3 1
A addMigratedEntries() 0 4 2
A removeMigratedEntries() 0 4 2
A prepareUpMigrations() 0 20 6
A upgrade() 0 30 4
A __construct() 0 12 2
A getMigrationFiles() 0 14 3
A applyMigrations() 0 20 4
A downgrade() 0 29 4
1
<?php
2
3
/**
4
 * Quantum PHP Framework
5
 *
6
 * An open source software development framework for PHP
7
 *
8
 * @package Quantum
9
 * @author Arman Ag. <[email protected]>
10
 * @copyright Copyright (c) 2018 Softberg LLC (https://softberg.org)
11
 * @link http://quantum.softberg.org/
12
 * @since 2.9.7
13
 */
14
15
namespace Quantum\Migration;
16
17
use Quantum\Libraries\Storage\Exceptions\FileSystemException;
18
use Quantum\Libraries\Database\Exceptions\DatabaseException;
19
use Quantum\Libraries\Storage\Factories\FileSystemFactory;
20
use Quantum\Libraries\Database\Factories\TableFactory;
21
use Quantum\Libraries\Lang\Exceptions\LangException;
22
use Quantum\Migration\Exceptions\MigrationException;
23
use Quantum\Migration\Templates\MigrationTemplate;
24
use Quantum\Config\Exceptions\ConfigException;
25
use Quantum\App\Exceptions\BaseException;
26
use Quantum\Libraries\Storage\FileSystem;
27
use Quantum\Libraries\Database\Database;
28
use Quantum\Di\Exceptions\DiException;
29
use ReflectionException;
30
31
/**
32
 * Class MigrationManager
33
 * @package Quantum\Migration
34
 */
35
class MigrationManager
36
{
37
38
    /**
39
     * Migration direction for upgrade
40
     */
41
    const UPGRADE = 'up';
42
43
    /**
44
     * Migration direction for downgrade
45
     */
46
    const DOWNGRADE = 'down';
47
48
    /**
49
     * Available actions
50
     */
51
    const ACTIONS = ['create', 'alter', 'rename', 'drop'];
52
53
    /**
54
     * Supported drivers
55
     */
56
    const DRIVERS = ['mysql', 'pgsql', 'sqlite'];
57
58
    /**
59
     * @var array
60
     */
61
    private $migrations = [];
62
63
    /**
64
     * @var TableFactory
65
     */
66
    private $tableFactory;
67
68
    /**
69
     * @var string
70
     */
71
    private $migrationFolder;
72
73
    /**
74
     * @var FileSystem
75
     */
76
    private $fs;
77
78
    /**
79
     * @var Database
80
     */
81
    private $db;
82
83
    /**
84
     * @throws BaseException
85
     * @throws DiException
86
     * @throws FileSystemException
87
     * @throws ConfigException
88
     * @throws ReflectionException
89
     */
90
    public function __construct()
91
    {
92
        $this->fs = FileSystemFactory::get();
93
94
        $this->db = Database::getInstance();
95
96
        $this->tableFactory = new TableFactory();
97
98
        $this->migrationFolder = base_dir() . DS . 'migrations';
99
100
        if (!$this->fs->isDirectory($this->migrationFolder)) {
101
            throw FileSystemException::directoryNotExists($this->migrationFolder);
102
        }
103
    }
104
105
    /**
106
     * Generates new migration file
107
     * @param string $table
108
     * @param string $action
109
     * @return string
110
     * @throws MigrationException
111
     * @throws LangException
112
     */
113
    public function generateMigration(string $table, string $action): string
114
    {
115
        if (!in_array($action, self::ACTIONS)) {
116
            throw MigrationException::unsupportedAction($action);
117
        }
118
119
        $migrationName = $action . '_table_' . strtolower($table) . '_' . time();
120
121
        $migrationTemplate = MigrationTemplate::{$action}($migrationName, strtolower($table));
122
123
        $this->fs->put($this->migrationFolder . DS . $migrationName . '.php', $migrationTemplate);
124
125
        return $migrationName;
126
    }
127
128
    /**
129
     * Applies migrations
130
     * @param string $direction
131
     * @param int|null $step
132
     * @return int|null
133
     * @throws BaseException
134
     * @throws ConfigException
135
     * @throws DatabaseException
136
     * @throws DiException
137
     * @throws LangException
138
     * @throws MigrationException
139
     */
140
    public function applyMigrations(string $direction, ?int $step = null): ?int
141
    {
142
        $databaseDriver = $this->db->getConfigs()['driver'];
143
144
        if (!in_array($databaseDriver, self::DRIVERS)) {
145
            throw MigrationException::driverNotSupported($databaseDriver);
146
        }
147
148
        switch ($direction) {
149
            case self::UPGRADE:
150
                $migrated = $this->upgrade($step);
151
                break;
152
            case self::DOWNGRADE:
153
                $migrated = $this->downgrade($step);
154
                break;
155
            default:
156
                throw MigrationException::wrongDirection();
157
        }
158
159
        return $migrated;
160
    }
161
162
    /**
163
     * Runs up migrations
164
     * @param int|null $step
165
     * @return int
166
     * @throws DatabaseException
167
     * @throws LangException
168
     * @throws MigrationException
169
     */
170
    private function upgrade(?int $step = null): int
171
    {
172
        if (!$this->tableFactory->checkTableExists(MigrationTable::TABLE)) {
173
            $migrationTable = new MigrationTable();
174
            $migrationTable->up($this->tableFactory);
175
        }
176
177
        $this->prepareUpMigrations($step);
178
179
        if (empty($this->migrations)) {
180
            throw MigrationException::nothingToMigrate();
181
        }
182
183
        $migratedEntries = [];
184
185
        foreach ($this->migrations as $migrationFile) {
186
            $this->fs->require($migrationFile, true);
0 ignored issues
show
Unused Code introduced by
The call to Quantum\Libraries\Storage\FileSystem::require() has too many arguments starting with $migrationFile. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

186
            $this->fs->/** @scrutinizer ignore-call */ 
187
                       require($migrationFile, true);

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. Please note the @ignore annotation hint above.

Loading history...
187
188
            $migrationClassName = pathinfo($migrationFile, PATHINFO_FILENAME);
189
190
            $migration = new $migrationClassName();
191
192
            $migration->up($this->tableFactory);
193
194
            $migratedEntries[] = $migrationClassName;
195
        }
196
197
        $this->addMigratedEntries($migratedEntries);
198
199
        return count($migratedEntries);
200
    }
201
202
    /**
203
     * Runs down migrations
204
     * @param int|null $step
205
     * @return int
206
     * @throws DatabaseException
207
     * @throws LangException
208
     * @throws MigrationException
209
     */
210
    private function downgrade(?int $step): int
211
    {
212
        if (!$this->tableFactory->checkTableExists(MigrationTable::TABLE)) {
213
            throw DatabaseException::tableDoesNotExists(MigrationTable::TABLE);
214
        }
215
216
        $this->prepareDownMigrations($step);
217
218
        if (empty($this->migrations)) {
219
            throw MigrationException::nothingToMigrate();
220
        }
221
222
        $migratedEntries = [];
223
224
        foreach ($this->migrations as $migrationFile) {
225
            $this->fs->require($migrationFile, true);
0 ignored issues
show
Unused Code introduced by
The call to Quantum\Libraries\Storage\FileSystem::require() has too many arguments starting with $migrationFile. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

225
            $this->fs->/** @scrutinizer ignore-call */ 
226
                       require($migrationFile, true);

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. Please note the @ignore annotation hint above.

Loading history...
226
227
            $migrationClassName = pathinfo($migrationFile, PATHINFO_FILENAME);
228
229
            $migration = new $migrationClassName();
230
231
            $migration->down($this->tableFactory);
232
233
            $migratedEntries[] = $migrationClassName;
234
        }
235
236
        $this->removeMigratedEntries($migratedEntries);
237
238
        return count($migratedEntries);
239
    }
240
241
    /**
242
     * Prepares up migrations
243
     * @param int|null $step
244
     * @throws MigrationException
245
     * @throws DatabaseException
246
     *
247
     */
248
    private function prepareUpMigrations(?int $step = null)
249
    {
250
        $migratedEntries = $this->getMigratedEntries();
251
        $migrationFiles = $this->getMigrationFiles();
252
253
        if (empty($migratedEntries) && empty($migrationFiles)) {
254
            throw MigrationException::nothingToMigrate();
255
        }
256
257
        foreach ($migrationFiles as $timestamp => $migrationFile) {
258
            foreach ($migratedEntries as $migratedEntry) {
259
                if (pathinfo($migrationFile, PATHINFO_FILENAME) == $migratedEntry['migration']) {
260
                    continue 2;
261
                }
262
            }
263
264
            $this->migrations[$timestamp] = $migrationFile;
265
        }
266
267
        ksort($this->migrations);
268
    }
269
270
    /**
271
     * Prepares down migrations
272
     * @param int|null $step
273
     * @throws DatabaseException
274
     * @throws MigrationException
275
     */
276
    private function prepareDownMigrations(?int $step = null)
277
    {
278
        $migratedEntries = $this->getMigratedEntries();
279
280
        if (empty($migratedEntries)) {
281
            throw MigrationException::nothingToMigrate();
282
        }
283
284
        foreach ($migratedEntries as $migratedEntry) {
285
            $exploded = explode('_', $migratedEntry['migration']);
286
            $this->migrations[array_pop($exploded)] = $this->migrationFolder . DS . $migratedEntry['migration'] . '.php';
287
        }
288
289
        if (!is_null($step)) {
290
            $this->migrations = array_slice($this->migrations, count($this->migrations) - $step, $step, true);
291
        }
292
293
        krsort($this->migrations);
294
    }
295
296
    /**
297
     * Gets migration files
298
     * @return array
299
     */
300
    private function getMigrationFiles(): array
301
    {
302
        $migrationsFiles = $this->fs->glob($this->migrationFolder . DS . '*.php');
303
304
        $migrations = [];
305
306
        if (!empty($migrationsFiles)) {
307
            foreach ($migrationsFiles as $migration) {
308
                $exploded = explode('_', pathinfo($migration, PATHINFO_FILENAME));
0 ignored issues
show
Bug introduced by
It seems like pathinfo($migration, Qua...tion\PATHINFO_FILENAME) can also be of type array; however, parameter $string of explode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

308
                $exploded = explode('_', /** @scrutinizer ignore-type */ pathinfo($migration, PATHINFO_FILENAME));
Loading history...
309
                $migrations[array_pop($exploded)] = $migration;
310
            }
311
        }
312
313
        return $migrations;
314
    }
315
316
    /**
317
     * Gets migrated entries from migrations table
318
     * @return array
319
     * @throws DatabaseException
320
     */
321
    private function getMigratedEntries(): array
322
    {
323
        return Database::query("SELECT * FROM " . MigrationTable::TABLE);
324
    }
325
326
    /**
327
     * Adds migrated entries to migrations table
328
     * @param array $entries
329
     * @throws DatabaseException
330
     */
331
    private function addMigratedEntries(array $entries)
332
    {
333
        foreach ($entries as $entry) {
334
            Database::execute('INSERT INTO ' . MigrationTable::TABLE . '(migration) VALUES(:migration)', ['migration' => $entry]);
335
        }
336
    }
337
338
    /**
339
     * Removes migrated entries from migrations table
340
     * @param array $entries
341
     * @throws DatabaseException
342
     */
343
    private function removeMigratedEntries(array $entries)
344
    {
345
        foreach ($entries as $entry) {
346
            Database::execute('DELETE FROM ' . MigrationTable::TABLE . ' WHERE migration=:migration', ['migration' => $entry]);
347
        }
348
    }
349
}