Passed
Push — master ( 9d6058...4098f7 )
by Gaetano
06:09
created

GenerateCommand   B

Complexity

Total Complexity 46

Size/Duplication

Total Lines 336
Duplicated Lines 0 %

Test Coverage

Coverage 88.89%

Importance

Changes 0
Metric Value
eloc 194
dl 0
loc 336
ccs 128
cts 144
cp 0.8889
rs 8.72
c 0
b 0
f 0
wmc 46

7 Methods

Rating   Name   Duplication   Size   Complexity  
A listAvailableTypes() 0 11 3
A migrationContextFromParameters() 0 12 5
A configure() 0 17 1
A getGeneratingExecutors() 0 11 3
B generateMigrationFile() 0 54 11
F execute() 0 119 18
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 78
     */
27
    protected function configure()
28 78
    {
29 78
        $this->setName('kaliop:migration:generate')
30 78
            ->setDescription('Generate a blank migration definition file.')
31 78
            ->addOption('format', null, InputOption::VALUE_REQUIRED, 'The format of migration file to generate (' . implode(', ', $this->availableMigrationFormats) . ')', 'yml')
32 78
            ->addOption('type', null, InputOption::VALUE_REQUIRED, 'The type of migration to generate (' . implode(', ', $this->availableTypes) . ')', '')
33 78
            ->addOption('mode', null, InputOption::VALUE_REQUIRED, 'The mode of the migration (' . implode(', ', $this->availableModes) . ')', 'create')
34 78
            ->addOption('match-type', null, InputOption::VALUE_REQUIRED, 'The type of identifier used to find the entity to generate the migration for', null)
35 78
            ->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 78
            ->addOption('match-except', null, InputOption::VALUE_NONE, 'Used to match all entities except the ones satisfying the match-value condition', null)
37 78
            ->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 78
            ->addOption('dbserver', null, InputOption::VALUE_REQUIRED, 'The type of the database server the sql migration is for, when type=db (mysql, postgresql, ...)', 'mysql')
39 78
            ->addOption('admin-login', 'a', InputOption::VALUE_REQUIRED, "Login of admin account used whenever elevated privileges are needed (user id 14 used by default)")
40 78
            ->addOption('list-types', null, InputOption::VALUE_NONE, 'Use this option to list all available migration types and their match conditions')
41 78
            ->addArgument('bundle', InputArgument::REQUIRED, 'The bundle to generate the migration definition file in. eg.: AcmeMigrationBundle')
42 78
            ->addArgument('name', InputArgument::OPTIONAL, 'The migration name (will be prefixed with current date)', null)
43
            ->setHelp(<<<EOT
44
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 78
    <info> php ezpublish/console ka:mig:gen --list-types whatever</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
    }
78
79
    /**
80
     * Run the command and display the results.
81 31
     *
82
     * @param InputInterface $input
83 31
     * @param OutputInterface $output
84 31
     * @return null|int null or 0 if everything went fine, or an error code
85 31
     * @throws \InvalidArgumentException When an unsupported file type is selected
86 31
     */
87 31
    public function execute(InputInterface $input, OutputInterface $output)
88 31
    {
89 31
        if ($input->getOption('list-types')) {
90 31
            $this->listAvailableTypes($output);
91 31
            return;
92
        }
93 31
94
        $bundleName = $input->getArgument('bundle');
95
        $name = $input->getArgument('name');
96
        $fileType = $input->getOption('format');
97
        $migrationType = $input->getOption('type');
98 31
        $matchType = $input->getOption('match-type');
99 6
        $matchValue = $input->getOption('match-value');
100 2
        $matchExcept = $input->getOption('match-except');
101 4
        $mode = $input->getOption('mode');
102 2
        $dbServer = $input->getOption('dbserver');
103
104 2
        if ($bundleName == $this->thisBundle) {
105
            throw new \InvalidArgumentException("It is not allowed to create migrations in bundle '$bundleName'");
106
        }
107
108 31
        // be kind to lazy users
109
        if ($migrationType == '') {
110
            if ($fileType == 'sql') {
111
                $migrationType = 'db';
112 31
            } elseif ($fileType == 'php') {
113
                $migrationType = 'php';
114
            } else {
115
                $migrationType = 'generic';
116 31
            }
117
        }
118 31
119 1
        if (!in_array($fileType, $this->availableMigrationFormats)) {
120 1
            throw new \InvalidArgumentException('Unsupported migration file format ' . $fileType);
121 1
        }
122
123
        if (!in_array($mode, $this->availableModes)) {
124 1
            throw new \InvalidArgumentException('Unsupported migration mode ' . $mode);
125 1
        }
126 1
127 1
        $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

127
        $migrationDirectory = $this->getMigrationDirectory(/** @scrutinizer ignore-type */ $bundleName);
Loading history...
128
129
        if (!is_dir($migrationDirectory)) {
130
            $output->writeln(sprintf(
131
                "Migrations directory <info>%s</info> does not exist. I will create it now....",
132
                $migrationDirectory
133
            ));
134
135
            if (mkdir($migrationDirectory, self::DIR_CREATE_PERMISSIONS, true)) {
136
                $output->writeln(sprintf(
137
                    "Migrations directory <info>%s</info> has been created",
138 31
                    $migrationDirectory
139 2
                ));
140
            } else {
141
                throw new FileException(sprintf(
142
                    "Failed to create migrations directory %s.",
143 31
                    $migrationDirectory
144 31
                ));
145 31
            }
146 31
        }
147 31
148 31
        // allow to generate migrations for many entities
149 31
        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

149
        if (strpos(/** @scrutinizer ignore-type */ $matchValue, ',') !== false ) {
Loading history...
150
            $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

150
            $matchValue = explode(',', /** @scrutinizer ignore-type */ $matchValue);
Loading history...
151
        }
152 31
153
        $parameters = array(
154
            'dbserver' => $dbServer,
155 31
            'matchType' => $matchType,
156
            'matchValue' => $matchValue,
157 2
            'matchExcept' => $matchExcept,
158 1
            'mode' => $mode,
159
            'lang' => $input->getOption('lang'),
160 2
            'adminLogin' => $input->getOption('admin-login')
161 2
        );
162
163 29
        $date = date('YmdHis');
164
165 2
        switch ($fileType) {
166 2
            case 'sql':
167 1
                /// @todo this logic should come from the DefinitionParser, really
168
                if ($name != '') {
169
                    $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

169
                    $name = '_' . ltrim(/** @scrutinizer ignore-type */ $name, '_');
Loading history...
170 2
                }
171 2
                $fileName = $date . '_' . $dbServer . $name . '.sql';
172
                break;
173
174 2
            case 'php':
175 2
                /// @todo this logic should come from the DefinitionParser, really
176
                $className = ltrim($name, '_');
177 2
                if ($className == '') {
178 2
                    $className = 'Migration';
179
                }
180
                // Make sure that php class names are unique, not only migration definition file names
181 27
                $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

181
                $existingMigrations = count(/** @scrutinizer ignore-type */ glob($migrationDirectory . '/*_' . $className . '*.php'));
Loading history...
182 1
                if ($existingMigrations) {
183
                    $className = $className . sprintf('%03d', $existingMigrations + 1);
184 27
                }
185
                $parameters = array_merge($parameters, array(
186
                    'class_name' => $className
187 31
                ));
188
                $fileName = $date . '_' . $className . '.php';
189 31
                break;
190
191 31
            default:
192
                if ($name == '') {
193 31
                    $name = 'placeholder';
194
                }
195
                $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

195
                $fileName = $date . '_' . /** @scrutinizer ignore-type */ $name . '.' . $fileType;
Loading history...
196 31
        }
197
198
        $path = $migrationDirectory . '/' . $fileName;
199
200
        $warning = $this->generateMigrationFile($path, $fileType, $migrationType, $parameters);
201
202
        $output->writeln(sprintf("Generated new migration file: <info>%s</info>", $path));
203
204
        if ($warning != '') {
205
            $output->writeln("<comment>$warning</comment>");
206
        }
207
    }
208
209 31
    /**
210
     * Generates a migration definition file.
211 31
     * @todo allow non-filesystem storage
212
     *
213
     * @param string $path filename to file to generate (full path)
214 31
     * @param string $fileType The type of migration file to generate
215 29
     * @param string $migrationType The type of migration to generate
216 27
     * @param array $parameters passed on to twig
217
     * @return string A warning message in case file generation was OK but there was something weird
218 6
     * @throws \Exception
219 6
     */
220 6
    protected function generateMigrationFile($path, $fileType, $migrationType, array $parameters = array())
221
    {
222
        $warning = '';
223
224 6
        switch ($migrationType) {
225 6
            case 'db':
226
            case 'generic':
227
            case 'php':
228
                // Generate migration file by template
229 25
                $template = $migrationType . 'Migration.' . $fileType . '.twig';
230 25
                $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

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