Failed Conditions
Pull Request — master (#972)
by Asmir
02:43
created

TableMetadataStorage::ensureInitialized()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 17
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 3.0067

Importance

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