Passed
Pull Request — master (#104)
by Arman
02:55
created

MigrationManager::prepareUpMigrations()   A

Complexity

Conditions 6
Paths 5

Size

Total Lines 20
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 6
eloc 10
c 1
b 0
f 0
nc 5
nop 1
dl 0
loc 20
rs 9.2222
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.8.0
13
 */
14
15
namespace Quantum\Migration;
16
17
use Quantum\Exceptions\FileSystemException;
18
use Quantum\Exceptions\MigrationException;
19
use Quantum\Libraries\Storage\FileSystem;
20
use Quantum\Exceptions\DatabaseException;
21
use Quantum\Libraries\Database\Database;
22
use Quantum\Factory\TableFactory;
23
24
/**
25
 * Class MigrationManager
26
 * @package Quantum\Migration
27
 */
28
class MigrationManager
29
{
30
31
    /**
32
     * Migration direction for upgrade
33
     */
34
    const UPGRADE = 'up';
35
36
    /**
37
     * Migration direction for downgrade
38
     */
39
    const DOWNGRADE = 'down';
40
41
    /**
42
     * @var array
43
     */
44
    private $actions = ['create', 'alter', 'rename', 'drop'];
45
46
    /**
47
     * @var array
48
     */
49
    private $drivers = ['mysql', 'pgsql', 'sqlite'];
50
51
    /**
52
     * @var array
53
     */
54
    private $migrations = [];
55
56
    /**
57
     * @var TableFactory
58
     */
59
    private $tableFactory;
60
61
    /**
62
     * @var string
63
     */
64
    private $migrationFolder;
65
66
    /**
67
     * @var FileSystem
68
     */
69
    private $fs;
70
71
    /**
72
     * @var Database
73
     */
74
    private $db;
75
76
    /**
77
     * MigrationManager constructor.
78
     * @throws FileSystemException
79
     */
80
    public function __construct()
81
    {
82
        $this->fs = new FileSystem();
83
84
        $this->db = Database::getInstance();
85
86
        $this->tableFactory = new TableFactory();
87
88
        $this->migrationFolder = base_dir() . DS . 'migrations';
89
90
        if (!$this->fs->isDirectory($this->migrationFolder)) {
91
            throw FileSystemException::directoryNotExists($this->migrationFolder);
92
        }
93
    }
94
95
    /**
96
     * Generates new migration file
97
     * @param string $table
98
     * @param string $action
99
     * @return string
100
     * @throws MigrationException
101
     */
102
    public function generateMigration(string $table, string $action): string
103
    {
104
        if (!in_array($action, $this->actions)) {
105
            throw MigrationException::unsupportedAction($action);
106
        }
107
108
        $migrationName = $action . '_table_' . strtolower($table) . '_' . time();
109
110
        $migrationTemplate = MigrationTemplate::{$action}($migrationName, strtolower($table));
111
112
        $this->fs->put($this->migrationFolder . DS . $migrationName . '.php', $migrationTemplate);
113
114
        return $migrationName;
115
    }
116
117
    /**
118
     * Applies migrations
119
     * @param string $direction
120
     * @param int|null $step
121
     * @return int|null
122
     * @throws DatabaseException
123
     * @throws MigrationException
124
     * @throws \Quantum\Exceptions\AppException
125
     * @throws \Quantum\Exceptions\ConfigException
126
     * @throws \Quantum\Exceptions\DiException
127
     * @throws \ReflectionException
128
     */
129
    public function applyMigrations(string $direction, ?int $step = null): ?int
130
    {
131
        $databaseDriver = $this->db->getConfigs()['driver'];
132
133
        if (!in_array($databaseDriver, $this->drivers)) {
134
            throw MigrationException::unsupportedDriver($databaseDriver);
135
        }
136
137
        switch ($direction) {
138
            case self::UPGRADE:
139
                $migrated = $this->upgrade($step);
140
                break;
141
            case self::DOWNGRADE:
142
                $migrated = $this->downgrade($step);
143
                break;
144
            default:
145
                throw MigrationException::wrongDirection();
146
        }
147
148
        return $migrated;
149
    }
150
151
    /**
152
     * Runs up migrations
153
     * @param int|null $step
154
     * @return int
155
     * @throws MigrationException
156
     * @throws DatabaseException
157
     */
158
    private function upgrade(?int $step = null): int
159
    {
160
        if (!$this->tableFactory->checkTableExists(MigrationTable::TABLE)) {
161
            $migrationTable = new MigrationTable();
162
            $migrationTable->up($this->tableFactory);
163
        }
164
165
        $this->prepareUpMigrations($step);
166
167
        if (empty($this->migrations)) {
168
            throw MigrationException::nothingToMigrate();
169
        }
170
171
        $migratedEntries = [];
172
173
        foreach ($this->migrations as $migrationFile) {
174
            $this->fs->require($migrationFile, true);
175
176
            $migrationClassName = pathinfo($migrationFile, PATHINFO_FILENAME);
177
178
            $migration = new $migrationClassName();
179
180
            $migration->up($this->tableFactory);
181
182
            $migratedEntries[] = $migrationClassName;
183
        }
184
185
        $this->addMigratedEntries($migratedEntries);
186
187
        return count($migratedEntries);
188
    }
189
190
    /**
191
     * Runs down migrations
192
     * @param int|null $step
193
     * @return int
194
     * @throws MigrationException
195
     * @throws DatabaseException
196
     */
197
    private function downgrade(?int $step): int
198
    {
199
        $this->prepareDownMigrations($step);
200
201
        if (empty($this->migrations)) {
202
            throw MigrationException::nothingToMigrate();
203
        }
204
205
        $migratedEntries = [];
206
207
        foreach ($this->migrations as $migrationFile) {
208
            $this->fs->require($migrationFile, true);
209
210
            $migrationClassName = pathinfo($migrationFile, PATHINFO_FILENAME);
211
212
            $migration = new $migrationClassName();
213
214
            $migration->down($this->tableFactory);
215
216
            $migratedEntries[] = $migrationClassName;
217
        }
218
219
        $this->removeMigratedEntries($migratedEntries);
220
221
        return count($migratedEntries);
222
    }
223
224
    /**
225
     * Prepares up migrations
226
     * @param int|null $step
227
     * @return void
228
     * @throws MigrationException
229
     * @throws DatabaseException
230
     */
231
    private function prepareUpMigrations(?int $step = null)
232
    {
233
        $migratedEntries = $this->getMigratedEntries();
234
        $migrationFiles = $this->getMigrationFiles();
235
236
        if (empty($migratedEntries) && empty($migrationFiles)) {
237
            throw MigrationException::nothingToMigrate();
238
        }
239
240
        foreach ($migrationFiles as $timestamp => $migrationFile) {
241
            foreach ($migratedEntries as $migratedEntry) {
242
                if (pathinfo($migrationFile, PATHINFO_FILENAME) == $migratedEntry['migration']) {
243
                    continue 2;
244
                }
245
            }
246
247
            $this->migrations[$timestamp] = $migrationFile;
248
        }
249
250
        ksort($this->migrations);
251
    }
252
253
    /**
254
     * Prepares down migrations
255
     * @param int|null $step
256
     * @return void
257
     * @throws MigrationException
258
     * @throws DatabaseException
259
     */
260
    private function prepareDownMigrations(?int $step = null)
261
    {
262
        $migratedEntries = $this->getMigratedEntries();
263
264
        if (empty($migratedEntries)) {
265
            throw MigrationException::nothingToMigrate();
266
        }
267
268
        foreach ($migratedEntries as $migratedEntry) {
269
            $exploded = explode('_', $migratedEntry['migration']);
270
            $this->migrations[array_pop($exploded)] = $this->migrationFolder . DS . $migratedEntry['migration'] . '.php';
271
        }
272
273
        if (!is_null($step)) {
274
            $this->migrations = array_slice($this->migrations, count($this->migrations) - $step, $step, true);
275
        }
276
277
        krsort($this->migrations);
278
    }
279
280
    /**
281
     * Gets migration files
282
     * @return array
283
     */
284
    private function getMigrationFiles(): array
285
    {
286
        $migrationsFiles = $this->fs->glob($this->migrationFolder . DS . '*.php');
287
288
        $migrations = [];
289
290
        if (!empty($migrationsFiles)) {
291
            foreach ($migrationsFiles as $migration) {
292
                $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

292
                $exploded = explode('_', /** @scrutinizer ignore-type */ pathinfo($migration, PATHINFO_FILENAME));
Loading history...
293
                $migrations[array_pop($exploded)] = $migration;
294
            }
295
        }
296
297
        return $migrations;
298
    }
299
300
    /**
301
     * Gets migrated entries from migrations table
302
     * @return array
303
     * @throws DatabaseException
304
     */
305
    private function getMigratedEntries(): array
306
    {
307
        return Database::query("SELECT * FROM " . MigrationTable::TABLE);
308
    }
309
310
    /**
311
     * Adds migrated entries to migrations table
312
     * @param array $entries
313
     * @throws DatabaseException
314
     */
315
    private function addMigratedEntries(array $entries)
316
    {
317
        foreach ($entries as $entry) {
318
            Database::execute('INSERT INTO ' . MigrationTable::TABLE . '(migration) VALUES(:migration)', ['migration' => $entry]);
319
        }
320
    }
321
322
    /**
323
     * Removes migrated entries from migrations table
324
     * @param array $entries
325
     * @throws DatabaseException
326
     */
327
    private function removeMigratedEntries(array $entries)
328
    {
329
        foreach ($entries as $entry) {
330
            Database::execute('DELETE FROM ' . MigrationTable::TABLE . ' WHERE migration=:migration', ['migration' => $entry]);
331
        }
332
    }
333
334
}
335