Version::markMigrated()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 15
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 2

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 2
eloc 9
c 2
b 0
f 0
nc 2
nop 1
dl 0
loc 15
ccs 9
cts 9
cp 1
crap 2
rs 9.9666
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