Passed
Pull Request — master (#878)
by Asmir
02:42
created

TableMetadataStorage::getExecutedMigrations()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 30
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 20
CRAP Score 5

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 5
eloc 19
nc 5
nop 0
dl 0
loc 30
ccs 20
cts 20
cp 1
crap 5
rs 9.3222
c 1
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Doctrine\Migrations\Metadata\Storage;
6
7
use DateTimeImmutable;
8
use Doctrine\DBAL\Connection;
9
use Doctrine\DBAL\Connections\MasterSlaveConnection;
10
use Doctrine\DBAL\Platforms\AbstractPlatform;
11
use Doctrine\DBAL\Schema\AbstractSchemaManager;
12
use Doctrine\DBAL\Schema\Comparator;
13
use Doctrine\DBAL\Schema\Table;
14
use Doctrine\DBAL\Schema\TableDiff;
15
use Doctrine\DBAL\Types\Type;
16
use Doctrine\Migrations\Exception\MetadataStorageError;
17
use Doctrine\Migrations\Metadata\AvailableMigration;
18
use Doctrine\Migrations\Metadata\ExecutedMigration;
19
use Doctrine\Migrations\Metadata\ExecutedMigrationsSet;
20
use Doctrine\Migrations\MigrationRepository;
21
use Doctrine\Migrations\Version\Direction;
22
use Doctrine\Migrations\Version\ExecutionResult;
23
use Doctrine\Migrations\Version\Version;
24
use InvalidArgumentException;
25
use const CASE_LOWER;
26
use function array_change_key_case;
27
use function floatval;
28
use function round;
29
use function sprintf;
30
use function strlen;
31
use function strpos;
32
use function strtolower;
33
34
final class TableMetadataStorage implements MetadataStorage
35
{
36
    /** @var Connection */
37
    private $connection;
38
39
    /** @var AbstractSchemaManager */
40
    private $schemaManager;
41
42
    /** @var AbstractPlatform */
43
    private $platform;
44
45
    /** @var TableMetadataStorageConfiguration */
46
    private $configuration;
47
48
    /** @var MigrationRepository|null */
49
    private $migrationRepository;
50
51 65
    public function __construct(
52
        Connection $connection,
53
        ?MetadataStorageConfiguration $configuration = null,
54
        ?MigrationRepository $migrationRepository = null
55
    ) {
56 65
        $this->migrationRepository = $migrationRepository;
57 65
        $this->connection          = $connection;
58 65
        $this->schemaManager       = $connection->getSchemaManager();
59 65
        $this->platform            = $connection->getDatabasePlatform();
60
61 65
        if ($configuration !== null && ! ($configuration instanceof TableMetadataStorageConfiguration)) {
62
            throw new InvalidArgumentException(sprintf('%s accepts only %s as configuration', self::class, TableMetadataStorageConfiguration::class));
63
        }
64 65
        $this->configuration = $configuration ?: new TableMetadataStorageConfiguration();
0 ignored issues
show
introduced by
$configuration is of type null, thus it always evaluated to false.
Loading history...
65 65
    }
66
67 53
    public function getExecutedMigrations() : ExecutedMigrationsSet
68
    {
69 53
        $this->checkInitialization();
70 50
        $rows = $this->connection->fetchAll(sprintf('SELECT * FROM %s', $this->configuration->getTableName()));
71
72 50
        $migrations = [];
73 50
        foreach ($rows as $row) {
74 32
            $row = array_change_key_case($row, CASE_LOWER);
75
76 32
            $version = new Version($row[strtolower($this->configuration->getVersionColumnName())]);
77
78 32
            $executedAt = $row[strtolower($this->configuration->getExecutedAtColumnName())] ?? '';
79 32
            $executedAt = $executedAt !== ''
80 3
                ? DateTimeImmutable::createFromFormat($this->platform->getDateTimeFormatString(), $executedAt)
81 32
                : null;
82
83 32
            $executionTime = isset($row[strtolower($this->configuration->getExecutionTimeColumnName())])
84 3
                ? floatval($row[strtolower($this->configuration->getExecutionTimeColumnName())]/1000)
85 32
                : null;
86
87 32
            $migration = new ExecutedMigration(
88 32
                $version,
89 32
                $executedAt instanceof DateTimeImmutable ? $executedAt : null,
90 32
                $executionTime
91
            );
92
93 32
            $migrations[(string) $version] = $migration;
94
        }
95
96 50
        return new ExecutedMigrationsSet($migrations);
97
    }
98
99 2
    public function reset() : void
100
    {
101 2
        $this->checkInitialization();
102
103 2
        $this->connection->executeUpdate(
104 2
            sprintf(
105 2
                'DELETE FROM %s WHERE 1 = 1',
106 2
                $this->platform->quoteIdentifier($this->configuration->getTableName())
107
            )
108
        );
109 2
    }
110
111 35
    public function complete(ExecutionResult $result) : void
112
    {
113 35
        $this->checkInitialization();
114
115 35
        if ($result->getDirection() === Direction::DOWN) {
116 5
            $this->connection->delete($this->configuration->getTableName(), [
117 5
                $this->configuration->getVersionColumnName() => (string) $result->getVersion(),
118
            ]);
119
        } else {
120 35
            $this->connection->insert($this->configuration->getTableName(), [
121 35
                $this->configuration->getVersionColumnName() => (string) $result->getVersion(),
122 35
                $this->configuration->getExecutedAtColumnName() => $result->getExecutedAt(),
123 35
                $this->configuration->getExecutionTimeColumnName() => $result->getTime() === null ? null : round($result->getTime()*1000),
124
            ], [
125 35
                Type::STRING,
126
                Type::DATETIME,
127
                Type::INTEGER,
128
            ]);
129
        }
130 35
    }
131
132 62
    public function ensureInitialized() : void
133
    {
134 62
        $expectedSchemaChangelog = $this->getExpectedTable();
135
136 62
        if (! $this->isInitialized($expectedSchemaChangelog)) {
137 60
            $this->schemaManager->createTable($expectedSchemaChangelog);
138
139 60
            return;
140
        }
141
142 4
        $diff = $this->needsUpdate($expectedSchemaChangelog);
143 4
        if ($diff === null) {
144 2
            return;
145
        }
146
147 2
        $this->schemaManager->alterTable($diff);
148 2
        $this->updateMigratedVersionsFromV1orV2toV3();
149 2
    }
150
151 57
    private function needsUpdate(Table $expectedTable) : ?TableDiff
152
    {
153 57
        $comparator   = new Comparator();
154 57
        $currentTable = $this->schemaManager->listTableDetails($this->configuration->getTableName());
155 57
        $diff         = $comparator->diffTable($currentTable, $expectedTable);
156
157 57
        return $diff instanceof TableDiff ? $diff : null;
158
    }
159
160 64
    private function isInitialized(Table $expectedTable) : bool
161
    {
162 64
        if ($this->connection instanceof MasterSlaveConnection) {
163
            $this->connection->connect('master');
164
        }
165
166 64
        return $this->schemaManager->tablesExist([$expectedTable->getName()]);
167
    }
168
169 58
    private function checkInitialization() : void
170
    {
171 58
        $expectedTable = $this->getExpectedTable();
172
173 58
        if (! $this->isInitialized($expectedTable)) {
174 2
            throw MetadataStorageError::notInitialized();
175
        }
176
177 56
        if ($this->needsUpdate($expectedTable)!== null) {
178 1
            throw MetadataStorageError::notUpToDate();
179
        }
180 55
    }
181
182 64
    private function getExpectedTable() : Table
183
    {
184 64
        $schemaChangelog = new Table($this->configuration->getTableName());
185
186 64
        $schemaChangelog->addColumn(
187 64
            $this->configuration->getVersionColumnName(),
188 64
            'string',
189 64
            ['notnull' => true, 'length' => $this->configuration->getVersionColumnLength()]
190
        );
191 64
        $schemaChangelog->addColumn($this->configuration->getExecutedAtColumnName(), 'datetime', ['notnull' => false]);
192 64
        $schemaChangelog->addColumn($this->configuration->getExecutionTimeColumnName(), 'integer', ['notnull' => false]);
193
194 64
        $schemaChangelog->setPrimaryKey([$this->configuration->getVersionColumnName()]);
195
196 64
        return $schemaChangelog;
197
    }
198
199 2
    private function updateMigratedVersionsFromV1orV2toV3() : void
200
    {
201 2
        if ($this->migrationRepository === null) {
202 1
            return;
203
        }
204
205 1
        $availableMigrations = $this->migrationRepository->getMigrations()->getItems();
206 1
        $executedMigrations  = $this->getExecutedMigrations()->getItems();
207
208 1
        foreach ($availableMigrations as $availableMigration) {
209 1
            foreach ($executedMigrations as $k => $executedMigration) {
210 1
                if ($this->isAlreadyV3Format($availableMigration, $executedMigration)) {
211 1
                    continue;
212
                }
213
214 1
                $this->connection->update(
215 1
                    $this->configuration->getTableName(),
216
                    [
217 1
                        $this->configuration->getVersionColumnName() => (string) $availableMigration->getVersion(),
218
                    ],
219
                    [
220 1
                        $this->configuration->getVersionColumnName() => (string) $executedMigration->getVersion(),
221
                    ]
222
                );
223 1
                unset($executedMigrations[$k]);
224
            }
225
        }
226 1
    }
227
228 1
    private function isAlreadyV3Format(AvailableMigration $availableMigration, ExecutedMigration $executedMigration) : bool
229
    {
230 1
        return strpos(
231 1
            (string) $availableMigration->getVersion(),
232 1
            (string) $executedMigration->getVersion()
233
        ) !== (
234 1
                strlen((string) $availableMigration->getVersion()) -
235 1
                strlen((string) $executedMigration->getVersion())
236
            );
237
    }
238
}
239