Passed
Pull Request — master (#225)
by
unknown
06:11
created

GenerateCommand::getMigrationDirectory()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 20
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 5.1158

Importance

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

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

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

162
            $matchValue = explode(',', /** @scrutinizer ignore-type */ $matchValue);
Loading history...
163
        }
164
165
        $parameters = array(
166 40
            'type' => $migrationType,
167 40
            'mode' => $mode,
168 40
            'matchType' => $matchType,
169 40
            'matchValue' => $matchValue,
170 40
            'matchExcept' => $matchExcept,
171 40
            'dbserver' => $dbServer,
172
            /// @todo move these 2 params out of here, pass the context as template parameter instead
173 40
            'lang' => $input->getOption('lang'),
174 40
            'adminLogin' => $input->getOption('admin-login')
175
            /// @todo should we allow users to specify this ?
176
            //'forceSigchildEnabled' => null
177
        );
178
179 40
        $date = date('YmdHis');
180
181
        switch ($fileType) {
182 40
            case 'sql':
183
                /// @todo this logic should come from the DefinitionParser, really
184 2
                if ($name != '') {
185 1
                    $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

185
                    $name = '_' . ltrim(/** @scrutinizer ignore-type */ $name, '_');
Loading history...
186
                }
187 2
                $fileName = $date . '_' . $dbServer . $name . '.sql';
188 2
                break;
189
190 38
            case 'php':
191
                /// @todo this logic should come from the DefinitionParser, really
192 2
                $className = ltrim($name, '_');
193 2
                if ($className == '') {
194 1
                    $className = 'Migration';
195
                }
196
                // Make sure that php class names are unique, not only migration definition file names
197 2
                $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

197
                $existingMigrations = count(/** @scrutinizer ignore-type */ glob($migrationDirectory . '/*_' . $className . '*.php'));
Loading history...
198 2
                if ($existingMigrations) {
199
                    $className = $className . sprintf('%03d', $existingMigrations + 1);
200
                }
201 2
                $parameters = array_merge($parameters, array(
202 2
                    'class_name' => $className
203
                ));
204 2
                $fileName = $date . '_' . $className . '.php';
205 2
                break;
206
207
            default:
208 36
                if ($name == '') {
209 1
                    $name = 'placeholder';
210
                }
211 36
                $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

211
                $fileName = $date . '_' . /** @scrutinizer ignore-type */ $name . '.' . $fileType;
Loading history...
212
        }
213
214 40
        $filePath = $migrationDirectory . '/' . $fileName;
215
216 40
        $warning = $this->generateMigrationFile($migrationType, $mode, $fileType, $filePath, $parameters);
0 ignored issues
show
Bug introduced by
It seems like $migrationType can also be of type string[]; however, parameter $migrationType of Kaliop\eZMigrationBundle...generateMigrationFile() 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

216
        $warning = $this->generateMigrationFile(/** @scrutinizer ignore-type */ $migrationType, $mode, $fileType, $filePath, $parameters);
Loading history...
Bug introduced by
It seems like $mode can also be of type string[]; however, parameter $migrationMode of Kaliop\eZMigrationBundle...generateMigrationFile() 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

216
        $warning = $this->generateMigrationFile($migrationType, /** @scrutinizer ignore-type */ $mode, $fileType, $filePath, $parameters);
Loading history...
217
218 6
        $output->writeln(sprintf("Generated new migration file: <info>%s</info>", $filePath));
219
220 6
        if ($warning != '') {
221
            $output->writeln("<comment>$warning</comment>");
222
        }
223
224 6
        return 0;
225
    }
226
227
    /**
228
     * Generates a migration definition file.
229
     * @todo allow non-filesystem storage (delegate saving to a service, just as we do for loading)
230
     *
231
     * @param string $migrationType The type of migration to generate
232
     * @param string $migrationMode
233
     * @param string $fileType The type of migration file to generate
234
     * @param string $filePath filename to file to generate (full path)
235
     * @param array $parameters passed on to twig
236
     * @return string A warning message in case file generation was OK but there was something weird
237
     * @throws \Exception
238
     */
239 40
    protected function generateMigrationFile($migrationType, $migrationMode, $fileType, $filePath, array $parameters = array())
240
    {
241 40
        $warning = '';
242
243
        switch ($migrationType) {
244 40
            case 'db':
245 38
            case 'generic':
246 36
            case 'php':
247
                // Generate migration file by template
248 6
                $template = $migrationType . 'Migration.' . $fileType . '.twig';
249 6
                $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

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