Version::execute()   B
last analyzed

Complexity

Conditions 9
Paths 117

Size

Total Lines 64
Code Lines 40

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 30
CRAP Score 9.3752

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 9
eloc 40
c 1
b 0
f 0
nc 117
nop 2
dl 0
loc 64
ccs 30
cts 36
cp 0.8333
crap 9.3752
rs 7.6111

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/*
4
 * This file is part of the AntiMattr MongoDB Migrations Library, a library by Matthew Fitzgerald.
5
 *
6
 * (c) 2014 Matthew Fitzgerald
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace AntiMattr\MongoDB\Migrations;
13
14
use AntiMattr\MongoDB\Migrations\Collection\Statistics;
15
use AntiMattr\MongoDB\Migrations\Configuration\Configuration;
16
use AntiMattr\MongoDB\Migrations\Exception\AbortException;
17
use AntiMattr\MongoDB\Migrations\Exception\SkipException;
18
use Exception;
19
use MongoDB\BSON\UTCDateTime;
20
use MongoDB\Collection;
21
use MongoDB\Database;
22
23
/**
24
 * @author Matthew Fitzgerald <[email protected]>
25
 */
26
class Version
27
{
28
    const STATE_NONE = 0;
29
    const STATE_PRE = 1;
30
    const STATE_EXEC = 2;
31
    const STATE_POST = 3;
32
33
    /**
34
     * @var string
35
     */
36
    private $class;
37
38
    /**
39
     * @var \AntiMattr\MongoDB\Migrations\Configuration\Configuration
40
     */
41
    private $configuration;
42
43
    /**
44
     * @var \MongoDB\Database
45
     */
46
    private $db;
47
48
    /**
49
     * @var \AntiMattr\MongoDB\Migrations\AbstractMigration
50
     */
51
    protected $migration;
52
53
    /**
54
     * @var \AntiMattr\MongoDB\Migrations\OutputWriter
55
     */
56
    private $outputWriter;
57
58
    /**
59
     * The version in timestamp format (YYYYMMDDHHMMSS).
60
     *
61
     * @var int
62
     */
63
    private $version;
64
65
    /**
66
     * @var \AntiMattr\MongoDB\Migrations\Collection\Statistics[]
67
     */
68
    private $statistics = [];
69
70
    /**
71
     * @var int
72
     */
73
    private $time;
74
75
    /**
76
     * @var int
77
     */
78
    protected $state = self::STATE_NONE;
79
80 16
    public function __construct(Configuration $configuration, $version, $class)
81
    {
82 16
        $this->configuration = $configuration;
83 16
        $this->outputWriter = $configuration->getOutputWriter();
84 16
        $this->class = $class;
85 16
        $this->db = $configuration->getDatabase();
86 16
        $this->migration = $this->createMigration();
87 16
        $this->version = $version;
88 16
    }
89
90
    /**
91
     * @return \AntiMattr\MongoDB\Migrations\Configuration\Configuration $configuration
92
     */
93 3
    public function getConfiguration()
94
    {
95 3
        return $this->configuration;
96
    }
97
98 2
    public function getExecutionState()
99
    {
100 2
        switch ($this->state) {
101 2
            case self::STATE_PRE:
102
                return 'Pre-Checks';
103 2
            case self::STATE_POST:
104
                return 'Post-Checks';
105 2
            case self::STATE_EXEC:
106
                return 'Execution';
107
            default:
108 2
                return 'No State';
109
        }
110
    }
111
112
    /**
113
     * @return \AntiMattr\MongoDB\Migrations\AbstractMigration
114
     */
115 1
    public function getMigration()
116
    {
117 1
        return $this->migration;
118
    }
119
120
    /**
121
     * @return bool
122
     */
123 1
    public function isMigrated()
124
    {
125 1
        return $this->configuration->hasVersionMigrated($this);
126
    }
127
128
    /**
129
     * Returns the time this migration version took to execute.
130
     *
131
     * @return int $time The time this migration version took to execute
132
     */
133
    public function getTime()
134
    {
135
        return $this->time;
136
    }
137
138
    /**
139
     * @return string $version
140
     */
141 3
    public function getVersion()
142
    {
143 3
        return $this->version;
144
    }
145
146
    /**
147
     * @param \MongoDB\Collection
148
     */
149 4
    public function analyze(Collection $collection)
150
    {
151 4
        $statistics = $this->createStatistics();
152 4
        $statistics->setDatabase($this->db);
153 4
        $statistics->setCollection($collection);
154 4
        $name = $collection->getCollectionName();
155 4
        $this->statistics[$name] = $statistics;
156
157
        try {
158 4
            $statistics->updateBefore();
159 1
        } catch (\Exception $e) {
160 1
            $message = sprintf('     <info>Warning during %s: %s</info>',
161 1
                $this->getExecutionState(),
162 1
                $e->getMessage()
163
            );
164
165 1
            $this->outputWriter->write($message);
166
        }
167 4
    }
168
169
    /**
170
     * Execute this migration version up or down.
171
     *
172
     * @param string $direction The direction to execute the migration
173
     * @param bool   $replay    If the migration is being replayed
174
     *
175
     * @throws \Exception when migration fails
176
     */
177 5
    public function execute($direction, $replay = false)
178
    {
179 5
        if ('down' === $direction && $replay) {
180 1
            throw new AbortException('Cannot run \'down\' and replay it. Use replay with \'up\'');
181
        }
182
183
        try {
184 4
            $start = microtime(true);
185
186 4
            $this->state = self::STATE_PRE;
187
188 4
            $this->migration->{'pre' . ucfirst($direction)}($this->db);
189
190 4
            if ('up' === $direction) {
191 2
                $this->outputWriter->write("\n" . sprintf('  <info>++</info> migrating <comment>%s</comment>', $this->version) . "\n");
192
            } else {
193 2
                $this->outputWriter->write("\n" . sprintf('  <info>--</info> reverting <comment>%s</comment>', $this->version) . "\n");
194
            }
195
196 4
            $this->state = self::STATE_EXEC;
197
198 4
            $this->migration->$direction($this->db);
199
200 2
            $this->updateStatisticsAfter();
201
202 2
            if ('up' === $direction) {
203 1
                $this->markMigrated($replay);
204
            } else {
205 1
                $this->markNotMigrated();
206
            }
207
208 2
            $this->summarizeStatistics();
209
210 2
            $this->state = self::STATE_POST;
211 2
            $this->migration->{'post' . ucfirst($direction)}($this->db);
212
213 2
            $end = microtime(true);
214 2
            $this->time = round($end - $start, 2);
215 2
            if ('up' === $direction) {
216 1
                $this->outputWriter->write(sprintf("\n  <info>++</info> migrated (%ss)", $this->time));
217
            } else {
218 1
                $this->outputWriter->write(sprintf("\n  <info>--</info> reverted (%ss)", $this->time));
219
            }
220
221 2
            $this->state = self::STATE_NONE;
222 2
        } catch (SkipException $e) {
223
            // now mark it as migrated
224 2
            if ('up' === $direction) {
225 1
                $this->markMigrated();
226
            } else {
227 1
                $this->markNotMigrated();
228
            }
229
230 2
            $this->outputWriter->write(sprintf("\n  <info>SS</info> skipped (Reason: %s)", $e->getMessage()));
231
232 2
            $this->state = self::STATE_NONE;
233
        } catch (\Exception $e) {
234
            $this->outputWriter->write(sprintf(
235
                '<error>Migration %s failed during %s. Error %s</error>',
236
                $this->version, $this->getExecutionState(), $e->getMessage()
237
            ));
238
239
            $this->state = self::STATE_NONE;
240
            throw $e;
241
        }
242 4
    }
243
244
    /**
245
     * @param \MongoDB\Database
246
     * @param string $file
247
     *
248
     * @return array
249
     *
250
     * @throws RuntimeException
251
     * @throws InvalidArgumentException
252
     * @throws Exception
253
     */
254
    public function executeScript(Database $db, $file)
255
    {
256
        $scripts = $this->configuration->getMigrationsScriptDirectory();
257
        if (null === $scripts) {
0 ignored issues
show
introduced by
The condition null === $scripts is always false.
Loading history...
258
            throw new \RuntimeException('Missing Configuration for migrations script directory');
259
        }
260
261
        $path = realpath($scripts . '/' . $file);
262
        if (!file_exists($path)) {
263
            throw new \InvalidArgumentException(sprintf('Could not execute %s. File does not exist.', $path));
264
        }
265
266
        try {
267
            $js = file_get_contents($path);
268
            if (false === $js) {
269
                throw new \Exception('file_get_contents returned false');
270
            }
271
        } catch (\Exception $e) {
272
            throw $e;
273
        }
274
275
        return $db->command(['$eval' => $js, 'nolock' => true]);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $db->command(arra...$js, 'nolock' => true)) returns the type MongoDB\Driver\Cursor which is incompatible with the documented return type array.
Loading history...
276
    }
277
278
    /**
279
     * markMigrated.
280
     *
281
     * @param bool $replay This is a replayed migration, do an update instead of an insert
282
     */
283 4
    public function markMigrated($replay = false)
284
    {
285 4
        $this->configuration->createMigrationCollection();
286 4
        $collection = $this->configuration->getCollection();
287
288 4
        $document = ['v' => $this->version, 't' => $this->createMongoTimestamp()];
289
290 4
        if ($replay) {
291 1
            $query = ['v' => $this->version];
292
            // If the user asked for a 'replay' of a migration that
293
            // has not been run, it will be inserted anew
294 1
            $options = ['upsert' => true];
295 1
            $collection->updateOne($query, $document, $options);
296
        } else {
297 3
            $collection->insertOne($document);
298
        }
299 4
    }
300
301 3
    public function markNotMigrated()
302
    {
303 3
        $this->configuration->createMigrationCollection();
304 3
        $collection = $this->configuration->getCollection();
305 3
        $collection->deleteOne(['v' => $this->version]);
306 3
    }
307
308 4
    protected function updateStatisticsAfter()
309
    {
310 4
        foreach ($this->statistics as $name => $statistic) {
311
            try {
312 2
                $statistic->updateAfter();
313 1
                $name = $statistic->getCollection()->getCollectionName();
314 1
                $this->statistics[$name] = $statistic;
315 1
            } catch (\Exception $e) {
316 1
                $message = sprintf('     <info>Warning during %s: %s</info>',
317 1
                    $this->getExecutionState(),
318 1
                    $e->getMessage()
319
                );
320
321 2
                $this->outputWriter->write($message);
322
            }
323
        }
324 4
    }
325
326 2
    private function summarizeStatistics()
327
    {
328 2
        foreach ($this->statistics as $key => $statistic) {
329
            $this->outputWriter->write(sprintf("\n     Collection %s\n", $key));
330
331
            $line = '     ';
332
            $line .= 'metric ' . str_repeat(' ', 16 - strlen('metric'));
333
            $line .= 'before ' . str_repeat(' ', 20 - strlen('before'));
334
            $line .= 'after ' . str_repeat(' ', 20 - strlen('after'));
335
            $line .= 'difference ' . str_repeat(' ', 20 - strlen('difference'));
336
337
            $this->outputWriter->write($line . "\n     " . str_repeat('=', 80));
338
            $before = $statistic->getBefore();
339
            $after = $statistic->getAfter();
340
341
            foreach (Statistics::$metrics as $metric) {
342
                $valueBefore = isset($before[$metric]) ? $before[$metric] : 0;
343
                $valueAfter = isset($after[$metric]) ? $after[$metric] : 0;
344
                $difference = $valueAfter - $valueBefore;
345
346
                $nameMessage = $metric . str_repeat(' ', 16 - strlen($metric));
347
                $beforeMessage = $valueBefore . str_repeat(' ', 20 - strlen($valueBefore));
348
                $afterMessage = $valueAfter . str_repeat(' ', 20 - strlen($valueAfter));
349
                $differenceMessage = $difference . str_repeat(' ', 20 - strlen($difference));
350
351
                $line = sprintf(
352
                    '     %s %s %s %s',
353
                    $nameMessage,
354
                    $beforeMessage,
355
                    $afterMessage,
356
                    $differenceMessage
357
                );
358
                $this->outputWriter->write($line);
359
            }
360
        }
361 2
    }
362
363 1
    public function __toString()
364
    {
365 1
        return (string) $this->version;
366
    }
367
368
    /**
369
     * @return \AntiMattr\MongoDB\Migrations\AbstractMigration
370
     */
371 2
    protected function createMigration()
372
    {
373 2
        return new $this->class($this);
374
    }
375
376
    /**
377
     * @return UTCDateTime
378
     */
379
    protected function createMongoTimestamp()
380
    {
381
        return new UTCDateTime();
382
    }
383
384
    /**
385
     * @return \AntiMattr\MongoDB\Migrations\Collection\Statistics
386
     */
387
    protected function createStatistics()
388
    {
389
        return new Statistics();
390
    }
391
}
392