Passed
Pull Request — master (#41)
by
unknown
03:10
created

Version::markMigrated()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 15
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 2.1481

Importance

Changes 0
Metric Value
cc 2
eloc 9
nc 2
nop 1
dl 0
loc 15
ccs 6
cts 9
cp 0.6667
crap 2.1481
rs 9.9666
c 0
b 0
f 0
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\SkipException;
17
use AntiMattr\MongoDB\Migrations\Exception\AbortException;
18
use \MongoDB\Collection;
19
use \MongoDB\Database;
20
use Exception;
21
use MongoDB\BSON\UTCDateTime;
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\Connection
0 ignored issues
show
Bug introduced by
The type MongoDB\Connection was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
45
     */
46
    private $connection;
47
48
    /**
49
     * @var \MongoDB\Database
50
     */
51
    private $db;
52
53
    /**
54
     * @var \AntiMattr\MongoDB\Migrations\AbstractMigration
55
     */
56
    protected $migration;
57
58
    /**
59
     * @var \AntiMattr\MongoDB\Migrations\OutputWriter
60
     */
61
    private $outputWriter;
62
63
    /**
64
     * The version in timestamp format (YYYYMMDDHHMMSS).
65
     *
66
     * @var int
67
     */
68
    private $version;
69
70
    /**
71
     * @var \AntiMattr\MongoDB\Migrations\Collection\Statistics[]
72
     */
73
    private $statistics = [];
74
75
    /**
76
     * @var int
77
     */
78
    private $time;
79
80
    /**
81
     * @var int
82
     */
83
    protected $state = self::STATE_NONE;
84
85 9
    public function __construct(Configuration $configuration, $version, $class)
86
    {
87 9
        $this->configuration = $configuration;
88 9
        $this->outputWriter = $configuration->getOutputWriter();
89 9
        $this->class = $class;
90 9
        $this->connection = $configuration->getConnection();
0 ignored issues
show
Documentation Bug introduced by
It seems like $configuration->getConnection() of type MongoDB\Client is incompatible with the declared type MongoDB\Connection of property $connection.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
91 9
        $this->db = $configuration->getDatabase();
92 9
        $this->migration = $this->createMigration();
93 9
        $this->version = $version;
94 9
    }
95
96
    /**
97
     * @return \AntiMattr\MongoDB\Migrations\Configuration\Configuration $configuration
98
     */
99 2
    public function getConfiguration()
100
    {
101 2
        return $this->configuration;
102
    }
103
104 1
    public function getExecutionState()
105
    {
106 1
        switch ($this->state) {
107 1
            case self::STATE_PRE:
108
                return 'Pre-Checks';
109 1
            case self::STATE_POST:
110
                return 'Post-Checks';
111 1
            case self::STATE_EXEC:
112
                return 'Execution';
113
            default:
114 1
                return 'No State';
115
        }
116
    }
117
118
    /**
119
     * @return \AntiMattr\MongoDB\Migrations\AbstractMigration
120
     */
121 1
    public function getMigration()
122
    {
123 1
        return $this->migration;
124
    }
125
126
    /**
127
     * @return bool
128
     */
129 1
    public function isMigrated()
130
    {
131 1
        return $this->configuration->hasVersionMigrated($this);
132
    }
133
134
    /**
135
     * Returns the time this migration version took to execute.
136
     *
137
     * @return int $time The time this migration version took to execute
138
     */
139
    public function getTime()
140
    {
141
        return $this->time;
142
    }
143
144
    /**
145
     * @return string $version
146
     */
147 2
    public function getVersion()
148
    {
149 2
        return $this->version;
150
    }
151
152
    /**
153
     * @param \MongoDB\Collection
154
     */
155 1
    public function analyze(Collection $collection)
156
    {
157 1
        $statistics = $this->createStatistics();
158 1
        $statistics->setCollection($collection);
159 1
        $name = $collection->getCollectionName();
160 1
        $this->statistics[$name] = $statistics;
161
162
        try {
163 1
            $statistics->updateBefore();
164
        } catch (\Exception $e) {
165
            $message = sprintf('     <info>Warning during %s: %s</info>',
166
                $this->getExecutionState(),
167
                $e->getMessage()
168
            );
169
170
            $this->outputWriter->write($message);
171
        }
172 1
    }
173
174
    /**
175
     * Execute this migration version up or down.
176
     *
177
     * @param string $direction The direction to execute the migration
178
     * @param bool   $replay    If the migration is being replayed
179
     *
180
     * @throws \Exception when migration fails
181
     */
182 5
    public function execute($direction, $replay = false)
183
    {
184 5
        if ('down' === $direction && $replay) {
185 1
            throw new AbortException(
186 1
                'Cannot run \'down\' and replay it. Use replay with \'up\''
187
            );
188
        }
189
190
        try {
191 4
            $start = microtime(true);
192
193 4
            $this->state = self::STATE_PRE;
194
195 4
            $this->migration->{'pre' . ucfirst($direction)}($this->db);
196
197 4
            if ('up' === $direction) {
198 2
                $this->outputWriter->write("\n" . sprintf('  <info>++</info> migrating <comment>%s</comment>', $this->version) . "\n");
199
            } else {
200 2
                $this->outputWriter->write("\n" . sprintf('  <info>--</info> reverting <comment>%s</comment>', $this->version) . "\n");
201
            }
202
203 4
            $this->state = self::STATE_EXEC;
204
205 4
            $this->migration->$direction($this->db);
206
207 2
            $this->updateStatisticsAfter();
208
209 2
            if ('up' === $direction) {
210 1
                $this->markMigrated($replay);
211
            } else {
212 1
                $this->markNotMigrated();
213
            }
214
215 2
            $this->summarizeStatistics();
216
217 2
            $this->state = self::STATE_POST;
218 2
            $this->migration->{'post' . ucfirst($direction)}($this->db);
219
220 2
            $end = microtime(true);
221 2
            $this->time = round($end - $start, 2);
222 2
            if ('up' === $direction) {
223 1
                $this->outputWriter->write(sprintf("\n  <info>++</info> migrated (%ss)", $this->time));
224
            } else {
225 1
                $this->outputWriter->write(sprintf("\n  <info>--</info> reverted (%ss)", $this->time));
226
            }
227
228 2
            $this->state = self::STATE_NONE;
229 2
        } catch (SkipException $e) {
230
            // now mark it as migrated
231 2
            if ('up' === $direction) {
232 1
                $this->markMigrated();
233
            } else {
234 1
                $this->markNotMigrated();
235
            }
236
237 2
            $this->outputWriter->write(sprintf("\n  <info>SS</info> skipped (Reason: %s)", $e->getMessage()));
238
239 2
            $this->state = self::STATE_NONE;
240
        } catch (\Exception $e) {
241
            $this->outputWriter->write(sprintf(
242
                '<error>Migration %s failed during %s. Error %s</error>',
243
                $this->version, $this->getExecutionState(), $e->getMessage()
244
            ));
245
246
            $this->state = self::STATE_NONE;
247
            throw $e;
248
        }
249 4
    }
250
251
    /**
252
     * @param \MongoDB\Database
253
     * @param string $file
254
     *
255
     * @return array
256
     *
257
     * @throws RuntimeException
258
     * @throws InvalidArgumentException
259
     * @throws Exception
260
     */
261
    public function executeScript(Database $db, $file)
262
    {
263
        $scripts = $this->configuration->getMigrationsScriptDirectory();
264
        if (null === $scripts) {
0 ignored issues
show
introduced by
The condition null === $scripts is always false.
Loading history...
265
            throw new \RuntimeException('Missing Configuration for migrations script directory');
266
        }
267
268
        $path = realpath($scripts . '/' . $file);
269
        if (!file_exists($path)) {
270
            throw new \InvalidArgumentException(sprintf('Could not execute %s. File does not exist.', $path));
271
        }
272
273
        try {
274
            $js = file_get_contents($path);
275
            if (false === $js) {
276
                throw new \Exception('file_get_contents returned false');
277
            }
278
        } catch (\Exception $e) {
279
            throw $e;
280
        }
281
282
        $result = $db->command(['$eval' => $js, 'nolock' => true]);
283
284
        if (isset($result['errmsg'])) {
285
            throw new \Exception($result['errmsg'], isset($result['errno']) ? $result['errno'] : null);
286
        }
287
288
        return $result;
289
    }
290
291
    /**
292
     * markMigrated.
293
     *
294
     * @param bool $replay This is a replayed migration, do an update instead of an insert
295
     */
296 2
    public function markMigrated($replay = false)
297
    {
298 2
        $this->configuration->createMigrationCollection();
299 2
        $collection = $this->configuration->getCollection();
300
301 2
        $document = ['v' => $this->version, 't' => $this->createMongoTimestamp()];
302
303 2
        if ($replay) {
304
            $query = ['v' => $this->version];
305
            // If the user asked for a 'replay' of a migration that
306
            // has not been run, it will be inserted anew
307
            $options = ['upsert' => true];
308
            $collection->update($query, $document, $options);
0 ignored issues
show
Bug introduced by
The method update() does not exist on MongoDB\Collection. Did you maybe mean updateMany()? ( Ignorable by Annotation )

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

308
            $collection->/** @scrutinizer ignore-call */ 
309
                         update($query, $document, $options);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
309
        } else {
310 2
            $collection->insertOne($document);
311
        }
312 2
    }
313
314 2
    public function markNotMigrated()
315
    {
316 2
        $this->configuration->createMigrationCollection();
317 2
        $collection = $this->configuration->getCollection();
318 2
        $collection->deleteOne(['v' => $this->version]);
319 2
    }
320
321 3
    protected function updateStatisticsAfter()
322
    {
323 3
        foreach ($this->statistics as $name => $statistic) {
324
            try {
325 1
                $statistic->updateAfter();
326
                $name = $statistic->getCollection()->getName();
0 ignored issues
show
Bug introduced by
The method getName() does not exist on MongoDB\Collection. Did you maybe mean getNamespace()? ( Ignorable by Annotation )

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

326
                $name = $statistic->getCollection()->/** @scrutinizer ignore-call */ getName();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
327
                $this->statistics[$name] = $statistic;
328 1
            } catch (\Exception $e) {
329 1
                $message = sprintf('     <info>Warning during %s: %s</info>',
330 1
                    $this->getExecutionState(),
331 1
                    $e->getMessage()
332
                );
333
334 1
                $this->outputWriter->write($message);
335
            }
336
        }
337 3
    }
338
339 2
    private function summarizeStatistics()
340
    {
341 2
        foreach ($this->statistics as $key => $statistic) {
342
            $this->outputWriter->write(sprintf("\n     Collection %s\n", $key));
343
344
            $line = '     ';
345
            $line .= 'metric ' . str_repeat(' ', 16 - strlen('metric'));
346
            $line .= 'before ' . str_repeat(' ', 20 - strlen('before'));
347
            $line .= 'after ' . str_repeat(' ', 20 - strlen('after'));
348
            $line .= 'difference ' . str_repeat(' ', 20 - strlen('difference'));
349
350
            $this->outputWriter->write($line . "\n     " . str_repeat('=', 80));
351
            $before = $statistic->getBefore();
352
            $after = $statistic->getAfter();
353
354
            foreach (Statistics::$metrics as $metric) {
355
                $valueBefore = isset($before[$metric]) ? $before[$metric] : 0;
356
                $valueAfter = isset($after[$metric]) ? $after[$metric] : 0;
357
                $difference = $valueAfter - $valueBefore;
358
359
                $nameMessage = $metric . str_repeat(' ', 16 - strlen($metric));
360
                $beforeMessage = $valueBefore . str_repeat(' ', 20 - strlen($valueBefore));
361
                $afterMessage = $valueAfter . str_repeat(' ', 20 - strlen($valueAfter));
362
                $differenceMessage = $difference . str_repeat(' ', 20 - strlen($difference));
363
364
                $line = sprintf(
365
                    '     %s %s %s %s',
366
                    $nameMessage,
367
                    $beforeMessage,
368
                    $afterMessage,
369
                    $differenceMessage
370
                );
371
                $this->outputWriter->write($line);
372
            }
373
        }
374 2
    }
375
376 1
    public function __toString()
377
    {
378 1
        return $this->version;
379
    }
380
381
    /**
382
     * @return \AntiMattr\MongoDB\Migrations\AbstractMigration
383
     */
384 1
    protected function createMigration()
385
    {
386 1
        return new $this->class($this);
387
    }
388
389
    /**
390
     * @return UTCDateTime
391
     */
392
    protected function createMongoTimestamp()
393
    {
394
        return new UTCDateTime();
395
    }
396
397
    /**
398
     * @return \AntiMattr\MongoDB\Migrations\Collection\Statistics
399
     */
400
    protected function createStatistics()
401
    {
402
        return new Statistics();
403
    }
404
}
405