Completed
Pull Request — master (#914)
by Asmir
02:48
created

TableMetadataStorage::isInitialized()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

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