Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
100.00% |
93 / 93 |
|
100.00% |
13 / 13 |
CRAP | |
100.00% |
1 / 1 |
Migrate | |
100.00% |
93 / 93 |
|
100.00% |
13 / 13 |
32 | |
100.00% |
1 / 1 |
__construct | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
run | |
100.00% |
29 / 29 |
|
100.00% |
1 / 1 |
12 | |||
initMigration | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
2 | |||
mergeMigrations | |
100.00% |
15 / 15 |
|
100.00% |
1 / 1 |
3 | |||
getMigrated | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
migrate | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
setMigrated | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
lockTable | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
3 | |||
unlockTable | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
getMigrations | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
2 | |||
getMigrationFiles | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getTableQuery | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setOutput | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 |
1 | <?php |
2 | |
3 | declare(strict_types=1); |
4 | |
5 | namespace Engelsystem\Database\Migration; |
6 | |
7 | use Engelsystem\Application; |
8 | use Exception; |
9 | use Illuminate\Database\Query\Builder; |
10 | use Illuminate\Database\Schema\Blueprint; |
11 | use Illuminate\Database\Schema\Builder as SchemaBuilder; |
12 | use Illuminate\Support\Collection; |
13 | use Illuminate\Support\Str; |
14 | use Throwable; |
15 | |
16 | class Migrate |
17 | { |
18 | /** @var callable */ |
19 | protected $output; |
20 | |
21 | protected string $table = 'migrations'; |
22 | |
23 | /** |
24 | * Migrate constructor |
25 | */ |
26 | public function __construct(protected SchemaBuilder $schema, protected Application $app) |
27 | { |
28 | $this->output = function (): void { |
29 | }; |
30 | } |
31 | |
32 | /** |
33 | * Run a migration |
34 | */ |
35 | public function run( |
36 | string $path, |
37 | Direction $direction = Direction::UP, |
38 | bool $oneStep = false, |
39 | bool $forceMigration = false, |
40 | bool $prune = false |
41 | ): void { |
42 | if ($prune) { |
43 | ($this->output)('Dropping all tables'); |
44 | $this->schema->dropAllTables(); |
45 | |
46 | if ($direction == Direction::DOWN) { |
47 | return; |
48 | } |
49 | } |
50 | |
51 | $this->initMigration(); |
52 | |
53 | $this->lockTable($forceMigration); |
54 | $migrations = $this->mergeMigrations( |
55 | $this->getMigrations($path), |
56 | $this->getMigrated() |
57 | ); |
58 | |
59 | if ($direction === Direction::DOWN) { |
60 | $migrations = $migrations->reverse(); |
61 | } |
62 | |
63 | try { |
64 | foreach ($migrations as $migration) { |
65 | /** @var array $migration */ |
66 | $name = $migration['migration']; |
67 | |
68 | if ( |
69 | ($direction === Direction::UP && isset($migration['id'])) |
70 | || ($direction === Direction::DOWN && !isset($migration['id'])) |
71 | ) { |
72 | ($this->output)('Skipping ' . $name); |
73 | continue; |
74 | } |
75 | |
76 | ($this->output)('Migrating ' . $name . ' (' . $direction->value . ')'); |
77 | |
78 | if (isset($migration['path'])) { |
79 | $this->migrate($migration['path'], $name, $direction); |
80 | } |
81 | $this->setMigrated($name, $direction); |
82 | |
83 | if ($oneStep) { |
84 | break; |
85 | } |
86 | } |
87 | } catch (Throwable $e) { |
88 | $this->unlockTable(); |
89 | |
90 | throw $e; |
91 | } |
92 | |
93 | $this->unlockTable(); |
94 | } |
95 | |
96 | /** |
97 | * Setup migration tables |
98 | */ |
99 | public function initMigration(): void |
100 | { |
101 | if ($this->schema->hasTable($this->table)) { |
102 | return; |
103 | } |
104 | |
105 | $this->schema->create($this->table, function (Blueprint $table): void { |
106 | $table->increments('id'); |
107 | $table->string('migration'); |
108 | }); |
109 | } |
110 | |
111 | /** |
112 | * Merge file migrations with already migrated tables |
113 | */ |
114 | protected function mergeMigrations(Collection $migrations, Collection $migrated): Collection |
115 | { |
116 | $return = $migrated; |
117 | $return->transform(function ($migration) use ($migrations) { |
118 | $migration = (array) $migration; |
119 | if ($migrations->contains('migration', $migration['migration'])) { |
120 | $migration += $migrations |
121 | ->where('migration', $migration['migration']) |
122 | ->first(); |
123 | } |
124 | |
125 | return $migration; |
126 | }); |
127 | |
128 | $migrations->each(function ($migration) use ($return): void { |
129 | if ($return->contains('migration', $migration['migration'])) { |
130 | return; |
131 | } |
132 | |
133 | $return->add($migration); |
134 | }); |
135 | |
136 | return $return; |
137 | } |
138 | |
139 | /** |
140 | * Get all migrated migrations |
141 | */ |
142 | protected function getMigrated(): Collection |
143 | { |
144 | return $this->getTableQuery() |
145 | ->orderBy('id') |
146 | ->where('migration', '!=', 'lock') |
147 | ->get(); |
148 | } |
149 | |
150 | /** |
151 | * Migrate a migration |
152 | */ |
153 | protected function migrate(string $file, string $migration, Direction $direction = Direction::UP): void |
154 | { |
155 | require_once $file; |
156 | |
157 | $className = Str::studly(preg_replace('/\d+_/', '', $migration)); |
158 | /** @var Migration $class */ |
159 | $class = $this->app->make('Engelsystem\\Migrations\\' . $className); |
160 | |
161 | if (method_exists($class, $direction->value)) { |
162 | $class->{$direction->value}(); |
163 | } |
164 | } |
165 | |
166 | /** |
167 | * Set a migration to migrated |
168 | */ |
169 | protected function setMigrated(string $migration, Direction $direction = Direction::UP): void |
170 | { |
171 | $table = $this->getTableQuery(); |
172 | |
173 | if ($direction === Direction::DOWN) { |
174 | $table->where(['migration' => $migration])->delete(); |
175 | return; |
176 | } |
177 | |
178 | $table->insert(['migration' => $migration]); |
179 | } |
180 | |
181 | /** |
182 | * Lock the migrations table |
183 | * |
184 | * |
185 | * @throws Throwable |
186 | */ |
187 | protected function lockTable(bool $forceMigration = false): void |
188 | { |
189 | $this->schema->getConnection()->transaction(function () use ($forceMigration): void { |
190 | $lock = $this->getTableQuery() |
191 | ->where('migration', 'lock') |
192 | ->lockForUpdate() |
193 | ->first(); |
194 | |
195 | if ($lock && !$forceMigration) { |
196 | throw new Exception('Unable to acquire migration table lock'); |
197 | } |
198 | |
199 | $this->getTableQuery() |
200 | ->insert(['migration' => 'lock']); |
201 | }); |
202 | } |
203 | |
204 | /** |
205 | * Unlock a previously locked table |
206 | */ |
207 | protected function unlockTable(): void |
208 | { |
209 | $this->getTableQuery() |
210 | ->where('migration', 'lock') |
211 | ->delete(); |
212 | } |
213 | |
214 | /** |
215 | * Get a list of migration files |
216 | */ |
217 | protected function getMigrations(string $dir): Collection |
218 | { |
219 | $files = $this->getMigrationFiles($dir); |
220 | |
221 | $migrations = new Collection(); |
222 | foreach ($files as $dir) { |
223 | $name = str_replace('.php', '', basename($dir)); |
224 | $migrations[] = [ |
225 | 'migration' => $name, |
226 | 'path' => $dir, |
227 | ]; |
228 | } |
229 | |
230 | return $migrations->sortBy(function ($value) { |
231 | return $value['migration']; |
232 | }); |
233 | } |
234 | |
235 | /** |
236 | * List all migration files from the given directory |
237 | */ |
238 | protected function getMigrationFiles(string $dir): array |
239 | { |
240 | return glob($dir . '/*_*.php'); |
241 | } |
242 | |
243 | /** |
244 | * Init a table query |
245 | */ |
246 | protected function getTableQuery(): Builder |
247 | { |
248 | return $this->schema->getConnection()->table($this->table); |
249 | } |
250 | |
251 | /** |
252 | * Set the output function |
253 | */ |
254 | public function setOutput(callable $output): void |
255 | { |
256 | $this->output = $output; |
257 | } |
258 | } |