Completed
Push — master ( b2ce91...23c0f4 )
by Gaetano
12:18 queued 02:46
created

GenerateCommand::generateMigrationFile()   C

Complexity

Conditions 13
Paths 31

Size

Total Lines 57
Code Lines 37

Duplication

Lines 13
Ratio 22.81 %

Code Coverage

Tests 29
CRAP Score 13.3008

Importance

Changes 0
Metric Value
dl 13
loc 57
ccs 29
cts 33
cp 0.8788
rs 6.5962
c 0
b 0
f 0
cc 13
eloc 37
nc 31
nop 4
crap 13.3008

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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 71
    protected function configure()
27
    {
28 71
        $this->setName('kaliop:migration:generate')
29 71
            ->setDescription('Generate a blank migration definition file.')
30 71
            ->addOption('format', null, InputOption::VALUE_REQUIRED, 'The format of migration file to generate (' . implode(', ', $this->availableMigrationFormats) . ')', 'yml')
31 71
            ->addOption('type', null, InputOption::VALUE_REQUIRED, 'The type of migration to generate (' . implode(', ', $this->availableTypes) . ')', '')
32 71
            ->addOption('mode', null, InputOption::VALUE_REQUIRED, 'The mode of the migration (' . implode(', ', $this->availableModes) . ')', 'create')
33 71
            ->addOption('match-type', null, InputOption::VALUE_REQUIRED, 'The type of identifier used to find the entity to generate the migration for', null)
34 71
            ->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 71
            ->addOption('match-except', null, InputOption::VALUE_NONE, 'Used to match all entities except the ones satisfying the match-value condition', null)
36 71
            ->addOption('lang', null, InputOption::VALUE_REQUIRED, 'The language of the migration (eng-GB, ger-DE, ...)', 'eng-GB')
37 71
            ->addOption('dbserver', null, InputOption::VALUE_REQUIRED, 'The type of the database server the sql migration is for, when type=db (mysql, postgresql, ...)', 'mysql')
38 71
            ->addOption('role', null, InputOption::VALUE_REQUIRED, 'Deprecated: The role identifier (or id) that you would like to update, for type=role', null)
39 71
            ->addArgument('bundle', InputArgument::REQUIRED, 'The bundle to generate the migration definition file in. eg.: AcmeMigrationBundle')
40 71
            ->addArgument('name', InputArgument::OPTIONAL, 'The migration name (will be prefixed with current date)', null)
41 71
            ->setHelp(<<<EOT
42 71
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 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 31
    public function execute(InputInterface $input, OutputInterface $output)
82
    {
83 31
        $bundleName = $input->getArgument('bundle');
84 31
        $name = $input->getArgument('name');
85 31
        $fileType = $input->getOption('format');
86 31
        $migrationType = $input->getOption('type');
87 31
        $role = $input->getOption('role');
88 31
        $matchType = $input->getOption('match-type');
89 31
        $matchValue = $input->getOption('match-value');
90 31
        $matchExcept = $input->getOption('match-except');
91 31
        $mode = $input->getOption('mode');
92 31
        $dbServer = $input->getOption('dbserver');
93
94 31
        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 31
        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 31
        if ($migrationType == '') {
110 6
            if ($fileType == 'sql') {
111 2
                $migrationType = 'db';
112 4
            } elseif ($fileType == 'php') {
113 2
                $migrationType = 'php';
114
            } else {
115 2
                $migrationType = 'generic';
116
            }
117
        }
118
119 31
        if (!in_array($fileType, $this->availableMigrationFormats)) {
120
            throw new \InvalidArgumentException('Unsupported migration file format ' . $fileType);
121
        }
122
123 31
        if (!in_array($mode, $this->availableModes)) {
124
            throw new \InvalidArgumentException('Unsupported migration mode ' . $mode);
125
        }
126
127 31
        $migrationDirectory = $this->getMigrationDirectory($bundleName);
128
129 31
        if (!is_dir($migrationDirectory)) {
130 1
            $output->writeln(sprintf(
131 1
                "Migrations directory <info>%s</info> does not exist. I will create it now....",
132 1
                $migrationDirectory
133
            ));
134
135 1
            if (mkdir($migrationDirectory, self::DIR_CREATE_PERMISSIONS, true)) {
136 1
                $output->writeln(sprintf(
137 1
                    "Migrations directory <info>%s</info> has been created",
138 1
                    $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 31
        if (strpos($matchValue, ',') !== false ) {
150 2
            $matchValue = explode(',', $matchValue);
151
        }
152
153
        $parameters = array(
154 31
            'dbserver' => $dbServer,
155 31
            'matchType' => $matchType,
156 31
            'matchValue' => $matchValue,
157 31
            'matchExcept' => $matchExcept,
158 31
            'mode' => $mode,
159 31
            'lang' => $input->getOption('lang')
160
        );
161
162 31
        $date = date('YmdHis');
163
164
        switch ($fileType) {
165 31
            case 'sql':
166
                /// @todo this logic should come from the DefinitionParser, really
167 2
                if ($name != '') {
168 1
                    $name = '_' . ltrim($name, '_');
169
                }
170 2
                $fileName = $date . '_' . $dbServer . $name . '.sql';
171 2
                break;
172
173 29
            case 'php':
174
                /// @todo this logic should come from the DefinitionParser, really
175 2
                $className = ltrim($name, '_');
176 2
                if ($className == '') {
177 1
                    $className = 'Migration';
178
                }
179
                // Make sure that php class names are unique, not only migration definition file names
180 2
                $existingMigrations = count(glob($migrationDirectory . '/*_' . $className . '*.php'));
181 2
                if ($existingMigrations) {
182
                    $className = $className . sprintf('%03d', $existingMigrations + 1);
183
                }
184 2
                $parameters = array_merge($parameters, array(
185 2
                    'class_name' => $className
186
                ));
187 2
                $fileName = $date . '_' . $className . '.php';
188 2
                break;
189
190
            default:
191 27
                if ($name == '') {
192 1
                    $name = 'placeholder';
193
                }
194 27
                $fileName = $date . '_' . $name . '.' . $fileType;
195
        }
196
197 31
        $path = $migrationDirectory . '/' . $fileName;
198
199 31
        $warning = $this->generateMigrationFile($path, $fileType, $migrationType, $parameters);
200
201 31
        $output->writeln(sprintf("Generated new migration file: <info>%s</info>", $path));
202
203 31
        if ($warning != '') {
204
            $output->writeln("<comment>$warning</comment>");
205
        }
206 31
    }
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 31
    protected function generateMigrationFile($path, $fileType, $migrationType, array $parameters = array())
219
    {
220 31
        $warning = '';
221
222
        switch ($migrationType) {
223 31
            case 'db':
224 29
            case 'generic':
225 27
            case 'php':
226
                // Generate migration file by template
227 6
                $template = $migrationType . 'Migration.' . $fileType . '.twig';
228 6
                $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 6
                if (!is_file($templatePath . $template)) {
230
                    throw new \Exception("The combination of migration type '$migrationType' is not supported with format '$fileType'");
231
                }
232
233 6
                $code = $this->getContainer()->get('twig')->render($this->thisBundle . ':MigrationTemplate:' . $template, $parameters);
234 6
                break;
235
236
            default:
237
                // Generate migration file by executor
238 25
                $executors = $this->getGeneratingExecutors();
239 25
                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 25
                $executor = $this->getMigrationService()->getExecutor($migrationType);
243
244 25
                $context = array();
245 25 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 25
                    $context['defaultLanguageCode'] = $parameters['lang'];
247
                }
248
249 25
                $matchCondition = array($parameters['matchType'] => $parameters['matchValue']);
250 25
                if ($parameters['matchExcept']) {
251 2
                    $matchCondition = array(MatcherInterface::MATCH_NOT => $matchCondition);
252
                }
253 25
                $data = $executor->generateMigration($matchCondition, $parameters['mode'], $context);
254
255 25
                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 25
                    case 'yml':
261 16
                        $code = Yaml::dump($data, 5);
262 16
                        break;
263 9
                    case 'json':
264 9
                        $code = json_encode($data, JSON_PRETTY_PRINT);
265 9
                        break;
266
                    default:
267
                        throw new \Exception("The combination of migration type '$migrationType' is not supported with format '$fileType'");
268
                }
269
        }
270
271 31
        file_put_contents($path, $code);
272
273 31
        return $warning;
274
    }
275
276
    /**
277
     * @param string $bundleName a bundle name or filesystem path to a directory
278
     * @return string
279
     */
280 31
    protected function getMigrationDirectory($bundleName)
281
    {
282
        // Allow direct usage of a directory path instead of a bundle name
283 31
        if (strpos($bundleName, '/') !== false && is_dir($bundleName)) {
284
            return rtrim($bundleName, '/');
285
        }
286
287 31
        $activeBundles = array();
288 31
        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 31
            $activeBundles[] = $bundle->getName();
290
        }
291 31
        asort($activeBundles);
292 31
        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 31
        $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 31
        $migrationDirectory = $bundle->getPath() . '/' . $this->getContainer()->get('ez_migration_bundle.helper.config.resolver')->getParameter('kaliop_bundle_migration.version_directory');
298
299 31
        return $migrationDirectory;
300
    }
301
302
    /// @todo move somewhere else. Maybe to the MigrationService itself ?
303 25 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 25
        $migrationService = $this->getMigrationService();
306 25
        $executors = $migrationService->listExecutors();
307 25
        foreach($executors as $key => $name) {
308 25
            $executor = $migrationService->getExecutor($name);
309 25
            if (!$executor instanceof MigrationGeneratorInterface) {
310 25
                unset($executors[$key]);
311
            }
312
        }
313 25
        return $executors;
314
    }
315
}
316