Completed
Push — master ( 55532e...954958 )
by Asmir
13s queued 13s
created

DbalExecutor::addSql()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 4
nc 2
nop 3
dl 0
loc 9
ccs 5
cts 5
cp 1
crap 2
rs 10
c 1
b 0
f 0
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\Stopwatch;
20
use Doctrine\Migrations\Tools\BytesFormatter;
21
use Psr\Log\LoggerInterface;
22
use Throwable;
23
use function count;
24
use function ucfirst;
25
26
/**
27
 * The DbalExecutor class is responsible for executing a single migration version.
28
 *
29
 * @internal
30
 */
31
final class DbalExecutor implements Executor
32
{
33
    /** @var Connection */
34
    private $connection;
35
36
    /** @var SchemaDiffProvider */
37
    private $schemaProvider;
38
39
    /** @var ParameterFormatter */
40
    private $parameterFormatter;
41
42
    /** @var Stopwatch */
43
    private $stopwatch;
44
45
    /** @var array<int, string> */
46
    private $sql = [];
47
48
    /** @var mixed[] */
49
    private $params = [];
50
51
    /** @var mixed[] */
52
    private $types = [];
53
54
    /** @var MetadataStorage */
55
    private $metadataStorage;
56
57
    /** @var LoggerInterface */
58
    private $logger;
59
60
    /** @var EventDispatcher */
61
    private $dispatcher;
62
63 13
    public function __construct(
64
        MetadataStorage $metadataStorage,
65
        EventDispatcher $dispatcher,
66
        Connection $connection,
67
        SchemaDiffProvider $schemaProvider,
68
        LoggerInterface $logger,
69
        ParameterFormatter $parameterFormatter,
70
        Stopwatch $stopwatch
71
    ) {
72 13
        $this->connection         = $connection;
73 13
        $this->schemaProvider     = $schemaProvider;
74 13
        $this->parameterFormatter = $parameterFormatter;
75 13
        $this->stopwatch          = $stopwatch;
76 13
        $this->metadataStorage    = $metadataStorage;
77 13
        $this->logger             = $logger;
78 13
        $this->dispatcher         = $dispatcher;
79 13
    }
80
81
    /**
82
     * @return string[]
83
     */
84 1
    public function getSql() : array
85
    {
86 1
        return $this->sql;
87
    }
88
89
    /**
90
     * @return mixed[]
91
     */
92 1
    public function getParams() : array
93
    {
94 1
        return $this->params;
95
    }
96
97
    /**
98
     * @return mixed[]
99
     */
100 1
    public function getTypes() : array
101
    {
102 1
        return $this->types;
103
    }
104
105
    /**
106
     * @param mixed[] $params
107
     * @param mixed[] $types
108
     */
109 9
    public function addSql(string $sql, array $params = [], array $types = []) : void
110
    {
111 9
        $this->sql[] = $sql;
112
113 9
        if (count($params) === 0) {
114 6
            return;
115
        }
116
117 7
        $this->addQueryParams($params, $types);
118 7
    }
119
120 11
    public function execute(
121
        MigrationPlan $plan,
122
        MigratorConfiguration $configuration
123
    ) : ExecutionResult {
124 11
        $result = new ExecutionResult($plan->getVersion(), $plan->getDirection(), new DateTimeImmutable());
125
126 11
        $this->startMigration($plan, $configuration);
127
128
        try {
129 11
            $this->executeMigration(
130 11
                $plan,
131 11
                $result,
132 11
                $configuration
133
            );
134
135 7
            $result->setSql($this->sql, $this->params, $this->types);
136 4
        } catch (SkipMigration $e) {
137 1
            $result->setSkipped(true);
138
139 1
            $this->migrationEnd($e, $plan, $result, $configuration);
140 3
        } catch (Throwable $e) {
141 3
            $result->setError(true, $e);
142
143 3
            $this->migrationEnd($e, $plan, $result, $configuration);
144
145 3
            throw $e;
146
        }
147
148 8
        return $result;
149
    }
150
151 11
    private function startMigration(
152
        MigrationPlan $plan,
153
        MigratorConfiguration $configuration
154
    ) : void {
155 11
        $this->sql    = [];
156 11
        $this->params = [];
157 11
        $this->types  = [];
158
159 11
        $this->dispatcher->dispatchVersionEvent(
160 11
            Events::onMigrationsVersionExecuting,
161 11
            $plan,
162 11
            $configuration
163
        );
164
165 11
        if (! $plan->getMigration()->isTransactional()) {
166
            return;
167
        }
168
169
        // only start transaction if in transactional mode
170 11
        $this->connection->beginTransaction();
171 11
    }
172
173 11
    private function executeMigration(
174
        MigrationPlan $plan,
175
        ExecutionResult $result,
176
        MigratorConfiguration $configuration
177
    ) : ExecutionResult {
178 11
        $stopwatchEvent = $this->stopwatch->start('execute');
179
180 11
        $migration = $plan->getMigration();
181 11
        $direction = $plan->getDirection();
182
183 11
        $result->setState(State::PRE);
184
185 11
        $fromSchema = $this->getFromSchema($configuration);
186
187 11
        $migration->{'pre' . ucfirst($direction)}($fromSchema);
188
189 11
        $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

189
        $this->logger->info(/** @scrutinizer ignore-type */ ...$this->getMigrationHeader($plan, $migration, $direction));
Loading history...
190
191 11
        $result->setState(State::EXEC);
192
193 11
        $toSchema = $this->schemaProvider->createToSchema($fromSchema);
194
195 11
        $result->setToSchema($toSchema);
196
197 11
        $migration->$direction($toSchema);
198
199 8
        foreach ($migration->getSql() as $sqlData) {
200 8
            $this->addSql(...$sqlData);
0 ignored issues
show
Bug introduced by
$sqlData is expanded, but the parameter $sql of Doctrine\Migrations\Version\DbalExecutor::addSql() does not expect variable arguments. ( Ignorable by Annotation )

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

200
            $this->addSql(/** @scrutinizer ignore-type */ ...$sqlData);
Loading history...
201
        }
202
203 8
        foreach ($this->schemaProvider->getSqlDiffToMigrate($fromSchema, $toSchema) as $sql) {
204
            $this->addSql($sql);
205
        }
206
207 8
        if (count($this->sql) !== 0) {
208 8
            if (! $configuration->isDryRun()) {
209 8
                $this->executeResult($configuration);
210
            } else {
211 8
                foreach ($this->sql as $idx => $query) {
212
                    $this->outputSqlQuery($idx, $query);
213
                }
214
            }
215
        } else {
216
            $this->logger->warning('Migration {version} was executed but did not result in any SQL statements.', [
217
                'version' => (string) $plan->getVersion(),
218
            ]);
219
        }
220
221 8
        $result->setState(State::POST);
222
223 8
        $migration->{'post' . ucfirst($direction)}($toSchema);
224 8
        $stopwatchEvent->stop();
225
226 8
        $result->setTime($stopwatchEvent->getDuration());
227 8
        $result->setMemory($stopwatchEvent->getMemory());
228
229
        $params = [
230 8
            'version' => (string) $plan->getVersion(),
231 8
            'time' => $stopwatchEvent->getDuration(),
232 8
            'memory' => BytesFormatter::formatBytes($stopwatchEvent->getMemory()),
233 8
            'direction' => $direction === Direction::UP ? 'migrated' : 'reverted',
234
        ];
235
236 8
        $this->logger->info('Migration {version} {direction} (took {time}ms, used {memory} memory)', $params);
237
238 8
        if (! $configuration->isDryRun()) {
239 8
            $this->metadataStorage->complete($result);
240
        }
241
242 7
        if ($migration->isTransactional()) {
243
            //commit only if running in transactional mode
244 7
            $this->connection->commit();
245
        }
246
247 7
        $plan->markAsExecuted($result);
248 7
        $result->setState(State::NONE);
249
250 7
        $this->dispatcher->dispatchVersionEvent(
251 7
            Events::onMigrationsVersionExecuted,
252 7
            $plan,
253 7
            $configuration
254
        );
255
256 7
        return $result;
257
    }
258
259
    /**
260
     * @return mixed[]
261
     */
262 11
    private function getMigrationHeader(MigrationPlan $planItem, AbstractMigration $migration, string $direction) : array
263
    {
264 11
        $versionInfo = (string) $planItem->getVersion();
265 11
        $description = $migration->getDescription();
266
267 11
        if ($description !== '') {
268 1
            $versionInfo .= ' (' . $description . ')';
269
        }
270
271 11
        $params = ['version_name' => $versionInfo];
272
273 11
        if ($direction === Direction::UP) {
274 9
            return ['++ migrating {version_name}', $params];
275
        }
276
277 2
        return ['++ reverting {version_name}', $params];
278
    }
279
280 4
    private function migrationEnd(Throwable $e, MigrationPlan $plan, ExecutionResult $result, MigratorConfiguration $configuration) : void
281
    {
282 4
        $migration = $plan->getMigration();
283 4
        if ($migration->isTransactional()) {
284
            //only rollback transaction if in transactional mode
285 4
            $this->connection->rollBack();
286
        }
287
288 4
        $plan->markAsExecuted($result);
289 4
        $this->logResult($e, $result, $plan);
290
291 4
        $this->dispatcher->dispatchVersionEvent(
292 4
            Events::onMigrationsVersionSkipped,
293 4
            $plan,
294 4
            $configuration
295
        );
296
297 4
        if ($configuration->isDryRun() || $result->isSkipped() || $result->hasError()) {
298 4
            return;
299
        }
300
301
        $this->metadataStorage->complete($result);
302
    }
303
304 4
    private function logResult(Throwable $e, ExecutionResult $result, MigrationPlan $plan) : void
305
    {
306 4
        if ($result->isSkipped()) {
307 1
            $this->logger->error(
308 1
                'Migration {version} skipped during {state}. Reason: "{reason}"',
309
                [
310 1
                    'version' => (string) $plan->getVersion(),
311 1
                    'reason' => $e->getMessage(),
312 1
                    'state' => $this->getExecutionStateAsString($result->getState()),
313
                ]
314
            );
315 3
        } elseif ($result->hasError()) {
316 3
            $this->logger->error(
317 3
                'Migration {version} failed during {state}. Error: "{error}"',
318
                [
319 3
                    'version' => (string) $plan->getVersion(),
320 3
                    'error' => $e->getMessage(),
321 3
                    'state' => $this->getExecutionStateAsString($result->getState()),
322
                ]
323
            );
324
        }
325 4
    }
326
327 8
    private function executeResult(MigratorConfiguration $configuration) : void
328
    {
329 8
        foreach ($this->sql as $key => $query) {
330 8
            $stopwatchEvent = $this->stopwatch->start('query');
331
332 8
            $this->outputSqlQuery($key, $query);
333
334 8
            if (! isset($this->params[$key])) {
335 6
                $this->connection->executeQuery($query);
336
            } else {
337 6
                $this->connection->executeQuery($query, $this->params[$key], $this->types[$key]);
338
            }
339
340 8
            $stopwatchEvent->stop();
341
342 8
            if (! $configuration->getTimeAllQueries()) {
343 2
                continue;
344
            }
345
346 6
            $this->logger->debug('{duration}ms', [
347 6
                'duration' => $stopwatchEvent->getDuration(),
348
            ]);
349
        }
350 8
    }
351
352
    /**
353
     * @param mixed[]|int $params
354
     * @param mixed[]|int $types
355
     */
356 7
    private function addQueryParams($params, $types) : void
357
    {
358 7
        $index                = count($this->sql) - 1;
359 7
        $this->params[$index] = $params;
360 7
        $this->types[$index]  = $types;
361 7
    }
362
363 8
    private function outputSqlQuery(int $idx, string $query) : void
364
    {
365 8
        $params = $this->parameterFormatter->formatParameters(
366 8
            $this->params[$idx] ?? [],
367 8
            $this->types[$idx] ?? []
368
        );
369
370 8
        $this->logger->debug('{query} {params}', [
371 8
            'query' => $query,
372 8
            'params' => $params,
373
        ]);
374 8
    }
375
376 11
    private function getFromSchema(MigratorConfiguration $configuration) : Schema
377
    {
378
        // if we're in a dry run, use the from Schema instead of reading the schema from the database
379 11
        if ($configuration->isDryRun() && $configuration->getFromSchema() !== null) {
380
            return $configuration->getFromSchema();
381
        }
382
383 11
        return $this->schemaProvider->createFromSchema();
384
    }
385
386 4
    private function getExecutionStateAsString(int $state) : string
387
    {
388
        switch ($state) {
389 4
            case State::PRE:
390
                return 'Pre-Checks';
391 4
            case State::POST:
392 1
                return 'Post-Checks';
393 3
            case State::EXEC:
394 3
                return 'Execution';
395
            default:
396
                return 'No State';
397
        }
398
    }
399
}
400