Completed
Push — master ( 0f594c...43e3de )
by Asmir
22s queued 11s
created

TableMetadataStorage::isAlreadyV3Format()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 1

Importance

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