Completed
Push — master ( 97fb46...770b11 )
by Gaetano
15:34 queued 07:06
created

GenerateCommand   B

Complexity

Total Complexity 49

Size/Duplication

Total Lines 351
Duplicated Lines 0 %

Test Coverage

Coverage 81.71%

Importance

Changes 0
Metric Value
eloc 201
dl 0
loc 351
ccs 134
cts 164
cp 0.8171
rs 8.48
c 0
b 0
f 0
wmc 49

7 Methods

Rating   Name   Duplication   Size   Complexity  
A listAvailableTypes() 0 11 3
B migrationContextFromParameters() 0 16 7
A configure() 0 17 1
A getGeneratingExecutors() 0 11 3
B generateMigrationFile() 0 54 11
F execute() 0 131 19
A getMigrationDirectory() 0 20 5

How to fix   Complexity   

Complex Class

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

1
<?php
2
3
namespace Kaliop\eZMigrationBundle\Command;
4
5
use Symfony\Component\Console\Input\InputArgument;
6
use Symfony\Component\Console\Input\InputInterface;
7
use Symfony\Component\Console\Input\InputOption;
8
use Symfony\Component\Console\Output\OutputInterface;
9
use Symfony\Component\HttpFoundation\File\Exception\FileException;
10
use Symfony\Component\Yaml\Yaml;
11
use Kaliop\eZMigrationBundle\API\MigrationGeneratorInterface;
12
use Kaliop\eZMigrationBundle\API\MatcherInterface;
13
use Kaliop\eZMigrationBundle\API\EnumerableMatcherInterface;
14
15
class GenerateCommand extends AbstractCommand
16
{
17
    const DIR_CREATE_PERMISSIONS = 0755;
18
19
    private $availableMigrationFormats = array('yml', 'php', 'sql', 'json');
20
    private $availableModes = array('create', 'update', 'delete');
21
    private $availableTypes = array('content', 'content_type', 'content_type_group', 'language', 'object_state', 'object_state_group', 'role', 'section', 'generic', 'db', 'php', '...');
22
    private $thisBundle = 'EzMigrationBundle';
23
24
    /**
25
     * Configure the console command
26
     */
27 94
    protected function configure()
28
    {
29 94
        $this->setName('kaliop:migration:generate')
30 94
            ->setDescription('Generate a blank migration definition file.')
31 94
            ->addOption('format', null, InputOption::VALUE_REQUIRED, 'The format of migration file to generate (' . implode(', ', $this->availableMigrationFormats) . ')', 'yml')
32 94
            ->addOption('type', null, InputOption::VALUE_REQUIRED, 'The type of migration to generate (' . implode(', ', $this->availableTypes) . ')', '')
33 94
            ->addOption('mode', null, InputOption::VALUE_REQUIRED, 'The mode of the migration (' . implode(', ', $this->availableModes) . ')', 'create')
34 94
            ->addOption('match-type', null, InputOption::VALUE_REQUIRED, 'The type of identifier used to find the entity to generate the migration for', null)
35 94
            ->addOption('match-value', null, InputOption::VALUE_REQUIRED, 'The identifier value used to find the entity to generate the migration for. Can have many values separated by commas', null)
36 94
            ->addOption('match-except', null, InputOption::VALUE_NONE, 'Used to match all entities except the ones satisfying the match-value condition', null)
37 94
            ->addOption('lang', 'l', InputOption::VALUE_REQUIRED, 'The language of the migration (eng-GB, ger-DE, ...). If null, the default language of the current siteaccess is used')
38 94
            ->addOption('dbserver', null, InputOption::VALUE_REQUIRED, 'The type of the database server the sql migration is for, when type=db (mysql, postgresql, ...)', 'mysql')
39 94
            ->addOption('admin-login', 'a', InputOption::VALUE_REQUIRED, "Login of admin account used whenever elevated privileges are needed (user id 14 used by default)")
40 94
            ->addOption('list-types', null, InputOption::VALUE_NONE, 'Use this option to list all available migration types and their match conditions')
41 94
            ->addArgument('bundle', InputArgument::OPTIONAL, 'The bundle to generate the migration definition file in. eg.: AcmeMigrationBundle')
42 94
            ->addArgument('name', InputArgument::OPTIONAL, 'The migration name (will be prefixed with current date)', null)
43 94
            ->setHelp(<<<EOT
44 94
The <info>kaliop:migration:generate</info> command generates a skeleton migration definition file:
45
46
    <info>php ezpublish/console kaliop:migration:generate bundleName</info>
47
48
You can optionally specify the file type to generate with <info>--format</info>, as well a name for the migration:
49
50
    <info>php ezpublish/console kaliop:migration:generate --format=json bundleName migrationName</info>
51
52
For SQL type migration you can optionally specify the database server type the migration is for with <info>--dbserver</info>:
53
54
    <info>php ezpublish/console kaliop:migration:generate --format=sql bundleName</info>
55
56
For content/content_type/language/object_state/role/section migrations you need to specify the entity that you want to generate the migration for:
57
58
    <info>php ezpublish/console kaliop:migration:generate --type=content --match-type=content_id --match-value=10,14 bundleName</info>
59
60
For role type migration you will receive a yaml file with the current role definition. You must define ALL the policies
61
you wish for the role. Any not defined will be removed. Example for updating an existing role:
62
63
    <info>php ezpublish/console kaliop:migration:generate --type=role --mode=update --match-type=identifier --match-value=Anonymous bundleName</info>
64
65
For freeform php migrations, you will receive a php class definition
66
67
    <info>php ezpublish/console kaliop:migration:generate --format=php bundlename classname</info>
68
69
To list all available migration types for generation, as well as the corresponding match-types, run:
70
71
    <info>php ezpublish/console ka:mig:gen --list-types</info>
72
73
Note that you can pass in a custom directory path instead of a bundle name, but, if you do, you will have to use the <info>--path</info>
74
option when you run the <info>migrate</info> command.
75
EOT
76
            );
77 94
    }
78
79
    /**
80
     * Run the command and display the results.
81
     *
82
     * @param InputInterface $input
83
     * @param OutputInterface $output
84
     * @return null|int null or 0 if everything went fine, or an error code
85
     * @throws \InvalidArgumentException When an unsupported file type is selected
86
     */
87 34
    public function execute(InputInterface $input, OutputInterface $output)
88
    {
89 34
        $this->setOutput($output);
90 34
        $this->setVerbosity($output->getVerbosity());
91
92 34
        if ($input->getOption('list-types')) {
93
            $this->listAvailableTypes($output);
94
            return 0;
95
        }
96
97 34
        $bundleName = $input->getArgument('bundle');
98 34
        if ($bundleName === null) {
99
            // throw same exception as SF would when declaring 'bundle' as mandatory arg
100
            throw new \RuntimeException('Not enough arguments (missing: "bundle").');
101
        }
102 34
        $name = $input->getArgument('name');
103 34
        $fileType = $input->getOption('format');
104 34
        $migrationType = $input->getOption('type');
105 34
        $matchType = $input->getOption('match-type');
106 34
        $matchValue = $input->getOption('match-value');
107 34
        $matchExcept = $input->getOption('match-except');
108 34
        $mode = $input->getOption('mode');
109 34
        $dbServer = $input->getOption('dbserver');
110
111 34
        if ($bundleName == $this->thisBundle) {
112
            throw new \InvalidArgumentException("It is not allowed to create migrations in bundle '$bundleName'");
113
        }
114
115
        // be kind to lazy users
116 34
        if ($migrationType == '') {
117 6
            if ($fileType == 'sql') {
118 2
                $migrationType = 'db';
119 4
            } elseif ($fileType == 'php') {
120 2
                $migrationType = 'php';
121
            } else {
122 2
                $migrationType = 'generic';
123
            }
124
        }
125
126 34
        if (!in_array($fileType, $this->availableMigrationFormats)) {
127
            throw new \InvalidArgumentException('Unsupported migration file format ' . $fileType);
128
        }
129
130 34
        if (!in_array($mode, $this->availableModes)) {
131
            throw new \InvalidArgumentException('Unsupported migration mode ' . $mode);
132
        }
133
134 34
        $migrationDirectory = $this->getMigrationDirectory($bundleName);
0 ignored issues
show
Bug introduced by
It seems like $bundleName can also be of type string[]; however, parameter $bundleName of Kaliop\eZMigrationBundle...getMigrationDirectory() 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

134
        $migrationDirectory = $this->getMigrationDirectory(/** @scrutinizer ignore-type */ $bundleName);
Loading history...
135
136 34
        if (!is_dir($migrationDirectory)) {
137 1
            $output->writeln(sprintf(
138 1
                "Migrations directory <info>%s</info> does not exist. I will create it now....",
139 1
                $migrationDirectory
140
            ));
141
142 1
            if (mkdir($migrationDirectory, self::DIR_CREATE_PERMISSIONS, true)) {
143 1
                $output->writeln(sprintf(
144 1
                    "Migrations directory <info>%s</info> has been created",
145 1
                    $migrationDirectory
146
                ));
147
            } else {
148
                throw new FileException(sprintf(
149
                    "Failed to create migrations directory %s.",
150
                    $migrationDirectory
151
                ));
152
            }
153
        }
154
155
        // allow to generate migrations for many entities
156 34
        if (strpos($matchValue, ',') !== false ) {
0 ignored issues
show
Bug introduced by
It seems like $matchValue can also be of type string[]; however, parameter $haystack of strpos() 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

156
        if (strpos(/** @scrutinizer ignore-type */ $matchValue, ',') !== false ) {
Loading history...
157 2
            $matchValue = explode(',', $matchValue);
0 ignored issues
show
Bug introduced by
It seems like $matchValue can also be of type string[]; however, parameter $string of explode() 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

157
            $matchValue = explode(',', /** @scrutinizer ignore-type */ $matchValue);
Loading history...
158
        }
159
160
        $parameters = array(
161 34
            'dbserver' => $dbServer,
162 34
            'matchType' => $matchType,
163 34
            'matchValue' => $matchValue,
164 34
            'matchExcept' => $matchExcept,
165 34
            'mode' => $mode,
166 34
            'lang' => $input->getOption('lang'),
167 34
            'adminLogin' => $input->getOption('admin-login')
168
            /// @todo should we allow users to specify this ?
169
            //'forceSigchildEnabled' => null
170
        );
171
172 34
        $date = date('YmdHis');
173
174
        switch ($fileType) {
175 34
            case 'sql':
176
                /// @todo this logic should come from the DefinitionParser, really
177 2
                if ($name != '') {
178 1
                    $name = '_' . ltrim($name, '_');
0 ignored issues
show
Bug introduced by
It seems like $name can also be of type string[]; however, parameter $str of ltrim() 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

178
                    $name = '_' . ltrim(/** @scrutinizer ignore-type */ $name, '_');
Loading history...
179
                }
180 2
                $fileName = $date . '_' . $dbServer . $name . '.sql';
181 2
                break;
182
183 32
            case 'php':
184
                /// @todo this logic should come from the DefinitionParser, really
185 2
                $className = ltrim($name, '_');
186 2
                if ($className == '') {
187 1
                    $className = 'Migration';
188
                }
189
                // Make sure that php class names are unique, not only migration definition file names
190 2
                $existingMigrations = count(glob($migrationDirectory . '/*_' . $className . '*.php'));
0 ignored issues
show
Bug introduced by
It seems like glob($migrationDirectory.... $className . '*.php') can also be of type false; however, parameter $var of count() does only seem to accept Countable|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

190
                $existingMigrations = count(/** @scrutinizer ignore-type */ glob($migrationDirectory . '/*_' . $className . '*.php'));
Loading history...
191 2
                if ($existingMigrations) {
192
                    $className = $className . sprintf('%03d', $existingMigrations + 1);
193
                }
194 2
                $parameters = array_merge($parameters, array(
195 2
                    'class_name' => $className
196
                ));
197 2
                $fileName = $date . '_' . $className . '.php';
198 2
                break;
199
200
            default:
201 30
                if ($name == '') {
202 1
                    $name = 'placeholder';
203
                }
204 30
                $fileName = $date . '_' . $name . '.' . $fileType;
0 ignored issues
show
Bug introduced by
Are you sure $name of type null|string|string[] can be used in concatenation? ( Ignorable by Annotation )

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

204
                $fileName = $date . '_' . /** @scrutinizer ignore-type */ $name . '.' . $fileType;
Loading history...
205
        }
206
207 34
        $path = $migrationDirectory . '/' . $fileName;
208
209 34
        $warning = $this->generateMigrationFile($path, $fileType, $migrationType, $parameters);
210
211 34
        $output->writeln(sprintf("Generated new migration file: <info>%s</info>", $path));
212
213 34
        if ($warning != '') {
214
            $output->writeln("<comment>$warning</comment>");
215
        }
216
217 34
        return 0;
218
    }
219
220
    /**
221
     * Generates a migration definition file.
222
     * @todo allow non-filesystem storage
223
     *
224
     * @param string $path filename to file to generate (full path)
225
     * @param string $fileType The type of migration file to generate
226
     * @param string $migrationType The type of migration to generate
227
     * @param array $parameters passed on to twig
228
     * @return string A warning message in case file generation was OK but there was something weird
229
     * @throws \Exception
230
     */
231 34
    protected function generateMigrationFile($path, $fileType, $migrationType, array $parameters = array())
232
    {
233 34
        $warning = '';
234
235
        switch ($migrationType) {
236 34
            case 'db':
237 32
            case 'generic':
238 30
            case 'php':
239
                // Generate migration file by template
240 6
                $template = $migrationType . 'Migration.' . $fileType . '.twig';
241 6
                $templatePath = $this->getApplication()->getKernel()->getBundle($this->thisBundle)->getPath() . '/Resources/views/MigrationTemplate/';
0 ignored issues
show
Bug introduced by
The method getKernel() does not exist on Symfony\Component\Console\Application. It seems like you code against a sub-type of Symfony\Component\Console\Application such as Symfony\Bundle\FrameworkBundle\Console\Application. ( Ignorable by Annotation )

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

241
                $templatePath = $this->getApplication()->/** @scrutinizer ignore-call */ getKernel()->getBundle($this->thisBundle)->getPath() . '/Resources/views/MigrationTemplate/';
Loading history...
242 6
                if (!is_file($templatePath . $template)) {
243
                    throw new \Exception("The combination of migration type '$migrationType' is not supported with format '$fileType'");
244
                }
245
246 6
                $code = $this->getContainer()->get('twig')->render($this->thisBundle . ':MigrationTemplate:' . $template, $parameters);
247 6
                break;
248
249
            default:
250
                // Generate migration file by executor
251 28
                $executors = $this->getGeneratingExecutors();
252 28
                if (!in_array($migrationType, $executors)) {
253
                    throw new \Exception("It is not possible to generate a migration of type '$migrationType': executor not found or not a generator");
254
                }
255
                /** @var MigrationGeneratorInterface $executor */
256 28
                $executor = $this->getMigrationService()->getExecutor($migrationType);
257
258 28
                $context = $this->migrationContextFromParameters($parameters);
259
260 28
                $matchCondition = array($parameters['matchType'] => $parameters['matchValue']);
261 28
                if ($parameters['matchExcept']) {
262 2
                    $matchCondition = array(MatcherInterface::MATCH_NOT => $matchCondition);
263
                }
264 28
                $data = $executor->generateMigration($matchCondition, $parameters['mode'], $context);
265
266 28
                if (!is_array($data) || !count($data)) {
0 ignored issues
show
introduced by
The condition is_array($data) is always true.
Loading history...
267
                    $warning = 'Note: the generated migration is empty';
268
                }
269
270
                switch ($fileType) {
271 28
                    case 'yml':
272 18
                        $code = Yaml::dump($data, 5);
273 18
                        break;
274 10
                    case 'json':
275 10
                        $code = json_encode($data, JSON_PRETTY_PRINT);
276 10
                        break;
277
                    default:
278
                        throw new \Exception("The combination of migration type '$migrationType' is not supported with format '$fileType'");
279
                }
280
        }
281
282 34
        file_put_contents($path, $code);
283
284 34
        return $warning;
285
    }
286
287
    protected function listAvailableTypes(OutputInterface $output)
288
    {
289
        $output->writeln('Specific migration types available for generation (besides sql,php, generic):');
290
        foreach ($this->getGeneratingExecutors() as $executorType) {
291
            $output->writeln($executorType);
292
            /** @var MigrationGeneratorInterface $executor */
293
            $executor = $this->getMigrationService()->getExecutor($executorType);
294
            if ($executor instanceof EnumerableMatcherInterface) {
295
                $conditions = $executor->listAllowedConditions();
296
                $conditions = array_diff($conditions, array('and', 'or', 'not'));
297
                $output->writeln("  corresponding match types:\n    - " . implode("\n    - ", $conditions));
298
            }
299
        }
300
    }
301
302
    /**
303
     * @param string $bundleName a bundle name or filesystem path to a directory
304
     * @return string
305
     */
306 34
    protected function getMigrationDirectory($bundleName)
307
    {
308
        // Allow direct usage of a directory path instead of a bundle name
309 34
        if (strpos($bundleName, '/') !== false && is_dir($bundleName)) {
310
            return rtrim($bundleName, '/');
311
        }
312
313 34
        $activeBundles = array();
314 34
        foreach ($this->getApplication()->getKernel()->getBundles() as $bundle) {
315 34
            $activeBundles[] = $bundle->getName();
316
        }
317 34
        asort($activeBundles);
318 34
        if (!in_array($bundleName, $activeBundles)) {
319
            throw new \InvalidArgumentException("Bundle '$bundleName' does not exist or it is not enabled. Try with one of:\n" . implode(', ', $activeBundles));
320
        }
321
322 34
        $bundle = $this->getApplication()->getKernel()->getBundle($bundleName);
323 34
        $migrationDirectory = $bundle->getPath() . '/' . $this->getContainer()->get('ez_migration_bundle.helper.config.resolver')->getParameter('kaliop_bundle_migration.version_directory');
324
325 34
        return $migrationDirectory;
326
    }
327
328
    /**
329
     * @todo move somewhere else. Maybe to the MigrationService itself ?
330
     * @return string[]
331
     */
332 28
    protected function getGeneratingExecutors()
333
    {
334 28
        $migrationService = $this->getMigrationService();
335 28
        $executors = $migrationService->listExecutors();
336 28
        foreach($executors as $key => $name) {
337 28
            $executor = $migrationService->getExecutor($name);
338 28
            if (!$executor instanceof MigrationGeneratorInterface) {
339 28
                unset($executors[$key]);
340
            }
341
        }
342 28
        return $executors;
343
    }
344
345
    /**
346
     * @see MigrationService::migrationContextFromParameters
347
     * @param array $parameters these come directly from cli options
348
     * @return array
349
     */
350 28
    protected function migrationContextFromParameters(array $parameters)
351
    {
352 28
        $context = array();
353
354 28
        if (isset($parameters['lang']) && $parameters['lang'] != '') {
355
            $context['defaultLanguageCode'] = $parameters['lang'];
356
        }
357 28
        if (isset($parameters['adminLogin']) && $parameters['adminLogin'] != '') {
358
            $context['adminUserLogin'] = $parameters['adminLogin'];
359
        }
360 28
        if (isset($parameters['forceSigchildEnabled']) && $parameters['forceSigchildEnabled'] !== null)
361
        {
362
            $context['forceSigchildEnabled'] = $parameters['forceSigchildEnabled'];
363
        }
364
365 28
        return $context;
366
    }
367
}
368