3 use Drush\Log\LogLevel;
4 use Drupal\Component\Assertion\Handle;
5 use Drush\Psysh\DrushHelpCommand;
6 use Drush\Psysh\DrushCommand;
8 use Psy\VersionUpdater\Checker;
11 * Implements hook_drush_command().
13 function cli_drush_command() {
14 $items['core-cli'] = array(
15 'description' => 'Open an interactive shell on a Drupal site.',
17 'aliases' => array('php'),
18 'bootstrap' => DRUSH_BOOTSTRAP_MAX,
19 'topics' => array('docs-repl'),
21 'version-history' => 'Use command history based on Drupal version (Default is per site).',
24 $items['docs-repl'] = array(
25 'description' => 'repl.md',
28 'bootstrap' => DRUSH_BOOTSTRAP_NONE,
29 'callback' => 'drush_print_file',
30 'callback arguments' => array(drush_get_context('DOC_PREFIX', DRUSH_BASE_PATH) . '/docs/repl.md'),
38 function drush_cli_core_cli() {
39 $drupal_major_version = drush_drupal_major_version();
40 $configuration = new \Psy\Configuration();
42 // Set the Drush specific history file path.
43 $configuration->setHistoryFile(drush_history_path_cli());
45 // Disable checking for updates. Our dependencies are managed with Composer.
46 $configuration->setUpdateCheck(Checker::NEVER);
48 $shell = new Shell($configuration);
50 if ($drupal_major_version >= 8) {
51 // Register the assertion handler so exceptions are thrown instead of errors
52 // being triggered. This plays nicer with PsySH.
54 $shell->setScopeVariables(['container' => \Drupal::getContainer()]);
56 // Add Drupal 8 specific casters to the shell configuration.
57 $configuration->addCasters(_drush_core_cli_get_casters());
60 // Add Drush commands to the shell.
61 $commands = [new DrushHelpCommand()];
63 foreach (drush_commands_categorize(_drush_core_cli_get_commands()) as $category_data) {
64 $category_title = (string) $category_data['title'];
65 foreach ($category_data['commands'] as $command_config) {
66 $command = new DrushCommand($command_config);
67 // Set the category label on each.
68 $command->setCategory($category_title);
69 $commands[] = $command;
73 $shell->addCommands($commands);
75 // PsySH will never return control to us, but our shutdown handler will still
76 // run after the user presses ^D. Mark this command as completed to avoid a
77 // spurious error message.
78 drush_set_context('DRUSH_EXECUTION_COMPLETED', TRUE);
80 // Run the terminate event before the shell is run. Otherwise, if the shell
81 // is forking processes (the default), any child processes will close the
82 // database connection when they are killed. So when we return back to the
83 // parent process after, there is no connection. This will be called after the
84 // command in preflight still, but the subscriber instances are already
85 // created from before. Call terminate() regardless, this is a no-op for all
86 // DrupalBoot classes except DrupalBoot8.
87 if ($bootstrap = drush_get_bootstrap_object()) {
88 $bootstrap->terminate();
91 // To fix the above problem in Drupal 7, the connection can be closed manually.
92 // This will make sure a new connection is created again in child loops. So
93 // any shutdown functions will still run ok after the shell has exited.
94 if ($drupal_major_version == 7) {
95 Database::closeConnection();
102 * Returns a filtered list of Drush commands used for CLI commands.
106 function _drush_core_cli_get_commands() {
107 $commands = drush_get_commands();
108 $ignored_commands = ['help', 'drush-psysh', 'php-eval', 'core-cli', 'php'];
109 $php_keywords = _drush_core_cli_get_php_keywords();
111 foreach ($commands as $name => $config) {
112 // Ignore some commands that don't make sense inside PsySH, are PHP keywords
113 // are hidden, or are aliases.
114 if (in_array($name, $ignored_commands) || in_array($name, $php_keywords) || !empty($config['hidden']) || ($name !== $config['command'])) {
115 unset($commands[$name]);
118 // Make sure the command aliases don't contain any PHP keywords.
119 if (!empty($config['aliases'])) {
120 $commands[$name]['aliases'] = array_diff($commands[$name]['aliases'], $php_keywords);
129 * Returns a mapped array of casters for use in the shell.
131 * These are Symfony VarDumper casters.
132 * See http://symfony.com/doc/current/components/var_dumper/advanced.html#casters
133 * for more information.
136 * An array of caster callbacks keyed by class or interface.
138 function _drush_core_cli_get_casters() {
140 'Drupal\Core\Entity\ContentEntityInterface' => 'Drush\Psysh\Caster::castContentEntity',
141 'Drupal\Core\Field\FieldItemListInterface' => 'Drush\Psysh\Caster::castFieldItemList',
142 'Drupal\Core\Field\FieldItemInterface' => 'Drush\Psysh\Caster::castFieldItem',
143 'Drupal\Core\Config\Entity\ConfigEntityInterface' => 'Drush\Psysh\Caster::castConfigEntity',
144 'Drupal\Core\Config\ConfigBase' => 'Drush\Psysh\Caster::castConfig',
145 'Drupal\Component\DependencyInjection\Container' => 'Drush\Psysh\Caster::castContainer',
150 * Returns the file path for the CLI history.
152 * This can either be site specific (default) or Drupal version specific.
156 function drush_history_path_cli() {
157 $cli_directory = drush_directory_cache('cli');
159 // If only the Drupal version is being used for the history.
160 if (drush_get_option('version-history', FALSE)) {
161 $drupal_major_version = drush_drupal_major_version();
162 $file_name = "drupal-$drupal_major_version";
164 // If there is an alias, use that in the site specific name. Otherwise,
165 // use a hash of the root path.
167 if ($alias = drush_get_context('DRUSH_TARGET_SITE_ALIAS')) {
168 $site = drush_sitealias_get_record($alias);
169 $site_suffix = $site['#name'];
172 $site_suffix = md5(DRUPAL_ROOT);
175 $file_name = "drupal-site-$site_suffix";
178 $full_path = "$cli_directory/$file_name";
180 // Output the history path if verbose is enabled.
181 if (drush_get_context('DRUSH_VERBOSE')) {
182 drush_log(dt('History: @full_path', ['@full_path' => $full_path]), LogLevel::INFO);
189 * Returns a list of PHP keywords.
191 * This will act as a blacklist for command and alias names.
195 function _drush_core_cli_get_php_keywords() {