Passed
Push — master ( 7af47c...4753c3 )
by Gaetano
10:00
created

StatusCommand   C

Complexity

Total Complexity 53

Size/Duplication

Total Lines 250
Duplicated Lines 0 %

Test Coverage

Coverage 43.22%

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 168
c 3
b 0
f 0
dl 0
loc 250
ccs 51
cts 118
cp 0.4322
rs 6.96
wmc 53

3 Methods

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

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\Input\InputInterface;
6
use Symfony\Component\Console\Output\OutputInterface;
7
use Symfony\Component\Console\Input\InputOption;
8
use Kaliop\eZMigrationBundle\API\Value\Migration;
9
use Kaliop\eZMigrationBundle\API\Value\MigrationDefinition;
10
use Symfony\Component\Console\Helper\Table;
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 94
    protected function configure()
23
    {
24 94
        $this->setName('kaliop:migration:status')
25 94
            ->setDescription('View the status of all (or a set of) migrations.')
26 94
            ->addOption('path', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, "The directory or file to load the migration definitions from")
27 94
            ->addOption('sort-by', null, InputOption::VALUE_REQUIRED, "Supported sorting order: name, execution", 'name')
28 94
            ->addOption('summary', null, InputOption::VALUE_NONE, "Only print summary information")
29 94
            ->addOption('todo', null, InputOption::VALUE_NONE, "Only print list of migrations to execute (full path to each)")
30 94
            ->addOption('show-path', null, InputOption::VALUE_NONE, "Print migration path instead of status")
31
            ->setHelp(<<<EOT
32
The <info>kaliop:migration:status</info> command displays the status of all available migrations:
33
34
    <info>./ezpublish/console kaliop:migration:status</info>
35
36
You can optionally specify the path to migration versions with <info>--path</info>:
37
38
    <info>./ezpublish/console kaliop:migrations:status --path=/path/to/bundle/version_directory --path=/path/to/bundle/version_directory/single_migration_file</info>
39 94
EOT
40
            );
41 34
    }
42
43 34
    public function execute(InputInterface $input, OutputInterface $output)
44 34
    {
45
        $this->setOutput($output);
46 34
        $this->setVerbosity($output->getVerbosity());
47
48 34
        $migrationsService = $this->getMigrationService();
49 34
50
        $displayPath = $input->getOption('show-path');
51 34
        $migrationDefinitions = $migrationsService->getMigrationsDefinitions($input->getOption('path'));
0 ignored issues
show
Bug introduced by
It seems like $input->getOption('path') can also be of type boolean and null and string; however, parameter $paths of Kaliop\eZMigrationBundle...MigrationsDefinitions() does only seem to accept array, 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

51
        $migrationDefinitions = $migrationsService->getMigrationsDefinitions(/** @scrutinizer ignore-type */ $input->getOption('path'));
Loading history...
52
        $migrations = $migrationsService->getMigrations();
53
54
        if (!count($migrationDefinitions) && !count($migrations)) {
55
            $output->writeln('<info>No migrations found</info>');
56
            return 0;
57 34
        }
58 34
59 34
        // create a unique list of all migrations (coming from db) and definitions (coming from disk)
60
        $index = array();
61 34
        foreach ($migrationDefinitions as $migrationDefinition) {
62
            $index[$migrationDefinition->name] = array('definition' => $migrationDefinition);
63
        }
64
        foreach ($migrations as $migration) {
65
            if (isset($index[$migration->name])) {
66
                $index[$migration->name]['migration'] = $migration;
67
            } else {
68
                $index[$migration->name] = array('migration' => $migration);
69
70
                // no definition, but a migration is there. Check if the definition sits elsewhere on disk than we expect it to be...
71
                // q: what if we have a loader which does not work with is_file? Could we remove this check?
72
                if ($migration->path != '' && is_file($migration->path)) {
73
                    try {
74
                        $migrationDefinitionCollection = $migrationsService->getMigrationsDefinitions(array($migration->path));
75
                        if (count($migrationDefinitionCollection)) {
76
                            $index[$migration->name]['definition'] = $migrationDefinitionCollection->reset();
77
                        }
78
                    } catch (\Exception $e) {
79
                        /// @todo one day we should be able to limit the kind of exceptions we have to catch here...
80
                    }
81 34
                }
82
            }
83 34
        }
84 34
85
        if (!$input->getOption('summary')) {
86
            $this->sortMigrationIndex($index, $input->getOption('sort-by'));
0 ignored issues
show
Bug introduced by
It seems like $input->getOption('sort-by') can also be of type string[]; however, parameter $sortBy of Kaliop\eZMigrationBundle...d::sortMigrationIndex() 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

86
            $this->sortMigrationIndex($index, /** @scrutinizer ignore-type */ $input->getOption('sort-by'));
Loading history...
87 34
        }
88
89
        if (!$input->getOption('summary') && !$input->getOption('todo')) {
90
            if (count($index) > 50000) {
91 34
                $output->writeln("WARNING: printing the status table might take a while as it contains many rows. Please wait...");
92
            }
93
            $output->writeln("\n <info>==</info> All Migrations\n");
94
        }
95
96
        $summary = array(
97
            self::STATUS_INVALID => array('Invalid', 0),
98
            Migration::STATUS_TODO => array('To do', 0),
99
            Migration::STATUS_STARTED => array('Started', 0),
100 34
            Migration::STATUS_DONE => array('Done', 0),
101
            Migration::STATUS_SUSPENDED => array('Suspended', 0),
102 34
            Migration::STATUS_FAILED => array('Failed', 0),
103 34
            Migration::STATUS_SKIPPED => array('Skipped', 0),
104 34
            Migration::STATUS_PARTIALLY_DONE => array('Partially done', 0),
105 34
        );
106 34
        $data = array();
107 34
108 2
        $i = 1;
109 2
        foreach ($index as $name => $value) {
110
            if (!isset($value['migration'])) {
111 34
                $migrationDefinition = $migrationsService->parseMigrationDefinition($value['definition']);
112
                $notes = $displayPath ? $migrationDefinition->path : '';
113 34
                if ($migrationDefinition->status != MigrationDefinition::STATUS_PARSED) {
114
                    if (!$displayPath) {
115
                        $notes = '<error>' . $migrationDefinition->parsingError . '</error>';
116 34
                    }
117 34
                    $summary[self::STATUS_INVALID][1]++;
118 34
                } else {
119 34
                    $summary[Migration::STATUS_TODO][1]++;
120 34
                }
121 34
                if ($input->getOption('todo')) {
122
                    $data[] = $migrationDefinition->path;
123
                } else {
124
                    $data[] = array(
125
                        $i++,
126
                        $name,
127
                        '<error>not executed</error>',
128
                        '',
129
                        $notes
130
                    );
131
132
                }
133
            } else {
134
                /** @var Migration $migration */
135
                $migration = $value['migration'];
136
137
                if (!isset($summary[$migration->status])) {
138
                    $summary[$migration->status] = array($migration->status, 0);
139
                }
140
                $summary[$migration->status][1]++;
141
                if ($input->getOption('summary')) {
142
                    continue;
143
                }
144
145
                if ($input->getOption('todo')) {
146
                    if ($migration->status == Migration::STATUS_TODO) {
147
                        $data[] = $migration->path;
148
                    }
149
150
                    continue;
151
                }
152
153
                switch ($migration->status) {
154
                    case Migration::STATUS_DONE:
155
                        $status = '<info>executed</info>';
156
                        break;
157
                    case Migration::STATUS_STARTED:
158
                        $status = '<comment>execution started</comment>';
159
                        break;
160
                    case Migration::STATUS_TODO:
161
                        // bold to-migrate!
162
                        $status = '<error>not executed</error>';
163
                        break;
164
                    case Migration::STATUS_SKIPPED:
165
                        $status = '<comment>skipped</comment>';
166
                        break;
167
                    case Migration::STATUS_PARTIALLY_DONE:
168
                        $status = '<comment>partially executed</comment>';
169
                        break;
170
                    case Migration::STATUS_SUSPENDED:
171
                        $status = '<comment>suspended</comment>';
172
                        break;
173
                    case Migration::STATUS_FAILED:
174
                        $status = '<error>failed</error>';
175
                        break;
176
                }
177
                if ($displayPath) {
178
                    $notes = $migration->path;
179
                } else {
180
                    $notes = array();
181
                    if ($migration->executionError != '') {
182
                        $notes[] = "<error>{$migration->executionError}</error>";
183
                    }
184
                    if (!isset($value['definition'])) {
185
                        $notes[] = '<comment>The migration definition file can not be found any more</comment>';
186
                    } else {
187
                        $migrationDefinition = $value['definition'];
188
                        if (md5($migrationDefinition->rawDefinition) != $migration->md5) {
189 34
                            $notes[] = '<comment>The migration definition file has now a different checksum</comment>';
190
                        }
191
                        if ($migrationDefinition->path != $migrationDefinition->path) {
192
                            $notes[] = '<comment>The migration definition file has now moved</comment>';
193
                        }
194 34
                    }
195
                    $notes = implode(' ', $notes);
196
                }
197
198
                $data[] = array(
199
                    $i++,
200
                    $migration->name,
201 34
                    $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...
202
                    ($migration->executionDate != null ? date("Y-m-d H:i:s", $migration->executionDate) : ''),
203
                    $notes
204
                );
205
            }
206
        }
207
208 34
        if ($input->getOption('todo')) {
209
            foreach ($data as $migrationData) {
210
                $output->writeln("$migrationData", OutputInterface::OUTPUT_RAW|OutputInterface::VERBOSITY_QUIET);
211 34
            }
212
            return 0;
213 34
        }
214 34
215 34
        if ($input->getOption('summary')) {
216 34
            $output->writeln("\n <info>==</info> Migrations Summary\n");
217
            // do not print info about the not yet supported case
218
            unset($summary[Migration::STATUS_PARTIALLY_DONE]);
219
            $data = $summary;
220
            $headers = array('Status', 'Count');
221
        } else {
222
            $headers = array('#', 'Migration', 'Status', 'Executed on', $displayPath ? 'Path' : 'Notes');
223
        }
224
225
        $table = new Table($output);
226
        $table
227
            ->setHeaders($headers)
228
            ->setRows($data);
229
        $table->render();
230
    }
231
232
    /**
233
     * @param array[] $index
234
     * @param string $sortBy
235
     * @throws \Exception
236
     */
237
    protected function sortMigrationIndex(array &$index, $sortBy)
238
    {
239
        switch($sortBy) {
240
            case 'execution':
241
                uasort($index, function($m1, $m2) {
242
                    if (isset($m1['migration']) && $m1['migration']->executionDate !== null) {
243
                        if (isset($m2['migration']) && $m2['migration']->executionDate !== null) {
244
                            return $m1['migration']->executionDate - $m2['migration']->executionDate;
245
                        } else {
246
                            // m2 not executed: send to bottom
247
                            return -1;
248
                        }
249
                    } else {
250
                        if (isset($m2['migration']) && $m2['migration']->executionDate !== null) {
251
                            // m1 not executed: send to bottom
252
                            return 1;
253
                        } else {
254
                            // both not executed: compare by name
255
                            return strcmp(
256
                                isset($m1['migration']) ? $m1['migration']->name : $m1['definition']->name,
257
                                isset($m2['migration']) ? $m2['migration']->name : $m2['definition']->name
258
                            );
259
                        }
260
                    }
261
                });
262
                break;
263
            case 'name':
264
                ksort($index);
265
                break;
266
            default:
267
                throw new \Exception("Unsupported sort order: '$sortBy'");
268
        }
269
    }
270
}
271