/** * 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. $directory = \ModuleBuilder\Factory::getEnvironment()->getHooksDirectory(); // 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 = \ModuleBuilder\Factory::getEnvironment()->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; }
/** * 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_task_handler_report_plugins = \ModuleBuilder\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() { // Sanity checks already done at this point; no need to catch exception. $mb_task_handler_report = \ModuleBuilder\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'); }
/** * Gather hook documentation files. * * This retrieves a list of api hook documentation files from drupal.org's * version control server. */ protected function gatherHookDocumentationFiles() { $directory = \ModuleBuilder\Factory::getEnvironment()->getHooksDirectory(); // Fetch data about the files we need to download. $hook_files = $this->getHookFileUrls($directory); //print_r($hook_files); // Retrieve each file and store it in the hooks directory, overwriting what's currently there foreach ($hook_files as $file_name => $data) { $file_contents = drupal_http_request($data['url']); // TODO: replace with call to environment output. //_module_builder_drush_print("writing $directory/$file_name", 2); file_put_contents("{$directory}/{$file_name}", $destination . $file_contents->data); } return $hook_files; }
/** * Gather hook documentation files. * * This retrieves a list of api hook documentation files from drupal.org's * version control server. */ protected function gatherHookDocumentationFiles() { $directory = \ModuleBuilder\Factory::getEnvironment()->getHooksDirectory(); // Fetch data about the files we need to download. $hook_files = $this->getHookFileUrls($directory); //print_r($hook_files); // For testing only: skip downloading, just process. /* module_builder_process_hook_data($hook_files); return $hook_files; */ // Retrieve each file and store it in the hooks directory, overwriting what's currently there foreach ($hook_files as $file_name => $data) { $file_contents = drupal_http_request($data['url']); // TODO: replace with call to environment output. //_module_builder_drush_print("writing $directory/$file_name", 2); file_put_contents("{$directory}/{$file_name}", $destination . $file_contents->data); } // inform that hook documentation has been downloaded. drupal_set_message(t("Module Builder has just downloaded hook documentation to your %dir directory from CVS. This documentation contains detailed descriptions and usage examples of each of Drupal's hooks. Please view the files for more information, or view them online at the <a href=\"!api\">Drupal API documentation</a> site.", array('%dir' => 'files/' . variable_get('module_builder_hooks_directory', 'hooks'), '!api' => url('http://api.drupal.org/')))); return $hook_files; }
/** * Return the main body of the file code. */ function code_body() { $component_data = $this->getRootComponentData(); // Sanity checks already done at this point; no need to catch exception. $mb_task_handler_analyze = \ModuleBuilder\Factory::getTask('AnalyzeModule'); $hooks = $mb_task_handler_analyze->getInventedHooks($component_data['root_name']); $module_root_name = $component_data['root_name']; $module_root_name_title_case = ucfirst($component_data['root_name']); $module_readable_name = $component_data['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; }
/** * Define the component data this component needs to function. */ protected function componentDataDefinition() { $component_data_definition = array('root_name' => array('label' => 'Module machine name', 'default' => 'my_module', 'required' => TRUE), 'readable_name' => array('label' => 'Module readable name', 'default' => function ($component_data) { return ucwords(str_replace('_', ' ', $component_data['root_name'])); }, 'required' => FALSE), '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', 'description' => 'The machine names of the modules this module should have as dependencies.', 'default' => array(), 'required' => FALSE, 'format' => 'array'), 'module_help_text' => array('label' => 'Module help text', 'description' => 'The text to show on the site help page. This automatically adds hook_help().', 'default' => NULL, 'required' => FALSE), 'settings_form' => array('label' => "Admin settings form", 'required' => FALSE, 'format' => 'boolean', 'component' => 'AdminSettingsForm'), 'services' => array('label' => "Services", 'description' => 'A list of machine names of services for this module to provide.', 'required' => FALSE, 'format' => 'array', 'component' => 'Service'), 'permissions' => array('label' => "Permissions", 'description' => 'A list of machine names of permissions for this module to provide.', 'required' => FALSE, 'format' => 'array', 'component' => 'Permissions'), '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_task_handler_report_presets = \ModuleBuilder\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 \ModuleBuilder\Exception\InvalidInputException(strtr("Undefined hook preset group !name", array('!name' => htmlspecialchars($given_preset_name, ENT_QUOTES, 'UTF-8')))); } // DX: check the preset is properly defined. if (!is_array($hook_presets[$given_preset_name]['hooks'])) { throw new \ModuleBuilder\Exception\InvalidInputException(strtr("Incorrectly defined hook preset group !name", array('!name' => htmlspecialchars($given_preset_name, ENT_QUOTES, 'UTF-8')))); } // 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_task_handler_report_hooks = \ModuleBuilder\Factory::getTask('ReportHookData'); $hook_options = $mb_task_handler_report_hooks->listHookNamesOptions(); return $hook_options; }, 'options_structured' => function (&$property_info) { $mb_task_handler_report_hooks = \ModuleBuilder\Factory::getTask('ReportHookData'); $hook_options = $mb_task_handler_report_hooks->listHookOptionsStructured(); return $hook_options; }, 'processing' => function ($value, &$component_data, &$property_info) { $mb_task_handler_report_hooks = \ModuleBuilder\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_task_handler_report_plugins = \ModuleBuilder\Factory::getTask('ReportPluginData'); $options = $mb_task_handler_report_plugins->listPluginNamesOptions(); return $options; }, 'component' => 'Plugin'), 'router_items' => array('label' => "required router paths, eg 'path/foo'", 'required' => FALSE, 'format' => 'array', 'component' => 'RouterItem'), 'camel_case_name' => array('computed' => TRUE, 'default' => function ($component_data) { $pieces = explode('_', $component_data['root_name']); $pieces = array_map('ucfirst', $pieces); return implode('', $pieces); })); return $component_data_definition; }
/** * Get hook information declared by Module Builder. * * This invokes our own hook, hook_module_builder_info(), as well as adding * hardcoded info such as module file locations, which can't be deduced from * either api.php files or hook_hook_info(). */ protected function getHookInfo() { // Get data by invoking our hook. $data = \ModuleBuilder\Factory::getEnvironment()->invokeInfoHook(); // Add our data. $result = $this->getAdditionalHookInfo(); $data = array_merge($data, $result); return $data; }
/** * Return a file footer. */ function code_footer() { $footer = \ModuleBuilder\Factory::getEnvironment()->getSetting('footer', ''); return $footer; }
/** * 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 the Collect task 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) { // Sanity checks already done at this point; no need to catch exception. $mb_task_handler_report = \ModuleBuilder\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. // 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 = \ModuleBuilder\Factory::getEnvironment()->getCoreMajorVersion(); $template_base_path_module = \ModuleBuilder\Factory::getEnvironment()->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 = $this->parseTemplate($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; }