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