Completed
Branch feature/pre-split (e801ec)
by Anton
03:11
created

Migrator::rollback()   B

Complexity

Conditions 3
Paths 3

Size

Total Lines 30
Code Lines 13

Duplication

Lines 30
Ratio 100 %

Importance

Changes 0
Metric Value
dl 30
loc 30
c 0
b 0
f 0
cc 3
eloc 13
nc 3
nop 1
rs 8.8571
1
<?php
2
/**
3
 * Spiral Framework.
4
 *
5
 * @license   MIT
6
 * @author    Anton Titov (Wolfy-J)
7
 */
8
namespace Spiral\Migrations;
9
10
use Spiral\Core\Component;
11
use Spiral\Core\Container\SingletonInterface;
12
use Spiral\Database\DatabaseManager;
13
use Spiral\Database\Entities\Driver;
14
use Spiral\Database\Entities\Table;
15
use Spiral\Migrations\Configs\MigrationsConfig;
16
use Spiral\Migrations\Migration\State;
17
18
/**
19
 * MigrationManager component.
20
 */
21
class Migrator extends Component implements SingletonInterface
22
{
23
    /**
24
     * @var MigrationsConfig
25
     */
26
    private $config = null;
27
28
    /**
29
     * @invisible
30
     * @var DatabaseManager
31
     */
32
    private $dbal = null;
33
34
    /**
35
     * @invisible
36
     * @var RepositoryInterface
37
     */
38
    protected $repository = null;
39
40
    /**
41
     * @param MigrationsConfig    $config
42
     * @param DatabaseManager     $dbal
43
     * @param RepositoryInterface $repository
44
     */
45
    public function __construct(
46
        MigrationsConfig $config,
47
        DatabaseManager $dbal,
48
        RepositoryInterface $repository
49
    ) {
50
        $this->config = $config;
51
        $this->dbal = $dbal;
52
        $this->repository = $repository;
53
    }
54
55
    /**
56
     * {@inheritdoc}
57
     */
58
    public function isConfigured(): bool
59
    {
60
        return $this->stateTable()->exists();
61
    }
62
63
    /**
64
     * {@inheritdoc}
65
     */
66
    public function configure()
67
    {
68
        if ($this->isConfigured()) {
69
            return;
70
        }
71
72
        //Migrations table is pretty simple.
73
        $schema = $this->stateTable()->getSchema();
74
75
        /*
76
         * Schema update will automatically sync all needed data
77
         */
78
        $schema->column('id')->primary();
0 ignored issues
show
Bug introduced by
The method column() does not exist on Spiral\Database\Schemas\TableInterface. Did you maybe mean hasColumn()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
79
        $schema->column('migration')->string(255);
0 ignored issues
show
Bug introduced by
The method column() does not exist on Spiral\Database\Schemas\TableInterface. Did you maybe mean hasColumn()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
80
        $schema->column('time_executed')->datetime();
0 ignored issues
show
Bug introduced by
The method column() does not exist on Spiral\Database\Schemas\TableInterface. Did you maybe mean hasColumn()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
81
        $schema->index(['migration']);
82
83
        $schema->save();
84
    }
85
86
    /**
87
     * @return RepositoryInterface
88
     */
89
    public function getRepository(): RepositoryInterface
90
    {
91
        return $this->repository;
92
    }
93
94
    /**
95
     * Get every available migration with valid meta information.
96
     *
97
     * @return MigrationInterface[]
98
     */
99
    public function getMigrations(): array
100
    {
101
        $result = [];
102
        foreach ($this->repository->getMigrations() as $migration) {
103
            //Populating migration status and execution time (if any)
104
            $result[] = $migration->withState($this->resolveStatus($migration->getState()));
105
        }
106
107
        return $result;
108
    }
109
110
    /**
111
     * Execute one migration and return it's instance.
112
     *
113
     * @param CapsuleInterface $capsule Default capsule to be used if none given.
114
     *
115
     * @return MigrationInterface|null
116
     */
117 View Code Duplication
    public function run(CapsuleInterface $capsule = null)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
118
    {
119
        $capsule = $capsule ?? new MigrationCapsule($this->dbal);
120
121
        /**
122
         * @var MigrationInterface $migration
123
         */
124
        foreach ($this->getMigrations() as $migration) {
125
            if ($migration->getState()->getStatus() != State::STATUS_PENDING) {
126
                continue;
127
            }
128
129
            //Isolate migration commands in a capsule
130
            $migration = $migration->withCapsule($capsule);
131
132
            //Executing migration inside global transaction
133
            $this->execute(function () use ($migration) {
134
                $migration->up();
135
            });
136
137
            //Registering record in database
138
            $this->stateTable()->insert([
139
                'migration'     => $migration->getState()->getName(),
140
                'time_executed' => new \DateTime('now')
141
            ]);
142
143
            return $migration;
144
        }
145
146
        return null;
147
    }
148
149
    /**
150
     * Rollback last migration and return it's instance.
151
     *
152
     * @param CapsuleInterface $capsule Default capsule to be used if none given.
153
     *
154
     * @return MigrationInterface|null
155
     */
156 View Code Duplication
    public function rollback(CapsuleInterface $capsule = null)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
157
    {
158
        $capsule = $capsule ?? new MigrationCapsule($this->dbal);
159
160
        /**
161
         * @var MigrationInterface $migration
162
         */
163
        foreach (array_reverse($this->getMigrations()) as $migration) {
164
            if ($migration->getState()->getStatus() != State::STATUS_EXECUTED) {
165
                continue;
166
            }
167
168
            //Isolate migration commands in a capsule
169
            $migration = $migration->withCapsule($capsule);
170
171
            //Executing migration inside global transaction
172
            $this->execute(function () use ($migration) {
173
                $migration->down();
174
            });
175
176
            //Flushing DB record
177
            $this->stateTable()->delete([
178
                'migration' => $migration->getState()->getName()
179
            ])->run();
180
181
            return $migration;
182
        }
183
184
        return null;
185
    }
186
187
    /**
188
     * Migration table, all migration information will be stored in it.
189
     *
190
     * @return Table
191
     */
192
    protected function stateTable(): Table
193
    {
194
        return $this->dbal->database($this->config->getDatabase())->table($this->config->getTable());
195
    }
196
197
    /**
198
     * Clarify migration state with valid status and execution time
199
     *
200
     * @param State $meta
201
     *
202
     * @return State
203
     */
204
    protected function resolveStatus(State $meta)
205
    {
206
        //Fetch migration information from database
207
        $state = $this->stateTable()->select('id', 'time_executed')->where([
208
            'migration' => $meta->getName()
209
        ])->run()->fetch();
210
211
        if (empty($state['time_executed'])) {
212
            return $meta->withStatus(State::STATUS_PENDING);
213
        }
214
215
        return $meta->withStatus(
216
            State::STATUS_EXECUTED,
217
            new \DateTime(
218
                $state['time_executed'],
219
                $this->stateTable()->getDatabase()->getDriver()->getTimezone()
220
            )
221
        );
222
    }
223
224
    /**
225
     * Run given code under transaction open for every driver.
226
     *
227
     * @param \Closure $closure
228
     *
229
     * @throws \Throwable
230
     */
231
    protected function execute(\Closure $closure)
232
    {
233
        $this->beginTransactions();
234
        try {
235
            call_user_func($closure);
236
        } catch (\Throwable $e) {
237
            $this->rollbackTransactions();
238
            throw $e;
239
        }
240
241
        $this->commitTransactions();
242
    }
243
244
    /**
245
     * Begin transaction for every available driver (we don't know what database migration related
246
     * to).
247
     */
248
    protected function beginTransactions()
249
    {
250
        foreach ($this->getDrivers() as $driver) {
251
            $driver->beginTransaction();
252
        }
253
    }
254
255
    /**
256
     * Rollback transaction for every available driver.
257
     */
258
    protected function rollbackTransactions()
259
    {
260
        foreach ($this->getDrivers() as $driver) {
261
            $driver->rollbackTransaction();
262
        }
263
    }
264
265
    /**
266
     * Commit transaction for every available driver.
267
     */
268
    protected function commitTransactions()
269
    {
270
        foreach ($this->getDrivers() as $driver) {
271
            $driver->commitTransaction();
272
        }
273
    }
274
275
    /**
276
     * Get all available drivers.
277
     *
278
     * @return Driver[]
279
     */
280
    protected function getDrivers()
281
    {
282
        $drivers = [];
283
284
        foreach ($this->dbal->getDatabases() as $database) {
285
            if (!in_array($database->getDriver(), $drivers, true)) {
286
                $drivers[] = $database->getDriver();
287
            }
288
        }
289
290
        return $drivers;
291
    }
292
}