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
![]() |
|||
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 |