diff --git a/src-symfony-compatibility/v6/Style/DrushStyle.php b/src-symfony-compatibility/v6/Style/DrushStyle.php new file mode 100644 index 0000000000..aade71c683 --- /dev/null +++ b/src-symfony-compatibility/v6/Style/DrushStyle.php @@ -0,0 +1,68 @@ +comment($question . ': yes.'); + return true; + } elseif (Drush::negative()) { + // Automatically cancel confirmations if the --no argument was supplied. + $this->warning($question . ': no.'); + return false; + } + return parent::confirm($question, $default); + } + + public function choice(string $question, array $choices, mixed $default = null): mixed + { + // Display the choices without their keys. + $choices_values = array_values($choices); + $return = parent::choice($question, $choices_values, $default); + + return array_search($return, $choices); + } + + public function warning(string|array $message) + { + $this->block($message, 'WARNING', 'fg=black;bg=yellow', ' ! ', true); + } + + public function note(string|array $message) + { + $this->block($message, 'NOTE', 'fg=black;bg=yellow', ' ! '); + } + + public function caution(string|array $message) + { + $this->block($message, 'CAUTION', 'fg=black;bg=yellow', ' ! ', true); + } + + /** + * @return mixed + */ + public function askRequired($question) + { + $question = new Question($question); + $question->setValidator(function (?string $value) { + // FALSE is not considered as empty value because question helper use + // it as negative answer on confirmation questions. + if ($value === null || $value === '') { + throw new \UnexpectedValueException('This value is required.'); + } + + return $value; + }); + + return $this->askQuestion($question); + } +} diff --git a/src-symfony-compatibility/v6/Symfony/BootstrapCompilerPass.php b/src-symfony-compatibility/v6/Symfony/BootstrapCompilerPass.php new file mode 100644 index 0000000000..20765649d4 --- /dev/null +++ b/src-symfony-compatibility/v6/Symfony/BootstrapCompilerPass.php @@ -0,0 +1,31 @@ +has('bootstrap.manager')) { + return; + } + + $definition = $container->findDefinition( + 'bootstrap.manager' + ); + + $taggedServices = $container->findTaggedServiceIds( + 'bootstrap.boot' + ); + foreach ($taggedServices as $id => $tags) { + $definition->addMethodCall( + 'add', + [new Reference($id)] + ); + } + } +} diff --git a/src-symfony-compatibility/v6/Symfony/BufferedConsoleOutput.php b/src-symfony-compatibility/v6/Symfony/BufferedConsoleOutput.php new file mode 100644 index 0000000000..96b8af58ab --- /dev/null +++ b/src-symfony-compatibility/v6/Symfony/BufferedConsoleOutput.php @@ -0,0 +1,51 @@ +stderr = new BufferedOutput($this->getVerbosity(), $this->isDecorated(), $this->getFormatter()); + } + + /** + * {@inheritdoc} + */ + public function getErrorOutput(): BufferedOutput + { + return $this->stderr; + } + + /** + * {@inheritdoc} + */ + public function setErrorOutput(OutputInterface $error): void + { + $this->stderr = $error; + } + + public function section(): ConsoleSectionOutput + { + // @todo + } +} diff --git a/src-symfony-compatibility/v6/Symfony/DrushStyleInjector.php b/src-symfony-compatibility/v6/Symfony/DrushStyleInjector.php new file mode 100644 index 0000000000..2f000eef8c --- /dev/null +++ b/src-symfony-compatibility/v6/Symfony/DrushStyleInjector.php @@ -0,0 +1,16 @@ +input(), $commandData->output()); + } +} diff --git a/src-symfony-compatibility/v6/Symfony/IndiscriminateInputDefinition.php b/src-symfony-compatibility/v6/Symfony/IndiscriminateInputDefinition.php new file mode 100644 index 0000000000..0bfc534239 --- /dev/null +++ b/src-symfony-compatibility/v6/Symfony/IndiscriminateInputDefinition.php @@ -0,0 +1,47 @@ +tokens = $argv; + // strip the application name + array_shift($this->tokens); + + parent::__construct($argv, $definition); + } + + /** + * {@inheritdoc} + */ + public function getOption($name) + { + if (array_key_exists($name, $this->options)) { + return $this->options[$name]; + } + if ($this->definition->hasOption($name)) { + return $this->definition->getOption($name)->getDefault(); + } + return false; + } + + protected function setTokens(array $tokens): void + { + $this->tokens = $tokens; + } + + /** + * {@inheritdoc} + */ + protected function parse(): void + { + $parseOptions = true; + $this->parsed = $this->tokens; + while (null !== $token = array_shift($this->parsed)) { + if ($parseOptions && '' == $token) { + $this->parseArgument($token); + } elseif ($parseOptions && '--' == $token) { + $parseOptions = false; + } elseif ($parseOptions && 0 === strpos($token, '--')) { + $this->parseLongOption($token); + } elseif ($parseOptions && '-' === $token[0] && '-' !== $token) { + $this->parseShortOption($token); + } else { + $this->parseArgument($token); + } + } + // Put back any options that were injected. + $this->options += $this->additionalOptions; + } + + /** + * Parses a short option. + * + * @param string $token The current token + */ + private function parseShortOption($token): void + { + $name = substr($token, 1); + + if (strlen($name) > 1) { + if ($this->definition->hasShortcut($name[0]) && $this->definition->getOptionForShortcut($name[0])->acceptValue()) { + // an option with a value (with no space) + $this->addShortOption($name[0], substr($name, 1)); + } else { + $this->parseShortOptionSet($name); + } + } else { + $this->addShortOption($name, null); + } + } + + /** + * Parses a short option set. + * + * @param string $name The current token + */ + private function parseShortOptionSet($name): void + { + $len = strlen($name); + for ($i = 0; $i < $len; ++$i) { + if (!$this->definition->hasShortcut($name[$i])) { + $this->addShortOption($name[$i]); + } + + $option = $this->definition->getOptionForShortcut($name[$i]); + if ($option->acceptValue()) { + $this->addLongOption($option->getName(), $i === $len - 1 ? null : substr($name, $i + 1)); + + break; + } else { + $this->addLongOption($option->getName(), null); + } + } + } + + /** + * Parses a long option. + * + * @param string $token The current token + */ + private function parseLongOption($token): void + { + $name = substr($token, 2); + + if (false !== $pos = strpos($name, '=')) { + if (0 === strlen($value = substr($name, $pos + 1))) { + // if no value after "=" then substr() returns "" since php7 only, false before + // see http://php.net/manual/fr/migration70.incompatible.php#119151 + if (\PHP_VERSION_ID < 70000 && false === $value) { + $value = ''; + } + array_unshift($this->parsed, $value); + } + $this->addLongOption(substr($name, 0, $pos), $value); + } else { + $this->addLongOption($name, null); + } + } + + /** + * Parses an argument. + * + * @param string $token The current token + * + * @throws RuntimeException When too many arguments are given + */ + private function parseArgument($token): void + { + $c = count($this->arguments); + + // if input is expecting another argument, add it + if ($this->definition->hasArgument($c)) { + $arg = $this->definition->getArgument($c); + $this->arguments[$arg->getName()] = $arg->isArray() ? [$token] : $token; + + // if last argument isArray(), append token to last argument + } elseif ($this->definition->hasArgument($c - 1) && $this->definition->getArgument($c - 1)->isArray()) { + $arg = $this->definition->getArgument($c - 1); + $this->arguments[$arg->getName()][] = $token; + + // unexpected argument + } else { + $all = $this->definition->getArguments(); + if (count($all)) { + throw new RuntimeException(sprintf('Too many arguments, expected arguments "%s", provided arguments "%s".', implode('" "', array_keys($all)), implode('" "', array_keys($this->arguments)))); + } + + throw new RuntimeException(sprintf('No arguments expected, got "%s".', $token)); + } + } + + /** + * Adds a short option value. + * + * @param string $shortcut The short option key + * @param mixed $value The value for the option + * + * @throws RuntimeException When option given doesn't exist + */ + private function addShortOption($shortcut, $value): void + { + if (!$this->definition->hasShortcut($shortcut)) { + // Hard to know what to do with unknown short options. Maybe + // these should be added to the end of the arguments. This would only + // be a good strategy if the last argument was an array argument. + // We'll try adding as a long option for now. + $this->addLongOption($shortcut, $value); + } + + $this->addLongOption($this->definition->getOptionForShortcut($shortcut)->getName(), $value); + } + + public function injectAdditionalOptions($additionalOptions): void + { + $this->additionalOptions += $additionalOptions; + $this->options += $additionalOptions; + } + + /** + * Adds a long option value. + * + * @param string $name The long option key + * @param mixed $value The value for the option + * + * @throws RuntimeException When option given doesn't exist + */ + private function addLongOption($name, $value): void + { + if (!$this->definition->hasOption($name)) { + // If we don't know anything about this option, then we'll + // assume it is generic. + $this->options[$name] = $value; + return; + } + + $option = $this->definition->getOption($name); + + if (null !== $value && !$option->acceptValue()) { + throw new RuntimeException(sprintf('The "--%s" option does not accept a value.', $name)); + } + + if (in_array($value, ['', null], true) && $option->acceptValue() && count($this->parsed)) { + // if option accepts an optional or mandatory argument + // let's see if there is one provided + $next = array_shift($this->parsed); + if ((isset($next[0]) && '-' !== $next[0]) || in_array($next, ['', null], true)) { + $value = $next; + } else { + array_unshift($this->parsed, $next); + } + } + + if (null === $value) { + if ($option->isValueRequired()) { + throw new RuntimeException(sprintf('The "--%s" option requires a value.', $name)); + } + + if (!$option->isArray() && !$option->isValueOptional()) { + $value = true; + } + } + + if ($option->isArray()) { + $this->options[$name][] = $value; + } else { + $this->options[$name] = $value; + } + } + + /** + * {@inheritdoc} + */ + public function getFirstArgument() + { + foreach ($this->tokens as $token) { + if ($token && '-' === $token[0]) { + continue; + } + + return $token; + } + } + + /** + * {@inheritdoc} + */ + public function hasParameterOption($values, $onlyParams = false): bool + { + $values = (array) $values; + + foreach ($this->tokens as $token) { + if ($onlyParams && $token === '--') { + return false; + } + foreach ($values as $value) { + if ($token === $value || 0 === strpos($token, $value . '=')) { + return true; + } + } + } + + return false; + } + + /** + * {@inheritdoc} + */ + public function getParameterOption($values, $default = false, $onlyParams = false) + { + $values = (array) $values; + $tokens = $this->tokens; + + while (0 < count($tokens)) { + $token = array_shift($tokens); + if ($onlyParams && $token === '--') { + return false; + } + + foreach ($values as $value) { + if ($token === $value || 0 === strpos($token, $value . '=')) { + if (false !== $pos = strpos($token, '=')) { + return substr($token, $pos + 1); + } + + return array_shift($tokens); + } + } + } + + return $default; + } + + /** + * Returns a stringified representation of the args passed to the command. + * + * @return string + */ + public function __toString() + { + $tokens = array_map(function ($token) { + if (preg_match('{^(-[^=]+=)(.+)}', $token, $match)) { + return $match[1] . $this->escapeToken($match[2]); + } + + if ($token && $token[0] !== '-') { + return $this->escapeToken($token); + } + + return $token; + }, $this->tokens); + + return implode(' ', $tokens); + } +} diff --git a/src/Preflight/Preflight.php b/src/Preflight/Preflight.php index 5447c51180..d32efb29c4 100644 --- a/src/Preflight/Preflight.php +++ b/src/Preflight/Preflight.php @@ -206,7 +206,7 @@ public function loadSymfonyCompatabilityAutoloader(): ClassLoader 3 => false, // Drupal 8 4 => 'v4', // Drupal 9 5 => 'v4', // Early Drupal 10 (Symfony 5 works with Symfony 4 classes, so we don't keep an extra copy) - 6 => 'v4', // Drupal 10 (We'll change this to v6 in just a sec) + 6 => 'v6', // Drupal 10 ]; if (empty($compatibilityMap[$symfonyMajorVersion])) { diff --git a/tests/unish/Controllers/RuntimeController.php b/tests/unish/Controllers/RuntimeController.php index 005500fd0a..745d886771 100644 --- a/tests/unish/Controllers/RuntimeController.php +++ b/tests/unish/Controllers/RuntimeController.php @@ -121,6 +121,9 @@ protected function initializeRuntime($root, $argv) // Require the Composer autoloader for Drupal (if different) $loader = $this->preflight->loadSiteAutoloader(); + // Load the Symfony compatability layer autoloader + $this->preflight->loadSymfonyCompatabilityAutoloader(); + // Create the Symfony Application et. al. $this->input = $this->preflight->createInput(); $this->application = new \Drush\Application('Drush Commandline Tool (Unish-scaffolded)', Drush::getVersion());