StatusCommand   C
last analyzed

Complexity

Total Complexity 57

Size/Duplication

Total Lines 276
Duplicated Lines 0 %

Test Coverage

Coverage 85.62%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 179
dl 0
loc 276
ccs 125
cts 146
cp 0.8562
rs 5.04
c 2
b 0
f 0
wmc 57

3 Methods

Rating   Name   Duplication   Size   Complexity  
A configure() 0 10 1
B sortMigrationIndex() 0 31 11
F execute() 0 213 45

How to fix   Complexity   

Complex Class

Complex classes like StatusCommand 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 StatusCommand, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Kaliop\eZMigrationBundle\Command;
4
5
use Symfony\Component\Console\Helper\Table;
6
use Symfony\Component\Console\Input\InputInterface;
7
use Symfony\Component\Console\Input\InputOption;
8
use Symfony\Component\Console\Output\OutputInterface;
9
use Kaliop\eZMigrationBundle\API\Value\Migration;
10
use Kaliop\eZMigrationBundle\API\Value\MigrationDefinition;
11
12
/**
13
 * Command to display the status of migrations.
14
 *
15
 * @todo add option to skip displaying already executed migrations
16
 * @todo allow sorting migrations by their execution date
17
 */
18
class StatusCommand extends AbstractCommand
19
{
20
    const STATUS_INVALID = -1;
21
22
    protected function configure()
23 148
    {
24
        $this->setName('kaliop:migration:status')
25 148
            ->setDescription('View the status of all (or a set of) migrations.')
26 148
            ->addOption('path', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, "The directory or file to load the migration definitions from")
27 148
            ->addOption('sort-by', null, InputOption::VALUE_REQUIRED, "Supported sorting order: name, execution", 'name')
28 148
            ->addOption('summary', null, InputOption::VALUE_NONE, "Only print summary information")
29 148
            ->addOption('todo', null, InputOption::VALUE_NONE, "Only print list of migrations to execute (full path to each)")
30 148
            ->addOption('show-path', null, InputOption::VALUE_NONE, "Print migration path instead of notes")
31 148
            ->setHelp(<<<EOT
32 148
The <info>kaliop:migration:status</info> command displays the status of all available migrations:
33 148
34
    <info>php bin/console kaliop:migration:status</info>
35
36
You can optionally specify the path to migration definitions with <info>--path</info>:
37
38
    <info>php bin/console kaliop:migration:status --path=/path/to/bundle/version_directory --path=/path/to/bundle/version_directory/single_migration_file</info>
39
EOT
40
            );
41
    }
42 148
43
    public function execute(InputInterface $input, OutputInterface $output)
44 47
    {
45
        $this->setOutput($output);
46 47
        $this->setVerbosity($output->getVerbosity());
47 47
48
        $migrationsService = $this->getMigrationService();
49 47
50
        $displayPath = $input->getOption('show-path');
51 47
        $paths = $this->normalizePaths($input->getOption('path'));
52 47
        $migrationDefinitions = $migrationsService->getMigrationsDefinitions($paths);
53 47
        $migrations = $migrationsService->getMigrationsByPaths($paths);
54 47
55
        if (!count($migrationDefinitions) && !count($migrations)) {
56 47
            $output->writeln('<info>No migrations found</info>');
57
            return 0;
58
        }
59
60
        // create a unique list of all migrations (coming from db) and definitions (coming from disk)
61
62
        $index = array();
63 47
        foreach ($migrationDefinitions as $migrationDefinition) {
64 47
            $index[$migrationDefinition->name] = array('definition' => $migrationDefinition);
65 47
        }
66
        foreach ($migrations as $migration) {
67 47
            if (isset($index[$migration->name])) {
68 5
                $index[$migration->name]['migration'] = $migration;
69
            } else {
70
                $index[$migration->name] = array('migration' => $migration);
71 5
72
                // no definition, but a migration is there. Check if the definition sits elsewhere on disk than we expect it to be...
73
                // q: what if we have a loader which does not work with is_file? Could we remove this check? Also, path
74
                // is usually relative to the app's root dir
75
                if ($migration->path != '' /*&& is_file($migration->path)*/) {
76 5
                    try {
77
                        $migrationDefinitionCollection = $migrationsService->getMigrationsDefinitions(array($migration->path));
78 5
                        if (count($migrationDefinitionCollection)) {
79 5
                            // the migration has been executed, but it is in a path outside what we are examining here.
80
                            // We  add it as a note
81
                            $index[$migration->name]['definition'] = $migrationDefinitionCollection->reset();
82 5
                            $index[$migration->name]['notes'] = array('<comment>The migration definition file is in a custom path</comment>');
83 5
                        }
84
                    } catch (\Exception $e) {
85
                        /// @todo one day we should be able to limit the kind of exceptions we have to catch here...
86
                    }
87
                }
88
            }
89
        }
90
91
        // In case the user has passed in path(s), and there are migration defs in those paths which have the same name
92
        // as executed/skipped migrations in the db which are outside the path, they will be listed as 'to execute',
93
        // which is misleading. We add the following loop to fix that, so that the status command will match more closely
94
        // the 'migrate' command
95
        if (count($paths)) {
96 47
            foreach ($index as $migrationName => $data) {
97 3
                if (!isset($data['migration'])) {
98 3
                    $migration = $migrationsService->getMigration($migrationName);
99 3
                    if ($migration !== null) {
100 3
                        $index[$migration->name]['migration'] = $migration;
101 1
                    }
102
                }
103
            }
104
        }
105
106
        if (!$input->getOption('summary')) {
107 47
            $this->sortMigrationIndex($index, $input->getOption('sort-by'));
108 46
        }
109
110
        if (!$input->getOption('summary') && !$input->getOption('todo')) {
111 47
            if (count($index) > 50000) {
112 45
                $output->writeln("WARNING: printing the status table might take a while as it contains many rows. Please wait...");
113
            }
114
            $output->writeln("\n <info>==</info> All Migrations\n");
115 45
        }
116
117
        $summary = array(
118
            self::STATUS_INVALID => array('Invalid', 0),
119 47
            Migration::STATUS_TODO => array('To do', 0),
120
            Migration::STATUS_STARTED => array('Started', 0),
121
            Migration::STATUS_DONE => array('Done', 0),
122
            Migration::STATUS_SUSPENDED => array('Suspended', 0),
123
            Migration::STATUS_FAILED => array('Failed', 0),
124
            Migration::STATUS_SKIPPED => array('Skipped', 0),
125
            Migration::STATUS_PARTIALLY_DONE => array('Partially done', 0),
126
        );
127
        $data = array();
128 47
129
        $i = 1;
130 47
        foreach ($index as $name => $value) {
131 47
            if (!isset($value['migration'])) {
132 47
                $migrationDefinition = $migrationsService->parseMigrationDefinition($value['definition']);
133 46
                $notes = $displayPath ? $migrationDefinition->path : '';
134 46
                if ($migrationDefinition->status != MigrationDefinition::STATUS_PARSED) {
135 46
                    if (!$displayPath) {
136 2
                        $notes = '<error>' . $migrationDefinition->parsingError . '</error>';
137 2
                    }
138
                    $summary[self::STATUS_INVALID][1]++;
139 2
                } else {
140
                    $summary[Migration::STATUS_TODO][1]++;
141 46
                }
142
                if ($input->getOption('todo')) {
143 46
                    $data[] = $migrationDefinition->path;
144 1
                } else {
145
                    $data[] = array(
146 45
                        $i++,
147 45
                        $name,
148 45
                        '<error>not executed</error>',
149 45
                        '',
150 45
                        $notes
151 46
                    );
152
153
                }
154
            } else {
155
                /** @var Migration $migration */
156
                $migration = $value['migration'];
157 6
158
                if (!isset($summary[$migration->status])) {
159 6
                    $summary[$migration->status] = array($migration->status, 0);
160
                }
161
                $summary[$migration->status][1]++;
162 6
                if ($input->getOption('summary')) {
163 6
                    continue;
164
                }
165
166
                if ($input->getOption('todo')) {
167 6
                    if ($migration->status == Migration::STATUS_TODO) {
168 1
                        $data[] = $migration->path;
169 1
                    }
170
171
                    continue;
172 1
                }
173
174
                switch ($migration->status) {
175 5
                    case Migration::STATUS_DONE:
176
                        $status = '<info>executed</info>';
177 1
                        break;
178 1
                    case Migration::STATUS_STARTED:
179
                        $status = '<comment>execution started</comment>';
180
                        break;
181
                    case Migration::STATUS_TODO:
182
                        // bold to-migrate!
183
                        $status = '<error>not executed</error>';
184 3
                        break;
185 3
                    case Migration::STATUS_SKIPPED:
186
                        $status = '<comment>skipped</comment>';
187 1
                        break;
188 1
                    case Migration::STATUS_PARTIALLY_DONE:
189
                        $status = '<comment>partially executed</comment>';
190
                        break;
191
                    case Migration::STATUS_SUSPENDED:
192
                        $status = '<comment>suspended</comment>';
193
                        break;
194
                    case Migration::STATUS_FAILED:
195
                        $status = '<error>failed</error>';
196
                        break;
197
                }
198
                if ($displayPath) {
199 5
                    $notes = $migration->path;
200 1
                } else {
201
                    $notes = array();
202 4
                    if ($migration->executionError != '') {
203 4
                        $notes[] = "<error>{$migration->executionError}</error>";
204
                    }
205
                    if (!isset($value['definition'])) {
206 4
                        $notes[] = '<comment>The migration definition file can not be found any more</comment>';
207
                    } else {
208
                        $migrationDefinition = $value['definition'];
209 4
                        if (md5($migrationDefinition->rawDefinition) != $migration->md5) {
210 4
                            $notes[] = '<comment>The migration definition file has now a different checksum</comment>';
211
                        }
212
                        if ($migrationDefinition->path != $migration->path) {
213 4
                            $notes[] = '<comment>The migration definition file has now moved</comment>';
214
                        }
215
                    }
216
                    if (isset($value['notes'])) {
217 4
                        $notes = array_merge($notes, $value['notes']);
218 3
                    }
219
                    $notes = implode(' ', $notes);
220 4
                }
221
222
                $data[] = array(
223 5
                    $i++,
224 5
                    $migration->name,
225 5
                    $status,
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $status does not seem to be defined for all execution paths leading up to this point.
Loading history...
226 5
                    ($migration->executionDate != null ? date("Y-m-d H:i:s", $migration->executionDate) : ''),
227 5
                    $notes
228 5
                );
229
            }
230
        }
231
232
        if ($input->getOption('todo')) {
233 47
            foreach ($data as $migrationData) {
234 1
                $output->writeln("$migrationData", OutputInterface::OUTPUT_RAW|OutputInterface::VERBOSITY_QUIET);
235 1
            }
236
            return 0;
237 1
        }
238
239
        if ($input->getOption('summary')) {
240 46
            $output->writeln("\n <info>==</info> Migrations Summary\n");
241 1
            // do not print info about the not yet supported case
242
            unset($summary[Migration::STATUS_PARTIALLY_DONE]);
243 1
            $data = $summary;
244 1
            $headers = array('Status', 'Count');
245 1
        } else {
246
            $headers = array('#', 'Migration', 'Status', 'Executed on', $displayPath ? 'Path' : 'Notes');
247 45
        }
248
249
        $table = new Table($output);
250 46
        $table
251
            ->setHeaders($headers)
252 46
            ->setRows($data);
253 46
        $table->render();
254 46
255 46
        return 0;
256
    }
257
258
    /**
259
     * @param array[] $index
260
     * @param string $sortBy
261
     * @throws \Exception
262 46
     */
263
    protected function sortMigrationIndex(array &$index, $sortBy)
264 46
    {
265 46
        switch ($sortBy) {
266
            case 'execution':
267 1
                uasort($index, function($m1, $m2) {
268 1
                    if (isset($m1['migration']) && $m1['migration']->executionDate !== null) {
269 1
                        if (isset($m2['migration']) && $m2['migration']->executionDate !== null) {
270
                            return $m1['migration']->executionDate - $m2['migration']->executionDate;
271
                        } else {
272
                            // m2 not executed: send to bottom
273
                            return -1;
274
                        }
275 1
                    } else {
276
                        if (isset($m2['migration']) && $m2['migration']->executionDate !== null) {
277 1
                            // m1 not executed: send to bottom
278
                            return 1;
279
                        } else {
280 1
                            // both not executed: compare by name
281 1
                            return strcmp(
282 1
                                isset($m1['migration']) ? $m1['migration']->name : $m1['definition']->name,
283
                                isset($m2['migration']) ? $m2['migration']->name : $m2['definition']->name
284
                            );
285
                        }
286 1
                    }
287 1
                });
288 46
                break;
289 46
            case 'name':
290 46
                ksort($index);
291
                break;
292
            default:
293
                throw new \InvalidArgumentException("Unsupported sort order: '$sortBy'");
294 46
        }
295
    }
296
}
297