5 * The drush engines API implementation and helpers.
8 use Drush\Log\LogLevel;
11 * Obtain all engine types info and normalize with defaults.
13 * @see hook_drush_engine_type_info().
15 function drush_get_engine_types_info() {
16 $info = drush_command_invoke_all('drush_engine_type_info');
17 foreach ($info as $type => $data) {
18 $info[$type] += array(
23 'sub-options' => array(),
24 'config-aliases' => array(),
25 'add-options-to-command' => FALSE,
26 'combine-help' => FALSE,
34 * Return a structured array of engines of a specific type.
36 * Engines are pluggable subsystems. Each engine of a specific type will
37 * implement the same set of API functions and perform the same high-level
38 * task using a different backend or approach.
40 * This function/hook is useful when you have a selection of several mutually
41 * exclusive options to present to a user to select from.
43 * Other commands are able to extend this list and provide their own engines.
44 * The hook can return useful information to help users decide which engine
45 * they need, such as description or list of available engine options.
47 * The engine path element will automatically default to a subdirectory (within
48 * the directory of the commandfile that implemented the hook) with the name of
49 * the type of engine - e.g. an engine "wget" of type "handler" provided by
50 * the "pm" commandfile would automatically be found if the file
51 * "pm/handler/wget.inc" exists and a specific path is not provided.
57 * A structured array of engines.
59 function drush_get_engines($engine_type) {
60 $info = drush_get_engine_types_info();
61 if (!isset($info[$engine_type])) {
62 return drush_set_error('DRUSH_UNKNOWN_ENGINE_TYPE', dt('Unknown engine type !engine_type', array('!engine_type' => $engine_type)));
66 'info' => $info[$engine_type],
69 $list = drush_commandfile_list();
70 $hook = 'drush_engine_' . str_replace('-', '_', $engine_type);
71 foreach ($list as $commandfile => $path) {
72 if (drush_command_hook($commandfile, $hook)) {
73 $function = $commandfile . '_' . $hook;
74 $result = $function();
75 foreach ($result as $engine_name => $engine) {
78 'commandfile' => $commandfile,
80 'sub-options' => array(),
81 'drupal dependencies' => array(),
84 // Legacy engines live in a subdirectory
85 // of the commandfile that declared them.
86 $engine_path = sprintf("%s/%s", dirname($path), $engine_type);
87 if (file_exists($engine_path)) {
88 $engine['path'] = $engine_path;
90 // Build engine class name, in case the engine doesn't provide it.
91 // The class name is based on the engine type and name, converted
92 // from snake_case to CamelCase.
93 // For example for type 'package_handler' and engine 'git_drupalorg'
94 // the class is \Drush\PackageHandler\GitDrupalorg
95 elseif (!isset($engine['class'])) {
98 $parts[] = str_replace(' ', '', ucwords(strtr($engine_type, '_', ' ')));
99 $parts[] = str_replace(' ', '', ucwords(strtr($engine_name, '_', ' ')));
100 $engine['class'] = implode('\\', $parts);
103 $engines['engines'][$engine_name] = $engine;
112 * Take a look at all of the available engines,
113 * and create topic commands for each one that
116 function drush_get_engine_topics() {
118 $info = drush_get_engine_types_info();
119 foreach ($info as $engine => $data) {
120 if (array_key_exists('topic', $data)) {
121 $items[$data['topic']] = array(
122 'description' => $data['description'],
125 'bootstrap' => DRUSH_BOOTSTRAP_DRUSH,
126 'callback' => 'drush_engine_topic_command',
127 'callback arguments' => array($engine),
135 * Include, instantiate and validate command engines.
137 * @return FALSE if a engine doesn't validate.
139 function drush_load_command_engines($command) {
141 foreach ($command['engines'] as $engine_type => $config) {
142 $result = drush_load_command_engine($command, $engine_type);
143 // Stop loading engines if any of them fails.
144 if ($result === FALSE) {
152 * Returns engine config supplied in the command definition.
154 function drush_get_command_engine_config($command, $engine_type, $metadata = array()) {
155 if (isset($command['engines'][$engine_type])) {
156 $metadata = array_merge($metadata, $command['engines'][$engine_type]);
162 * Selects and loads an engine implementing the given type.
164 * Loaded engines are stored as a context.
166 function drush_load_command_engine($command, $engine_type, $metadata = array()) {
167 drush_log(dt("Loading !engine engine.", array('!engine' => $engine_type), LogLevel::BOOTSTRAP));
169 $config = drush_get_command_engine_config($command, $engine_type, $metadata);
170 $engine_info = drush_get_engines($engine_type);
171 $engine = drush_select_engine($config, $engine_info);
172 $version = drush_drupal_major_version();
174 $context = $engine_type . '_engine_' . $engine . '_' . $version;
175 $instance = drush_get_context($context, FALSE);
176 if ($instance != FALSE) {
177 drush_set_engine($engine_type, $instance);
180 $instance = drush_load_engine($engine_type, $engine, $config);
181 if ($instance == FALSE) {
184 drush_set_context($context, $instance);
190 * Add command structure info from each engine type back into the command.
192 function drush_merge_engine_data(&$command) {
193 // First remap engine data from the shortcut location
194 // ($command['engine_type']) to the standard location
195 // ($command['engines']['engine_type'])
196 $info = drush_get_engine_types_info();
197 foreach ($info as $engine_type => $info) {
198 if (isset($command[$engine_type])) {
199 $config = $command[$engine_type];
200 foreach ($info['config-aliases'] as $engine_option_alias_name => $engine_option_standard_name) {
201 if (array_key_exists($engine_option_alias_name, $config)) {
202 $config[$engine_option_standard_name] = $config[$engine_option_alias_name];
203 unset($config[$engine_option_alias_name]);
206 // Convert single string values of 'require-engine-capability' to an array.
207 if (isset($config['require-engine-capability']) && is_string($config['require-engine-capability'])) {
208 $config['require-engine-capability'] = array($config['require-engine-capability']);
210 $command['engines'][$engine_type] = $config;
214 foreach ($command['engines'] as $engine_type => $config) {
215 // Normalize engines structure.
216 if (!is_array($config)) {
217 unset($command['engines'][$engine_type]);
218 $command['engines'][$config] = array();
219 $engine_type = $config;
222 // Get all implementations for this engine type.
223 $engine_info = drush_get_engines($engine_type);
224 if ($engine_info === FALSE) {
228 // Complete command-declared engine type with default info.
229 $command['engines'][$engine_type] += $engine_info['info'];
230 $config = $command['engines'][$engine_type];
232 $engine_data = array();
234 // If there's a single implementation for this engine type, it will be
235 // loaded by default, and makes no sense to provide a command line option
236 // to select the only flavor (ie. --release_info=updatexml), so we won't
237 // add an option in this case.
238 // Additionally, depending on the command, it may be convenient to extend
239 // the command with the engine options.
240 if (count($engine_info['engines']) == 1) {
241 if ($config['add-options-to-command'] !== FALSE) {
242 // Add options and suboptions of the engine type and
243 // the sole implementation.
244 $engine = key($engine_info['engines']);
245 $data = $engine_info['engines'][$engine];
246 $engine_data += array(
247 'options' => $config['options'] + $data['options'],
248 'sub-options' => $config['sub-options'] + $data['sub-options'],
252 // Otherwise, provide a command option to choose between engines and add
253 // the engine options and sub-options.
255 // Add engine type global options and suboptions.
256 $engine_data += array(
257 'options' => $config['options'],
258 'sub-options' => $config['sub-options'],
261 // If the 'combine-help' flag is set in the engine config,
262 // then we will combine all of the help items into the help
263 // text for $config['option'].
264 $combine_help = $config['combine-help'];
265 $combine_help_data = array();
267 // Process engines in order. First the default engine, the rest alphabetically.
268 $default = drush_select_engine($config, $engine_info);
269 $engines = array_keys($engine_info['engines']);
271 array_unshift($engines, $default);
272 $engines = array_unique($engines);
273 foreach ($engines as $engine) {
274 $data = $engine_info['engines'][$engine];
275 // Check to see if the command requires any particular
276 // capabilities. If no capabilities are required, then
277 // all engines are acceptable.
278 $engine_is_usable = TRUE;
279 if (array_key_exists('require-engine-capability', $config)) {
280 // See if the engine declares that it provides any
281 // capabilities. If no capabilities are listed, then
282 // it is assumed that the engine can satisfy all requirements.
283 if (array_key_exists('engine-capabilities', $data)) {
284 $engine_is_usable = FALSE;
285 // If 'require-engine-capability' is TRUE instead of an array,
286 // then only engines that are universal (do not declare any
287 // particular capabilities) are usable.
288 if (is_array($config['require-engine-capability'])) {
289 foreach ($config['require-engine-capability'] as $required) {
290 // We need an engine that provides any one of the requirements.
291 if (in_array($required, $data['engine-capabilities'])) {
292 $engine_is_usable = TRUE;
298 if ($engine_is_usable) {
299 $command['engines'][$engine_type]['usable'][] = $engine;
300 if (!isset($data['hidden'])) {
301 $option = $config['option'] . '=' . $engine;
302 $engine_data['options'][$option]['description'] = array_key_exists('description', $data) ? $data['description'] : NULL;
304 $engine_data['options'][$option]['hidden'] = TRUE;
305 if (drush_get_context('DRUSH_VERBOSE') || ($default == $engine) || !isset($data['verbose-only'])) {
306 $combine_help_data[$engine] = $engine . ': ' . $data['description'];
309 if (isset($data['options'])) {
310 $engine_data['sub-options'][$option] = $data['options'];
312 if (isset($data['sub-options'])) {
313 $engine_data['sub-options'] += $data['sub-options'];
318 if (!empty($combine_help_data)) {
319 $engine_selection_option = $config['option'];
320 if (!is_array($engine_data['options'][$engine_selection_option])) {
321 $engine_data['options'][$engine_selection_option] = array('description' => $config['options'][$engine_selection_option]);
323 if (drush_get_context('DRUSH_VERBOSE')) {
324 $engine_data['options'][$engine_selection_option]['description'] .= "\n" . dt("All available values are:") . "\n - " . implode("\n - ", $combine_help_data) . "\n";
327 $engine_data['options'][$engine_selection_option]['description'] .= " " . dt("Available: ") . implode(', ', array_keys($combine_help_data)) . ". ";
329 $engine_data['options'][$engine_selection_option]['description'] .= dt("Default is !default.", array('!default' => $default));
332 // If the help options are not combined, then extend the
333 // default engine description.
334 $desc = $engine_info['engines'][$default]['description'];
335 $engine_info['engines'][$default]['description'] = dt('Default !type engine.', array('!type' => $engine_type)) . ' ' . $desc;
338 // This was simply array_merge_recursive($command, $engine_data), but this
339 // function has an undesirable behavior when merging primative types.
340 // If there is a 'key' => 'value' in $command, and the same 'key' => 'value'
341 // exists in $engine data, then the result will be 'key' => array('value', 'value');.
342 // This is NOT what we want, so we provide our own 'overlay' function instead.
343 $command = _drush_array_overlay_recursive($command, $engine_data);
347 // Works like array_merge_recursive(), but does not convert primative
348 // types into arrays. Ever.
349 function _drush_array_overlay_recursive($a, $b) {
350 foreach ($b as $key => $value) {
351 if (!isset($a[$key]) || !is_array($a[$key])) {
355 $a[$key] = _drush_array_overlay_recursive($a[$key], $b[$key]);
362 * Implementation of command hook for docs-output-formats
364 function drush_engine_topic_command($engine) {
365 $engine_instances = drush_get_engines($engine);
366 $option = $engine_instances['info']['option'];
368 if (isset($engine_instances['info']['topic-file'])) {
369 // To do: put this file next to the commandfile that defines the
370 // engine type, not in the core docs directory.
371 $docs_dir = drush_get_context('DOC_PREFIX', DRUSH_BASE_PATH);
372 $path = $engine_instances['info']['topic-file'];
373 $docs_file = (drush_is_absolute_path($path) ? '' : $docs_dir . '/') . $path;
374 $doc_text = drush_html_to_text(file_get_contents($docs_file));
376 elseif (isset($engine_instances['info']['topic-text'])) {
377 $doc_text = $engine_instances['info']['topic-text'];
380 return drush_set_error('DRUSH_BAD_ENGINE_TOPIC', dt("The engine !engine did not define its topic command correctly.", array('!engine' => $engine)));
383 // Look at each instance of the engine; if it has an html
384 // file in the the 'topics' folder named after itself, then
385 // include the file contents in the engine topic text.
386 $instances = $engine_instances['engines'];
388 foreach ($instances as $instance => $config) {
389 if (isset($config['description'])) {
390 $doc_text .= "\n\n::: --$option=$instance :::\n" . $config['description'];
391 $path = $config['path'] . '/topics/' . $instance . '.html';
392 if (file_exists($path)) {
393 $doc_text .= "\n" . drush_html_to_text(file_get_contents($path));
395 $additional_topic_text = drush_command_invoke_all('drush_engine_topic_additional_text', $engine, $instance, $config);
396 if (!empty($additional_topic_text)) {
397 $doc_text .= "\n\n" . implode("\n\n", $additional_topic_text);
402 // Write the topic text to a file so that is can be paged
403 $file = drush_save_data_to_temp_file($doc_text);
404 drush_print_file($file);
408 * Selects an engine between the available ones.
412 * - preferred engine, if available.
413 * - user supplied engine via cli.
414 * - default engine from engine type / command declaration.
415 * - the first engine available.
417 * @param array $config
418 * Engine type configuration. My be overridden in command declaration.
419 * @param array $engine_info
420 * Engine type declaration.
421 * @param string $default
427 function drush_select_engine($config, $engine_info, $preferred = NULL) {
428 $engines = array_keys($engine_info['engines']);
430 if (in_array($preferred, $engines)) {
434 if (!empty($config['option'])) {
435 $engine = drush_get_option($config['option'], FALSE);
436 if ($engine && in_array($engine, $engines)) {
441 if (isset($config['default']) && in_array($config['default'], $engines)) {
442 return $config['default'];
445 return current($engines);
449 * Loads and validate an engine of the given type.
451 * @param string $type
453 * @param string $engine
455 * @param array $config
456 * Engine configuration. Tipically it comes from a command declaration.
459 * TRUE or instanced object of available class on success. FALSE on fail.
461 function drush_load_engine($type, $engine, $config = array()) {
462 $engine_info = drush_get_engines($type);
463 $engine = drush_select_engine($config, $engine_info, $engine);
464 $config['engine-info'] = $engine_info['engines'][$engine];
466 // Check engine dependency on drupal modules before include.
467 $dependencies = $config['engine-info']['drupal dependencies'];
468 foreach ($dependencies as $dependency) {
469 if (!drush_module_exists($dependency)) {
470 return drush_set_error('DRUSH_ENGINE_DEPENDENCY_ERROR', dt('!engine_type: !engine engine needs the following modules installed/enabled to run: !dependencies.', array('!engine_type' => $type, '!engine' => $engine, '!dependencies' => implode(', ', $dependencies))));
474 $result = drush_include_engine($type, $engine, $config);
475 if (is_object($result)) {
476 $valid = method_exists($result, 'validate') ? $result->validate() : TRUE;
478 drush_set_engine($type, $result);
482 $function = strtr($type, '-', '_') . '_validate';
483 $valid = function_exists($function) ? call_user_func($function) : TRUE;
492 * Include the engine code for a specific named engine of a certain type.
494 * If the engine type has implemented hook_drush_engine_$type the path to the
495 * engine specified in the array will be used.
497 * If a class named in the form drush_$type_$engine exists, it will return an
498 * instance of the class.
500 * @param string $type
501 * The type of engine.
502 * @param string $engine
503 * The name for the engine to include.
504 * @param array $config
505 * Parameters for the engine class constructor.
508 * TRUE or instanced object of available class on success. FALSE on fail.
510 function drush_include_engine($type, $engine, $config = NULL) {
511 $engine_info = drush_get_engines($type);
513 // Pick the engine name that actually implements the requested engine.
514 $engine = isset($engine_info['engines'][$engine]['implemented-by']) ? $engine_info['engines'][$engine]['implemented-by'] : $engine;
516 // Legacy engines live in a subdirectory of the commandfile
517 // that declares them. We need to explicitly include the file.
518 if (isset($engine_info['engines'][$engine]['path'])) {
519 $path = $engine_info['engines'][$engine]['path'];
520 if (!drush_include($path, $engine)) {
521 return drush_set_error('DRUSH_ENGINE_INCLUDE_FAILED', dt('Unable to include the !type engine !engine from !path.' , array('!path' => $path, '!type' => $type, '!engine' => $engine)));
523 // Legacy engines may be implemented in a magic class name.
524 $class = 'drush_' . $type . '_' . str_replace('-', '_', $engine);
525 if (class_exists($class)) {
526 $instance = new $class($config);
527 $instance->engine_type = $type;
528 $instance->engine = $engine;
535 return drush_get_class($engine_info['engines'][$engine]['class'], array($type, $engine, $config));
539 * Return the engine of the specified type that was loaded by the Drush command.
541 function drush_get_engine($type) {
542 return drush_get_context($type . '_engine', FALSE);
546 * Called by the Drush command (@see _drush_load_command_engines())
547 * to cache the active engine instance.
549 function drush_set_engine($type, $instance) {
550 drush_set_context($type . '_engine', $instance);
554 * Add engine topics to the command topics, if any.
556 function drush_engine_add_help_topics(&$command) {
557 $engine_types = drush_get_engine_types_info();
558 foreach ($command['engines'] as $engine_type => $config) {
559 $info = $engine_types[$engine_type];
560 if (isset($info['topics'])) {
561 $command['topics'] = array_merge($command['topics'], $info['topics']);
563 if (isset($info['topic'])) {
564 $command['topics'][] = $info['topic'];