├── Drush └── Commands │ ├── CodeBuilderDevDrushCommands.php │ └── CodeBuilderDrushCommands.php ├── Environment └── DrushModuleBuilderDevel.php ├── README.md ├── composer.json └── module_builder.drush.inc /Drush/Commands/CodeBuilderDevDrushCommands.php: -------------------------------------------------------------------------------- 1 | getRoot(); 33 | $drupal_version = Drush::bootstrap()->getVersion($drupal_root); 34 | 35 | \DrupalCodeBuilder\Factory::setEnvironmentLocalClass('WriteTestsSampleLocation') 36 | ->setCoreVersionNumber($drupal_version); 37 | 38 | $task_handler_collect = \DrupalCodeBuilder\Factory::getTask('Testing\CollectTesting'); 39 | 40 | $job_list = $task_handler_collect->getJobList(); 41 | 42 | $results = []; 43 | $this->io()->progressStart(count($job_list)); 44 | foreach ($job_list as $job) { 45 | $task_handler_collect->collectComponentDataIncremental([$job], $results); 46 | $this->io()->progressAdvance(1); 47 | } 48 | $this->io()->progressFinish(); 49 | 50 | $hooks_directory = \DrupalCodeBuilder\Factory::getEnvironment()->getHooksDirectory(); 51 | 52 | $output->writeln("Data on hooks, services, and plugin types has been copied to {$hooks_directory} and processed."); 53 | 54 | return TRUE; 55 | } 56 | 57 | /** 58 | * Outputs the data for a single collect job. 59 | * 60 | * TODO: Make this a bit nicer - needs a numeric argument! 61 | */ 62 | #[\Drush\Attributes\Command(name: 'cb-update-devel', aliases: ['cbud'])] 63 | #[\Drush\Attributes\Argument(name: 'job', description: 'Numeric key of the collect job to process in the job list array.')] 64 | #[\Drush\Attributes\Help(hidden: true)] 65 | #[\Drush\Attributes\Bootstrap(level: DrupalBootLevels::FULL)] 66 | public function commandTestCollect(OutputInterface $output, int $job) { 67 | $drupal_root = Drush::bootstrapManager()->getRoot(); 68 | $drupal_version = Drush::bootstrap()->getVersion($drupal_root); 69 | 70 | \DrupalCodeBuilder\Factory::setEnvironmentLocalClass('Drush') 71 | ->setCoreVersionNumber($drupal_version); 72 | 73 | $task_handler_collect = \DrupalCodeBuilder\Factory::getTask('Testing\CollectTesting'); 74 | 75 | $job_list = $task_handler_collect->getJobList(); 76 | 77 | if (!isset($job_list[$job])) { 78 | throw new \InvalidArgumentException("Job $job not found."); 79 | } 80 | 81 | // Get the helper from the DCB container. 82 | $collector_helper = \DrupalCodeBuilder\Factory::getContainer()->get($job_list[$job]['collector']); 83 | $job_data = $collector_helper->collect([$job_list[$job]]); 84 | 85 | dump($job_data); 86 | 87 | return TRUE; 88 | } 89 | 90 | } 91 | -------------------------------------------------------------------------------- /Drush/Commands/CodeBuilderDrushCommands.php: -------------------------------------------------------------------------------- 1 | getRoot(); 53 | $drupal_version = Drush::bootstrap()->getVersion($drupal_root); 54 | 55 | // Set up the DCB factory. 56 | // Ensure compatibility with module_builder_devel, which if enabled uses a 57 | // differnet format for the stored analysis data. 58 | if (\Drupal::moduleHandler()->moduleExists('module_builder_devel')) { 59 | $environment = new DrushModuleBuilderDevel(); 60 | 61 | \DrupalCodeBuilder\Factory::setEnvironment($environment) 62 | ->setCoreVersionNumber($drupal_version); 63 | } 64 | else { 65 | \DrupalCodeBuilder\Factory::setEnvironmentLocalClass('Drush') 66 | ->setCoreVersionNumber($drupal_version); 67 | } 68 | } 69 | 70 | /** 71 | * Add the 'data-location' option to our commands. 72 | * 73 | * @hook option @code_builder 74 | * @option data-location Directory in which to store data. Use a relative path 75 | * to store within public://, an absolute path otherwise. Defaults to 76 | * public://code-builder. This should typically be set in drushrc.php to 77 | * permamently store data in a custom location. 78 | */ 79 | public function optionDataLocation($options = ['data-location' => 'code-builder']) {} 80 | 81 | /** 82 | * Sets a default for the data-location option, and set in Drush context. 83 | * 84 | * This is done in an init hook so interact hooks have the location set and 85 | * thus have access to component data. 86 | * 87 | * @hook init @code_builder 88 | */ 89 | public function initDataLocationOption(InputInterface $input, AnnotationData $annotationData) { 90 | $location = $input->getOption('data-location'); 91 | 92 | // Have to set this back into the options context, as otherwise 93 | // drush_get_option() which the Environment calls won't have it. 94 | // See https://github.com/drush-ops/drush/issues/2907 95 | // TODO: Find a better way to pass this to the library. 96 | // TODO: Stop calling this 'data' in the library. 97 | // drush_set_option('data', $location); 98 | } 99 | 100 | /** 101 | * Generate code to add to or create a Drupal module. 102 | * 103 | * @command cb:module 104 | * 105 | * @param string $module_name The module name. If this is a '.', the module at 106 | * the current location is used. Will be prompted for if omitted. 107 | * @param string $component_type The component type. Will be prompted for if 108 | * omitted, which allows entering multiple values to build more than one 109 | * component. 110 | * @option parent The directory in which to create a new module. Defaults to 111 | * 'modules/custom', or 'modules' if the 'custom' subdirectory doesn't 112 | * exist. A '.' means the current location. This option is ignored if the 113 | * module already exists. 114 | * @option dry-run If specified, no files are written. 115 | * @usage drush cb:module 116 | * Build a Drupal component for a module, with interactive prompt. 117 | * @usage drush cb:module . 118 | * Build a Drupal component for the module at the current location, with 119 | * interactive prompt. 120 | * @usage drush cb:module my_module module 121 | * Build the basic module 'my_module'. 122 | * @usage drush cb:module my_module plugins 123 | * Add plugins to the module 'my_module'. If the module doesn't exist, it 124 | * will be created. 125 | * @bootstrap DRUSH_BOOTSTRAP_DRUPAL_FULL 126 | * @aliases cbm 127 | * @code_builder 128 | */ 129 | public function commandBuildComponent( 130 | InputInterface $input, 131 | OutputInterface $output, 132 | $module_name, 133 | $component_type, 134 | $options = [ 135 | // Our default is complicated. 136 | 'parent' => NULL, 137 | 'dry-run' => FALSE, 138 | ] 139 | ) { 140 | // Interactive mode is required, bail otherwise. 141 | if (!$input->isInteractive()) { 142 | throw new CommandFailedException("The cb:module command must be run in interactive mode."); 143 | } 144 | 145 | try { 146 | $task_handler_generate = \DrupalCodeBuilder\Factory::getTask('Generate', 'module'); 147 | } 148 | catch (\DrupalCodeBuilder\Exception\SanityException $e) { 149 | // Show a message and end with failure. 150 | $this->io()->error($this->getSanityLevelMessage($e)); 151 | 152 | return Command::FAILURE; 153 | } 154 | 155 | /** @var \MutableTypedData\Data\DataItem */ 156 | $component_data = $this->componentData; 157 | 158 | $extension_base_path = $this->getComponentFolder('module', $module_name, $options['parent']); 159 | 160 | if ($this->moduleExists($module_name)) { 161 | $analyse_extension_task = \DrupalCodeBuilder\Factory::getTask('AnalyseExtension'); 162 | $existing_extension = $analyse_extension_task->createExtension('module', $extension_base_path); 163 | } 164 | 165 | $files = $task_handler_generate->generateComponent($component_data, [], NULL, $existing_extension ?? NULL); 166 | 167 | $this->outputComponentFiles($output, $extension_base_path, $files, $options['dry-run']); 168 | } 169 | 170 | /** 171 | * Set the module name to the current directory if not provided. 172 | * 173 | * @hook init cb:module 174 | */ 175 | public function initializeBuildComponent(InputInterface $input, AnnotationData $annotationData) { 176 | $module_name = $input->getArgument('module_name'); 177 | if ($module_name == '.') { 178 | // If no module name is given, or it's the special value '.', take the 179 | // current directory to be the module name parameter. 180 | $current_directory = $this->getConfig()->get('env.cwd'); 181 | 182 | $module_name = basename($current_directory); 183 | 184 | // TODO: check that this is actually a module? 185 | 186 | // Output a message to say this is what we've done. 187 | $this->io()->text("Working module set to {$module_name}."); 188 | 189 | // Later validation will check this is actually a module. 190 | $input->setArgument('module_name', $module_name); 191 | } 192 | } 193 | 194 | /** 195 | * Gets the module name and component type if not provided. 196 | * 197 | * @hook interact cb:module 198 | */ 199 | public function interactBuildComponent(InputInterface $input, OutputInterface $output, AnnotationData $annotationData) { 200 | // Get the generator task. 201 | try { 202 | $task_handler_generate = \DrupalCodeBuilder\Factory::getTask('Generate', 'module'); 203 | } 204 | catch (\DrupalCodeBuilder\Exception\SanityException $e) { 205 | // Set the required arguments to dummy value to silence a complaint about 206 | // them. 207 | $input->setArgument('module_name', 'made-up!'); 208 | $input->setArgument('component_type', 'made-up!'); 209 | 210 | return Command::FAILURE; 211 | } 212 | 213 | /** @var \MutableTypedData\Data\DataItem */ 214 | $component_data = $task_handler_generate->getRootComponentData(); 215 | 216 | // Initialize an array of values. 217 | $build_values = []; 218 | 219 | // Get the module name if not provided. 220 | if (empty($input->getArgument('module_name'))) { 221 | 222 | $module_name = suggest( 223 | label: 'Enter the name of an existing module to add to it, or a new module name to create it', 224 | required: true, 225 | // This doesn't work as well as with Symfony -- you have to backspace 226 | // to clear it :( 227 | // default: 'my_module', 228 | options: function ($input) { 229 | $module_names = $this->getModuleNames(); 230 | sort($module_names); 231 | 232 | return preg_grep("@$input@", $module_names); 233 | }, 234 | scroll: 20, 235 | ); 236 | 237 | $input->setArgument('module_name', $module_name); 238 | } 239 | 240 | // Determine whether the given module name is for an existing module. 241 | $module_name = $input->getArgument('module_name'); 242 | $module_exists = $this->moduleExists($module_name); 243 | 244 | $subcomponent_property_options = $this->getSubComponentPropertyNames($component_data); 245 | $subcomponent_property_names = array_keys($subcomponent_property_options); 246 | 247 | // Get the component type if not provided. 248 | if (empty($input->getArgument('component_type'))) { 249 | $options = []; 250 | 251 | // If the module doesn't exist yet, first option is to just build its 252 | // basics. 253 | if (!$module_exists) { 254 | $options['module'] = 'Module only'; 255 | } 256 | 257 | $options += $subcomponent_property_options; 258 | 259 | if ($module_exists) { 260 | $prompt = dt("This module already exists. Choose component types to add to it"); 261 | } 262 | else { 263 | $prompt = dt("This module doesn't exist. Choose component types to start it with"); 264 | } 265 | 266 | $component_types = multiselect( 267 | label: $prompt, 268 | options: $options, 269 | // Show the lot. 270 | scroll: count($options), 271 | ); 272 | 273 | // Set any old value on the $component_type command parameter, so the 274 | // command thinks that it's now set. 275 | $input->setArgument('component_type', 'made-up!'); 276 | } 277 | else { 278 | // TODO: validate component type if supplied 279 | 280 | $component_types[] = $input->getArgument('component_type'); 281 | } 282 | 283 | // TODO: validate module if supplied 284 | 285 | // TODO Initialize component data with the base type and root name. 286 | $component_data->root_name = $module_name; 287 | 288 | // Mark the properties we don't want to prompt for. 289 | $properties_to_skip = []; 290 | if ($component_types === ['module']) { 291 | // Don't prompt for any of the subcomponents. 292 | $properties_to_skip = $subcomponent_property_names; 293 | 294 | // TODO -- whole different pathway, just collect the non-complex. 295 | } 296 | else { 297 | if ($module_exists) { 298 | // Don't prompt for anything that is *not* a subcomponent, as we won't 299 | // be building the basic module. 300 | $properties_to_skip = array_diff($component_data->getPropertyNames(), $subcomponent_property_names);; 301 | } 302 | 303 | // Mark the subcomponents that weren't selected. 304 | $properties_to_skip = array_merge($properties_to_skip, array_diff($subcomponent_property_names, $component_types)); 305 | } 306 | 307 | // Set things to internal so the use is not prompted for them. 308 | // Skip the root_name, since we have already got it. 309 | $component_data->root_name->setInternal(TRUE); 310 | 311 | foreach ($properties_to_skip as $property_name) { 312 | $component_data->{$property_name}->setInternal(TRUE); 313 | 314 | // ARRGH babysit the annoying MTD bug with single-valued complex data 315 | // getting instantiated once you look at it! 316 | if ($component_data->isComplex()) { 317 | $component_data->removeItem($property_name); 318 | } 319 | } 320 | 321 | // Collect data for the requested components. 322 | $breadcrumb = []; 323 | $this->interactCollectProperties($component_data, $breadcrumb); 324 | 325 | // Set the data on this class, so the comand callback can get them. 326 | // TODO: This is a hack because it's not possible to define dynamic 327 | // arguments. 328 | // See https://github.com/consolidation/annotated-command/issues/115 329 | // TODO: The above TODO is ancient -- see if it's still relevant. 330 | $this->componentData = $component_data; 331 | $this->buildValues = $build_values; 332 | } 333 | 334 | /** 335 | * Gets the names of data properties which we count as subcomponents. 336 | * 337 | * @param $component_data 338 | * The root component data. 339 | * 340 | * @return 341 | * An associative array whose keys are the property names which are 342 | * subcomponents, and whose values are the property labels. The array is 343 | * sorted by the label. 344 | */ 345 | protected function getSubComponentPropertyNames(DataItem $component_data): array { 346 | foreach ($component_data as $property_name => $property_data) { 347 | if ($property_data->isComplex()) { 348 | $return[$property_name] = $property_data->getLabel(); 349 | continue; 350 | } 351 | // Bit of a hack: non-complex properties that request generators. 352 | if (in_array(get_class($property_data->getDefinition()), [MergingGeneratorDefinition::class, DeferredGeneratorDefinition::class])) { 353 | $return[$property_name] = $property_data->getLabel(); 354 | continue; 355 | } 356 | } 357 | 358 | // Total hack: hooks. 359 | // TODO: This won't work when we support other root component types! 360 | $return['hooks'] = $component_data->hooks->getLabel(); 361 | 362 | natcasesort($return); 363 | 364 | return $return; 365 | } 366 | 367 | /** 368 | * Interactively collects values for a data item. 369 | * 370 | * This recurses into itself for complex properties. 371 | * 372 | * @param DataItem $data 373 | * THe data item. 374 | * @param $breadcrumb 375 | * An array of the component labels forming a trail into the component data 376 | * hierarchy to the current point. This is output each time focus moves to a 377 | * different component (inculding back out to one we've been to before) in 378 | * order to help users keep track of what they are entering. 379 | * 380 | * @return 381 | * The values array with the user-entered values added to it. 382 | */ 383 | protected function interactCollectProperties(DataItem $data, $breadcrumb): void { 384 | $address = $data->getAddress(); 385 | $level = substr_count($address, ':'); 386 | $breadcrumb[] = $data->getLabel(); 387 | 388 | // Show breadcrumb, but not on the first level. 389 | // This helps to give the user an overview of where they are in the data. 390 | if ($level > 1) { 391 | $this->outputDataBreadcrumb('Current item', $breadcrumb); 392 | } 393 | 394 | if ($data->isComplex()) { 395 | // At the top-level, we know the user requested this because of the 396 | // initial component selection. 397 | if ($level > 1) { 398 | $enter_complex = confirm( 399 | label: "Enter details for {$data->getLabel()}?", 400 | default: FALSE, 401 | ); 402 | if (!$enter_complex) { 403 | return; 404 | } 405 | } 406 | 407 | if ($data->isMultiple()) { 408 | // Multi-valued complex data. 409 | $delta = 0; 410 | do { 411 | $delta_item = $data->createItem(); 412 | 413 | // Add to the breadcrumb to pass into the recursion. 414 | $delta_breadcrumb = $breadcrumb; 415 | // Use human-friendly index. 416 | $breadcrumb_index = $delta + 1; 417 | $delta_breadcrumb[] = "Item {$breadcrumb_index}"; 418 | 419 | foreach ($delta_item as $data_item) { 420 | $this->interactCollectProperties($data_item, $delta_breadcrumb); 421 | } 422 | 423 | // Increase the delta for the next loop and the cardinality check. 424 | // (TODO: Cardinality check but probably YAGNI.) 425 | $delta++; 426 | 427 | $enter_another = confirm( 428 | label: dt("Enter more {$data->getLabel()} items?"), 429 | default: FALSE, 430 | ); 431 | } 432 | while ($enter_another == TRUE); 433 | } 434 | else { 435 | // Single-valued complex data. 436 | foreach ($data as $data_item) { 437 | $this->interactCollectProperties($data_item, $breadcrumb); 438 | } 439 | } 440 | } 441 | else { 442 | // Simple data. 443 | if ($data->getType() == 'boolean') { 444 | // Boolean. 445 | $data->applyDefault(); 446 | 447 | $value = confirm( 448 | label: dt("Add a {$data->getLabel()}?"), 449 | // TODO: setting this to $data->isRequired() is weird -- are some 450 | // booleans set to required when FALSE is an OK answer? 451 | required: FALSE, 452 | default: (bool) $data->value, 453 | ); 454 | 455 | $data->set($value); 456 | } 457 | elseif ($data->hasOptions()) { 458 | // Options, either single or multiple. 459 | if (count($data->getOptions()) > 20) { 460 | // Large option set. 461 | $options_callback = function (string $value) use ($data) { 462 | $options = $data->getOptions(); 463 | $option_keys = array_keys($options); 464 | 465 | // Escape regex characters and the delimiters. 466 | $pattern = preg_quote($value, '@'); 467 | // Match case-insensitively, to make it easier to work with event name 468 | // constants. 469 | $regex = '@' . $pattern . '@i'; 470 | // Allow the '_' and '.' characters to be used interchangeably. 471 | // The '_' MUST go first in the $search array, as if '\.' goes first, then 472 | // the underscores in the $replace string will get found in the second 473 | // pass. 474 | $regex = str_replace(['_', '\.'], '[._]', $regex); 475 | $matched_keys = preg_grep($regex, $option_keys); 476 | 477 | $results = []; 478 | if (!$data->isRequired()) { 479 | // Can't be an empty string, WTF. 480 | $results[' '] = '-- None --'; 481 | } 482 | foreach ($matched_keys as $key) { 483 | // TODO: See https://github.com/drupal-code-builder/drupal-code-builder/issues/334. 484 | $label = $options[$key]->getLabel(); 485 | $results[$key] = $key . (($key == $label) ? '' : ' - ' . $label); 486 | } 487 | 488 | return $results; 489 | }; 490 | 491 | if ($data->isMultiple()) { 492 | $value = multisearch( 493 | label: 'Enter the ' . $data->getLabel(), 494 | options: $options_callback, 495 | required: $data->isRequired(), 496 | scroll: 20, 497 | ); 498 | 499 | $data->set($value); 500 | } 501 | else { 502 | $value = search( 503 | label: 'Enter the ' . $data->getLabel(), 504 | options: $options_callback, 505 | scroll: 20, 506 | ); 507 | 508 | // Babysit stupid empty value. 509 | if ($value != ' ') { 510 | $data->set($value); 511 | } 512 | } 513 | } 514 | else { 515 | // Small option set. 516 | $options = []; 517 | 518 | // Have to babysit single-option non-required :( 519 | if (!$data->isMultiple() && !$data->isRequired()) { 520 | $options[''] = 'None'; 521 | } 522 | 523 | foreach ($data->getOptions() as $value => $option) { 524 | $options[$value] = $option->getLabel(); 525 | } 526 | 527 | if ($data->isMultiple()) { 528 | $value = multiselect( 529 | label: 'Enter the ' . $data->getLabel(), 530 | options: $options, 531 | required: $data->isRequired(), 532 | // scroll: 20, 533 | ); 534 | 535 | $data->set($value); 536 | } 537 | else { 538 | $value = select( 539 | label: 'Enter the ' . $data->getLabel(), 540 | options: $options, 541 | scroll: 20, 542 | ); 543 | } 544 | 545 | $data->set($value); 546 | } 547 | } 548 | else { 549 | // Text value. 550 | if ($data->isMultiple()) { 551 | $value = text( 552 | label: "Enter the {$data->getLabel()} as a comma-separated list of values", 553 | required: $data->isRequired(), 554 | ); 555 | 556 | // TODO: trim! 557 | $value = explode(',', $value); 558 | 559 | $data->set($value); 560 | } 561 | else { 562 | if ($validators = $data->getValidators()) { 563 | $validation = function ($value) use ($data) { 564 | $data->set($value); 565 | $violations = $data->validate(); 566 | if (empty($violations)) { 567 | return NULL; 568 | } 569 | 570 | $messages = []; 571 | foreach ($violations as $address => $violation_messages) { 572 | foreach ($violation_messages as $violation_message) { 573 | $messages[] = $violation_message; 574 | } 575 | } 576 | return implode(', ', $messages); 577 | }; 578 | } 579 | 580 | $data->applyDefault(); 581 | 582 | $value = text( 583 | label: "Enter the {$data->getLabel()}", 584 | required: $data->isRequired(), 585 | default: $data->value ?? '', 586 | validate: $validation ?? NULL, 587 | ); 588 | 589 | if (!empty($value)) { 590 | $data->set($value); 591 | } 592 | } 593 | } 594 | } 595 | 596 | return; 597 | 598 | // TODO: mine old code for things I've not converted yet! 599 | 600 | // Show breadcrumb, but not on the first level. 601 | // This helps to give the user an overview of where they are in the data. 602 | if (count($breadcrumb) > 1) { 603 | $this->outputDataBreadcrumb($output, 'Current item', $breadcrumb); 604 | } 605 | 606 | // Get the name of the first property, so we can put that in the breadcrumb 607 | // in case we recurse further. The first property of any component is 608 | // typically some sort of ID or name for it. 609 | $first_property_name = reset(array_keys($data_info)); 610 | 611 | foreach ($data_info as $property_name => &$property_info) { 612 | if (!empty($property_info['skip'])) { 613 | // TODO! prepare it so it gets defaults! 614 | continue; 615 | } 616 | 617 | $task_handler_generate->prepareComponentDataProperty($property_name, $property_info, $values); 618 | 619 | // Show a breadcrumb for the first property after exiting a compound 620 | // property, that is, when coming out of a nesting level. 621 | if (!empty($breadcrumb_left_nesting)) { 622 | $this->outputDataBreadcrumb($output, 'Back to', $breadcrumb); 623 | 624 | $breadcrumb_left_nesting = FALSE; 625 | } 626 | 627 | if ($property_info['format'] == 'compound') { 628 | // Compound property: collect multiple items, recursing into this 629 | // method for each item. 630 | // Treat top-level compound properties as required, since the user 631 | // selected them in the initial component menu, so should not be asked 632 | // again. 633 | if ($data_info[$property_name]['required'] || count($breadcrumb) == 1) { 634 | $output->writeln("Enter details for {$data_info[$property_name]['label']} (at least one required):"); 635 | } 636 | else { 637 | $question = new \Symfony\Component\Console\Question\ConfirmationQuestion( 638 | dt("Enter details for {$data_info[$property_name]['label']}?"), 639 | FALSE 640 | ); 641 | $enter_compound = $this->io()->askQuestion($question); 642 | 643 | if (!$enter_compound) { 644 | // Zap any defaults the prepare step might have put in, so that if 645 | // the user has said they don't want anything here, there's 646 | // actually nothing here. 647 | $values[$property_name] = []; 648 | 649 | // Move on to the next property. 650 | continue; 651 | } 652 | } 653 | 654 | $nested_breadcrumb = $breadcrumb; 655 | $nested_breadcrumb[] = $property_info['label']; 656 | 657 | $value = []; 658 | $cardinality = $property_info['cardinality'] ?? -1; 659 | $delta = 0; 660 | do { 661 | // Initialize a new child item so a default value can be placed 662 | // into it, or take a default if one already exists for this item. 663 | $value[$delta] = $values[$property_name][$delta] ?? []; 664 | 665 | // Add to the breadcrumb to pass into the recursion. 666 | $item_breadcrumb = $nested_breadcrumb; 667 | // Don't show a delta if the cardinality is 1. 668 | if ($cardinality != 1) { 669 | // Use human-friendly index. 670 | $breadcrumb_delta = $delta + 1; 671 | $item_breadcrumb[] = "Item {$breadcrumb_delta}"; 672 | } 673 | 674 | $value[$delta] = $this->interactCollectProperties( 675 | $task_handler_generate, 676 | $output, 677 | $data_info->{$property_name}, 678 | $item_breadcrumb 679 | ); 680 | 681 | // Increase the delta for the next loop and the cardinality check. 682 | $delta++; 683 | 684 | if ($delta != $cardinality) { 685 | $question = new \Symfony\Component\Console\Question\ConfirmationQuestion( 686 | dt("Enter more {$data_info[$property_name]['label']}?"), 687 | FALSE 688 | ); 689 | $enter_another = $this->io()->askQuestion($question); 690 | } 691 | else { 692 | // Reached maximum cardinality: loop must end. 693 | $enter_another = FALSE; 694 | } 695 | } 696 | while ($enter_another == TRUE); 697 | 698 | // Mark that we've just exited a level of nesting for the breadcrumb. 699 | $breadcrumb_left_nesting = TRUE; 700 | 701 | $values[$property_name] = $value; 702 | } 703 | else { 704 | // Simple property. 705 | 706 | // Special case for top-level boolean: the user has already effectively 707 | // stated the value for this is TRUE, when selecting it in the initial 708 | // menu. 709 | if ($property_info['format'] == 'boolean' && count($breadcrumb) == 1) { 710 | $values[$property_name] = TRUE; 711 | continue; 712 | } 713 | 714 | $default = $values[$property_name]; 715 | 716 | $value = $this->askQuestionForProperty($property_info, $default); 717 | 718 | $values[$property_name] = $value; 719 | 720 | // For the first property (which should be non-compound), take the 721 | // value and put it into the breadcrumb, so further output of the 722 | // breadcrumb that includes this level has a name rather than just 723 | // 'item DELTA'. 724 | if ($property_name == $first_property_name) { 725 | array_pop($breadcrumb); 726 | // TODO: prefix this with the label? But we don't have that at this 727 | // point! 728 | $breadcrumb[] = $value; 729 | } 730 | } 731 | } 732 | } 733 | 734 | /** 735 | * Output a breadcrumb, showing where the user is in the data structure. 736 | * 737 | * @param string $label 738 | * A label describing the breadcrumb. 739 | * @param string[] $breadcrumb 740 | * An array of strings representing the current position. 741 | */ 742 | protected function outputDataBreadcrumb($label, $breadcrumb) { 743 | $breadcrumb_string = implode(' » ', $breadcrumb); 744 | $this->io()->writeln("$label: $breadcrumb_string" . "\n"); 745 | } 746 | 747 | /** 748 | * Ask the user a question for a single property. 749 | * 750 | * TODO: make this return the Question object instead? 751 | * 752 | * @param $property_info 753 | * The property info array. It should already have been run through the 754 | * generator task's prepareComponentDataProperty(). 755 | * @param $default 756 | * The default value. 757 | * 758 | * @return 759 | * The user-entered value, or the default if nothing is given. 760 | */ 761 | protected function askQuestionForProperty($property_info, $default) { 762 | //dump('askQuestionForProperty'); 763 | //dump($property_info); 764 | // TODO: convert this to a child class of Symfony Question. 765 | 766 | // DCB might give us a default of an array for properties that expect an 767 | // array, but Symfony wants a string. 768 | if ($default === []) { 769 | $default = ''; 770 | } 771 | 772 | if (isset($property_info['options'])) { 773 | // Question with options, either string or array format. 774 | $options = $property_info['options']; 775 | 776 | // If the property has extra options, add then to the autocompleter 777 | // values. 778 | if (isset($property_info['options_extra'])) { 779 | // Only use the keys from the extra options array, as the values are 780 | // only meant to be labels. 781 | $extra_options = array_keys($property_info['options_extra']); 782 | 783 | // The prompt will show an explanation that further options may be used. 784 | $autocomplete_options = $extra_options; 785 | } 786 | else { 787 | // Only the values are available for autocompletion, not the labels. 788 | $autocomplete_options = array_keys($options); 789 | } 790 | 791 | if ($property_info['format'] == 'array') { 792 | if (!is_array($default)) { 793 | $default = [$default]; 794 | } 795 | 796 | // Multi-valued property. 797 | // TODO: consider adding the explanation message on its own line first -- 798 | // but need to work out how to format it, in the face of nonexistent 799 | // documentation in Symfony code. 800 | $value = []; 801 | 802 | $question = new \Symfony\Component\Console\Question\ChoiceQuestion( 803 | $this->getQuestionPromptForProperty("Enter the @label, one per line, empty line to finish", $property_info), 804 | $options, 805 | // Feed the default values one by one. 806 | array_shift($default) 807 | ); 808 | $question->setAutocompleterValues($autocomplete_options); 809 | // Hack to work around the question not allowing an empty answer. 810 | // See https://github.com/drush-ops/drush/issues/2931 811 | $question->setValidator(function ($answer) use ($autocomplete_options) { 812 | // Allow an empty answer. 813 | if (empty($answer)) { 814 | return $answer; 815 | } 816 | 817 | // Keep the normal Console error message for an invalid option. 818 | if (!in_array(strtolower($answer), array_map('strtolower', $autocomplete_options))) { 819 | throw new \Exception("Value \"{$answer}\" is invalid."); 820 | } 821 | 822 | return $answer; 823 | }); 824 | 825 | // TODO: bug in Symfony, autocomplete only works on the first value in 826 | // a multiselect question. To work around, ask a series of questions, 827 | // allowing the user to end the process with an empty response. 828 | do { 829 | $single_value = $this->io()->askQuestion($question); 830 | 831 | if (!empty($single_value)) { 832 | $value[] = $single_value; 833 | } 834 | 835 | // For subsequent iterations, the question should not show options. 836 | $question = new \Symfony\Component\Console\Question\Question( 837 | $this->getQuestionPromptForProperty("Enter further @label, one per line, empty line to finish", $property_info), 838 | array_shift($default) 839 | ); 840 | $question->setAutocompleterValues($autocomplete_options); 841 | // Hack to work around the question not allowing an empty answer. 842 | // See https://github.com/drush-ops/drush/issues/2931 843 | $question->setValidator(function ($answer) use ($autocomplete_options) { 844 | // Allow an empty answer. 845 | if (empty($answer)) { 846 | return $answer; 847 | } 848 | 849 | // Keep the normal Console error message for an invalid option. 850 | if (!in_array(strtolower($answer), array_map('strtolower', $autocomplete_options))) { 851 | throw new \Exception("Value \"{$answer}\" is invalid."); 852 | } 853 | 854 | return $answer; 855 | }); 856 | } 857 | while (!empty($single_value)); 858 | } 859 | else { 860 | // Single-valued property. 861 | // Non-required properties need a 'none' option, as Symfony won't 862 | // accept an empty value. 863 | if (!$property_info['required']) { 864 | $options = array_merge(['none' => 'None'], $options) ; 865 | 866 | $default = 'none'; 867 | } 868 | 869 | $question = new \Symfony\Component\Console\Question\ChoiceQuestion( 870 | $this->getQuestionPromptForProperty("Enter the @label", $property_info), 871 | $options, 872 | $default 873 | ); 874 | $question->setAutocompleterValues($autocomplete_options); 875 | 876 | // Note that this bypasses DrushStyle::choice()'s override, which 877 | // converts the options to be keyed numerically, thus hiding the machine 878 | // name. Good or bad thing for us? 879 | $value = $this->io()->askQuestion($question); 880 | 881 | // Get rid of the 'none' value if that was the default. 882 | if ($value === 'none') { 883 | $value = ''; 884 | } 885 | } 886 | } 887 | elseif ($property_info['format'] == 'array') { 888 | // Array without options to choose from. 889 | // TODO: consider adding the explanation message on its own line first -- 890 | // but need to work out how to format it, in the face of nonexistent 891 | // documentation in Symfony code. 892 | do { 893 | $question = new \Symfony\Component\Console\Question\Question( 894 | $this->getQuestionPromptForProperty("Enter the @label, one per line, empty line to finish", $property_info), 895 | $default 896 | ); 897 | // Hack to work around the question not allowing an empty answer. 898 | // See https://github.com/drush-ops/drush/issues/2931 899 | $question->setValidator(function ($answer) { return $answer; }); 900 | 901 | $single_value = $this->io()->askQuestion($question); 902 | 903 | if (!empty($single_value)) { 904 | $value[] = $single_value; 905 | } 906 | } 907 | while (!empty($single_value)); 908 | 909 | return $value; 910 | } 911 | elseif ($property_info['format'] == 'boolean') { 912 | // Boolean property. 913 | $question = new \Symfony\Component\Console\Question\ConfirmationQuestion( 914 | $this->getQuestionPromptForProperty("Do you want a @label", $property_info), 915 | $default 916 | ); 917 | // Note that booleans are always required: you have to answer either TRUE 918 | // or FALSE. 919 | 920 | $value = $this->io()->askQuestion($question); 921 | } 922 | elseif ($property_info['format'] == 'string') { 923 | // String property. 924 | $question = new \Symfony\Component\Console\Question\Question( 925 | $this->getQuestionPromptForProperty("Enter the @label", $property_info), 926 | $default 927 | ); 928 | 929 | if (!$property_info['required']) { 930 | // Hack to work around the question not allowing an empty answer. 931 | // See https://github.com/drush-ops/drush/issues/2931 932 | $question->setValidator(function ($answer) { return $answer; }); 933 | } 934 | 935 | $value = $this->io()->askQuestion($question); 936 | } 937 | else { 938 | // TODO: use the machine name rather than the label! 939 | throw new \Exception("Unable to ask question for property " . $property_info['label']); 940 | } 941 | 942 | return $value; 943 | } 944 | 945 | /** 946 | * Gets the prompt string for a question. 947 | * 948 | * Helper for askQuestionForProperty(). 949 | * 950 | * TODO Refactor this into a custom QuestionHelper class. 951 | * 952 | * @param string $text 953 | * The text for the question, which should contain a '@label' placeholder. 954 | * @param $property_info 955 | * The property's info array. 956 | * 957 | * @return string 958 | * The text with the label inserted for the placeholder, and the 959 | * description, if any, appended. 960 | */ 961 | protected function getQuestionPromptForProperty($text, $property_info) { 962 | $prompt = str_replace('@label', $property_info['label'], $text); 963 | if (isset($property_info['description'])) { 964 | $prompt .= "\n"; 965 | // Needs a single character indent, apparently. 966 | $prompt .= ' (' . $property_info['description'] . ')'; 967 | } 968 | if (isset($property_info['options_extra'])) { 969 | // TODO: Should go after the options list, rather than before, but not 970 | // possible without custom question class probably. 971 | $prompt .= "\n"; 972 | // Needs a single character indent, apparently. 973 | $prompt .= ' (Additional options available in autocompletion.)'; 974 | } 975 | return $prompt; 976 | } 977 | 978 | /** 979 | * @hook validate cb:module 980 | */ 981 | public function validateBuildComponent(CommandData $commandData) { 982 | $input = $commandData->input(); 983 | 984 | // Validate the module name. 985 | $module_name = $input->getArgument('module_name'); 986 | 987 | // If the module doesn't already exist, ensure it's a valid machine name. 988 | // return new CommandError("Invalid module name $module_name."); 989 | 990 | // TODO: validate the component if given on the command line. 991 | } 992 | 993 | /** 994 | * Returns the names of all modules in the current site, enabled or not. 995 | * 996 | * @return string[] 997 | * An array of module machine names. 998 | */ 999 | protected function getModuleNames() { 1000 | return array_keys($this->getModules()); 1001 | } 1002 | 1003 | /** 1004 | * Determines whether a module with the given name exists. 1005 | * 1006 | * @param string $module_name 1007 | * A module name. 1008 | * 1009 | * @return bool 1010 | * TRUE if the module exists (enabled or not). FALSE if it does not. 1011 | */ 1012 | protected function moduleExists($module_name) { 1013 | $extensions = $this->getModules(); 1014 | return isset($extensions[$module_name]); 1015 | } 1016 | 1017 | /** 1018 | * Returns a list of all modules in the current site, enabled or not. 1019 | * 1020 | * TODO: broken! 1021 | * 1022 | * @return string[] 1023 | * An array whose keys are module names and whose values are the relative 1024 | * paths to the .info.yml files. 1025 | */ 1026 | protected function getModuleList() { 1027 | // The state service keeps a static cache, no need for us to do too. 1028 | $system_module_files = \Drupal::state()->get('system.module.files', []); 1029 | return $system_module_files; 1030 | } 1031 | 1032 | /** 1033 | * Returns extension objects for modules in the current site, enabled or not. 1034 | * 1035 | * @return \Drupal\Core\Extension\Extension[] 1036 | * An array of extension objects. 1037 | */ 1038 | protected function getModules() { 1039 | if (empty($this->extensions)) { 1040 | $listing = new \Drupal\Core\Extension\ExtensionDiscovery(\Drupal::root()); 1041 | $this->extensions = $listing->scan('module'); 1042 | } 1043 | 1044 | return $this->extensions; 1045 | } 1046 | 1047 | /** 1048 | * Gets a list of a module's files. 1049 | * 1050 | * @param string $module_name 1051 | * A module name. 1052 | * 1053 | * @return string[] 1054 | * An array whose keys are pathnames relative to the module folder, and 1055 | * whose values are the absolute pathnames. If the module doesn't exist, the 1056 | * array is emtpy. 1057 | */ 1058 | protected function getModuleFiles($module_name) { 1059 | $module_files = $this->getModuleList(); 1060 | if (isset($module_files[$module_name])) { 1061 | $module_path = dirname($module_files[$module_name]); 1062 | $module_files = []; 1063 | 1064 | 1065 | $finder = new \Symfony\Component\Finder\Finder(); 1066 | $finder->files()->in($module_path); 1067 | 1068 | foreach ($finder as $file) { 1069 | $module_files[$file->getRelativePathname()] = $file->getRealPath(); 1070 | } 1071 | 1072 | return $module_files; 1073 | } 1074 | else { 1075 | return []; 1076 | } 1077 | } 1078 | 1079 | /** 1080 | * Output generated text, to terminal or to file. 1081 | * 1082 | * @param OutputInterface $output 1083 | * The output. 1084 | * @param $component_dir 1085 | * The base folder for the component. May or may not exist. 1086 | * @param $filename 1087 | * The array of files to write. Keys are filenames relative to the 1088 | * $component_dir, values are strings for the file contents. 1089 | * @param $dry_run 1090 | * Whether this is a dry run, i.e. files should not be written. 1091 | */ 1092 | protected function outputComponentFiles(OutputInterface $output, $component_dir, $files, $dry_run) { 1093 | if (!$output->isQuiet()) { 1094 | foreach ($files as $filename => $code) { 1095 | $this->io()->writeln("Proposed $filename:" . "\n"); 1096 | 1097 | $output->write($code); 1098 | } 1099 | } 1100 | 1101 | // Determine whether to write files. 1102 | $write_files = !$dry_run; 1103 | 1104 | // If we're not writing files, we're done. 1105 | if (!$write_files) { 1106 | return; 1107 | } 1108 | 1109 | $table = new Table($output); 1110 | $table 1111 | ->setHeaderTitle('File status summary') 1112 | ->setHeaders(['Filename', 'Merge status', 'Git status']); 1113 | 1114 | // If the component folder doesn't exist, we definitely know that no files 1115 | // are managed by git. 1116 | $check_git = file_exists($component_dir); 1117 | 1118 | $files_exist = []; 1119 | foreach ($files as $filename => $code_file) { 1120 | $needs_warning = FALSE; 1121 | 1122 | if (!$code_file->fileExists()) { 1123 | // Brand new file, no warning needed. 1124 | $merge_message = 'New'; 1125 | } 1126 | elseif ($code_file->fileIsMerged()) { 1127 | // Exists, but merged. 1128 | $merge_message = 'Merged'; 1129 | } 1130 | else { 1131 | // Exists, and not merged. 1132 | $merge_message = 'Overwrite'; 1133 | } 1134 | 1135 | if (!$code_file->fileExists()) { 1136 | $git_message = 'New'; 1137 | } 1138 | else { 1139 | $git_message = 'Unmanaged'; 1140 | 1141 | if ($check_git) { 1142 | // Perform the 'git status' command in the module folder, to allow for 1143 | // the case where the module has its own git repository. 1144 | $command = "cd {$component_dir} && git status {$filename} --porcelain"; 1145 | $descriptorspec = [ 1146 | 0 => ["pipe", "r"], 1147 | 1 => ["pipe", "w"], 1148 | 2 => ["pipe", "w"], 1149 | ]; 1150 | $pipes = []; 1151 | $resource = proc_open($command, $descriptorspec, $pipes); 1152 | 1153 | $git_exec_status = stream_get_contents($pipes[1]); 1154 | $git_exec_error = stream_get_contents($pipes[2]); 1155 | 1156 | proc_close($resource); 1157 | 1158 | if (!empty($git_exec_error)) { 1159 | // An error means there is no git repository anywhere above the file 1160 | // location. 1161 | $git_message = 'Unmanaged'; 1162 | 1163 | // Don't bother making further calls to check git since there's no 1164 | // repository. 1165 | $check_git = FALSE; 1166 | } 1167 | elseif (empty($git_exec_status)) { 1168 | // Nothing from git means that the file is clean. 1169 | // NOOO could mean unmanaged because no git AT ALL 1170 | $git_message = 'OK'; 1171 | } 1172 | else { 1173 | $git_status_code = substr($git_exec_status, 0, 2); 1174 | 1175 | $git_message = match ($git_status_code) { 1176 | '??' => 'Unmanaged', 1177 | ' M', 1178 | // Staged or partially staged file. 1179 | 'A ', 1180 | 'M ', 1181 | 'MM' => 'Uncommitted changes!', 1182 | }; 1183 | } 1184 | } 1185 | } 1186 | 1187 | $table->addRow([ 1188 | $filename, 1189 | $merge_message, 1190 | $git_message, 1191 | ]); 1192 | } 1193 | 1194 | $table->render(); 1195 | 1196 | $write_mode = select( 1197 | label: 'Write files?', 1198 | default: 'all', 1199 | options: [ 1200 | 'all' => 'Write all files', 1201 | 'prompt' => 'Prompt for overwriting files', 1202 | 'no' => "Don't write any files", 1203 | ], 1204 | ); 1205 | 1206 | // If no file writing requested, we're done. 1207 | if ($write_mode == 'no') { 1208 | return; 1209 | } 1210 | 1211 | $files_exist = []; 1212 | foreach ($files as $filename => $code) { 1213 | $filepath = $component_dir . '/' . $filename; 1214 | // TODO: add option for handling this: 1215 | // - prompt for overwrite 1216 | // - check git status before overwrite and overwrite if git clean 1217 | // - force overwrite 1218 | if (file_exists($filepath) && $write_mode == 'prompt') { 1219 | $question = new \Symfony\Component\Console\Question\ConfirmationQuestion( 1220 | dt('File ' . $filename . ' exists. Overwrite this file?'), 1221 | FALSE 1222 | ); 1223 | $overwrite = $this->io()->askQuestion($question); 1224 | 1225 | if (!$overwrite) { 1226 | continue; 1227 | } 1228 | } 1229 | 1230 | // Because the filename part can contain subdirectories, check these exist 1231 | // too. 1232 | $subdir = dirname($filepath); 1233 | if (!is_dir($subdir)) { 1234 | $result = mkdir($subdir, 0777, TRUE); 1235 | if ($result && !$output->isQuiet()) { 1236 | if ($subdir == $component_dir) { 1237 | $output->writeln("Module directory $component_dir created"); 1238 | } 1239 | else { 1240 | $output->writeln("Module subdirectory $subdir created"); 1241 | } 1242 | } 1243 | } 1244 | 1245 | // Add to file option. 1246 | // If the file doesn't exist, we skip this and silently write it anyway. 1247 | // TODO: add to file option is a bit broken these days anyway. 1248 | /* 1249 | if (drush_get_option('add') && file_exists($filepath)) { 1250 | $fh = fopen($filepath, 'a'); 1251 | fwrite($fh, $code); 1252 | fclose($fh); 1253 | continue; 1254 | } 1255 | */ 1256 | 1257 | file_put_contents($filepath, $code); 1258 | } 1259 | } 1260 | 1261 | /** 1262 | * Get the folder where a generated component's files should be written to. 1263 | * 1264 | * @param $component_type 1265 | * The type of the component. One of 'module' or 'theme'. 1266 | * @param $component_name 1267 | * The component name. 1268 | * @param $parent_dir 1269 | * The 'parent' option from the command. 1270 | * 1271 | * @return 1272 | * The full system path for the component's folder, without a trailing slash. 1273 | */ 1274 | protected function getComponentFolder($component_type, $component_name, $parent_dir) { 1275 | $drupal_root = Drush::bootstrapManager()->getRoot(); 1276 | 1277 | // First try: if the component exists, we write there: nice and simple. 1278 | $component_path = $this->getExistingExtensionPath('module', $component_name); 1279 | 1280 | if (!empty($component_path)) { 1281 | return $drupal_root . '/' . $component_path; 1282 | } 1283 | 1284 | // Third try: 'parent' option was given. 1285 | if (!empty($parent_dir)) { 1286 | // The --parent option allows the user to specify a location for the new 1287 | // module folder. 1288 | if (substr($parent_dir, 0 , 1) == '.') { 1289 | // An initial . means to start from the current directory rather than 1290 | // the modules folder, which allows submodules to be created where the 1291 | // user is standing. 1292 | $module_dir = $this->getConfig()->get('env.cwd') . '/'; 1293 | // Remove both the . and the following /. 1294 | $parent_dir = substr($parent_dir, 2); 1295 | if ($parent_dir) { 1296 | // If there's anything left (since just '.' is a valid option), 1297 | // append it. 1298 | $module_dir .= $parent_dir; 1299 | } 1300 | if (substr($module_dir, -1) != '/') { 1301 | // Append a final '/' in case the terminal autocomplete didn't. 1302 | $module_dir .= '/'; 1303 | } 1304 | } 1305 | else { 1306 | // If there's no dot, assume that an existing module is meant. 1307 | // (Would anyone enter a complete path for this??? If we do need this, 1308 | // then consider recursing into this for the parent path??) 1309 | $module_dir .= drupal_get_path($component_type, $parent_dir) . '/'; 1310 | } 1311 | return $module_dir . $component_name; 1312 | } 1313 | 1314 | // Fourth and final try: build it based on the module folder structure. 1315 | $possible_folders = [ 1316 | '/modules/custom', 1317 | '/modules', 1318 | ]; 1319 | foreach ($possible_folders as $folder) { 1320 | if (is_dir($drupal_root . $folder)) { 1321 | return $drupal_root . $folder . '/' . $component_name; 1322 | } 1323 | } 1324 | } 1325 | 1326 | /** 1327 | * Returns the path to the module if it has previously been written. 1328 | * 1329 | * @return 1330 | * A Drupal-relative path to the module folder, or NULL if the module 1331 | * does not already exist. 1332 | */ 1333 | protected function getExistingExtensionPath(string $component_type, string $extension_name): ?string { 1334 | $registered_in_drupal = \Drupal::service('extension.list.' . $component_type)->exists($extension_name); 1335 | if ($registered_in_drupal) { 1336 | $extension = \Drupal::service('extension.list.' . $component_type)->get($extension_name); 1337 | 1338 | // The user may have deleted the module entirely, and in this situation 1339 | // Drupal's extension system would still have told us it exists. 1340 | $really_exists = file_exists($extension->getPath()); 1341 | if ($really_exists) { 1342 | return $extension->getPath(); 1343 | } 1344 | } 1345 | 1346 | return NULL; 1347 | } 1348 | 1349 | /** 1350 | * Update analysis data on Drupal components. 1351 | * 1352 | * @command cb:update 1353 | * 1354 | * @usage drush cb:update 1355 | * Update data on Drupal components, storing in the default location. 1356 | * @usage drush cb:update --data-location=relative/path 1357 | * Update data on hooks, storing data in public://relative/path. 1358 | * @usage drush cb:update --data-location=/absolute/path 1359 | * Update data on hooks, storing data in /absolute/path. 1360 | * @bootstrap DRUSH_BOOTSTRAP_DRUPAL_FULL 1361 | * @aliases cbu 1362 | * @code_builder 1363 | */ 1364 | public function commandUpdateDefinitions(OutputInterface $output) { 1365 | // Get our task handler. This performs a sanity check which throws an 1366 | // exception. 1367 | $task_handler_collect = $this->getCodeBuilderTask('Collect'); 1368 | 1369 | $job_list = $task_handler_collect->getJobList(); 1370 | 1371 | $results = []; 1372 | $this->io()->progressStart(count($job_list)); 1373 | foreach ($job_list as $job) { 1374 | $task_handler_collect->collectComponentDataIncremental([$job], $results); 1375 | $this->io()->progressAdvance(1); 1376 | } 1377 | $this->io()->progressFinish(); 1378 | 1379 | $hooks_directory = \DrupalCodeBuilder\Factory::getEnvironment()->getHooksDirectory(); 1380 | 1381 | $output->writeln("Drupal Code Builder's analysis has detected the following in your Drupal codebase:"); 1382 | 1383 | $table = new Table($output); 1384 | $table->setHeaders(array('Type', 'Count')); 1385 | foreach ($results as $label => $count) { 1386 | $rows[] = [ucfirst($label), $count]; 1387 | } 1388 | $table->setRows($rows); 1389 | $table->render(); 1390 | 1391 | $output->writeln("Data has been processed and written to {$hooks_directory}."); 1392 | 1393 | return TRUE; 1394 | } 1395 | 1396 | /** 1397 | * List stored analysis data on Drupal components. 1398 | * 1399 | * @command cb:list 1400 | * 1401 | * @option type Which type of data to list. The valid options are defined 1402 | * by DrupalCodeBuilder, and include: 1403 | * 'all': show everything. 1404 | * 'hooks': show hooks. 1405 | * 'plugins': show plugin types. 1406 | * 'services': show services. 1407 | * 'tags': show tagged service types. 1408 | * 'fields': show field types. 1409 | * @usage drush cb:list 1410 | * List stored analysis data on Drupal components. 1411 | * @usage drush cb:list --type=plugins 1412 | * List stored analysis data on Drupal plugin types. 1413 | * @bootstrap DRUSH_BOOTSTRAP_DRUPAL_FULL 1414 | * @aliases cbl 1415 | * @code_builder 1416 | */ 1417 | public function commandListDefinitions(OutputInterface $output, $options = ['type' => 'all']) { 1418 | // TODO: add a --format option, same as 'drush list'. 1419 | // TODO: add a --filter option, same as 'drush list'. 1420 | // TODO: restore listing hook presets. 1421 | 1422 | // Callback for array_walk() to concatenate the array key and value. 1423 | $list_walker = function (&$value, $key) { 1424 | $value = "{$key}: $value"; 1425 | }; 1426 | 1427 | $task_report = $this->getCodeBuilderTask('ReportSummary'); 1428 | 1429 | $data = $task_report->listStoredData(); 1430 | 1431 | if ($options['type'] != 'all') { 1432 | if (!isset($data[$options['type']])) { 1433 | throw new \Exception("Invalid type '{$options['type']}'."); 1434 | } 1435 | 1436 | $data = array_intersect_key($data, [$options['type'] => TRUE]); 1437 | } 1438 | 1439 | foreach ($data as $type_data) { 1440 | $this->io()->title($type_data['label'] . ':'); 1441 | 1442 | if (is_array(reset($type_data['list']))) { 1443 | // Grouped list. 1444 | foreach ($type_data['list'] as $group_title => $group_list) { 1445 | $this->io()->section($group_title); 1446 | 1447 | array_walk($group_list, $list_walker); 1448 | $this->io()->listing($group_list); 1449 | } 1450 | } 1451 | else { 1452 | array_walk($type_data['list'], function (&$value, $key) { 1453 | $value = "{$key}: $value"; 1454 | }); 1455 | $this->io()->listing($type_data['list']); 1456 | } 1457 | } 1458 | 1459 | // Show a table summarizing counts. 1460 | if ($options['type'] == 'all') { 1461 | $table = new \Symfony\Component\Console\Helper\Table($output); 1462 | $table->setHeaders(array('Type', 'Count')); 1463 | foreach ($data as $type_data) { 1464 | $rows[] = [$type_data['label'], $type_data['count']]; 1465 | } 1466 | $table->setRows($rows); 1467 | $table->render(); 1468 | } 1469 | 1470 | $time = $task_report->lastUpdatedDate(); 1471 | $hooks_directory = \DrupalCodeBuilder\Factory::getEnvironment()->getHooksDirectory(); 1472 | $output->writeln(strtr("Component data retrieved from @dir.", array('@dir' => $hooks_directory))); 1473 | $output->writeln(strtr("Component data was processed on @time.", array( 1474 | '@time' => date(DATE_RFC822, $time), 1475 | ))); 1476 | } 1477 | 1478 | /** 1479 | * Gets a Drupal Code Builder Task handler. 1480 | * 1481 | * @param $task_type 1482 | * The type of task to pass to \DrupalCodeBuilder\Factory::getTask(). 1483 | * 1484 | * @return 1485 | * The task handler. 1486 | * 1487 | * @throws \Exception 1488 | * Throws an exception if there is a problem that would prevent the task's 1489 | * operation. 1490 | */ 1491 | protected function getCodeBuilderTask($task_type) { 1492 | try { 1493 | $task = \DrupalCodeBuilder\Factory::getTask($task_type); 1494 | } 1495 | catch (\DrupalCodeBuilder\Exception\SanityException $e) { 1496 | $this->handleSanityException($e); 1497 | } 1498 | return $task; 1499 | } 1500 | 1501 | /** 1502 | * Re-throws a DCB exception with a message. 1503 | * 1504 | * @param \DrupalCodeBuilder\Exception\SanityException $e 1505 | * The original exception thrown by the library. 1506 | * 1507 | * @throws \Exception 1508 | * Throws an exception with a message based on the given DCB exception. 1509 | */ 1510 | protected function handleSanityException(\DrupalCodeBuilder\Exception\SanityException $e) { 1511 | $failed_sanity_level = $e->getFailedSanityLevel(); 1512 | switch ($failed_sanity_level) { 1513 | case 'data_directory_exists': 1514 | $message = "The component data directory could not be created or is not writable."; 1515 | break; 1516 | case 'component_data_processed': 1517 | $message = "No component data was found. Run 'drush cb:update' to process component data from your site's code files."; 1518 | break; 1519 | } 1520 | throw new \Exception($message); 1521 | } 1522 | 1523 | /** 1524 | * Gets a message to show for different levels of DCB sanity failure. 1525 | * 1526 | * @param \DrupalCodeBuilder\Exception\SanityException $e 1527 | * The sanity exception. 1528 | * 1529 | * @return 1530 | * The message string. 1531 | */ 1532 | protected function getSanityLevelMessage(\DrupalCodeBuilder\Exception\SanityException $e): string { 1533 | return match ($e->getFailedSanityLevel()) { 1534 | 'data_directory_exists' => "The component data directory could not be created or is not writable.", 1535 | 'component_data_processed' => "No component data was found. Run 'drush cb:update' to process component data from your site's code files.", 1536 | }; 1537 | } 1538 | 1539 | } 1540 | -------------------------------------------------------------------------------- /Environment/DrushModuleBuilderDevel.php: -------------------------------------------------------------------------------- 1 | setCoreVersionNumber(drush_drupal_version()); 30 | } 31 | 32 | /** 33 | * Implementation of hook_drush_command(). 34 | */ 35 | function module_builder_drush_command() { 36 | $items = array(); 37 | 38 | $items['mb-build'] = array( 39 | 'callback' => 'drush_module_builder_callback_build_module', 40 | 'description' => "Generate the code for a new Drupal module, including file headers and hook implementations.", 41 | 'arguments' => array( 42 | 'module name' => "The machine name of the module. Use '.' to specify the current folder name.", 43 | 'hooks' => "Short names of hooks, separated by spaces.", 44 | 'presets:' => "Names of preset groups of hooks, e.g., 'block'." . "\n" . 45 | "Use the 'presets:' marker to separate this from prior commands, eg 'mymodule hook_a hook_b presets: block'.", 46 | 'plugins:' => "Sets of properties for one ore more plugins. " . 47 | "Each set is separated with a ':', and contains: \n" . 48 | " - the plugin type (this is the suffix of the plugin manager service ID)\n" . 49 | " - the plugin name\n" . 50 | " - (optional) a list of comma-separated service IDs to be injected into the plugin.\n" . 51 | "Use the 'plugins:' marker to separate this from prior commands. Example: 'mymodule hook_a hook_b plugins: block alpha : block beta current_user'.", 52 | 'routes:' => "Menu paths, separated by spaces. " . "\n" . 53 | "Use the 'routes:' marker to separate this from prior commands, eg 'mymodule hook_a hook_b routes: module/path module/otherpath'.", 54 | 'perms:' => "Sets of properties for one or more permissions. " . 55 | "Each set is separated with a ':', and contains: \n" . 56 | " - the permission machine name\n" . 57 | " - (optional) the permission description\n" . 58 | "Use the 'perms:' marker to separate this from prior commands, eg 'mymodule hook_a hook_b perms: 'administer my module' 'access my module-Access my Module description'. Quote strings that contain spaces.", 59 | 'theme:' => "Theme hooks, without the initial 'theme_', separated by spaces. " . "\n" . 60 | "Use the 'theme:' marker to separate this from prior commands, eg 'mymodule hook_a hook_b perms: my_themable'.", 61 | 'services:' => "IDs of services, separated by spaces. " . "\n", 62 | 'settings_form!' => "Add this argument to add a settings form to the module.", 63 | 'api!' => "Add this argument to add an api.php file to the module.", 64 | 'readme!' => "Add this argument to add a README file to the module.", 65 | 'tests!' => "Add this argument to add a Simpletest test case file to the module.", 66 | ), 67 | // Commented out, as only the first argument is required. 68 | // TODO: figure out how to specify this! 69 | //'required-arguments' => TRUE, 70 | 'aliases' => array('mb'), 71 | 'options' => array( 72 | 'noi' => "Disables interactive mode.", 73 | 'data' => "Location to read hook data. May be absolute, or relative to Drupal files dir. Defaults to 'files/hooks'.", 74 | 'build' => "Which module components to generate: 75 | - 'info' makes the info file. 76 | - 'readme' makes README file. 77 | - 'tests' makes the tests folder and test case file. 78 | - 'module', 'install' make the foo.module or foo.install file respectively. 79 | - 'FILE': If custom modules define other files to output, you can request those too, omitting the module root name part and any .inc extension, eg 'views' for 'foo.views.inc. 80 | - 'code' generates code files as needed: the module and install files, and any files requested by hooks. 81 | - 'all' generates everything, including any code files needed by the requested hooks. 82 | Default is 'all' if writing new files, 'code' if appending to file or outputting only to terminal.", 83 | 'write' => 'Write files to sites/all/modules. Will prompt to overwrite existing files; use yes to force. Use quiet to suppress output to the terminal.', 84 | 'go' => 'Write all module files and enable the new module. Take two commands into the shower? Not me.', 85 | 'add' => "Append hooks to module file. Implies 'write build=code'. Warning: will not check hooks already exist.", 86 | 'name' => 'Readable name of the module.', 87 | 'desc' => 'Description (for the admin module list).', 88 | 'helptext' => 'Module help text (for the system help).', 89 | 'dep' => 'Dependencies, separated by spaces, eg "forum views".', 90 | 'package' => 'Module package.', 91 | 'parent' => "Name of a module folder to place this new module into; use if this module is to be added to an existing package. Use '.' for the current working directory.", 92 | 'skip' => "Developer option: specifies a comma-separate list of property names to skip prompting for in interactive mode.", 93 | ), 94 | 'examples' => array( 95 | 'drush mb my_module menu cron nodeapi' => 96 | 'Generate module code with hook_menu, hook_cron, hook_nodeapi.', 97 | 'drush mb my_module --build=info --name="My module" --dep="forum views"' => 98 | 'Generate module info with readable name and dependencies.', 99 | 'drush mb my_module menu cron --write --name="My module" --dep="forum views"' => 100 | 'Generate both module files, write files and also output to terminal.', 101 | 'drush mb my_module menu cron --write ' => 102 | 'Generate module code, write files and also output to terminal.', 103 | 'drush mb my_module menu cron --write --quiet --name="My module" --dep="forum views"' => 104 | 'Generate both module files, write files and output nothing to terminal.', 105 | 'drush mb my_module menu cron --add'=> 106 | 'Generate code for hook_cron and add it to the existing my_module.module file.', 107 | 'drush mb my_module menu cron --write --parent=cck'=> 108 | 'Generate both module files, write files to a folder my_module inside the cck folder.', 109 | 'drush mb my_module menu cron --write --parent=.'=> 110 | 'Generate both module files, write files to a folder my_module in the current working directory.', 111 | ), 112 | ); 113 | 114 | $items['mb-component'] = array( 115 | 'callback' => 'drush_module_builder_callback_build_component', 116 | 'aliases' => array('mbc'), 117 | 'description' => "Generate a Drupal component, such as a module or profile.", 118 | 'arguments' => array( 119 | 'component type' => "The type of component, e.g., 'module'.", 120 | ), 121 | 'options' => array( 122 | 'write' => 'Write the component to the current site codebase. Will prompt to overwrite existing files; use yes to force. Use quiet to suppress output to the terminal.', 123 | ), 124 | ); 125 | 126 | $items['mb-download'] = array( 127 | 'callback' => 'drush_module_builder_callback_hook_download', 128 | 'description' => "Update module_builder hook data.", 129 | 'options' => array( 130 | 'data' => "Location to save downloaded files. May be absolute, or relative to Drupal files dir. Defaults to 'files/hooks'.", 131 | ), 132 | 'aliases' => array('mbdl'), 133 | ); 134 | 135 | $items['mb-list'] = array( 136 | 'callback' => 'drush_module_builder_callback_data_list', 137 | 'description' => "List the hooks module_builder knows about.", 138 | 'arguments' => array( 139 | 'modules' => '(optional) Names of modules, separated by spaces.', 140 | ), 141 | 'options' => array( 142 | 'raw' => "Outputs the raw debug hook data.", 143 | ), 144 | ); 145 | 146 | return $items; 147 | } 148 | 149 | /** 150 | * Implementation of hook_drush_help(). 151 | */ 152 | function module_builder_drush_help($section) { 153 | switch ($section) { 154 | case 'drush:mb-build': 155 | return dt("Generates and optionally writes module code with the specified components.\n" . 156 | "This can run in one of two modes:\n" . 157 | " - Interactive mode: You are presented with a prompt for each value. " . 158 | "This is the default mode.\n" . 159 | " - Direct mode: Enter all values as a single command. Enable this with the --noi option.\n" . 160 | "Interactive mode will accept any direct mode-style parameters passed in on the initial command."); 161 | } 162 | } 163 | 164 | /** 165 | * Handle a sanity exception from the library and output a message. 166 | * 167 | * @param ModuleBuilder\Exception\SanityException $e 168 | * A sanity exception object. 169 | */ 170 | function module_builder_handle_sanity_exception($e) { 171 | $failed_sanity_level = $e->getFailedSanityLevel(); 172 | switch ($failed_sanity_level) { 173 | case 'data_directory_exists': 174 | $message = "The component data directory could not be created or is not writable."; 175 | break; 176 | case 'component_data_processed': 177 | $message = "No component data was found. Run 'drush mb-download' to process component data from documentation files."; 178 | break; 179 | } 180 | drush_set_error(DRUSH_APPLICATION_ERROR, $message); 181 | } 182 | 183 | /** 184 | * Determines which mode we are in: interactive or direct. 185 | * 186 | * @return 187 | * TRUE if we are in interactive mode, FALSE for direct. 188 | */ 189 | function module_builder_determine_interactive_mode() { 190 | // Interactive mode is the default, overridden by the non-interactive option. 191 | $interactive = !drush_get_option(array('non-interactive', 'noi')); 192 | 193 | // This is a shortcut for developing, if you have --noi forced in your drush 194 | // config and need to switch back to interactive for testing. 195 | if (drush_get_option(array('interactive'))) { 196 | $interactive = TRUE; 197 | } 198 | 199 | return $interactive; 200 | } 201 | 202 | /** 203 | * Module builder drush command callback. 204 | * 205 | * Form: 206 | * $drush mb machine_name hookA hookB hookC 207 | * perms: permission_name 208 | * routes: path/to/thing another/path 209 | * theme: my_themeable another_themeable 210 | * readme! settings-form! tests! 211 | * Hook names may be short or long, e.g. both 'help' and 'hook_help' are 212 | * allowed. Parameters ending in a ':' are markers, which introduce a new type 213 | * of parameter, such as routes and plugins. Parameters ending in '!' are 214 | * booleans which add a single component such as a README file. 215 | */ 216 | function drush_module_builder_callback_build_module() { 217 | $commands = func_get_args(); 218 | 219 | // Check settings before we start. This sort of wastes the potential of using 220 | // exceptions, but it's polite to warn the user of problems before they've 221 | // spent ages typing in all the hook names in interactive mode. 222 | // Get our task handler. This performs a sanity check on the environment which 223 | // throws an exception. 224 | try { 225 | $mb_task_handler_generate = \DrupalCodeBuilder\Factory::getTask('Generate', 'module'); 226 | } 227 | catch (\DrupalCodeBuilder\Exception\SanityException $e) { 228 | // If the problem is that the hooks need downloading, we can recover from this. 229 | if ($e->getFailedSanityLevel() == 'component_data_processed') { 230 | if (drush_confirm(dt('No hook definitions found. Would you like to download them now?'))) { 231 | // Download the hooks so we can move on. 232 | $success = drush_module_builder_callback_hook_download(); 233 | if (!$success) { 234 | drush_set_error(DRUSH_APPLICATION_ERROR, 'Problem downloading hook data.'); 235 | return; 236 | } 237 | 238 | // Get the task handler that we were trying to get in the first place. 239 | $mb_task_handler_generate = \DrupalCodeBuilder\Factory::getTask('Generate', 'module'); 240 | } 241 | } 242 | // Otherwise, fail. 243 | else { 244 | drush_set_error(DRUSH_APPLICATION_ERROR, $e->getMessage()); 245 | return; 246 | } 247 | } 248 | 249 | // Extra drush-specific component data info for the module component. 250 | // This defines command prefixes and drush options. 251 | // This gets merged with the component data info returned from the Module 252 | // generator. 253 | $component_info_drush_extra = array( 254 | 'root_name' => array( 255 | 'drush_value_process' => 'module_builder_drush_component_root_name_process', 256 | ), 257 | 'hooks' => array( 258 | 'command_prefix' => 'hooks', 259 | // The list is huge, so bypass showing the options. 260 | 'drush_no_option_list' => TRUE, 261 | ), 262 | 'module_hook_presets' => array( 263 | 'command_prefix' => 'presets', 264 | ), 265 | 'readable_name' => array( 266 | 'drush_option' => 'name', 267 | ), 268 | 'short_description' => array( 269 | 'drush_option' => 'desc', 270 | ), 271 | 'module_help_text' => array( 272 | 'drush_option' => 'helptext', 273 | ), 274 | 'module_dependencies' => array( 275 | 'drush_option' => 'dep', 276 | ), 277 | 'module_package' => array( 278 | 'drush_option' => 'package', 279 | ), 280 | 'permissions' => array( 281 | 'command_prefix' => 'perms', 282 | ), 283 | 'services' => array( 284 | 'command_prefix' => 'services', 285 | ), 286 | 'plugins' => array( 287 | 'command_prefix' => 'plugins', 288 | ), 289 | 'forms' => array( 290 | 'command_prefix' => 'forms', 291 | ), 292 | 'theme_hooks' => array( 293 | 'command_prefix' => 'theme', 294 | ), 295 | 'router_items' => array( 296 | 'command_prefix' => 'routes', 297 | ), 298 | ); 299 | 300 | $component_data_extra = array(); 301 | 302 | // Extra component data for the build list and the bare code options. 303 | // What to build. 304 | $build = drush_get_option('build'); 305 | 306 | // write options: 307 | // - all -- everything we can do 308 | // - code -- code files, not info (module + install _ ..?) 309 | // - info -- only info fole 310 | // - module -- only module file 311 | // - install -- only install file 312 | // - ??? whatever hooks need 313 | 314 | // No build: set nice default. 315 | if (!$build) { 316 | // If we are adding, 'code' is implied 317 | if (drush_get_option('add')) { 318 | $build = 'code'; 319 | } 320 | // If we are writing or going, all. 321 | elseif (drush_get_option(array('write', 'go'))) { 322 | $build = 'all'; 323 | } 324 | // Otherwise, outputting to terminal: only module 325 | else { 326 | $build = 'code'; 327 | } 328 | } 329 | 330 | // Make a list out of the build option string. This may of course have only 331 | // one item in it. 332 | $build_list = explode(' ', $build); 333 | 334 | // Multi build: set a single string to switch on below. 335 | if (count($build_list) > 1) { 336 | $build = 'code'; 337 | } 338 | 339 | // Set the build list in the module data. 340 | // TODO: move all the above to a helper function! 341 | $component_data_extra['requested_build'] = array_fill_keys($build_list, TRUE); 342 | 343 | // The 'bare code' option. This doesn't fully work yet, as 'add' doesn't 344 | // fully work yet! TODO! 345 | $bare_code = drush_get_option('add'); 346 | $component_data_extra['bare_code'] = $bare_code; 347 | 348 | // Insert the 'hooks' prefix into the commands array, after the module name. 349 | // This privileges the hooks: they don't need a prefix and all commands up to 350 | // another prefix are taken to be hooks. 351 | array_splice($commands, 1, 0, 'hooks:'); 352 | 353 | // Call the main function to do the work of gathering data from the user and 354 | // building the files and optionally writing them. 355 | drush_module_builder_build_component($commands, 'module', $component_info_drush_extra, $component_data_extra); 356 | 357 | // Enable the module if requested. 358 | if (drush_get_option('go')) { 359 | pm_module_manage(array(array_shift($commands)), TRUE); 360 | } 361 | } 362 | 363 | /** 364 | * Command argument complete callback: mb-build. 365 | * 366 | * Debug this with, for example: 367 | * $ drush --early=includes/complete.inc --complete-debug drush mb foo ini 368 | * and comment out cache in drush_complete_get(). 369 | */ 370 | function module_builder_module_builder_callback_build_complete() { 371 | // We're too early in the drush bootstrap for this to be called, apparently. 372 | // So do it ourselves. 373 | module_builder_drush_init_helper(); 374 | 375 | // Add our environment handler, and check hook data is ready. 376 | try { 377 | // Drupal bootstrap isn't ready enough to check environment, because the 378 | // file API isn't loaded. This isn't bad, because here it's us, the caller, 379 | // that knows we can't pass the check. 380 | \DrupalCodeBuilder\Factory::getEnvironment()->skipSanityCheck(TRUE); 381 | 382 | $mb_task_handler_report = \DrupalCodeBuilder\Factory::getTask('ReportHookData'); 383 | } 384 | catch (\DrupalCodeBuilder\Exception\SanityException $e) { 385 | // Just do nothing if we're not ready: the actual command will deal with 386 | // error output to the user. 387 | return; 388 | } 389 | 390 | $data = $mb_task_handler_report->getHookDeclarations(); 391 | $complete = array(); 392 | foreach ($data as $hook_name => $hook_data) { 393 | $complete[] = $hook_name; 394 | 395 | // Add the short name too. 396 | $complete[] = substr($hook_name, 5); 397 | 398 | // TODO: in anticipation of us doing callbacks too, don't just chop off the 399 | // front! 400 | //if (preg_match('/^hook_/')) 401 | } 402 | 403 | return array( 404 | 'values' => $complete, 405 | ); 406 | } 407 | 408 | /** 409 | * Build a generic requested component. 410 | * 411 | * @param $component_type 412 | * The type of the component, e.g. 'module'. 413 | */ 414 | function drush_module_builder_callback_build_component($component_type = NULL) { 415 | $commands = func_get_args(); 416 | // Shift off the component name. 417 | array_shift($commands); 418 | 419 | // Determine whether we're in interactive mode. 420 | $interactive = !drush_get_option(array('non-interactive', 'noi')); 421 | 422 | // This is a shortcut for developing, if you have --noi forced in your drush 423 | // config and need to switch back to interactive for testing. 424 | if (drush_get_option(array('interactive'))) { 425 | $interactive = TRUE; 426 | } 427 | 428 | if (empty($component_type)) { 429 | if ($interactive) { 430 | $component_type = drush_prompt("Enter a component name", 'module', TRUE); 431 | } 432 | else { 433 | throw new Exception("No component name given."); 434 | } 435 | } 436 | 437 | drush_module_builder_build_component($commands, $component_type); 438 | } 439 | 440 | /** 441 | * Builds a Drupal component. 442 | * 443 | * This is a common helper for Drush command callbacks, that each deal with a 444 | * single type of component, e.g. modules or themes. 445 | * 446 | * This hands over to module_builder_drush_output_code() to output the code. 447 | * 448 | * @param $commands 449 | * The commands array; the original parameters from the Drush command callback. 450 | * @param $component_type 451 | * The component type, e.g., 'module'. 452 | * @param $component_info_drush_extra = array() 453 | * An array to merge into the component data info array. This may contain 454 | * additional information specific to drush. Possible keys for each property 455 | * info array are: 456 | * - 'command_prefix': A string that when suffixed with ':' acts as a marker 457 | * for this property in the commands array. All commands after this marker 458 | * taken as data for this property, until another marker is found or until 459 | * the end of the commands array. For example, the following list of 460 | * commands contains hooks and hook presets: 461 | * 'hooks: menu init presets: node field-widget' 462 | * - 'drush_option': A string giving the drush option to take this property's 463 | * value from. 464 | * - 'drush_no_option_list': Suppress the output of an option list for this 465 | * property. Useful for a property whose list of options is very large. 466 | * - 'drush_value_process': A callable to apply processing to the property's 467 | * value before passing it to the generator. This receives the value as its 468 | * parameter. 469 | * @param $component_data = array() 470 | * Initial component data, into which data obtained from the user will be 471 | * added. Command callbacks may wish to gather extra data, or add defaults. 472 | */ 473 | function drush_module_builder_build_component($commands, $component_type, $component_info_drush_extra = array(), $component_data = array()) { 474 | // Get the Generator task, specifying the component we want so we get a 475 | // sanity check based on that and our environment. 476 | try { 477 | $mb_task_handler_generate = \DrupalCodeBuilder\Factory::getTask('Generate', $component_type); 478 | } 479 | catch (\DrupalCodeBuilder\Exception\SanityException $e) { 480 | module_builder_handle_sanity_exception($e); 481 | return; 482 | } 483 | 484 | // Set the base type. 485 | $component_data['base'] = $component_type; 486 | 487 | // Get the component data info, and add in extra info specific to Drush. 488 | $component_data_info = $mb_task_handler_generate->getRootComponentDataInfo(); 489 | // Remove extra property info that's for properties we don't know about (such 490 | // as ones that only apply to certain versions of Drupal). 491 | foreach ($component_info_drush_extra as $property_name => $property_extra_info) { 492 | if (!isset($component_data_info[$property_name])) { 493 | unset($component_info_drush_extra[$property_name]); 494 | } 495 | } 496 | $component_data_info = array_merge_recursive($component_data_info, $component_info_drush_extra); 497 | 498 | // Developer option: skip specified properties. This allows quicker manual 499 | // testing of interactive mode. 500 | $skip = array_fill_keys(explode(',', drush_get_option('skip')), TRUE); 501 | $component_data_info = array_diff_key($component_data_info, $skip); 502 | 503 | // Split the commands array into: 504 | // - plain commands 505 | // - a nested array of prefixed commands 506 | // - an array of boolean flags. 507 | // E.g. given 'foo bar hooks: biz presets: bax qux!', we want: 508 | // - $commands as just 'foo bar' 509 | // - hooks as 'biz' 510 | // - presets as 'bax' 511 | // - boolean flags as 'qux' 512 | $plain_commands = array(); 513 | $prefixed_commands = array(); 514 | $boolean_commands = array(); 515 | foreach ($commands as $command) { 516 | if (strlen($command) > 1 && substr($command, -1) == ':') { 517 | // This is a preset marker. 518 | $prefix_marker = substr($command, 0, -1); 519 | 520 | // Set the array up and move on. 521 | $prefixed_commands[$prefix_marker] = array(); 522 | continue; 523 | } 524 | 525 | if (substr($command, -1) == '!') { 526 | // This is a boolean flag. 527 | $boolean_flag = substr($command, 0, -1); 528 | 529 | $boolean_commands[$boolean_flag] = TRUE; 530 | continue; 531 | } 532 | 533 | if (isset($prefix_marker)) { 534 | // Continue taking commands for this prefix until we find another prefix 535 | // marker or run out of commands. 536 | $prefixed_commands[$prefix_marker][] = $command; 537 | } 538 | else { 539 | $plain_commands[] = $command; 540 | } 541 | } 542 | 543 | // Determine whether we're in interactive mode. 544 | $interactive = !drush_get_option(array('non-interactive', 'noi')); 545 | 546 | // This is a shortcut for developing, if you have --noi forced in your drush 547 | // config and need to switch back to interactive for testing. 548 | if (drush_get_option(array('interactive'))) { 549 | $interactive = TRUE; 550 | } 551 | 552 | // Build the component data array from the given commands. 553 | // Work through the component data info, assembling the component data array 554 | // Each property info needs to be prepared, so iterate by reference. 555 | foreach ($component_data_info as $property_name => &$property_info) { 556 | // Prepare the single property: get options, default value, etc. 557 | $mb_task_handler_generate->prepareComponentDataProperty($property_name, $property_info, $component_data); 558 | 559 | // Initialize our value from the default that's been set by 560 | // prepareComponentDataProperty(). We try various things to set it. 561 | $value = $component_data[$property_name]; 562 | // Keep track of whether the value has come from the user or is still the 563 | // default. 564 | $user_specified = FALSE; 565 | 566 | // If the property is required, and there are plain command parameters 567 | // remaining, take one of those. 568 | if (!$user_specified && $property_info['required'] && $plain_commands) { 569 | $value = array_shift($plain_commands); 570 | $user_specified = TRUE; 571 | } 572 | 573 | // If the property has a prefix, and the commands included that prefix, 574 | // then take the commands for that prefix. 575 | if (!$user_specified && isset($property_info['command_prefix']) && !empty($prefixed_commands[$property_info['command_prefix']])) { 576 | $value = $prefixed_commands[$property_info['command_prefix']]; 577 | $user_specified = TRUE; 578 | } 579 | 580 | // If the property can be set with a command-line option, check that. 581 | if (!$user_specified && isset($property_info['drush_option'])) { 582 | $drush_option_value = drush_get_option($property_info['drush_option']); 583 | if (!empty($drush_option_value)) { 584 | $value = $drush_option_value; 585 | $user_specified = TRUE; 586 | } 587 | } 588 | 589 | // Boolean commands. 590 | if ($property_info['format'] == 'boolean') { 591 | if (isset($boolean_commands[$property_name])) { 592 | $value = TRUE; 593 | $user_specified = TRUE; 594 | } 595 | } 596 | 597 | // Process direct mode values for compound properties into the expected 598 | // format. 599 | if ($user_specified && ($property_info['format'] == 'compound')) { 600 | // A compound property in direct input mode uses a ':' to separate the 601 | // different child items. So for example: 602 | // plugins: block alpha : block beta 603 | $child_property_names = array_keys($property_info['properties']); 604 | 605 | $child_items = []; 606 | $delta = 0; 607 | foreach ($value as $child_value) { 608 | if ($child_value == ':') { 609 | // This starts a new delta. 610 | $delta++; 611 | 612 | // Restore the list of child property names. 613 | $child_property_names = array_keys($property_info['properties']); 614 | 615 | // Move on to the next single value. 616 | continue; 617 | } 618 | 619 | // Still here: take the value. 620 | $child_property_name = array_shift($child_property_names); 621 | 622 | // Split array properties on a comma. 623 | // TODO: either document this or rethink it. 624 | if ($property_info['properties'][$child_property_name]['format'] == 'array') { 625 | $child_value = explode(',', $child_value); 626 | } 627 | 628 | $child_items[$delta][$child_property_name] = $child_value; 629 | // Defaults for other child properties will be filled in by the 630 | // process stage. 631 | } 632 | 633 | $value = $child_items; 634 | } 635 | 636 | // If we're not in interactive mode, there's nothing more to do for this 637 | // command other than use the default value. That's already set in the 638 | // component data. 639 | 640 | if (!$user_specified && $interactive) { 641 | // Prompt the user for a property we've not already got user input for. 642 | 643 | // Turn empty defaults into a value that Drush won't output. 644 | $default = empty($component_data[$property_name]) ? NULL : $component_data[$property_name]; 645 | 646 | if ($property_info['format'] == 'compound') { 647 | // For compound properties, allow the user to enter as many items as 648 | // they like, prompting for all the child properties for each item. 649 | // Entering an empty value for the first child property ends the 650 | // handling of the compound property. 651 | $delta = 0; 652 | $child_property_names = array_keys($property_info['properties']); 653 | while (TRUE) { 654 | // Initialize a new child item so a default value can be placed 655 | // into it. 656 | $value[$delta] = []; 657 | 658 | foreach ($property_info['properties'] as $child_property_name => &$child_property_info) { 659 | // Prepare the child property so we get defaults. 660 | // (The call to prepare the compound property will have already 661 | // filled in defaults, but it's safe to call this again.) 662 | $mb_task_handler_generate->prepareComponentDataProperty($child_property_name, $child_property_info, $value[$delta]); 663 | 664 | // Turn empty defaults into a value that Drush won't output. 665 | $default = empty($value[$delta][$child_property_name]) ? NULL : $value[$delta][$child_property_name]; 666 | 667 | // The first child property gets special treatment: don't propose a 668 | // default for it, and force it to be non-required. This is because 669 | // otherwise it would be impossible to leave the loop of collecting 670 | // child items, as leaving this property empty is the way for the 671 | // user to cause that. 672 | $first_child = ($child_property_name == $child_property_names[0]); 673 | 674 | if ($first_child) { 675 | $default = ''; 676 | } 677 | 678 | // Add the parent label so we can use it in prompts. 679 | $child_property_info['parent_label'] = $property_info['label']; 680 | 681 | $child_value = module_builder_drush_interactive_prompt($child_property_name, $child_property_info, $component_data[$child_property_name], $default, $delta, $first_child); 682 | 683 | // If the first property is empty, stop collecting child items. 684 | if ($first_child && empty($child_value)) { 685 | // Bail on both the while and the foreach loops: XKCD dinosaur! 686 | // Remove the child item we created for this delta, as nothing's 687 | // been put in it. 688 | unset($value[$delta]); 689 | 690 | goto endcompound; 691 | } 692 | 693 | $value[$delta][$child_property_name] = $child_value; 694 | } 695 | 696 | $delta++; 697 | } 698 | endcompound: 699 | } 700 | else { 701 | $value = module_builder_drush_interactive_prompt($property_name, $property_info, $component_data, $default); 702 | } 703 | } // End interactive. 704 | 705 | // Split up the value if it should be an array. 706 | if ($property_info['format'] == 'array' && !is_array($value)) { 707 | $value = preg_split('/\s+/', $value, -1, PREG_SPLIT_NO_EMPTY); 708 | } 709 | 710 | // Process compound properties into the expected format. 711 | if ($property_info['format'] == 'compound' && !is_array($value)) { 712 | $value = preg_split('/\s+/', $value, -1, PREG_SPLIT_NO_EMPTY); 713 | 714 | // For now, a compound property is input in direct mode as a flat array, 715 | // where we take each array element to be the first child property. 716 | $child_properties = $property_info['properties']; 717 | $first_child_property = array_shift(array_keys($child_properties)); 718 | 719 | $items = []; 720 | foreach ($value as $single_value) { 721 | $items[][$first_child_property] = $single_value; 722 | // Defaults for other child properties will be filled in by the 723 | // process stage. 724 | } 725 | $value = $items; 726 | } 727 | 728 | // Perform any processing specific to drush. 729 | if (isset($property_info['drush_value_process'])) { 730 | $callback = $property_info['drush_value_process']; 731 | $value = $callback($value); 732 | } 733 | 734 | // Set the value in the component data array. 735 | $component_data[$property_name] = $value; 736 | } 737 | 738 | 739 | // Generate the component. 740 | $files = $mb_task_handler_generate->generateComponent($component_data); 741 | 742 | $component_dir = module_builder_get_component_folder($component_type, $component_data['root_name']); 743 | 744 | // Finally, output the files! 745 | module_builder_drush_output_code($component_dir, $files); 746 | } 747 | 748 | /** 749 | * Prompt the user for a single value. 750 | * 751 | * This can be used recursively for child properties. 752 | * 753 | * @param $property_name 754 | * The name of the property to prompt for. 755 | * @param $property_info 756 | * The info array for a single component property. 757 | * @param $component_data 758 | * The component data array, or a sub-array of it, such that the property's 759 | * value will be placed in the top level of this array. 760 | * @param $default 761 | * The default value for the property. 762 | * @param $delta 763 | * (optional) If the property is a child property, the delta of the current 764 | * component. 765 | * @param $first_child 766 | * (optional) If the property is a child property, TRUE to indicate it's the 767 | * first of the children. Defaults to FALSE. 768 | * 769 | * @return 770 | * The value obtained from the user. 771 | */ 772 | function module_builder_drush_interactive_prompt($property_name, $property_info, $component_data, $default, $delta = NULL, $first_child = FALSE) { 773 | // Show the user the available options, unless told not to. 774 | $numeric_options = FALSE; 775 | if (isset($property_info['options']) && empty($property_info['drush_no_option_list'])) { 776 | $numeric_options = TRUE; 777 | 778 | drush_print("Select from the following options. Enter either option index number or their value."); 779 | 780 | $option_by_index = array(); 781 | $i = 1; 782 | foreach ($property_info['options'] as $option => $label) { 783 | $option_by_index[$i] = $option; 784 | drush_print("[$i] $option: $label", 2); 785 | 786 | $i++; 787 | } 788 | 789 | if (!empty($property_info['options_allow_other'])) { 790 | drush_print("[...] Values not on this list may also be entered.", 2); 791 | } 792 | } 793 | 794 | // If this is the first child of a compound component, introduce the current 795 | // delta. 796 | if ($first_child) { 797 | $human_delta = $delta + 1; 798 | drush_print("{$property_info['parent_label']}: $human_delta"); 799 | } 800 | 801 | // Make a prompt string, letting the user know if the input is optional 802 | // so they don't waste time typing. 803 | switch ($property_info['format']) { 804 | case 'array': 805 | $prompt = "Enter the {$property_info['label']}, as a space separated list"; 806 | break; 807 | case 'boolean': 808 | $prompt = "Do you want a {$property_info['label']}"; 809 | $prompt .= ' ' . "(y/n)"; 810 | // Set the default. 811 | $default = empty($default) ? 'n' : 'y'; 812 | break; 813 | case 'string': 814 | $prompt = "Enter the {$property_info['label']}"; 815 | } 816 | if (empty($property_info['required']) && empty($component_data[$property_name])) { 817 | $prompt .= ' ' . "(optional)"; 818 | } 819 | if ($first_child) { 820 | $prompt .= ' ' . "(leave blank to finish entering {$property_info['parent_label']})"; 821 | } 822 | 823 | // Hack the 'required' property for first child, so the user can enter an 824 | // empty string and exit the child properties loop. 825 | $required = $property_info['required']; 826 | if ($first_child) { 827 | $required = FALSE; 828 | } 829 | 830 | // Indent child properties to make the structure clearer. 831 | if (!is_null($delta)) { 832 | $prompt = ' ' . $prompt; 833 | } 834 | 835 | $value = drush_prompt($prompt, $default, $required); 836 | 837 | // Split a space-separated list into an array now rather than later, so the 838 | // numeric options can be converted. 839 | if ($property_info['format'] == 'array') { 840 | if (empty($value)) { 841 | $value = []; 842 | } 843 | else { 844 | $value = preg_split('@\s+@', $value); 845 | } 846 | } 847 | 848 | // Convert boolean property input to an actual boolean. 849 | if ($property_info['format'] == 'boolean') { 850 | if (in_array(strtolower($value), ['no', 'n'])) { 851 | $value = FALSE; 852 | } 853 | } 854 | 855 | // Callback to convert a value given as an index in a list of options into 856 | // the actual option value. 857 | $convert_index_to_value_callback = function($item) use ($option_by_index) { 858 | if (is_numeric($item)) { 859 | return $option_by_index[$item]; 860 | } 861 | else { 862 | return $item; 863 | } 864 | }; 865 | 866 | // Convert a numeric option index value into the actual option value. 867 | if ($numeric_options) { 868 | // We have either an array of values or a single one. 869 | if (is_array($value)) { 870 | $value = array_map($convert_index_to_value_callback, $value); 871 | } 872 | else { 873 | $value = $convert_index_to_value_callback($value); 874 | } 875 | } 876 | 877 | return $value; 878 | } 879 | 880 | /** 881 | * Process the input for component base name for filesystem shorthands. 882 | * 883 | * This allows for the following in the component name: 884 | * - A single '.' specifies the current folder. 885 | * - A trailing slash is ignored, thus allowing autocompletion of folder names. 886 | * 887 | * @param $component_root_name 888 | * The given component root name. 889 | * 890 | * @return 891 | * The processed component root name. 892 | */ 893 | function module_builder_drush_component_root_name_process($component_root_name) { 894 | // Trim a final '/' from the module machine name, to allow use of tab 895 | // autocompletion on the command line. 896 | if (substr($component_root_name, -1) == '/') { 897 | $component_root_name = substr($component_root_name, 0, -1); 898 | } 899 | 900 | // An input machine name given as '.' means use the current folder as the 901 | // component name, thus write to the current folder. 902 | if ($component_root_name == '.') { 903 | $component_root_name = basename(drush_get_context('DRUSH_OLDCWD')); 904 | } 905 | 906 | return $component_root_name; 907 | } 908 | 909 | /** 910 | * Get the folder where a generated component's files should be written to. 911 | * 912 | * @param $component_type 913 | * The type of the component. One of 'module' or 'theme'. 914 | * @param $component_name 915 | * The component name. 916 | * 917 | * @return 918 | * The full system path for the component's folder, without a trailing slash. 919 | */ 920 | function module_builder_get_component_folder($component_type, $component_name) { 921 | $drupal_root = drush_get_context('DRUSH_DRUPAL_ROOT'); 922 | 923 | // First try: if the component exists, we write there: nice and simple. 924 | // In Drupal 8, drupal_get_filename() triggers an error for a component that 925 | // doesn't exist, so bypass that with a dummy error handler. 926 | set_error_handler(function() {}, E_USER_WARNING); 927 | 928 | $component_path = @drupal_get_path($component_type, $component_name); 929 | 930 | restore_error_handler(); 931 | 932 | if (!empty($component_path)) { 933 | return $drupal_root . '/' . $component_path; 934 | } 935 | 936 | // Third try: 'parent' option was given. 937 | if (drush_get_option('parent')) { 938 | // The --parent option allows the user to specify a location for the new module folder. 939 | $parent_dir = drush_get_option('parent'); 940 | if (substr($parent_dir, 0 , 1) == '.') { 941 | // An initial . means to start from the current directory rather than 942 | // the modules folder, which allows submodules to be created where the 943 | // user is standing. 944 | $module_dir = drush_get_context('DRUSH_OLDCWD') . '/'; 945 | // Remove both the . and the following /. 946 | $parent_dir = substr($parent_dir, 2); 947 | if ($parent_dir) { 948 | // If there's anything left (since just '.' is a valid option), append it. 949 | $module_dir .= $parent_dir . '/'; 950 | } 951 | } 952 | else { 953 | // If there's no dot, assume that an existing module is meant. 954 | // (Would anyone enter a complete path for this??? If we do need this, 955 | // then consider recursing into this for the parent path??) 956 | $module_dir .= drupal_get_path($component_type, $parent_dir) . '/'; 957 | } 958 | return $module_dir . $component_name; 959 | } 960 | 961 | // Fourth and final try: build it based on the module folder structure. 962 | 963 | // There is probably a proper way to do this but it's Sunday morning and 964 | // I want this to just work and so brute force appeals. 965 | require_once DRUSH_BASE_PATH . '/commands/pm/download.pm.inc'; 966 | $module_dir = _pm_download_destination($component_type); 967 | 968 | // Some versions of Drush don't give us the trailing /. 969 | if (substr($module_dir, -1, 1) != '/') { 970 | $module_dir .= '/'; 971 | } 972 | 973 | if ($component_type == 'module') { 974 | // Drush tries to put any module into 'contrib' if the folder exists; 975 | // hack this out and put the code in 'custom'. 976 | if (substr($module_dir, -8, 7) == 'contrib') { 977 | $module_dir_custom = substr_replace($module_dir, 'custom', -8, 7); 978 | if (is_dir($module_dir_custom)) { 979 | $module_dir = $module_dir_custom; 980 | } 981 | } 982 | } 983 | 984 | // $module_dir should now be a full path to the parent of the destination 985 | // folder, with a trailing slash. 986 | $module_dir .= $component_name; 987 | 988 | return $module_dir; 989 | } 990 | 991 | /** 992 | * Output generated text, to terminal or to file. 993 | * 994 | * @param $component_dir 995 | * The base folder for the component. May or may not exist. 996 | * @param $filename 997 | * The array of files to write. Keys are filenames relative to the 998 | * $component_dir, values are strings for the file contents. 999 | */ 1000 | function module_builder_drush_output_code($component_dir, $files) { 1001 | // Determine whether to output to terminal. 1002 | $output_to_terminal = !drush_get_option('quiet'); 1003 | 1004 | if ($output_to_terminal) { 1005 | foreach ($files as $filename => $code) { 1006 | drush_print("Proposed $filename:"); 1007 | drush_print_r($code); 1008 | } 1009 | } 1010 | 1011 | // Determine whether to write files. 1012 | // Determine whether to write this file. 1013 | // Add to file option implies write. 1014 | // Write & go option implies write. 1015 | $write_files = drush_get_option(array('write', 'add', 'go')); 1016 | 1017 | // If the options don't tell us to write, and we're in interactive mode, 1018 | // prompt the user for whether to write files. 1019 | if (!$write_files && module_builder_determine_interactive_mode()) { 1020 | $write_files = drush_confirm("Write code files? (Use the --write option to skip this prompt.)"); 1021 | } 1022 | 1023 | // If we're not writing files, we're done. 1024 | if (!$write_files) { 1025 | return; 1026 | } 1027 | 1028 | $files_exist = []; 1029 | foreach ($files as $filename => $code) { 1030 | $filepath = $component_dir . '/' . $filename; 1031 | if (file_exists($filepath)) { 1032 | if (!drush_confirm(dt('File ' . $filename . ' exists. Overwrite this file?'))) { 1033 | continue; 1034 | } 1035 | } 1036 | 1037 | // Because the filename part can contain subdirectories, check these exist 1038 | // too. 1039 | $subdir = dirname($filepath); 1040 | if (!is_dir($subdir)) { 1041 | $result = mkdir($subdir, 0777, TRUE); 1042 | if ($result && !drush_get_option('quiet')) { 1043 | if ($subdir == $component_dir) { 1044 | drush_print("Module directory $component_dir created"); 1045 | } 1046 | else { 1047 | drush_print("Module subdirectory $subdir created"); 1048 | } 1049 | } 1050 | } 1051 | 1052 | // Add to file option. 1053 | // If the file doesn't exist, we skip this and silently write it anyway. 1054 | // TODO: add to file option is a bit broken these days anyway. 1055 | if (drush_get_option('add') && file_exists($filepath)) { 1056 | $fh = fopen($filepath, 'a'); 1057 | fwrite($fh, $code); 1058 | fclose($fh); 1059 | continue; 1060 | } 1061 | 1062 | file_put_contents($filepath, $code); 1063 | } 1064 | } 1065 | 1066 | /** 1067 | * Callback for downloading hook data. 1068 | * 1069 | * @return 1070 | * Boolean indicating TRUE for success, FALSE for failure. 1071 | */ 1072 | function drush_module_builder_callback_hook_download() { 1073 | // Get our task handler. This performs a sanity check which throws an 1074 | // exception. 1075 | try { 1076 | $mb_task_handler_collect = \DrupalCodeBuilder\Factory::getTask('Collect'); 1077 | 1078 | // Hidden option for developers: downloads a subset of hooks to create the 1079 | // data for Drupal Code Builder's unit tests. 1080 | if (drush_get_option('test')) { 1081 | $mb_task_handler_collect = \DrupalCodeBuilder\Factory::getTask('Testing\CollectTesting'); 1082 | } 1083 | } 1084 | catch (\DrupalCodeBuilder\Exception\SanityException $e) { 1085 | module_builder_handle_sanity_exception($e); 1086 | } 1087 | 1088 | $mb_task_handler_collect->collectComponentData(); 1089 | 1090 | $hooks_directory = \DrupalCodeBuilder\Factory::getEnvironment()->getHooksDirectory(); 1091 | drush_print("Hook files have been downloaded to {$hooks_directory} and processed."); 1092 | return TRUE; 1093 | } 1094 | 1095 | /** 1096 | * Callback to list stored data on components. 1097 | */ 1098 | function drush_module_builder_callback_data_list() { 1099 | $commands = func_get_args(); 1100 | 1101 | // Get our task handler, which checks hook data is ready. 1102 | try { 1103 | $mb_task_handler_report = \DrupalCodeBuilder\Factory::getTask('ReportHookData'); 1104 | } 1105 | catch (\DrupalCodeBuilder\Exception\SanityException $e) { 1106 | module_builder_handle_sanity_exception($e); 1107 | return; 1108 | } 1109 | 1110 | $time = $mb_task_handler_report->lastUpdatedDate(); 1111 | $data = $mb_task_handler_report->listHookData(); 1112 | 1113 | if (drush_get_option('raw')) { 1114 | drush_print_r($data); 1115 | return; 1116 | } 1117 | 1118 | if (count($commands)) { 1119 | // Put the requested filenames into the keys of an array, and intersect them 1120 | // with the hook data. 1121 | $files_requested = array_fill_keys($commands, TRUE); 1122 | $data_requested = array_intersect_key($data, $files_requested); 1123 | } 1124 | else { 1125 | $data_requested = $data; 1126 | } 1127 | 1128 | if (!count($data_requested) && count($files_requested)) { 1129 | drush_print(t("No hooks found for the specified files.")); 1130 | } 1131 | 1132 | drush_print("Hooks:"); 1133 | foreach ($data_requested as $file => $hooks) { 1134 | drush_print("Group $file:", 2); 1135 | foreach ($hooks as $key => $hook) { 1136 | drush_print($hook['name'] . ': ' . $hook['description'], 4); 1137 | } 1138 | } 1139 | 1140 | // List presets. 1141 | try { 1142 | $mb_task_handler_report_presets = \DrupalCodeBuilder\Factory::getTask('ReportHookPresets'); 1143 | } 1144 | catch (\DrupalCodeBuilder\Exception\SanityException $e) { 1145 | module_builder_handle_sanity_exception($e); 1146 | return; 1147 | } 1148 | 1149 | $hook_presets = $mb_task_handler_report_presets->getHookPresets(); 1150 | foreach ($hook_presets as $hook_preset_name => $hook_preset_data) { 1151 | drush_print("Preset $hook_preset_name: " . $hook_preset_data['label'], 2); 1152 | foreach ($hook_preset_data['hooks'] as $hook) { 1153 | drush_print($hook, 4); 1154 | } 1155 | } 1156 | 1157 | if (drush_drupal_major_version() == 8) { 1158 | try { 1159 | $mb_task_handler_report_plugins = \DrupalCodeBuilder\Factory::getTask('ReportPluginData'); 1160 | } 1161 | catch (\DrupalCodeBuilder\Exception\SanityException $e) { 1162 | module_builder_handle_sanity_exception($e); 1163 | return; 1164 | } 1165 | 1166 | $data = $mb_task_handler_report_plugins->listPluginData(); 1167 | 1168 | drush_print("Plugins types:"); 1169 | foreach ($data as $plugin_type_id => $plugin_type_data) { 1170 | drush_print($plugin_type_id, 2); 1171 | } 1172 | 1173 | try { 1174 | $mb_task_handler_report_services = \DrupalCodeBuilder\Factory::getTask('ReportServiceData'); 1175 | } 1176 | catch (\DrupalCodeBuilder\Exception\SanityException $e) { 1177 | module_builder_handle_sanity_exception($e); 1178 | return; 1179 | } 1180 | 1181 | $data = $mb_task_handler_report_services->listServiceData(); 1182 | 1183 | drush_print("Services:"); 1184 | foreach ($data as $service_id => $service_info) { 1185 | drush_print($service_id, 2); 1186 | } 1187 | } 1188 | 1189 | $hooks_directory = \DrupalCodeBuilder\Factory::getEnvironment()->getHooksDirectory(); 1190 | drush_print(t("Component data retrieved from @dir.", array('@dir' => $hooks_directory))); 1191 | drush_print(t("Component data was processed on @time.", array( 1192 | '@time' => date(DATE_RFC822, $time), 1193 | ))); 1194 | } 1195 | --------------------------------------------------------------------------------