Completed
Push — master ( 65c32c...b73e64 )
by Neomerx
03:41
created

BaseMigrationRunner::createMigration()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 13
ccs 6
cts 6
cp 1
rs 9.8333
c 0
b 0
f 0
cc 2
nc 2
nop 2
crap 2
1
<?php namespace Limoncello\Application\Data;
2
3
/**
4
 * Copyright 2015-2018 [email protected]
5
 *
6
 * Licensed under the Apache License, Version 2.0 (the "License");
7
 * you may not use this file except in compliance with the License.
8
 * You may obtain a copy of the License at
9
 *
10
 * http://www.apache.org/licenses/LICENSE-2.0
11
 *
12
 * Unless required by applicable law or agreed to in writing, software
13
 * distributed under the License is distributed on an "AS IS" BASIS,
14
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
 * See the License for the specific language governing permissions and
16
 * limitations under the License.
17
 */
18
19
use DateTimeImmutable;
20
use Doctrine\DBAL\Connection;
21
use Doctrine\DBAL\DBALException;
22
use Doctrine\DBAL\Exception\InvalidArgumentException;
23
use Doctrine\DBAL\Schema\AbstractSchemaManager;
24
use Doctrine\DBAL\Schema\Table;
25
use Doctrine\DBAL\Types\Type;
26
use Error;
27
use Exception;
28
use Generator;
29
use Limoncello\Contracts\Commands\IoInterface;
30
use Limoncello\Contracts\Data\MigrationInterface;
31
use Psr\Container\ContainerExceptionInterface;
32
use Psr\Container\ContainerInterface;
33
use Psr\Container\NotFoundExceptionInterface;
34
35
/**
36
 * @package Limoncello\Application
37
 *
38
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
39
 */
40
abstract class BaseMigrationRunner
41
{
42
    /** Migrations table name */
43
    const MIGRATIONS_TABLE = '_migrations';
44
45
    /** Migration column name */
46
    const MIGRATIONS_COLUMN_ID = 'id';
47
48
    /** Migration column name */
49
    const MIGRATIONS_COLUMN_CLASS = 'class';
50
51
    /** Migration column name */
52
    const MIGRATIONS_COLUMN_MIGRATED_AT = 'migrated_at';
53
54
    /** Seeds table name */
55
    const SEEDS_TABLE = '_seeds';
56
57
    /**
58
     * @var IoInterface
59
     */
60
    private $inOut;
61
62
    /**
63
     * @return string[]
64
     */
65
    abstract protected function getMigrationClasses(): array;
66
67
    /**
68
     * @param IoInterface $inOut
69
     */
70 2
    public function __construct(IoInterface $inOut)
71
    {
72 2
        $this->setIO($inOut);
73
    }
74
75
    /**
76
     * @param ContainerInterface $container
77
     *
78
     * @return void
79
     *
80
     * @throws ContainerExceptionInterface
81
     * @throws NotFoundExceptionInterface
82
     * @throws DBALException
83
     */
84 1
    public function migrate(ContainerInterface $container): void
85
    {
86 1
        foreach ($this->getMigrations($container) as $class) {
87 1
            assert(is_string($class));
88 1
            $this->getIO()->writeInfo("Starting migration for `$class`..." . PHP_EOL, IoInterface::VERBOSITY_VERBOSE);
89 1
            if (($migration = $this->createMigration($class, $container)) !== null) {
90 1
                $migration->init($container)->migrate();
91 1
                $this->getIO()->writeInfo("Migration finished for `$class`." . PHP_EOL, IoInterface::VERBOSITY_NORMAL);
92
            }
93
        }
94
    }
95
96
    /**
97
     * @param ContainerInterface $container
98
     *
99
     * @return void
100
     *
101
     * @throws ContainerExceptionInterface
102
     * @throws InvalidArgumentException
103
     * @throws NotFoundExceptionInterface
104
     * @throws DBALException
105
     */
106 1
    public function rollback(ContainerInterface $container): void
107
    {
108 1
        foreach ($this->getRollbacks($container) as $class) {
109 1
            assert(is_string($class));
110 1
            $this->getIO()->writeInfo("Starting rollback for `$class`..." . PHP_EOL, IoInterface::VERBOSITY_VERBOSE);
111 1
            if (($migration = $this->createMigration($class, $container)) !== null) {
112 1
                $migration->init($container)->rollback();
113 1
                $this->getIO()->writeInfo("Rollback finished for `$class`." . PHP_EOL, IoInterface::VERBOSITY_NORMAL);
114
            }
115
        }
116
117 1
        $manager = $this->getConnection($container)->getSchemaManager();
118 1
        if ($manager->tablesExist([static::MIGRATIONS_TABLE]) === true) {
119 1
            $manager->dropTable(static::MIGRATIONS_TABLE);
120
        }
121 1
        if ($manager->tablesExist([static::SEEDS_TABLE]) === true) {
122 1
            $manager->dropTable(static::SEEDS_TABLE);
123
        }
124
    }
125
126
    /**
127
     * @return IoInterface
128
     */
129 2
    protected function getIO(): IoInterface
130
    {
131 2
        return $this->inOut;
132
    }
133
134
    /**
135
     * @param IoInterface $inOut
136
     *
137
     * @return self
138
     */
139 3
    private function setIO(IoInterface $inOut): self
140
    {
141 3
        $this->inOut = $inOut;
142
143 3
        return $this;
144
    }
145
146
    /**
147
     * @param ContainerInterface $container
148
     *
149
     * @return Generator
150
     *
151
     * @throws ContainerExceptionInterface
152
     * @throws NotFoundExceptionInterface
153
     *
154
     * @SuppressWarnings(PHPMD.ElseExpression)
155
     * @throws DBALException
156
     */
157 1
    private function getMigrations(ContainerInterface $container): Generator
158
    {
159 1
        $connection = $this->getConnection($container);
160 1
        $manager    = $connection->getSchemaManager();
161
162 1
        if ($manager->tablesExist([static::MIGRATIONS_TABLE]) === true) {
163 1
            $migrated = $this->readMigrated($connection);
164
        } else {
165 1
            $this->createMigrationsTable($manager);
166 1
            $migrated = [];
167
        }
168
169 1
        $notYetMigrated = array_diff($this->getMigrationClasses(), $migrated);
170
171 1
        foreach ($notYetMigrated as $class) {
172 1
            yield $class;
173 1
            $this->saveMigration($connection, $class);
174
        }
175
    }
176
177
    /**
178
     * @param ContainerInterface $container
179
     *
180
     * @return Generator
181
     *
182
     * @throws ContainerExceptionInterface
183
     * @throws NotFoundExceptionInterface
184
     * @throws InvalidArgumentException
185
     * @throws DBALException
186
     */
187 1
    private function getRollbacks(ContainerInterface $container): Generator
188
    {
189 1
        $connection = $this->getConnection($container);
190 1
        $migrated   = $this->readMigrated($connection);
191
192 1
        foreach (array_reverse($migrated, true) as $index => $class) {
193 1
            yield $class;
194 1
            $this->removeMigration($connection, $index);
195
        }
196
    }
197
198
    /**
199
     * @param ContainerInterface $container
200
     *
201
     * @return Connection
202
     *
203
     * @throws ContainerExceptionInterface
204
     * @throws NotFoundExceptionInterface
205
     */
206 1
    private function getConnection(ContainerInterface $container): Connection
207
    {
208 1
        return $container->get(Connection::class);
209
    }
210
211
    /**
212
     * @param AbstractSchemaManager $manager
213
     *
214
     * @return void
215
     *
216
     * @throws DBALException
217
     */
218 1
    private function createMigrationsTable(AbstractSchemaManager $manager): void
219
    {
220 1
        $table = new Table(static::MIGRATIONS_TABLE);
221
222
        $table
223 1
            ->addColumn(static::MIGRATIONS_COLUMN_ID, Type::INTEGER)
224 1
            ->setUnsigned(true)
225 1
            ->setAutoincrement(true);
226
        $table
227 1
            ->addColumn(static::MIGRATIONS_COLUMN_CLASS, Type::STRING)
228 1
            ->setLength(255);
229
        $table
230 1
            ->addColumn(static::MIGRATIONS_COLUMN_MIGRATED_AT, Type::DATETIME);
231
232 1
        $table->setPrimaryKey([static::MIGRATIONS_COLUMN_ID]);
233 1
        $table->addUniqueIndex([static::MIGRATIONS_COLUMN_CLASS]);
234
235 1
        $manager->createTable($table);
236
    }
237
238
    /**
239
     * @param Connection $connection
240
     *
241
     * @return array
242
     */
243 1
    private function readMigrated(Connection $connection): array
244
    {
245 1
        $builder  = $connection->createQueryBuilder();
246 1
        $migrated = [];
247
248 1
        if ($connection->getSchemaManager()->tablesExist([static::MIGRATIONS_TABLE]) === true) {
249
            $migrations = $builder
250 1
                ->select(static::MIGRATIONS_COLUMN_ID, static::MIGRATIONS_COLUMN_CLASS)
251 1
                ->from(static::MIGRATIONS_TABLE)
252 1
                ->orderBy(static::MIGRATIONS_COLUMN_ID)
253 1
                ->execute()
254 1
                ->fetchAll();
255 1
            foreach ($migrations as $migration) {
256 1
                $index            = $migration[static::MIGRATIONS_COLUMN_ID];
257 1
                $class            = $migration[static::MIGRATIONS_COLUMN_CLASS];
258 1
                $migrated[$index] = $class;
259
            }
260
        }
261
262 1
        return $migrated;
263
    }
264
265
    /**
266
     * @param Connection $connection
267
     * @param string $class
268
     *
269
     * @return void
270
     *
271
     * @throws DBALException
272
     * @throws Exception
273
     */
274 1
    private function saveMigration(Connection $connection, string $class): void
275
    {
276 1
        $format = $connection->getSchemaManager()->getDatabasePlatform()->getDateTimeFormatString();
277 1
        $now    = (new DateTimeImmutable())->format($format);
278 1
        $connection->insert(static::MIGRATIONS_TABLE, [
279 1
            static::MIGRATIONS_COLUMN_CLASS       => $class,
280 1
            static::MIGRATIONS_COLUMN_MIGRATED_AT => $now,
281
        ]);
282
    }
283
284
    /**
285
     * @param Connection $connection
286
     * @param int $index
287
     *
288
     * @return void
289
     *
290
     * @throws InvalidArgumentException
291
     *
292
     * @throws DBALException
293
     */
294 1
    private function removeMigration(Connection $connection, int $index): void
295
    {
296 1
        $connection->delete(static::MIGRATIONS_TABLE, [static::MIGRATIONS_COLUMN_ID => $index]);
297
    }
298
299
    /**
300
     * @param string             $class
301
     * @param ContainerInterface $container
302
     *
303
     * @return MigrationInterface|null
304
     */
305 2
    private function createMigration(string $class, ContainerInterface $container): ?MigrationInterface
306
    {
307 2
        $migration = null;
308
309
        try {
310
            /** @var MigrationInterface $migration */
311 2
            $migration = new $class($container);
312 1
        } catch (Error $exception) {
313 1
            $this->getIO()->writeWarning("Migration `$class` not found." . PHP_EOL);
314
        }
315
316 2
        return $migration;
317
    }
318
}
319