Completed
Push — master ( 5a501b...3982a6 )
by Gaetano
08:04
created

GenerateCommand   A

Complexity

Total Complexity 41

Size/Duplication

Total Lines 302
Duplicated Lines 8.28 %

Coupling/Cohesion

Components 1
Dependencies 7

Test Coverage

Coverage 0%

Importance

Changes 0
Metric Value
wmc 41
lcom 1
cbo 7
dl 25
loc 302
ccs 0
cts 212
cp 0
rs 9.1199
c 0
b 0
f 0

5 Methods

Rating   Name   Duplication   Size   Complexity  
A configure() 0 46 1
F execute() 0 126 19
C generateMigrationFile() 13 57 13
A getMigrationDirectory() 0 21 5
A getGeneratingExecutors() 12 12 3

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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
14
class GenerateCommand extends AbstractCommand
15
{
16
    const DIR_CREATE_PERMISSIONS = 0755;
17
18
    private $availableMigrationFormats = array('yml', 'php', 'sql', 'json');
19
    private $availableModes = array('create', 'update', 'delete');
20
    private $availableTypes = array('role', 'content', 'content_type', 'content_type_group', 'object_state_group', 'section', 'generic', 'db', 'php');
21
    private $thisBundle = 'EzMigrationBundle';
22
23
    /**
24
     * Configure the console command
25
     */
26
    protected function configure()
27
    {
28
        $this->setName('kaliop:migration:generate')
29
            ->setDescription('Generate a blank migration definition file.')
30
            ->addOption('format', null, InputOption::VALUE_REQUIRED, 'The format of migration file to generate (' . implode(', ', $this->availableMigrationFormats) . ')', 'yml')
31
            ->addOption('type', null, InputOption::VALUE_REQUIRED, 'The type of migration to generate (' . implode(', ', $this->availableTypes) . ')', '')
32
            ->addOption('mode', null, InputOption::VALUE_REQUIRED, 'The mode of the migration (' . implode(', ', $this->availableModes) . ')', 'create')
33
            ->addOption('match-type', null, InputOption::VALUE_REQUIRED, 'The type of identifier used to find the entity to generate the migration for', null)
34
            ->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)
35
            ->addOption('match-except', null, InputOption::VALUE_NONE, 'Used to match all entities except the ones satisfying the match-value condition', null)
36
            ->addOption('lang', null, InputOption::VALUE_REQUIRED, 'The language of the migration (eng-GB, ger-DE, ...)', 'eng-GB')
37
            ->addOption('dbserver', null, InputOption::VALUE_REQUIRED, 'The type of the database server the sql migration is for, when type=db (mysql, postgresql, ...)', 'mysql')
38
            ->addOption('role', null, InputOption::VALUE_REQUIRED, 'Deprecated: The role identifier (or id) that you would like to update, for type=role', null)
39
            ->addArgument('bundle', InputArgument::REQUIRED, 'The bundle to generate the migration definition file in. eg.: AcmeMigrationBundle')
40
            ->addArgument('name', InputArgument::OPTIONAL, 'The migration name (will be prefixed with current date)', null)
41
            ->setHelp(<<<EOT
42
The <info>kaliop:migration:generate</info> command generates a skeleton migration definition file:
43
44
    <info>php ezpublish/console kaliop:migration:generate bundlename</info>
45
46
You can optionally specify the file type to generate with <info>--format</info>, as well a name for the migration:
47
48
    <info>php ezpublish/console kaliop:migration:generate --format=json bundlename migrationname</info>
49
50
For SQL type migration you can optionally specify the database server type the migration is for with <info>--dbserver</info>:
51
52
    <info>php ezpublish/console kaliop:migration:generate --format=sql bundlename</info>
53
54
For role/content/content_type migrations you need to specify the entity that you want to generate the migration for:
55
56
    <info>php ezpublish/console kaliop:migration:generate --type=content --match-type=content_id --match-value=10,14 bundlename</info>
57
58
For role type migration you will receive a yaml file with the current role definition. You must define ALL the policies
59
you wish for the role. Any not defined will be removed.
60
61
    <info>php ezpublish/console kaliop:migration:generate --type=role --match-value=Anonymous bundlename</info>
62
63
For freeform php migrations, you will receive a php class definition
64
65
    <info>php ezpublish/console kaliop:migration:generate --format=php bundlename classname</info>
66
67
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>
68
option when you run the <info>migrate</info> command.
69
EOT
70
            );
71
    }
72
73
    /**
74
     * Run the command and display the results.
75
     *
76
     * @param InputInterface $input
77
     * @param OutputInterface $output
78
     * @return null|int null or 0 if everything went fine, or an error code
79
     * @throws \InvalidArgumentException When an unsupported file type is selected
80
     */
81
    public function execute(InputInterface $input, OutputInterface $output)
82
    {
83
        $bundleName = $input->getArgument('bundle');
84
        $name = $input->getArgument('name');
85
        $fileType = $input->getOption('format');
86
        $migrationType = $input->getOption('type');
87
        $role = $input->getOption('role');
88
        $matchType = $input->getOption('match-type');
89
        $matchValue = $input->getOption('match-value');
90
        $matchExcept = $input->getOption('match-except');
91
        $mode = $input->getOption('mode');
92
        $dbServer = $input->getOption('dbserver');
93
94
        if ($role != '') {
95
            $output->writeln('<error>The "role" option is deprecated since version 3.2 and will be removed in 4.0. Use "type=role", "match-type=identifier" and "match-value" instead.</error>');
96
            $migrationType = 'role';
97
            $matchType = 'identifier';
98
            $matchValue = $role;
99
            if ($mode == '') {
100
                $mode = 'update';
101
            }
102
        }
103
104
        if ($bundleName == $this->thisBundle) {
105
            throw new \InvalidArgumentException("It is not allowed to create migrations in bundle '$bundleName'");
106
        }
107
108
        // be kind to lazy users
109
        if ($migrationType == '') {
110
            if ($fileType == 'sql') {
111
                $migrationType = 'db';
112
            } elseif ($fileType == 'php') {
113
                $migrationType = 'php';
114
            } else {
115
                $migrationType = 'generic';
116
            }
117
        }
118
119
        if (!in_array($fileType, $this->availableMigrationFormats)) {
120
            throw new \InvalidArgumentException('Unsupported migration file format ' . $fileType);
121
        }
122
123
        if (!in_array($mode, $this->availableModes)) {
124
            throw new \InvalidArgumentException('Unsupported migration mode ' . $mode);
125
        }
126
127
        $migrationDirectory = $this->getMigrationDirectory($bundleName);
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
                    $migrationDirectory
139
                ));
140
            } else {
141
                throw new FileException(sprintf(
142
                    "Failed to create migrations directory %s.",
143
                    $migrationDirectory
144
                ));
145
            }
146
        }
147
148
        // allow to generate migrations for many entities
149
        if (strpos($matchValue, ',') !== false ) {
150
            $matchValue = explode(',', $matchValue);
151
        }
152
153
        $parameters = array(
154
            'dbserver' => $dbServer,
155
            'matchType' => $matchType,
156
            'matchValue' => $matchValue,
157
            'matchExcept' => $matchExcept,
158
            'mode' => $mode,
159
            'lang' => $input->getOption('lang')
160
        );
161
162
        $date = date('YmdHis');
163
164
        switch ($fileType) {
165
            case 'sql':
166
                /// @todo this logic should come from the DefinitionParser, really
167
                if ($name != '') {
168
                    $name = '_' . ltrim($name, '_');
169
                }
170
                $fileName = $date . '_' . $dbServer . $name . '.sql';
171
                break;
172
173
            case 'php':
174
                /// @todo this logic should come from the DefinitionParser, really
175
                $className = ltrim($name, '_');
176
                if ($className == '') {
177
                    $className = 'Migration';
178
                }
179
                // Make sure that php class names are unique, not only migration definition file names
180
                $existingMigrations = count(glob($migrationDirectory . '/*_' . $className . '*.php'));
181
                if ($existingMigrations) {
182
                    $className = $className . sprintf('%03d', $existingMigrations + 1);
183
                }
184
                $parameters = array_merge($parameters, array(
185
                    'class_name' => $className
186
                ));
187
                $fileName = $date . '_' . $className . '.php';
188
                break;
189
190
            default:
191
                if ($name == '') {
192
                    $name = 'placeholder';
193
                }
194
                $fileName = $date . '_' . $name . '.' . $fileType;
195
        }
196
197
        $path = $migrationDirectory . '/' . $fileName;
198
199
        $warning = $this->generateMigrationFile($path, $fileType, $migrationType, $parameters);
200
201
        $output->writeln(sprintf("Generated new migration file: <info>%s</info>", $path));
202
203
        if ($warning != '') {
204
            $output->writeln("<comment>$warning</comment>");
205
        }
206
    }
207
208
    /**
209
     * Generates a migration definition file.
210
     *
211
     * @param string $path filename to file to generate (full path)
212
     * @param string $fileType The type of migration file to generate
213
     * @param string $migrationType The type of migration to generate
214
     * @param array $parameters passed on to twig
215
     * @return string A warning message in case file generation was OK but there was something weird
216
     * @throws \Exception
217
     */
218
    protected function generateMigrationFile($path, $fileType, $migrationType, array $parameters = array())
219
    {
220
        $warning = '';
221
222
        switch ($migrationType) {
223
            case 'db':
224
            case 'generic':
225
            case 'php':
226
                // Generate migration file by template
227
                $template = $migrationType . 'Migration.' . $fileType . '.twig';
228
                $templatePath = $this->getApplication()->getKernel()->getBundle($this->thisBundle)->getPath() . '/Resources/views/MigrationTemplate/';
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Symfony\Component\Console\Application as the method getKernel() does only exist in the following sub-classes of Symfony\Component\Console\Application: Symfony\Bundle\FrameworkBundle\Console\Application. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
229
                if (!is_file($templatePath . $template)) {
230
                    throw new \Exception("The combination of migration type '$migrationType' is not supported with format '$fileType'");
231
                }
232
233
                $code = $this->getContainer()->get('twig')->render($this->thisBundle . ':MigrationTemplate:' . $template, $parameters);
234
                break;
235
236
            default:
237
                // Generate migration file by executor
238
                $executors = $this->getGeneratingExecutors();
239
                if (!in_array($migrationType, $executors)) {
240
                    throw new \Exception("It is not possible to generate a migration of type '$migrationType': executor not found or not a generator");
241
                }
242
                $executor = $this->getMigrationService()->getExecutor($migrationType);
243
244
                $context = array();
245 View Code Duplication
                if (isset($parameters['lang']) && $parameters['lang'] != '') {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
246
                    $context['defaultLanguageCode'] = $parameters['lang'];
247
                }
248
249
                $matchCondition = array($parameters['matchType'] => $parameters['matchValue']);
250
                if ($parameters['matchExcept']) {
251
                    $matchCondition = array(MatcherInterface::MATCH_NOT => $matchCondition);
252
                }
253
                $data = $executor->generateMigration($matchCondition, $parameters['mode'], $context);
254
255
                if (!is_array($data) || !count($data)) {
256
                    $warning = 'Note: the generated migration is empty';
257
                }
258
259 View Code Duplication
                switch ($fileType) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
260
                    case 'yml':
261
                        $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
                }
269
        }
270
271
        file_put_contents($path, $code);
272
273
        return $warning;
274
    }
275
276
    /**
277
     * @param string $bundleName a bundle name or filesystem path to a directory
278
     * @return string
279
     */
280
    protected function getMigrationDirectory($bundleName)
281
    {
282
        // Allow direct usage of a directory path instead of a bundle name
283
        if (strpos($bundleName, '/') !== false && is_dir($bundleName)) {
284
            return rtrim($bundleName, '/');
285
        }
286
287
        $activeBundles = array();
288
        foreach ($this->getApplication()->getKernel()->getBundles() as $bundle) {
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Symfony\Component\Console\Application as the method getKernel() does only exist in the following sub-classes of Symfony\Component\Console\Application: Symfony\Bundle\FrameworkBundle\Console\Application. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
289
            $activeBundles[] = $bundle->getName();
290
        }
291
        asort($activeBundles);
292
        if (!in_array($bundleName, $activeBundles)) {
293
            throw new \InvalidArgumentException("Bundle '$bundleName' does not exist or it is not enabled. Try with one of:\n" . implode(', ', $activeBundles));
294
        }
295
296
        $bundle = $this->getApplication()->getKernel()->getBundle($bundleName);
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Symfony\Component\Console\Application as the method getKernel() does only exist in the following sub-classes of Symfony\Component\Console\Application: Symfony\Bundle\FrameworkBundle\Console\Application. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
297
        $migrationDirectory = $bundle->getPath() . '/' . $this->getContainer()->get('ez_migration_bundle.helper.config.resolver')->getParameter('kaliop_bundle_migration.version_directory');
298
299
        return $migrationDirectory;
300
    }
301
302
    /// @todo move somewhere else. Maybe to the MigrationService itself ?
303 View Code Duplication
    protected function getGeneratingExecutors()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
304
    {
305
        $migrationService = $this->getMigrationService();
306
        $executors = $migrationService->listExecutors();
307
        foreach($executors as $key => $name) {
308
            $executor = $migrationService->getExecutor($name);
309
            if (!$executor instanceof MigrationGeneratorInterface) {
310
                unset($executors[$key]);
311
            }
312
        }
313
        return $executors;
314
    }
315
}
316