Passed
Push — master ( 07d7eb...5150f4 )
by Gaetano
10:15
created

GenerateCommand::migrationContextFromParameters()   B

Complexity

Conditions 7
Paths 8

Size

Total Lines 16
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 56

Importance

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

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

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

155
            $matchValue = explode(',', /** @scrutinizer ignore-type */ $matchValue);
Loading history...
156
        }
157 2
158 1
        $parameters = array(
159
            'dbserver' => $dbServer,
160 2
            'matchType' => $matchType,
161 2
            'matchValue' => $matchValue,
162
            'matchExcept' => $matchExcept,
163 29
            'mode' => $mode,
164
            'lang' => $input->getOption('lang'),
165 2
            'adminLogin' => $input->getOption('admin-login')
166 2
            /// @todo should we allow users to specify this ?
167 1
            //'forceSigchildHandling' => null
168
        );
169
170 2
        $date = date('YmdHis');
171 2
172
        switch ($fileType) {
173
            case 'sql':
174 2
                /// @todo this logic should come from the DefinitionParser, really
175 2
                if ($name != '') {
176
                    $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

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

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

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

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