Completed
Branch feature/pre-split (67216b)
by Anton
03:28
created

Migrator::rollback()   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 34
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 15
nc 4
nop 1
dl 0
loc 34
rs 8.5806
c 0
b 0
f 0
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\Exceptions\MigrationException;
17
use Spiral\Migrations\Migration\State;
18
19
/**
20
 * MigrationManager component.
21
 */
22
class Migrator extends Component implements SingletonInterface
23
{
24
    /**
25
     * @var MigrationsConfig
26
     */
27
    private $config = null;
28
29
    /**
30
     * @invisible
31
     * @var DatabaseManager
32
     */
33
    protected $dbal = null;
34
35
    /**
36
     * @invisible
37
     * @var RepositoryInterface
38
     */
39
    protected $repository = null;
40
41
    /**
42
     * @param MigrationsConfig    $config
43
     * @param DatabaseManager     $dbal
44
     * @param RepositoryInterface $repository
45
     */
46
    public function __construct(
47
        MigrationsConfig $config,
48
        DatabaseManager $dbal,
49
        RepositoryInterface $repository
50
    ) {
51
        $this->config = $config;
52
        $this->dbal = $dbal;
53
        $this->repository = $repository;
54
    }
55
56
    /**
57
     * {@inheritdoc}
58
     */
59
    public function isConfigured(): bool
60
    {
61
        return $this->stateTable()->exists();
62
    }
63
64
    /**
65
     * {@inheritdoc}
66
     */
67
    public function configure()
68
    {
69
        if ($this->isConfigured()) {
70
            return;
71
        }
72
73
        //Migrations table is pretty simple.
74
        $schema = $this->stateTable()->getSchema();
75
76
        /*
77
         * Schema update will automatically sync all needed data
78
         */
79
        $schema->primary('id');
80
        $schema->string('migration', 255)->nullable(false);
81
        $schema->datetime('time_executed')->datetime();
82
        $schema->index(['migration']);
83
84
        $schema->save();
85
    }
86
87
    /**
88
     * @return RepositoryInterface
89
     */
90
    public function getRepository(): RepositoryInterface
91
    {
92
        return $this->repository;
93
    }
94
95
    /**
96
     * Get every available migration with valid meta information.
97
     *
98
     * @return MigrationInterface[]
99
     */
100
    public function getMigrations(): array
101
    {
102
        $result = [];
103
        foreach ($this->repository->getMigrations() as $migration) {
104
            //Populating migration status and execution time (if any)
105
            $result[] = $migration->withState($this->resolveStatus($migration->getState()));
106
        }
107
108
        return $result;
109
    }
110
111
    /**
112
     * Execute one migration and return it's instance.
113
     *
114
     * @param CapsuleInterface $capsule Default capsule to be used if none given.
115
     *
116
     * @return MigrationInterface|null
117
     */
118
    public function run(CapsuleInterface $capsule = null)
119
    {
120
        $capsule = $capsule ?? new MigrationCapsule($this->dbal);
121
122
        if (!$this->isConfigured()) {
123
            throw new MigrationException("Unable to run migration, Migrator not configured");
124
        }
125
126
        /**
127
         * @var MigrationInterface $migration
128
         */
129
        foreach ($this->getMigrations() as $migration) {
130
            if ($migration->getState()->getStatus() != State::STATUS_PENDING) {
131
                continue;
132
            }
133
134
            //Isolate migration commands in a capsule
135
            $migration = $migration->withCapsule($capsule);
136
137
            //Executing migration inside global transaction
138
            $this->execute(function () use ($migration) {
139
                $migration->up();
140
            });
141
142
            //Registering record in database
143
            $this->stateTable()->insertOne([
144
                'migration'     => $migration->getState()->getName(),
145
                'time_executed' => new \DateTime('now')
146
            ]);
147
148
            return $migration;
149
        }
150
151
        return null;
152
    }
153
154
    /**
155
     * Rollback last migration and return it's instance.
156
     *
157
     * @param CapsuleInterface $capsule Default capsule to be used if none given.
158
     *
159
     * @return MigrationInterface|null
160
     */
161
    public function rollback(CapsuleInterface $capsule = null)
162
    {
163
        $capsule = $capsule ?? new MigrationCapsule($this->dbal);
164
165
        if (!$this->isConfigured()) {
166
            throw new MigrationException("Unable to run migration, Migrator not configured");
167
        }
168
169
        /**
170
         * @var MigrationInterface $migration
171
         */
172
        foreach (array_reverse($this->getMigrations()) as $migration) {
173
            if ($migration->getState()->getStatus() != State::STATUS_EXECUTED) {
174
                continue;
175
            }
176
177
            //Isolate migration commands in a capsule
178
            $migration = $migration->withCapsule($capsule);
179
180
            //Executing migration inside global transaction
181
            $this->execute(function () use ($migration) {
182
                $migration->down();
183
            });
184
185
            //Flushing DB record
186
            $this->stateTable()->delete([
187
                'migration' => $migration->getState()->getName()
188
            ])->run();
189
190
            return $migration;
191
        }
192
193
        return null;
194
    }
195
196
    /**
197
     * Migration table, all migration information will be stored in it.
198
     *
199
     * @return Table
200
     */
201
    protected function stateTable(): Table
202
    {
203
        return $this->dbal->database(
204
            $this->config->getDatabase()
205
        )->table(
206
            $this->config->getTable()
207
        );
208
    }
209
210
    /**
211
     * Clarify migration state with valid status and execution time
212
     *
213
     * @param State $meta
214
     *
215
     * @return State
216
     */
217
    protected function resolveStatus(State $meta)
218
    {
219
        //Fetch migration information from database
220
        $state = $this->stateTable()
1 ignored issue
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Spiral\Database\Builders\Prototypes\AbstractWhere as the method run() does only exist in the following sub-classes of Spiral\Database\Builders\Prototypes\AbstractWhere: Spiral\Database\Builders\DeleteQuery, Spiral\Database\Builders\Prototypes\AbstractAffect, Spiral\Database\Builders\SelectQuery, Spiral\Database\Builders\UpdateQuery. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
221
            ->select('id', 'time_executed')
222
            ->where(['migration' => $meta->getName()])
223
            ->run()
224
            ->fetch();
225
226
        if (empty($state['time_executed'])) {
227
            return $meta->withStatus(State::STATUS_PENDING);
228
        }
229
230
        return $meta->withStatus(
231
            State::STATUS_EXECUTED,
232
            new \DateTime(
233
                $state['time_executed'],
234
                $this->stateTable()->getDatabase()->getDriver()->getTimezone()
235
            )
236
        );
237
    }
238
239
    /**
240
     * Run given code under transaction open for every driver.
241
     *
242
     * @param \Closure $closure
243
     *
244
     * @throws \Throwable
245
     */
246
    protected function execute(\Closure $closure)
247
    {
248
        $this->beginTransactions();
249
        try {
250
            call_user_func($closure);
251
        } catch (\Throwable $e) {
252
            $this->rollbackTransactions();
253
            throw $e;
254
        }
255
256
        $this->commitTransactions();
257
    }
258
259
    /**
260
     * Begin transaction for every available driver (we don't know what database migration related
261
     * to).
262
     */
263
    protected function beginTransactions()
264
    {
265
        foreach ($this->getDrivers() as $driver) {
266
            $driver->beginTransaction();
267
        }
268
    }
269
270
    /**
271
     * Rollback transaction for every available driver.
272
     */
273
    protected function rollbackTransactions()
274
    {
275
        foreach ($this->getDrivers() as $driver) {
276
            $driver->rollbackTransaction();
277
        }
278
    }
279
280
    /**
281
     * Commit transaction for every available driver.
282
     */
283
    protected function commitTransactions()
284
    {
285
        foreach ($this->getDrivers() as $driver) {
286
            $driver->commitTransaction();
287
        }
288
    }
289
290
    /**
291
     * Get all available drivers.
292
     *
293
     * @return Driver[]
294
     */
295
    protected function getDrivers(): array
296
    {
297
        $drivers = [];
298
        foreach ($this->dbal->getDatabases() as $database) {
299
            $driver = $database->getDriver();
300
            if (!isset($drivers["{$driver->getName()}.{$driver->getSource()}"])) {
301
                $drivers["{$driver->getName()}.{$driver->getSource()}"] = $database->getDriver();
302
            }
303
        }
304
305
        return $drivers;
306
    }
307
}