3 namespace Drush\Drupal\Commands\core;
5 use Drush\Commands\DrushCommands;
7 use Drush\Log\LogLevel;
8 use Drush\Psysh\DrushCommand;
9 use Drush\Psysh\DrushHelpCommand;
10 use Drupal\Component\Assertion\Handle;
11 use Drush\Psysh\Shell;
12 use Psy\Configuration;
13 use Psy\VersionUpdater\Checker;
14 use Webmozart\PathUtil\Path;
16 class CliCommands extends DrushCommands
27 public function docs()
29 self::printFile(DRUSH_BASE_PATH. '/docs/repl.md');
34 * @description Open an interactive shell on a Drupal site.
35 * @aliases php,core:cli,core-cli
36 * @option $version-history Use command history based on Drupal version
37 * (Default is per site).
38 * @option $cwd Changes the working directory of the shell
39 * (Default is the project root directory)
43 public function cli(array $options = ['version-history' => false, 'cwd' => null])
45 $configuration = new Configuration();
47 // Set the Drush specific history file path.
48 $configuration->setHistoryFile($this->historyPath($options));
50 // Disable checking for updates. Our dependencies are managed with Composer.
51 $configuration->setUpdateCheck(Checker::NEVER);
53 $shell = new Shell($configuration);
56 // Register the assertion handler so exceptions are thrown instead of errors
57 // being triggered. This plays nicer with PsySH.
59 $shell->setScopeVariables(['container' => \Drupal::getContainer()]);
61 // Add Drupal 8 specific casters to the shell configuration.
62 $configuration->addCasters($this->getCasters());
64 // Add Drush commands to the shell.
65 $shell->addCommands([new DrushHelpCommand()]);
66 $shell->addCommands($this->getDrushCommands());
68 // PsySH will never return control to us, but our shutdown handler will still
69 // run after the user presses ^D. Mark this command as completed to avoid a
70 // spurious error message.
71 drush_set_context('DRUSH_EXECUTION_COMPLETED', true);
73 // Run the terminate event before the shell is run. Otherwise, if the shell
74 // is forking processes (the default), any child processes will close the
75 // database connection when they are killed. So when we return back to the
76 // parent process after, there is no connection. This will be called after the
77 // command in preflight still, but the subscriber instances are already
78 // created from before. Call terminate() regardless, this is a no-op for all
79 // DrupalBoot classes except DrupalBoot8.
80 if ($bootstrap = Drush::bootstrap()) {
81 $bootstrap->terminate();
84 // If the cwd option is passed, lets change the current working directory to wherever
85 // the user wants to go before we lift psysh.
86 if ($options['cwd']) {
87 chdir($options['cwd']);
94 * Returns a filtered list of Drush commands used for CLI commands.
98 protected function getDrushCommands()
100 $application = Drush::getApplication();
101 $commands = $application->all();
103 $ignored_commands = [
114 $php_keywords = $this->getPhpKeywords();
116 /** @var \Consolidation\AnnotatedCommand\AnnotatedCommand $command */
117 foreach ($commands as $name => $command) {
118 $definition = $command->getDefinition();
120 // Ignore some commands that don't make sense inside PsySH, are PHP keywords
121 // are hidden, or are aliases.
122 if (in_array($name, $ignored_commands) || in_array($name, $php_keywords) || ($name !== $command->getName())) {
123 unset($commands[$name]);
125 $aliases = $command->getAliases();
126 // Make sure the command aliases don't contain any PHP keywords.
127 if (!empty($aliases)) {
128 $command->setAliases(array_diff($aliases, $php_keywords));
133 return array_map(function ($command) {
134 return new DrushCommand($command);
139 * Returns a mapped array of casters for use in the shell.
141 * These are Symfony VarDumper casters.
142 * See http://symfony.com/doc/current/components/var_dumper/advanced.html#casters
143 * for more information.
146 * An array of caster callbacks keyed by class or interface.
148 protected function getCasters()
151 'Drupal\Core\Entity\ContentEntityInterface' => 'Drush\Psysh\Caster::castContentEntity',
152 'Drupal\Core\Field\FieldItemListInterface' => 'Drush\Psysh\Caster::castFieldItemList',
153 'Drupal\Core\Field\FieldItemInterface' => 'Drush\Psysh\Caster::castFieldItem',
154 'Drupal\Core\Config\Entity\ConfigEntityInterface' => 'Drush\Psysh\Caster::castConfigEntity',
155 'Drupal\Core\Config\ConfigBase' => 'Drush\Psysh\Caster::castConfig',
156 'Drupal\Component\DependencyInjection\Container' => 'Drush\Psysh\Caster::castContainer',
157 'Drupal\Component\Render\MarkupInterface' => 'Drush\Psysh\Caster::castMarkup',
162 * Returns the file path for the CLI history.
164 * This can either be site specific (default) or Drupal version specific.
166 * @param array $options
170 protected function historyPath(array $options)
172 $cli_directory = Path::join($this->getConfig()->cache(), 'cli');
173 $drupal_major_version = Drush::getMajorVersion();
175 // If there is no drupal version (and thus no root). Just use the current
177 // @todo Could use a global file within drush?
178 if (!$drupal_major_version) {
179 $file_name = 'global-' . md5($this->getConfig()->cwd());
180 } // If only the Drupal version is being used for the history.
181 else if ($options['version-history']) {
182 $file_name = "drupal-$drupal_major_version";
183 } // If there is an alias, use that in the site specific name. Otherwise,
184 // use a hash of the root path.
186 $aliasRecord = Drush::aliasManager()->getSelf();
188 if ($aliasRecord->name()) {
189 $site_suffix = ltrim($aliasRecord->name(), '@');
191 $drupal_root = Drush::bootstrapManager()->getRoot();
192 $site_suffix = md5($drupal_root);
195 $file_name = "drupal-site-$site_suffix";
198 $full_path = "$cli_directory/$file_name";
200 $this->logger()->info(dt('History: @full_path', ['@full_path' => $full_path]));
206 * Returns a list of PHP keywords.
208 * This will act as a blacklist for command and alias names.
212 protected function getPhpKeywords()