/** * Return the main body of the file code. */ function code_body() { $component_data = $this->getRootComponentData(); $mb_factory = module_builder_get_factory(); // Sanity checks already done at this point; no need to catch exception. $mb_task_handler_analyze = $mb_factory->getTask('AnalyzeModule'); $hooks = $mb_task_handler_analyze->getInventedHooks($component_data['module_root_name']); $module_root_name = $component_data['module_root_name']; $module_root_name_title_case = ucfirst($component_data['module_root_name']); $module_readable_name = $component_data['module_readable_name']; // Build an array of code pieces. $code_pieces = array(); // The docblock grouping. $code_pieces['group'] = <<<EOT /** * @addtogroup hooks * @{ */ EOT; foreach ($hooks as $hook_short_name => $parameters) { $code_pieces[$hook_short_name] = $this->hook_code($hook_short_name, $parameters); } return $code_pieces; }
/** * Gather hook documentation files. * * This retrieves a list of api hook documentation files from the current * Drupal install. On D7 these are files of the form MODULE.api.php and are * present in the codebase (rather than needing to be downloaded from an * online code repository viewer as is the case in previous versions of * Drupal). */ protected function gatherHookDocumentationFiles() { // Get the hooks directory. $mb_factory = module_builder_get_factory(); $directory = $mb_factory->environment->hooks_directory; // Get Drupal root folder as a file path. // DRUPAL_ROOT is defined both by Drupal and Drush. // @see _drush_bootstrap_drupal_root(), index.php. $drupal_root = DRUPAL_ROOT; $system_listing = $mb_factory->environment->systemListing('/\\.api\\.php$/', 'modules', 'filename'); // returns an array of objects, properties: uri, filename, name, // keyed by filename, eg 'comment.api.php' // What this does not give us is the originating module! //print_r($system_listing); foreach ($system_listing as $filename => $file) { // Extract the module name from the path. // WARNING: this is not always going to be correct: will fail in the // case of submodules. So Commerce is a big problem here. // We could instead assume we have MODULE.api.php, but some modules // have multiple API files with suffixed names, eg Services. // @todo: make this more robust, somehow! $matches = array(); preg_match('@modules/(?:contrib/)?(\\w+)@', $file->uri, $matches); //print_r($matches); $module = $matches[1]; //dsm($matches, $module); // Copy the file to the hooks directory. copy($drupal_root . '/' . $file->uri, $directory . '/' . $file->filename); $hook_files[$filename] = array('original' => $drupal_root . '/' . $file->uri, 'path' => $directory . '/' . $file->filename, 'destination' => '%module.module', 'group' => $module, 'module' => $module); } // We now have the basics. // We should now see if some modules have extra information for us. $this->getHookDestinations($hook_files); return $hook_files; }
/** * Get list of hook file URLS from any modules that declare them. * * @param $directory * The path to the module builder hooks directory. * * @return * An array of data about the files to download, keyed by (safe) filename: [system.core.php] => Array [path] => the full path this file should be saved to [url] => URL [destination] => %module.module [group] => core */ function getHookFileUrls($directory) { // Get data by invoking our hook. $mb_factory = module_builder_get_factory(); $data = $mb_factory->environment->invokeInfoHook(); foreach ($data as $module => $module_data) { $branch = $module_data['branch']; foreach ($module_data['hook_files'] as $hook_file => $destination) { $url = str_replace(array('%file', '%branch'), array($hook_file, $branch), $module_data['url']); // Create our own safe filename with module prefix. $hook_file_safe_name = "{$module}.{$hook_file}"; $urls[$hook_file_safe_name]['path'] = $directory . '/' . $hook_file_safe_name; $urls[$hook_file_safe_name]['url'] = $url; $urls[$hook_file_safe_name]['destination'] = $destination; if (isset($module_data['hook_destinations'])) { $urls[$hook_file_safe_name]['hook_destinations'] = array(); foreach ($module_data['hook_destinations'] as $destination => $hooks) { $urls[$hook_file_safe_name]['hook_destinations'] += array_fill_keys($hooks, $destination); } } if ($module_data['group'] == '#filenames') { $urls[$hook_file_safe_name]['group'] = str_replace('.php', '', $hook_file); } else { $urls[$hook_file_safe_name]['group'] = $module_data['group']; } } } //print_r($urls); return $urls; }
/** * Constructor method; sets the component data. * * @param $component_name * The identifier for the component. * @param $component_data * (optional) An array of data for the component. Any missing properties * (or all if this is entirely omitted) are given default values. Valid * properties are: * - 'class': The name of the annotation class that defines the plugin * type, e.g. 'Drupal\Core\Entity\Annotation\EntityType'. * TODO: since the classnames are unique regardless of namespace, figure * out if there is a way of just specifying the classname. */ function __construct($component_name, $component_data = array()) { // Set some default properties. $component_data += array(); $mb_factory = module_builder_get_factory('ModuleBuilderEnvironmentDrush'); $mb_task_handler_report_plugins = $mb_factory->getTask('ReportPluginData'); $plugin_data = $mb_task_handler_report_plugins->listPluginData(); $plugin_data = $plugin_data[$component_name]; $component_data['plugin_type_data'] = $plugin_data; parent::__construct($component_name, $component_data); }
/** * Declares the subcomponents for this component. * * These are not necessarily child classes, just components this needs. * * A hook implementation adds the module code file that it should go in. It's * safe for the same code file to be requested multiple times by different * hook implementation components. * * @return * An array of subcomponent names and types. */ protected function requiredComponents() { // TODO! Caching! $mb_factory = module_builder_get_factory(); // Sanity checks already done at this point; no need to catch exception. $mb_task_handler_report = $mb_factory->getTask('ReportHookData'); $hook_function_declarations = $mb_task_handler_report->getHookDeclarations(); //drush_print_r($hook_function_declarations[$this->name]); $this->hook_info = $hook_function_declarations[$this->name]; $filename = $hook_function_declarations[$this->name]['destination']; $this->code_file = $filename; return array($filename => 'ModuleCodeFile'); }
/** * Helper function for our requiredComponents(). * * (Move this back out if it needs to be used by other components in future?) * * Returns an array of hook data and templates for the requested hooks. * This is handled live rather than in process.inc to allow the user to alter * their custom hook templates. * * @return * An array whose keys are destination filenames with the token '%module', * and whose values are arrays of hook data. The hook data keys are: * - declaration: The function declaration, with the 'hook' part not * yet replaced. * - destination: The destination, with tokens still in place. * - template_files: A list of template file types, in order of preference, * keyed by filename and with the value TRUE if the hook code exists * in that template file. * - template (optional): The template code, if any was found. * Example: * 'destination file' => array( * 'hook_foo' => array('declaration' => DATA, 'template' => DATA) */ function getTemplates($module_data) { $mb_factory = module_builder_get_factory(); // Sanity checks already done at this point; no need to catch exception. $mb_task_handler_report = $mb_factory->getTask('ReportHookData'); // Build a clean list of the requested hooks, by filtering out the keys // with 0 values that come from UI form. $requested_hooks = array_filter($module_data['hooks']); //print_r($requested_hooks); // TODO: might not need this; easier to test truth than isset. // Get array of the hook function declarations from the downloaded hook data. // This is a complete list of all hooks that exist. // In the form: 'hook_foo' => array('declaration', 'destination') // This array is the order they are in the files from d.org: alphabetical apparently. // We don't care for this order! $hook_function_declarations = $mb_task_handler_report->getHookDeclarations(); // If we get NULL then no hook data exists: return NULL again. // TODO: check this sort of error at an earlier stage! if (is_null($hook_function_declarations)) { return NULL; } //drush_print_r($hook_function_declarations); // TODO: this should contain the name of the api.php file that provided it! // Add hook dependencies. foreach (array_keys($requested_hooks) as $hook_name) { if (!empty($hook_function_declarations[$hook_name]['dependencies'])) { //drush_print_r($hook_function_declarations[$hook_name]['dependencies']); foreach ($hook_function_declarations[$hook_name]['dependencies'] as $hook_dependency) { $requested_hooks[$hook_dependency] = TRUE; } } } // Trim this down to just the ones we care about. // By this point, both sets of array keys are standardized to lower case. $hook_function_declarations = array_intersect_key($hook_function_declarations, $requested_hooks); //print_r("hook_function_declarations: \n"); //drush_print_r($hook_function_declarations); // Filter out the requested hooks that don't have definitions. // We do this now as it's possible for a hook to have no definition because // the user doesn't have it, but have a template because we provide it, // eg views_api. // We do this by hand this time rather than array_intersect_key() so we can // make a list of hooks we're rejecting for (TODO!) eventual warning output. $rejected_hooks = array(); foreach (array_keys($requested_hooks) as $hook_name) { if (!isset($hook_function_declarations[$hook_name])) { unset($requested_hooks[$hook_name]); $rejected_hooks[] = $hook_name; } } // TODO: at this point we should check if we have any actual hooks to // process, and warn if not. // We should probably also do something with rejected hooks list. // Include generating component file, for parsing templates. $mb_factory->environment->loadInclude('process'); // Step 1: // Build up a list of the basic template files we want to parse. // - in each $hook_function_declarations item, place an ordered list of // all potential template files. We will set these to TRUE in step 2 // if they hold a template for the hook. // - meanwhile, build up list of template files we will want to check for // existence and parse. // Template filenames are of the following form, in the order they should be // checked, ie from most specific to most general: // - GROUP.hooks.template, eg node.hooks.template // (Though groups are still TODO: this is scaffold only for now!) // - FILENAME.template, where the modulename is replaced with 'hooks', hence // hooks.module.template, hooks.install.template, hooks.views.inc.template. // - hooks.template - the base template, final fallback // These are found in module_builder/templates/VERSION, and // in addition, a file may be overridden by being present in the user's // data directory. Though just what the 'data directory' means exactly is // not yet properly defined... $template_file_names = array(); foreach ($hook_function_declarations as $hook_name => $hook) { // TODO: $groupname_template = 'GROUP.hooks.template'; $filename_template = str_replace('%module', 'hooks', $hook['destination']) . '.template'; // Place in each $hook_function_declarations item an ordered list of // potential files from best fit to fallback. // These are keyed by filename and all with value FALSE initially. $hook_function_declarations[$hook_name]['template_files'] = array_fill_keys(array($filename_template, 'hooks.template'), FALSE); // Meanwhile, build up list of files we will want to check for existence and parse. // TODO: $template_file_names[$groupname_template] = TRUE; $template_file_names[$filename_template] = TRUE; $template_file_names['hooks.template'] = TRUE; } // print_r("template file names: \n"); // print_r($template_file_names); // print_r("hook_function_declarations are now:: \n"); // print_r($hook_function_declarations); // Step 2: // Now we parse the templates we need. // We look in two places: module_builder's own '/templates' folder, and the optional // location given for user data (the latter is in fact TODO...) // User templates override on a per-file basis, so a custom // node.hooks.template will only override that same file in the module data; // if the hook is not requested as part of a group then that file will not be considered. // (Though groups are broken for now...) $version = $mb_factory->environment->major_version; $template_base_path_module = $mb_factory->environment->getPath('templates') . '/' . $version; //print "base path: $template_base_path_module"; // $template_base_paths['module'] // $template_base_paths['user'] $template_data = array(); foreach (array_keys($template_file_names) as $filename) { $filepath = "{$template_base_path_module}/{$filename}"; if (file_exists($filepath)) { $template_file = file_get_contents($filepath); $template_data = module_builder_parse_template($template_file); // Trim the template data to the hooks we care about. $template_data = array_intersect_key($template_data, $requested_hooks); // Flag the template file in the hook list; ie, set to TRUE the template // file in the list which we first created as entirely FALSE. foreach (array_keys($template_data) as $hook_name) { $hook_function_declarations[$hook_name]['template_files'][$filename] = TRUE; } } } //print_r("hook_function_declarations now have template files \n"); //print_r($hook_function_declarations); // $template_data is now an array of the form: // [hook name] => array('template' => DATA) // in a pretty order which we want to hold on to. //print_r('template data is:'); //print_r($template_data); // Step 3a: // Build a new array of hook data, so that we take the order from the // template data, but using the same data structure as the // $hook_function_declarations array. // The big question here: once we have other template files, such as those // for groups, or user ones, how do we combine the order from all of them? // Or do we just have an overall order from the template files' order, and // then within that respect each of theirs, so in effect it's like // concatenating all the template files we use? $hook_data_return = array(); foreach (array_keys($template_data) as $hook_name) { $destination = $hook_function_declarations[$hook_name]['destination']; // Copy over the data we already had. $hook_data_return[$destination][$hook_name] = $hook_function_declarations[$hook_name]; // Copy over the template. // TODO: more here. $hook_data_return[$destination][$hook_name]['template'] = $template_data[$hook_name]['template']; } // Step 3b: // Not all hooks have template data, so fill these in too. foreach ($hook_function_declarations as $hook_name => $hook) { $destination = $hook_function_declarations[$hook_name]['destination']; if (!isset($hook_data_return[$destination][$hook_name])) { $hook_data_return[$destination][$hook_name] = $hook_function_declarations[$hook_name]; } // We have no template data, so fill in the sample from the api.php file, // as this is often informative. if (empty($hook_data_return[$destination][$hook_name]['template'])) { $hook_data_return[$destination][$hook_name]['template'] = $hook_function_declarations[$hook_name]['body']; } } //print_r('step 3:'); //print_r($hook_data_return); return $hook_data_return; }
/** * Define the component data this component needs to function. */ protected function componentDataDefinition() { $component_data_definition = array('module_root_name' => array('label' => 'Module machine name', 'default' => 'my_module', 'required' => TRUE), 'module_readable_name' => array('label' => 'Module readable name', 'default' => function ($component_data) { return ucwords(str_replace('_', ' ', $component_data['module_root_name'])); }, 'required' => FALSE), 'module_short_description' => array('label' => 'Module .info file description', 'default' => 'TODO: Description of module', 'required' => FALSE), 'module_package' => array('label' => 'Module .info file package', 'default' => NULL, 'required' => FALSE), 'module_dependencies' => array('label' => 'Module dependencies (space separated list)', 'default' => NULL, 'required' => FALSE), 'module_help_text' => array('label' => 'Module help text (adds hook_help())', 'default' => NULL, 'required' => FALSE), 'module_hook_presets' => array('label' => 'Hook preset groups', 'required' => FALSE, 'format' => 'array', 'options' => function (&$property_info) { /// ARGH how to make format that is good for both UI and drush? $mb_factory = module_builder_get_factory('ModuleBuilderEnvironmentDrush'); $mb_task_handler_report_presets = $mb_factory->getTask('ReportHookPresets'); $hook_presets = $mb_task_handler_report_presets->getHookPresets(); // Stash the hook presets in the property info so the processing // callback doesn't have to repeat the work. $property_info['_presets'] = $hook_presets; $options = array(); foreach ($hook_presets as $name => $info) { $options[$name] = $info['label']; } return $options; }, 'processing' => function ($value, &$component_data, &$property_info) { // Get the presets from where the 'options' callback left them. $hook_presets = $property_info['_presets']; foreach ($value as $given_preset_name) { if (!isset($hook_presets[$given_preset_name])) { throw new \ModuleBuilderException("Undefined hook preset group {$given_preset_name}."); } // DX: check the preset is properly defined. if (!is_array($hook_presets[$given_preset_name]['hooks'])) { throw new \ModuleBuilderException("Incorrectly defined hook preset group {$given_preset_name}."); } // Add the preset hooks list to the hooks array in the component // data. $hooks = $hook_presets[$given_preset_name]['hooks']; $component_data['hooks'] = array_merge($component_data['hooks'], $hooks); drush_print_r($component_data['hooks']); } }), 'hooks' => array('label' => 'Hook implementations', 'required' => FALSE, 'format' => 'array', 'options' => function (&$property_info) { $mb_factory = module_builder_get_factory('ModuleBuilderEnvironmentDrush'); $mb_task_handler_report_hooks = $mb_factory->getTask('ReportHookData'); $hook_options = $mb_task_handler_report_hooks->listHookNamesOptions(); return $hook_options; }, 'processing' => function ($value, &$component_data, &$property_info) { $mb_factory = module_builder_get_factory('ModuleBuilderEnvironmentDrush'); $mb_task_handler_report_hooks = $mb_factory->getTask('ReportHookData'); // Get the flat list of hooks, standardized to lower case. $hook_definitions = array_change_key_case($mb_task_handler_report_hooks->getHookDeclarations()); $hooks = array(); foreach ($component_data['hooks'] as $hook_name) { // Standardize to lowercase. $hook_name = strtolower($hook_name); // By default, accept the short definition of hooks, ie 'boot' for 'hook_boot'. if (isset($hook_definitions["hook_{$hook_name}"])) { $hooks["hook_{$hook_name}"] = TRUE; } elseif (isset($hook_definitions[$hook_name])) { $hooks[$hook_name] = TRUE; } } $component_data['hooks'] = $hooks; }), 'plugins' => array('label' => 'Plugins', 'required' => FALSE, 'format' => 'array', 'options' => function (&$property_info) { $mb_factory = module_builder_get_factory('ModuleBuilderEnvironmentDrush'); $mb_task_handler_report_plugins = $mb_factory->getTask('ReportPluginData'); $options = $mb_task_handler_report_plugins->listPluginNamesOptions(); return $options; }, 'component' => 'Plugin'), 'settings_form' => array('label' => "Admin settings form", 'required' => FALSE, 'format' => 'boolean', 'component' => 'AdminSettingsForm'), 'router_items' => array('label' => "required router paths, eg 'path/foo'", 'required' => FALSE, 'format' => 'array', 'component' => 'RouterItem'), 'module_camel_case_name' => array('computed' => TRUE, 'default' => function ($component_data) { $pieces = explode('_', $component_data['module_root_name']); $pieces = array_map('ucfirst', $pieces); return implode('', $pieces); })); return $component_data_definition; }
/** * Return a file footer. */ function code_footer() { $footer = module_builder_get_factory()->environment->getSetting('module_builder_footer', ''); return $footer; }