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
26 public function docs()
28 self::printFile(DRUSH_BASE_PATH. '/docs/repl.md');
33 * @description Open an interactive shell on a Drupal site.
34 * @aliases php,core:cli,core-cli
35 * @option $version-history Use command history based on Drupal version
36 * (Default is per site).
40 public function cli(array $options = ['version-history' => false])
42 $configuration = new Configuration();
44 // Set the Drush specific history file path.
45 $configuration->setHistoryFile($this->historyPath($options));
47 // Disable checking for updates. Our dependencies are managed with Composer.
48 $configuration->setUpdateCheck(Checker::NEVER);
50 $shell = new Shell($configuration);
53 // Register the assertion handler so exceptions are thrown instead of errors
54 // being triggered. This plays nicer with PsySH.
56 $shell->setScopeVariables(['container' => \Drupal::getContainer()]);
58 // Add Drupal 8 specific casters to the shell configuration.
59 $configuration->addCasters($this->getCasters());
61 // Add Drush commands to the shell.
62 $shell->addCommands([new DrushHelpCommand()]);
63 $shell->addCommands($this->getDrushCommands());
65 // PsySH will never return control to us, but our shutdown handler will still
66 // run after the user presses ^D. Mark this command as completed to avoid a
67 // spurious error message.
68 drush_set_context('DRUSH_EXECUTION_COMPLETED', true);
70 // Run the terminate event before the shell is run. Otherwise, if the shell
71 // is forking processes (the default), any child processes will close the
72 // database connection when they are killed. So when we return back to the
73 // parent process after, there is no connection. This will be called after the
74 // command in preflight still, but the subscriber instances are already
75 // created from before. Call terminate() regardless, this is a no-op for all
76 // DrupalBoot classes except DrupalBoot8.
77 if ($bootstrap = Drush::bootstrap()) {
78 $bootstrap->terminate();
85 * Returns a filtered list of Drush commands used for CLI commands.
89 protected function getDrushCommands()
91 $application = Drush::getApplication();
92 $commands = $application->all();
106 $php_keywords = $this->getPhpKeywords();
108 /** @var \Consolidation\AnnotatedCommand\AnnotatedCommand $command */
109 foreach ($commands as $name => $command) {
110 $definition = $command->getDefinition();
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) || ($name !== $command->getName())) {
115 unset($commands[$name]);
117 $aliases = $command->getAliases();
118 // Make sure the command aliases don't contain any PHP keywords.
119 if (!empty($aliases)) {
120 $command->setAliases(array_diff($aliases, $php_keywords));
125 return array_map(function ($command) {
126 return new DrushCommand($command);
131 * Returns a mapped array of casters for use in the shell.
133 * These are Symfony VarDumper casters.
134 * See http://symfony.com/doc/current/components/var_dumper/advanced.html#casters
135 * for more information.
138 * An array of caster callbacks keyed by class or interface.
140 protected function getCasters()
143 'Drupal\Core\Entity\ContentEntityInterface' => 'Drush\Psysh\Caster::castContentEntity',
144 'Drupal\Core\Field\FieldItemListInterface' => 'Drush\Psysh\Caster::castFieldItemList',
145 'Drupal\Core\Field\FieldItemInterface' => 'Drush\Psysh\Caster::castFieldItem',
146 'Drupal\Core\Config\Entity\ConfigEntityInterface' => 'Drush\Psysh\Caster::castConfigEntity',
147 'Drupal\Core\Config\ConfigBase' => 'Drush\Psysh\Caster::castConfig',
148 'Drupal\Component\DependencyInjection\Container' => 'Drush\Psysh\Caster::castContainer',
149 'Drupal\Component\Render\MarkupInterface' => 'Drush\Psysh\Caster::castMarkup',
154 * Returns the file path for the CLI history.
156 * This can either be site specific (default) or Drupal version specific.
158 * @param array $options
162 protected function historyPath(array $options)
164 $cli_directory = Path::join($this->getConfig()->cache(), 'cli');
165 $drupal_major_version = Drush::getMajorVersion();
167 // If there is no drupal version (and thus no root). Just use the current
169 // @todo Could use a global file within drush?
170 if (!$drupal_major_version) {
171 $file_name = 'global-' . md5($this->getConfig()->cwd());
172 } // If only the Drupal version is being used for the history.
173 else if ($options['version-history']) {
174 $file_name = "drupal-$drupal_major_version";
175 } // If there is an alias, use that in the site specific name. Otherwise,
176 // use a hash of the root path.
178 $aliasRecord = Drush::aliasManager()->getSelf();
180 if ($aliasRecord->name()) {
181 $site_suffix = ltrim($aliasRecord->name(), '@');
183 $drupal_root = Drush::bootstrapManager()->getRoot();
184 $site_suffix = md5($drupal_root);
187 $file_name = "drupal-site-$site_suffix";
190 $full_path = "$cli_directory/$file_name";
192 $this->logger()->info(dt('History: @full_path', ['@full_path' => $full_path]));
198 * Returns a list of PHP keywords.
200 * This will act as a blacklist for command and alias names.
204 protected function getPhpKeywords()