Passed
Push — main ( dd05fa...4730fb )
by Gaetano
08:25
created

GenerateCommand::migrationContextFromParameters()   A

Complexity

Conditions 5
Paths 4

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 5.9256

Importance

Changes 0
Metric Value
cc 5
eloc 6
nc 4
nop 1
dl 0
loc 12
ccs 4
cts 6
cp 0.6667
crap 5.9256
rs 9.6111
c 0
b 0
f 0
1
<?php
2
3
namespace Kaliop\eZMigrationBundle\Command;
4
5
use Kaliop\eZMigrationBundle\API\MigrationGeneratorInterface;
6
use Kaliop\eZMigrationBundle\API\MatcherInterface;
7
use Kaliop\eZMigrationBundle\API\EnumerableMatcherInterface;
8
use Kaliop\eZMigrationBundle\API\Event\MigrationGeneratedEvent;
9
use Kaliop\eZMigrationBundle\API\Exception\MigrationBundleException;
10
use Symfony\Component\Console\Input\InputArgument;
11
use Symfony\Component\Console\Input\InputInterface;
12
use Symfony\Component\Console\Input\InputOption;
13
use Symfony\Component\Console\Output\OutputInterface;
14
use Symfony\Component\HttpFoundation\File\Exception\FileException;
15
use Symfony\Component\Yaml\Yaml;
16
17
/**
18
 * @todo allow passing in more context options, esp. for content/generate migrations
19
 */
20
class GenerateCommand extends AbstractCommand
21
{
22
    const DIR_CREATE_PERMISSIONS = 0755;
23
24
    private $availableMigrationFormats = array('yml', 'php', 'sql', 'json');
25
    private $availableModes = array('create', 'update', 'delete');
26
    private $availableTypes = array('content', 'content_type', 'content_type_group', 'language', 'object_state', 'object_state_group', 'role', 'section', 'generic', 'db', 'php', '...');
27
    private $thisBundle = 'EzMigrationBundle';
28
29
    protected $eventName = 'ez_migration.migration_generated';
30
31
    /**
32
     * Configure the console command
33
     */
34 148
    protected function configure()
35
    {
36 148
        $this->setName('kaliop:migration:generate')
37 148
            ->setDescription('Generate a blank migration definition file.')
38 148
            ->addOption('format', null, InputOption::VALUE_REQUIRED, 'The format of migration file to generate (' . implode(', ', $this->availableMigrationFormats) . ')', 'yml')
39 148
            ->addOption('type', null, InputOption::VALUE_REQUIRED, 'The type of migration to generate (' . implode(', ', $this->availableTypes) . ')', '')
40 148
            ->addOption('mode', null, InputOption::VALUE_REQUIRED, 'The mode of the migration (' . implode(', ', $this->availableModes) . ')', 'create')
41 148
            ->addOption('match-type', null, InputOption::VALUE_REQUIRED, 'The type of identifier used to find the entity to generate the migration for', null)
42 148
            ->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)
43 148
            ->addOption('match-except', null, InputOption::VALUE_NONE, 'Used to match all entities except the ones satisfying the match-value condition', null)
44 148
            ->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')
45 148
            ->addOption('dbserver', null, InputOption::VALUE_REQUIRED, 'The type of the database server the sql migration is for, when type=db (mysql, postgresql, ...)', 'mysql')
46 148
            ->addOption('admin-login', 'a', InputOption::VALUE_REQUIRED, "Login of admin account used whenever elevated privileges are needed (user id 14 used by default)")
47 148
            ->addOption('list-types', null, InputOption::VALUE_NONE, 'Use this option to list all available migration types and their match conditions')
48 148
            ->addArgument('bundle', InputArgument::OPTIONAL, 'The bundle to generate the migration definition file in. eg.: AcmeMigrationBundle')
49 148
            ->addArgument('name', InputArgument::OPTIONAL, 'The migration name (will be prefixed with current date)', null)
50 148
            ->setHelp(<<<EOT
51 148
The <info>kaliop:migration:generate</info> command generates a skeleton migration definition file:
52
53
    <info>php bin/console kaliop:migration:generate bundleName</info>
54
55
You can optionally specify the file type to generate with <info>--format</info>, as well a name for the migration:
56
57
    <info>php bin/console kaliop:migration:generate --format=json bundleName migrationName</info>
58
59
For SQL type migration you can optionally specify the database server type the migration is for with <info>--dbserver</info>:
60
61
    <info>php bin/console kaliop:migration:generate --format=sql bundleName</info>
62
63
For content/content_type/language/object_state/role/section migrations you need to specify the entity that you want to generate the migration for:
64
65
    <info>php bin/console kaliop:migration:generate --type=content --match-type=content_id --match-value=10,14 --lang=all bundleName</info>
66
67
For role type migration you will receive a yaml file with the current role definition. You must define ALL the policies
68
you wish for the role. Any not defined will be removed. Example for updating an existing role:
69
70
    <info>php bin/console kaliop:migration:generate --type=role --mode=update --match-type=identifier --match-value=Anonymous bundleName</info>
71
72
For freeform php migrations, you will receive a php class definition
73
74
    <info>php bin/console kaliop:migration:generate --format=php bundlename classname</info>
75
76
To list all available migration types for generation, as well as the corresponding match-types, run:
77
78
    <info>php bin/console ka:mig:gen --list-types</info>
79
80
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>
81
option when you run the <info>migrate</info> command.
82
EOT
83
            );
84 148
    }
85
86
    /**
87
     * Run the command and display the results.
88
     *
89
     * @param InputInterface $input
90
     * @param OutputInterface $output
91
     * @return null|int null or 0 if everything went fine, or an error code
92
     * @throws \InvalidArgumentException When an unsupported file type is selected
93
     *
94
     * @todo for type=db, we could fold 'dbserver' option into 'mode'
95
     */
96 40
    public function execute(InputInterface $input, OutputInterface $output)
97
    {
98 40
        $this->setOutput($output);
99 40
        $this->setVerbosity($output->getVerbosity());
100
101 40
        if ($input->getOption('list-types')) {
102
            $this->listAvailableTypes($output);
103
            return 0;
104
        }
105
106 40
        $bundleName = $input->getArgument('bundle');
107 40
        if ($bundleName === null) {
108
            // throw same exception as SF would when declaring 'bundle' as mandatory arg
109
            throw new \RuntimeException('Not enough arguments (missing: "bundle").');
110
        }
111 40
        $name = $input->getArgument('name');
112 40
        $fileType = $input->getOption('format');
113 40
        $migrationType = $input->getOption('type');
114 40
        $matchType = $input->getOption('match-type');
115 40
        $matchValue = $input->getOption('match-value');
116 40
        $matchExcept = $input->getOption('match-except');
117 40
        $mode = $input->getOption('mode');
118 40
        $dbServer = $input->getOption('dbserver');
119
120 40
        if ($bundleName == $this->thisBundle) {
121
            throw new \InvalidArgumentException("It is not allowed to create migrations in bundle '$bundleName'");
122
        }
123
124
        // be kind to lazy users
125 40
        if ($migrationType == '') {
126 6
            if ($fileType == 'sql') {
127 2
                $migrationType = 'db';
128 4
            } elseif ($fileType == 'php') {
129 2
                $migrationType = 'php';
130
            } else {
131 2
                $migrationType = 'generic';
132
            }
133
        }
134
135 40
        if (!in_array($fileType, $this->availableMigrationFormats)) {
136
            throw new \InvalidArgumentException('Unsupported migration file format ' . $fileType);
137
        }
138
139 40
        if (!in_array($mode, $this->availableModes)) {
140
            throw new \InvalidArgumentException('Unsupported migration mode ' . $mode);
141
        }
142
143 40
        $migrationDirectory = $this->getMigrationDirectory($bundleName);
144
145 40
        if (!is_dir($migrationDirectory)) {
146 1
            $output->writeln(sprintf(
147 1
                "Migrations directory <info>%s</info> does not exist. I will create it now....",
148
                $migrationDirectory
149
            ));
150
151 1
            if (mkdir($migrationDirectory, self::DIR_CREATE_PERMISSIONS, true)) {
152 1
                $output->writeln(sprintf(
153 1
                    "Migrations directory <info>%s</info> has been created",
154
                    $migrationDirectory
155
                ));
156
            } else {
157
                throw new FileException(sprintf(
158
                    "Failed to create migrations directory %s.",
159
                    $migrationDirectory
160
                ));
161
            }
162
        }
163
164
        // allow to generate migrations for many entities
165 40
        if (strpos($matchValue, ',') !== false) {
166 2
            $matchValue = explode(',', $matchValue);
167
        }
168
169
        $parameters = array(
170 40
            'type' => $migrationType,
171 40
            'mode' => $mode,
172 40
            'matchType' => $matchType,
173 40
            'matchValue' => $matchValue,
174 40
            'matchExcept' => $matchExcept,
175 40
            'dbserver' => $dbServer,
176
            /// @todo move these 2 params out of here, pass the context as template parameter instead
177 40
            'lang' => $input->getOption('lang'),
178 40
            'adminLogin' => $input->getOption('admin-login')
179
        );
180
181
        $date = date('YmdHis');
182
183 40
        switch ($fileType) {
184
            case 'sql':
185 40
                /// @todo this logic should come from the DefinitionParser, really
186 40
                if ($name != '') {
187
                    $name = '_' . ltrim($name, '_');
188 2
                }
189 1
                $fileName = $date . '_' . $dbServer . $name . '.sql';
190
                break;
191 2
192 2
            case 'php':
193
                /// @todo this logic should come from the DefinitionParser, really
194 38
                $className = ltrim($name, '_');
195
                if ($className == '') {
196 2
                    $className = 'Migration';
197 2
                }
198 1
                // Make sure that php class names are unique, not only migration definition file names
199
                $existingMigrations = count(glob($migrationDirectory . '/*_' . $className . '*.php'));
200
                if ($existingMigrations) {
201 2
                    $className = $className . sprintf('%03d', $existingMigrations + 1);
202 2
                }
203
                $parameters = array_merge($parameters, array(
204
                    'class_name' => $className
205 2
                ));
206 2
                $fileName = $date . '_' . $className . '.php';
207
                break;
208 2
209 2
            default:
210
                if ($name == '') {
211
                    $name = 'placeholder';
212 36
                }
213 1
                $fileName = $date . '_' . $name . '.' . $fileType;
214
        }
215 36
216
        $filePath = $migrationDirectory . '/' . $fileName;
217
218 40
        $warning = $this->generateMigrationFile($migrationType, $mode, $fileType, $filePath, $parameters);
219
220 40
        $output->writeln(sprintf("Generated new migration file: <info>%s</info>", $filePath));
221
222 40
        if ($warning != '') {
223
            $output->writeln("<comment>$warning</comment>");
224 40
        }
225 3
226
        return 0;
227
    }
228 40
229
    /**
230
     * Generates a migration definition file.
231
     * @todo allow non-filesystem storage (delegate saving to a service, just as we do for loading)
232
     *
233
     * @param string $migrationType The type of migration to generate
234
     * @param string $migrationMode
235
     * @param string $fileType The type of migration file to generate
236
     * @param string $filePath filename to file to generate (full path)
237
     * @param array $parameters passed on to twig
238
     * @return string A warning message in case file generation was OK but there was something weird
239
     * @throws \Exception
240
     */
241
    protected function generateMigrationFile($migrationType, $migrationMode, $fileType, $filePath, array $parameters = array())
242
    {
243 40
        $warning = '';
244
245 40
        switch ($migrationType) {
246
            case 'db':
247 40
            case 'generic':
248 40
            case 'php':
249 38
                // Generate migration file by template
250 36
                $template = $migrationType . 'Migration.' . $fileType . '.twig';
251
                $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

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