Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
93 / 93
100.00% covered (success)
100.00%
13 / 13
CRAP
100.00% covered (success)
100.00%
1 / 1
Migrate
100.00% covered (success)
100.00%
93 / 93
100.00% covered (success)
100.00%
13 / 13
32
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%
29 / 29
100.00% covered (success)
100.00%
1 / 1
12
 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
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    /**
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}