Completed
Push — master ( 6e739b...0f594c )
by Asmir
22s queued 12s
created

DbalExecutor::migrationEnd()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 15
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 9
c 1
b 0
f 0
nc 2
nop 4
dl 0
loc 15
ccs 10
cts 10
cp 1
crap 2
rs 9.9666
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Doctrine\Migrations\Version;
6
7
use DateTimeImmutable;
8
use Doctrine\DBAL\Connection;
9
use Doctrine\DBAL\Schema\Schema;
10
use Doctrine\Migrations\AbstractMigration;
11
use Doctrine\Migrations\EventDispatcher;
12
use Doctrine\Migrations\Events;
13
use Doctrine\Migrations\Exception\SkipMigration;
14
use Doctrine\Migrations\Metadata\MigrationPlan;
15
use Doctrine\Migrations\Metadata\Storage\MetadataStorage;
16
use Doctrine\Migrations\MigratorConfiguration;
17
use Doctrine\Migrations\ParameterFormatter;
18
use Doctrine\Migrations\Provider\SchemaDiffProvider;
19
use Doctrine\Migrations\Query\Query;
20
use Doctrine\Migrations\Tools\BytesFormatter;
21
use Psr\Log\LoggerInterface;
22
use Symfony\Component\Stopwatch\Stopwatch;
23
use Throwable;
24
use function count;
25
use function ucfirst;
26
27
/**
28
 * The DbalExecutor class is responsible for executing a single migration version.
29
 *
30
 * @internal
31
 */
32
final class DbalExecutor implements Executor
33
{
34
    /** @var Connection */
35
    private $connection;
36
37
    /** @var SchemaDiffProvider */
38
    private $schemaProvider;
39
40
    /** @var ParameterFormatter */
41
    private $parameterFormatter;
42
43
    /** @var Stopwatch */
44
    private $stopwatch;
45
46
    /** @var Query[] */
47
    private $sql = [];
48
49
    /** @var MetadataStorage */
50
    private $metadataStorage;
51
52
    /** @var LoggerInterface */
53
    private $logger;
54
55
    /** @var EventDispatcher */
56
    private $dispatcher;
57
58 17
    public function __construct(
59
        MetadataStorage $metadataStorage,
60
        EventDispatcher $dispatcher,
61
        Connection $connection,
62
        SchemaDiffProvider $schemaProvider,
63
        LoggerInterface $logger,
64
        ParameterFormatter $parameterFormatter,
65
        Stopwatch $stopwatch
66
    ) {
67 17
        $this->connection         = $connection;
68 17
        $this->schemaProvider     = $schemaProvider;
69 17
        $this->parameterFormatter = $parameterFormatter;
70 17
        $this->stopwatch          = $stopwatch;
71 17
        $this->metadataStorage    = $metadataStorage;
72 17
        $this->logger             = $logger;
73 17
        $this->dispatcher         = $dispatcher;
74 17
    }
75
76
    /**
77
     * @return Query[]
78
     */
79 1
    public function getSql() : array
80
    {
81 1
        return $this->sql;
82
    }
83
84 11
    public function addSql(Query $sqlQuery) : void
85
    {
86 11
        $this->sql[] = $sqlQuery;
87 11
    }
88
89 15
    public function execute(
90
        MigrationPlan $plan,
91
        MigratorConfiguration $configuration
92
    ) : ExecutionResult {
93 15
        $result = new ExecutionResult($plan->getVersion(), $plan->getDirection(), new DateTimeImmutable());
94
95 15
        $this->startMigration($plan, $configuration);
96
97
        try {
98 15
            $this->executeMigration(
99 15
                $plan,
100 15
                $result,
101 15
                $configuration
102
            );
103
104 11
            $result->setSql($this->sql);
105 4
        } catch (SkipMigration $e) {
106 1
            $result->setSkipped(true);
107
108 1
            $this->migrationEnd($e, $plan, $result, $configuration);
109 3
        } catch (Throwable $e) {
110 3
            $result->setError(true, $e);
111
112 3
            $this->migrationEnd($e, $plan, $result, $configuration);
113
114 3
            throw $e;
115
        }
116
117 12
        return $result;
118
    }
119
120 15
    private function startMigration(
121
        MigrationPlan $plan,
122
        MigratorConfiguration $configuration
123
    ) : void {
124 15
        $this->sql = [];
125
126 15
        $this->dispatcher->dispatchVersionEvent(
127 15
            Events::onMigrationsVersionExecuting,
128 15
            $plan,
129 15
            $configuration
130
        );
131
132 15
        if (! $plan->getMigration()->isTransactional()) {
133 1
            return;
134
        }
135
136
        // only start transaction if in transactional mode
137 14
        $this->connection->beginTransaction();
138 14
    }
139
140 15
    private function executeMigration(
141
        MigrationPlan $plan,
142
        ExecutionResult $result,
143
        MigratorConfiguration $configuration
144
    ) : ExecutionResult {
145 15
        $stopwatchEvent = $this->stopwatch->start('execute');
146
147 15
        $migration = $plan->getMigration();
148 15
        $direction = $plan->getDirection();
149
150 15
        $result->setState(State::PRE);
151
152 15
        $fromSchema = $this->getFromSchema($configuration);
153
154 15
        $migration->{'pre' . ucfirst($direction)}($fromSchema);
155
156 15
        $this->logger->info(...$this->getMigrationHeader($plan, $migration, $direction));
0 ignored issues
show
Bug introduced by
It seems like $this->getMigrationHeade...$migration, $direction) can also be of type array<string,string>; however, parameter $message of Psr\Log\LoggerInterface::info() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

156
        $this->logger->info(/** @scrutinizer ignore-type */ ...$this->getMigrationHeader($plan, $migration, $direction));
Loading history...
157
158 15
        $result->setState(State::EXEC);
159
160 15
        $toSchema = $this->schemaProvider->createToSchema($fromSchema);
161
162 15
        $result->setToSchema($toSchema);
163
164 15
        $migration->$direction($toSchema);
165
166 12
        foreach ($migration->getSql() as $sqlQuery) {
167 10
            $this->addSql($sqlQuery);
168
        }
169
170 12
        foreach ($this->schemaProvider->getSqlDiffToMigrate($fromSchema, $toSchema) as $sql) {
171
            $this->addSql(new Query($sql));
172
        }
173
174 12
        if (count($this->sql) !== 0) {
175 10
            if (! $configuration->isDryRun()) {
176 9
                $this->executeResult($configuration);
177
            } else {
178 10
                foreach ($this->sql as $query) {
179 1
                    $this->outputSqlQuery($query);
180
                }
181
            }
182
        } else {
183 2
            $this->logger->warning('Migration {version} was executed but did not result in any SQL statements.', [
184 2
                'version' => (string) $plan->getVersion(),
185
            ]);
186
        }
187
188 12
        $result->setState(State::POST);
189
190 12
        $migration->{'post' . ucfirst($direction)}($toSchema);
191
192 12
        $stopwatchEvent->stop();
193 12
        $periods    = $stopwatchEvent->getPeriods();
194 12
        $lastPeriod = $periods[count($periods) -1];
195
196 12
        $result->setTime((float) $lastPeriod->getDuration()/1000);
197 12
        $result->setMemory($lastPeriod->getMemory());
198
199
        $params = [
200 12
            'version' => (string) $plan->getVersion(),
201 12
            'time' => $lastPeriod->getDuration(),
202 12
            'memory' => BytesFormatter::formatBytes($lastPeriod->getMemory()),
203 12
            'direction' => $direction === Direction::UP ? 'migrated' : 'reverted',
204
        ];
205
206 12
        $this->logger->info('Migration {version} {direction} (took {time}ms, used {memory} memory)', $params);
207
208 12
        if (! $configuration->isDryRun()) {
209 11
            $this->metadataStorage->complete($result);
210
        }
211
212 11
        if ($migration->isTransactional()) {
213
            //commit only if running in transactional mode
214 10
            $this->connection->commit();
215
        }
216
217 11
        $plan->markAsExecuted($result);
218 11
        $result->setState(State::NONE);
219
220 11
        $this->dispatcher->dispatchVersionEvent(
221 11
            Events::onMigrationsVersionExecuted,
222 11
            $plan,
223 11
            $configuration
224
        );
225
226 11
        return $result;
227
    }
228
229
    /**
230
     * @return mixed[]
231
     */
232 15
    private function getMigrationHeader(MigrationPlan $planItem, AbstractMigration $migration, string $direction) : array
233
    {
234 15
        $versionInfo = (string) $planItem->getVersion();
235 15
        $description = $migration->getDescription();
236
237 15
        if ($description !== '') {
238 1
            $versionInfo .= ' (' . $description . ')';
239
        }
240
241 15
        $params = ['version_name' => $versionInfo];
242
243 15
        if ($direction === Direction::UP) {
244 13
            return ['++ migrating {version_name}', $params];
245
        }
246
247 2
        return ['++ reverting {version_name}', $params];
248
    }
249
250 4
    private function migrationEnd(Throwable $e, MigrationPlan $plan, ExecutionResult $result, MigratorConfiguration $configuration) : void
251
    {
252 4
        $migration = $plan->getMigration();
253 4
        if ($migration->isTransactional()) {
254
            //only rollback transaction if in transactional mode
255 4
            $this->connection->rollBack();
256
        }
257
258 4
        $plan->markAsExecuted($result);
259 4
        $this->logResult($e, $result, $plan);
260
261 4
        $this->dispatcher->dispatchVersionEvent(
262 4
            Events::onMigrationsVersionSkipped,
263 4
            $plan,
264 4
            $configuration
265
        );
266 4
    }
267
268 4
    private function logResult(Throwable $e, ExecutionResult $result, MigrationPlan $plan) : void
269
    {
270 4
        if ($result->isSkipped()) {
271 1
            $this->logger->error(
272 1
                'Migration {version} skipped during {state}. Reason: "{reason}"',
273
                [
274 1
                    'version' => (string) $plan->getVersion(),
275 1
                    'reason' => $e->getMessage(),
276 1
                    'state' => $this->getExecutionStateAsString($result->getState()),
277
                ]
278
            );
279 3
        } elseif ($result->hasError()) {
280 3
            $this->logger->error(
281 3
                'Migration {version} failed during {state}. Error: "{error}"',
282
                [
283 3
                    'version' => (string) $plan->getVersion(),
284 3
                    'error' => $e->getMessage(),
285 3
                    'state' => $this->getExecutionStateAsString($result->getState()),
286
                ]
287
            );
288
        }
289 4
    }
290
291 9
    private function executeResult(MigratorConfiguration $configuration) : void
292
    {
293 9
        foreach ($this->sql as $key => $query) {
294 9
            $this->outputSqlQuery($query);
295
296 9
            $stopwatchEvent = $this->stopwatch->start('query');
297 9
            $this->connection->executeUpdate($query->getStatement(), $query->getParameters(), $query->getTypes());
298 9
            $stopwatchEvent->stop();
299
300 9
            if (! $configuration->getTimeAllQueries()) {
301 2
                continue;
302
            }
303
304 7
            $this->logger->debug('{duration}ms', [
305 7
                'duration' => $stopwatchEvent->getDuration(),
306
            ]);
307
        }
308 9
    }
309
310 10
    private function outputSqlQuery(Query $query) : void
311
    {
312 10
        $params = $this->parameterFormatter->formatParameters(
313 10
            $query->getParameters(),
314 10
            $query->getTypes()
315
        );
316
317 10
        $this->logger->debug('{query} {params}', [
318 10
            'query' => $query->getStatement(),
319 10
            'params' => $params,
320
        ]);
321 10
    }
322
323 15
    private function getFromSchema(MigratorConfiguration $configuration) : Schema
324
    {
325
        // if we're in a dry run, use the from Schema instead of reading the schema from the database
326 15
        if ($configuration->isDryRun() && $configuration->getFromSchema() !== null) {
327
            return $configuration->getFromSchema();
328
        }
329
330 15
        return $this->schemaProvider->createFromSchema();
331
    }
332
333 4
    private function getExecutionStateAsString(int $state) : string
334
    {
335
        switch ($state) {
336 4
            case State::PRE:
337
                return 'Pre-Checks';
338 4
            case State::POST:
339 1
                return 'Post-Checks';
340 3
            case State::EXEC:
341 3
                return 'Execution';
342
            default:
343
                return 'No State';
344
        }
345
    }
346
}
347