MigrationManager::__construct()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 6
nc 2
nop 0
dl 0
loc 12
rs 10
c 1
b 0
f 0
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 3.0.0
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
     * Migration direction for upgrade
39
     */
40
    public const UPGRADE = 'up';
41
42
    /**
43
     * Migration direction for downgrade
44
     */
45
    public const DOWNGRADE = 'down';
46
47
    /**
48
     * Available actions
49
     */
50
    public const ACTIONS = ['create', 'alter', 'rename', 'drop'];
51
52
    /**
53
     * Supported drivers
54
     */
55
    public const DRIVERS = ['mysql', 'pgsql', 'sqlite'];
56
57
    /**
58
     * @var array
59
     */
60
    private $migrations = [];
61
62
    /**
63
     * @var TableFactory
64
     */
65
    private $tableFactory;
66
67
    /**
68
     * @var string
69
     */
70
    private $migrationFolder;
71
72
    /**
73
     * @var FileSystem
74
     */
75
    private $fs;
76
77
    /**
78
     * @var Database
79
     */
80
    private $db;
81
82
    /**
83
     * @throws BaseException
84
     * @throws DiException
85
     * @throws FileSystemException
86
     * @throws ConfigException
87
     * @throws ReflectionException
88
     */
89
    public function __construct()
90
    {
91
        $this->fs = FileSystemFactory::get();
92
93
        $this->db = Database::getInstance();
94
95
        $this->tableFactory = new TableFactory();
96
97
        $this->migrationFolder = base_dir() . DS . 'migrations';
98
99
        if (!$this->fs->isDirectory($this->migrationFolder)) {
100
            throw FileSystemException::directoryNotExists($this->migrationFolder);
101
        }
102
    }
103
104
    /**
105
     * Generates new migration file
106
     * @param string $table
107
     * @param string $action
108
     * @return string
109
     * @throws MigrationException
110
     * @throws LangException
111
     */
112
    public function generateMigration(string $table, string $action): string
113
    {
114
        if (!in_array($action, self::ACTIONS)) {
115
            throw MigrationException::unsupportedAction($action);
116
        }
117
118
        $migrationName = $action . '_table_' . strtolower($table) . '_' . time();
119
120
        $migrationTemplate = MigrationTemplate::{$action}($migrationName, strtolower($table));
121
122
        $this->fs->put($this->migrationFolder . DS . $migrationName . '.php', $migrationTemplate);
123
124
        return $migrationName;
125
    }
126
127
    /**
128
     * Applies migrations
129
     * @param string $direction
130
     * @param int|null $step
131
     * @return int|null
132
     * @throws BaseException
133
     * @throws ConfigException
134
     * @throws DatabaseException
135
     * @throws DiException
136
     * @throws LangException
137
     * @throws MigrationException
138
     */
139
    public function applyMigrations(string $direction, ?int $step = null): ?int
140
    {
141
        $databaseDriver = $this->db->getConfigs()['driver'];
142
143
        if (!in_array($databaseDriver, self::DRIVERS)) {
144
            throw MigrationException::driverNotSupported($databaseDriver);
145
        }
146
147
        switch ($direction) {
148
            case self::UPGRADE:
149
                $migrated = $this->upgrade($step);
150
                break;
151
            case self::DOWNGRADE:
152
                $migrated = $this->downgrade($step);
153
                break;
154
            default:
155
                throw MigrationException::wrongDirection();
156
        }
157
158
        return $migrated;
159
    }
160
161
    /**
162
     * Runs up migrations
163
     * @param int|null $step
164
     * @return int
165
     * @throws DatabaseException
166
     * @throws LangException
167
     * @throws MigrationException
168
     */
169
    private function upgrade(?int $step = null): int
170
    {
171
        if (!$this->tableFactory->checkTableExists(MigrationTable::TABLE)) {
172
            $migrationTable = new MigrationTable();
173
            $migrationTable->up($this->tableFactory);
174
        }
175
176
        $this->prepareUpMigrations($step);
177
178
        if (empty($this->migrations)) {
179
            throw MigrationException::nothingToMigrate();
180
        }
181
182
        $migratedEntries = [];
183
184
        foreach ($this->migrations as $migrationFile) {
185
            $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

185
            $this->fs->/** @scrutinizer ignore-call */ 
186
                       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...
186
187
            $migrationClassName = pathinfo($migrationFile, PATHINFO_FILENAME);
188
189
            $migration = new $migrationClassName();
190
191
            $migration->up($this->tableFactory);
192
193
            $migratedEntries[] = $migrationClassName;
194
        }
195
196
        $this->addMigratedEntries($migratedEntries);
197
198
        return count($migratedEntries);
199
    }
200
201
    /**
202
     * Runs down migrations
203
     * @param int|null $step
204
     * @return int
205
     * @throws DatabaseException
206
     * @throws LangException
207
     * @throws MigrationException
208
     */
209
    private function downgrade(?int $step): int
210
    {
211
        if (!$this->tableFactory->checkTableExists(MigrationTable::TABLE)) {
212
            throw DatabaseException::tableDoesNotExists(MigrationTable::TABLE);
213
        }
214
215
        $this->prepareDownMigrations($step);
216
217
        if (empty($this->migrations)) {
218
            throw MigrationException::nothingToMigrate();
219
        }
220
221
        $migratedEntries = [];
222
223
        foreach ($this->migrations as $migrationFile) {
224
            $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

224
            $this->fs->/** @scrutinizer ignore-call */ 
225
                       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...
225
226
            $migrationClassName = pathinfo($migrationFile, PATHINFO_FILENAME);
227
228
            $migration = new $migrationClassName();
229
230
            $migration->down($this->tableFactory);
231
232
            $migratedEntries[] = $migrationClassName;
233
        }
234
235
        $this->removeMigratedEntries($migratedEntries);
236
237
        return count($migratedEntries);
238
    }
239
240
    /**
241
     * Prepares up migrations
242
     * @param int|null $step
243
     * @throws MigrationException
244
     * @throws DatabaseException
245
     *
246
     */
247
    private function prepareUpMigrations(?int $step = null)
248
    {
249
        $migratedEntries = $this->getMigratedEntries();
250
        $migrationFiles = $this->getMigrationFiles();
251
252
        if ($migratedEntries === [] && $migrationFiles === []) {
253
            throw MigrationException::nothingToMigrate();
254
        }
255
256
        foreach ($migrationFiles as $timestamp => $migrationFile) {
257
            foreach ($migratedEntries as $migratedEntry) {
258
                if (pathinfo($migrationFile, PATHINFO_FILENAME) == $migratedEntry['migration']) {
259
                    continue 2;
260
                }
261
            }
262
263
            $this->migrations[$timestamp] = $migrationFile;
264
        }
265
266
        ksort($this->migrations);
267
    }
268
269
    /**
270
     * Prepares down migrations
271
     * @param int|null $step
272
     * @throws DatabaseException
273
     * @throws MigrationException
274
     */
275
    private function prepareDownMigrations(?int $step = null)
276
    {
277
        $migratedEntries = $this->getMigratedEntries();
278
279
        if ($migratedEntries === []) {
280
            throw MigrationException::nothingToMigrate();
281
        }
282
283
        foreach ($migratedEntries as $migratedEntry) {
284
            $exploded = explode('_', $migratedEntry['migration']);
285
            $this->migrations[array_pop($exploded)] = $this->migrationFolder . DS . $migratedEntry['migration'] . '.php';
286
        }
287
288
        if (!is_null($step)) {
289
            $this->migrations = array_slice($this->migrations, count($this->migrations) - $step, $step, true);
290
        }
291
292
        krsort($this->migrations);
293
    }
294
295
    /**
296
     * Gets migration files
297
     * @return array
298
     */
299
    private function getMigrationFiles(): array
300
    {
301
        $migrationsFiles = $this->fs->glob($this->migrationFolder . DS . '*.php');
302
303
        $migrations = [];
304
305
        if (!empty($migrationsFiles)) {
306
            foreach ($migrationsFiles as $migration) {
307
                $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

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