Failed Conditions
Pull Request — master (#919)
by Asmir
02:37
created

DbalExecutor   A

Complexity

Total Complexity 40

Size/Duplication

Total Lines 329
Duplicated Lines 0 %

Test Coverage

Coverage 93.59%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 153
c 1
b 0
f 0
dl 0
loc 329
ccs 146
cts 156
cp 0.9359
rs 9.2
wmc 40

13 Methods

Rating   Name   Duplication   Size   Complexity  
A getSql() 0 3 1
A __construct() 0 16 1
A logResult() 0 18 3
A executeResult() 0 21 4
A getExecutionStateAsString() 0 11 4
A getMigrationHeader() 0 16 3
A outputSqlQuery() 0 10 1
B executeMigration() 0 84 9
A addSql() 0 3 1
A migrationEnd() 0 22 5
A getFromSchema() 0 8 3
A execute() 0 29 3
A startMigration() 0 20 2

How to fix   Complexity   

Complex Class

Complex classes like DbalExecutor often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use DbalExecutor, and based on these observations, apply Extract Interface, too.

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\Stopwatch;
21
use Doctrine\Migrations\Tools\BytesFormatter;
22
use Psr\Log\LoggerInterface;
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 mixed[] */
50
    private $params = [];
0 ignored issues
show
introduced by
Class DbalExecutor contains write-only property $params.
Loading history...
51
52
    /** @var mixed[] */
53
    private $types = [];
0 ignored issues
show
introduced by
Class DbalExecutor contains write-only property $types.
Loading history...
54
55
    /** @var MetadataStorage */
56
    private $metadataStorage;
57
58
    /** @var LoggerInterface */
59
    private $logger;
60
61
    /** @var EventDispatcher */
62
    private $dispatcher;
63
64 13
    public function __construct(
65
        MetadataStorage $metadataStorage,
66
        EventDispatcher $dispatcher,
67
        Connection $connection,
68
        SchemaDiffProvider $schemaProvider,
69
        LoggerInterface $logger,
70
        ParameterFormatter $parameterFormatter,
71
        Stopwatch $stopwatch
72
    ) {
73 13
        $this->connection         = $connection;
74 13
        $this->schemaProvider     = $schemaProvider;
75 13
        $this->parameterFormatter = $parameterFormatter;
76 13
        $this->stopwatch          = $stopwatch;
77 13
        $this->metadataStorage    = $metadataStorage;
78 13
        $this->logger             = $logger;
79 13
        $this->dispatcher         = $dispatcher;
80 13
    }
81
82
    /**
83
     * @return Query[]
84
     */
85 1
    public function getSql() : array
86
    {
87 1
        return $this->sql;
88
    }
89
90 9
    public function addSql(Query $sqlQuery) : void
91
    {
92 9
        $this->sql[] = $sqlQuery;
93 9
    }
94
95 11
    public function execute(
96
        MigrationPlan $plan,
97
        MigratorConfiguration $configuration
98
    ) : ExecutionResult {
99 11
        $result = new ExecutionResult($plan->getVersion(), $plan->getDirection(), new DateTimeImmutable());
100
101 11
        $this->startMigration($plan, $configuration);
102
103
        try {
104 11
            $this->executeMigration(
105 11
                $plan,
106 11
                $result,
107 11
                $configuration
108
            );
109
110 7
            $result->setSql($this->sql);
111 4
        } catch (SkipMigration $e) {
112 1
            $result->setSkipped(true);
113
114 1
            $this->migrationEnd($e, $plan, $result, $configuration);
115 3
        } catch (Throwable $e) {
116 3
            $result->setError(true, $e);
117
118 3
            $this->migrationEnd($e, $plan, $result, $configuration);
119
120 3
            throw $e;
121
        }
122
123 8
        return $result;
124
    }
125
126 11
    private function startMigration(
127
        MigrationPlan $plan,
128
        MigratorConfiguration $configuration
129
    ) : void {
130 11
        $this->sql    = [];
131 11
        $this->params = [];
132 11
        $this->types  = [];
133
134 11
        $this->dispatcher->dispatchVersionEvent(
135 11
            Events::onMigrationsVersionExecuting,
136 11
            $plan,
137 11
            $configuration
138
        );
139
140 11
        if (! $plan->getMigration()->isTransactional()) {
141
            return;
142
        }
143
144
        // only start transaction if in transactional mode
145 11
        $this->connection->beginTransaction();
146 11
    }
147
148 11
    private function executeMigration(
149
        MigrationPlan $plan,
150
        ExecutionResult $result,
151
        MigratorConfiguration $configuration
152
    ) : ExecutionResult {
153 11
        $stopwatchEvent = $this->stopwatch->start('execute');
154
155 11
        $migration = $plan->getMigration();
156 11
        $direction = $plan->getDirection();
157
158 11
        $result->setState(State::PRE);
159
160 11
        $fromSchema = $this->getFromSchema($configuration);
161
162 11
        $migration->{'pre' . ucfirst($direction)}($fromSchema);
163
164 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

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