diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-06-13 11:38:20 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-06-13 17:49:15 +0900 |
| commit | 69c372ba0eca61b05260d6d208445d9e69d14e34 (patch) | |
| tree | ab837346f5729a5a4a67ccd83462033511b41c89 | |
| parent | efe5bdb1987411a473d4af15451a376d20928245 (diff) | |
| download | php-shirabe-69c372ba0eca61b05260d6d208445d9e69d14e34.tar.gz php-shirabe-69c372ba0eca61b05260d6d208445d9e69d14e34.tar.zst php-shirabe-69c372ba0eca61b05260d6d208445d9e69d14e34.zip | |
fix(console): flatten Application inheritance to restore overrides
Composer\Console\Application embedded Symfony's Application as an
`inner` field and delegated to it, so polymorphic calls inside the
Symfony base (e.g. doRun -> $this->getLongVersion()) resolved to
Symfony's own methods and never reached Composer's overrides. As a
result `--version` bypassed Composer's getLongVersion()/doRun()
entirely.
Flatten the PHP inheritance chain into the single shirabe Application
struct: take in the Symfony base methods (parent-calling overrides kept
under a `base_` prefix) and drop the `inner` delegation. Replace the
Symfony Application struct in shirabe-external-packages with an
`Application` trait that the merged struct implements, so commands and
descriptors can reference it without a reverse crate dependency.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
21 files changed, 1899 insertions, 1888 deletions
diff --git a/crates/shirabe-external-packages/src/symfony/console/application.rs b/crates/shirabe-external-packages/src/symfony/console/application.rs index 8838eaf..f46bbfa 100644 --- a/crates/shirabe-external-packages/src/symfony/console/application.rs +++ b/crates/shirabe-external-packages/src/symfony/console/application.rs @@ -1,1707 +1,42 @@ use crate::symfony::console::command::command::Command; -use crate::symfony::console::command::complete_command::CompleteCommand; -use crate::symfony::console::command::dump_completion_command::DumpCompletionCommand; -use crate::symfony::console::command::help_command::HelpCommand; -use crate::symfony::console::command::lazy_command::LazyCommand; -use crate::symfony::console::command::list_command::ListCommand; -use crate::symfony::console::command::signalable_command_interface::SignalableCommandInterface; -use crate::symfony::console::command_loader::command_loader_interface::CommandLoaderInterface; use crate::symfony::console::completion::completion_input::CompletionInput; use crate::symfony::console::completion::completion_suggestions::CompletionSuggestions; -use crate::symfony::console::console_events::ConsoleEvents; -use crate::symfony::console::event::console_command_event::ConsoleCommandEvent; -use crate::symfony::console::event::console_error_event::ConsoleErrorEvent; -use crate::symfony::console::event::console_signal_event::ConsoleSignalEvent; -use crate::symfony::console::event::console_terminate_event::ConsoleTerminateEvent; -use crate::symfony::console::exception::command_not_found_exception::CommandNotFoundException; -use crate::symfony::console::exception::exception_interface::ExceptionInterface; -use crate::symfony::console::exception::logic_exception::LogicException; -use crate::symfony::console::exception::namespace_not_found_exception::NamespaceNotFoundException; -use crate::symfony::console::exception::runtime_exception::RuntimeException; -use crate::symfony::console::formatter::output_formatter::OutputFormatter; -use crate::symfony::console::helper::debug_formatter_helper::DebugFormatterHelper; -use crate::symfony::console::helper::formatter_helper::{FormatBlockMessages, FormatterHelper}; -use crate::symfony::console::helper::helper::Helper; use crate::symfony::console::helper::helper_set::HelperSet; -use crate::symfony::console::helper::process_helper::ProcessHelper; -use crate::symfony::console::helper::question_helper::QuestionHelper; -use crate::symfony::console::input::argv_input::ArgvInput; -use crate::symfony::console::input::array_input::ArrayInput; -use crate::symfony::console::input::input_argument::InputArgument; -use crate::symfony::console::input::input_aware_interface::InputAwareInterface; use crate::symfony::console::input::input_definition::InputDefinition; -use crate::symfony::console::input::input_interface::InputInterface; -use crate::symfony::console::input::input_option::InputOption; -use crate::symfony::console::output::console_output::ConsoleOutput; -use crate::symfony::console::output::console_output_interface::ConsoleOutputInterface; -use crate::symfony::console::output::output_interface::{self, OutputInterface}; -use crate::symfony::console::signal_registry::signal_registry::SignalRegistry; -use crate::symfony::console::style::style_interface::StyleInterface; -use crate::symfony::console::style::symfony_style::SymfonyStyle; -use crate::symfony::console::terminal::Terminal; -use crate::symfony::contracts::event_dispatcher::event_dispatcher_interface::EventDispatcherInterface; -use crate::symfony::contracts::service::reset_interface::ResetInterface; use indexmap::IndexMap; -use shirabe_php_shim::PhpMixed; use std::cell::RefCell; use std::rc::Rc; -/// An Application is the container for a collection of commands. -/// -/// It is the main entry point of a Console application. -/// -/// This class is optimized for a standard CLI environment. -#[derive(Debug)] -pub struct Application { - commands: IndexMap<String, Rc<RefCell<dyn Command>>>, - want_helps: bool, - running_command: Option<Rc<RefCell<dyn Command>>>, - name: String, - version: String, - command_loader: Option<Box<dyn CommandLoaderInterface>>, - catch_exceptions: bool, - auto_exit: bool, - definition: Option<Rc<RefCell<InputDefinition>>>, - helper_set: Option<Rc<RefCell<HelperSet>>>, - dispatcher: Option<Rc<RefCell<dyn EventDispatcherInterface>>>, - terminal: Terminal, - default_command: String, - single_command: bool, - initialized: bool, - signal_registry: Option<SignalRegistry>, - signals_to_dispatch_event: Vec<i64>, -} +/// `Symfony\Component\Console\Application` is a concrete class in PHP, but it is ported here as a +/// trait rather than a struct. +/// Refer to shirabe::console::Application for the reason. +pub trait Application: std::fmt::Debug { + fn get_name(&self) -> String; -impl Application { - pub fn __construct(name: &str, version: &str) -> Self { - let mut this = Application { - commands: IndexMap::new(), - want_helps: false, - running_command: None, - name: name.to_string(), - version: version.to_string(), - command_loader: None, - catch_exceptions: true, - auto_exit: true, - definition: None, - helper_set: None, - dispatcher: None, - terminal: Terminal::new(), - default_command: "list".to_string(), - single_command: false, - initialized: false, - signal_registry: None, - signals_to_dispatch_event: Vec::new(), - }; - if shirabe_php_shim::defined("SIGINT") && SignalRegistry::is_supported() { - this.signal_registry = Some(SignalRegistry::new()); - this.signals_to_dispatch_event = vec![ - shirabe_php_shim::SIGINT, - shirabe_php_shim::SIGTERM, - shirabe_php_shim::SIGUSR1, - shirabe_php_shim::SIGUSR2, - ]; - } - this - } + fn get_version(&self) -> String; - /// @final - pub fn set_dispatcher(&mut self, dispatcher: Rc<RefCell<dyn EventDispatcherInterface>>) { - // TODO(plugin): the event dispatcher drives ConsoleEvents listeners (plugins). - self.dispatcher = Some(dispatcher); - } + fn get_help(&self) -> String; - pub fn set_command_loader(&mut self, command_loader: Box<dyn CommandLoaderInterface>) { - self.command_loader = Some(command_loader); - } + fn is_single_command(&self) -> bool; - pub fn get_signal_registry(&self) -> anyhow::Result<&SignalRegistry> { - match &self.signal_registry { - None => Err(RuntimeException(shirabe_php_shim::RuntimeException { - message: "Signals are not supported. Make sure that the `pcntl` extension is installed and that \"pcntl_*\" functions are not disabled by your php.ini's \"disable_functions\" directive.".to_string(), - code: 0, - }) - .into()), - Some(signal_registry) => Ok(signal_registry), - } - } + fn extract_namespace(&self, name: &str, limit: Option<i64>) -> String; - pub fn set_signals_to_dispatch_event(&mut self, signals_to_dispatch_event: Vec<i64>) { - self.signals_to_dispatch_event = signals_to_dispatch_event; - } + fn find_namespace(&mut self, namespace: &str) -> anyhow::Result<String>; - /// Runs the current application. - /// - /// Returns 0 if everything went fine, or an error code. - /// - /// Throws \Exception when running fails. Bypass this when set_catch_exceptions(). - pub fn run( + fn all( &mut self, - input: Option<Rc<RefCell<dyn InputInterface>>>, - output: Option<Rc<RefCell<dyn OutputInterface>>>, - ) -> anyhow::Result<i64> { - if shirabe_php_shim::function_exists("putenv") { - shirabe_php_shim::putenv(&format!("LINES={}", self.terminal.get_height())); - shirabe_php_shim::putenv(&format!("COLUMNS={}", self.terminal.get_width())); - } - - let input: Rc<RefCell<dyn InputInterface>> = match input { - None => Rc::new(RefCell::new(ArgvInput::new(None, None)?)), - Some(input) => input, - }; - - let output: Rc<RefCell<dyn OutputInterface>> = match output { - None => Rc::new(RefCell::new(ConsoleOutput::new(None, None, None)?)), - Some(output) => output, - }; - - // TODO: PHP installs a temporary `set_exception_handler($renderException)` and cooperates - // with Symfony's ErrorHandler to keep/restore it. PHP's process-global exception handler - // stack has no Rust equivalent; the rendering itself is invoked directly in the catch - // branch below. Review needed for the handler save/restore dance. - let render_exception = - |this: &Application, e: &anyhow::Error, output: &Rc<RefCell<dyn OutputInterface>>| { - // if ($output instanceof ConsoleOutputInterface) render to its error output - // TODO(review): downcasting a `dyn OutputInterface` to `ConsoleOutputInterface` - // is not directly expressible; the ConsoleOutputInterface branch needs design. - this.render_throwable(e, output.clone()); - }; - - let result = (|| -> anyhow::Result<i64> { - self.configure_io(&input, &output)?; - - let exit_code = self.do_run(input.clone(), output.clone())?; - - Ok(exit_code) - })(); - - let mut exit_code = match result { - Ok(exit_code) => exit_code, - Err(e) => { - if !self.catch_exceptions { - return Err(e); - } - - render_exception(self, &e, &output); - - // $exitCode = $e->getCode(); - // is_numeric($exitCode) ? max(1, (int) $exitCode) : 1 - // TODO(review): anyhow::Error has no PHP-style getCode(); the exit code derived - // from the exception's `code` field needs the downcast strategy decided. - let exit_code = shirabe_php_shim::php_exception_get_code(&e); - if shirabe_php_shim::is_numeric_string(&exit_code.to_string()) { - let exit_code = exit_code; - if exit_code <= 0 { 1 } else { exit_code } - } else { - 1 - } - } - }; - - // finally: handler restore. See TODO above; no-op here. - - if self.auto_exit { - if exit_code > 255 { - exit_code = 255; - } - - shirabe_php_shim::exit(exit_code); - } - - Ok(exit_code) - } - - /// Runs the current application. - /// - /// Returns 0 if everything went fine, or an error code. - pub fn do_run( - &mut self, - input: Rc<RefCell<dyn InputInterface>>, - output: Rc<RefCell<dyn OutputInterface>>, - ) -> anyhow::Result<i64> { - if input.borrow().has_parameter_option( - PhpMixed::from(vec![ - PhpMixed::from("--version".to_string()), - PhpMixed::from("-V".to_string()), - ]), - true, - ) { - output - .borrow() - .writeln(&[self.get_long_version()], output_interface::OUTPUT_NORMAL); - - return Ok(0); - } - - // Makes ArgvInput::getFirstArgument() able to distinguish an option from an argument. - match input.borrow_mut().bind(&self.get_definition().borrow()) { - Ok(()) => {} - Err(e) => { - // Errors must be ignored, full binding/validation happens later when the command is known. - if !is_exception_interface(&e) { - return Err(e); - } - } - } - - let mut input = input; - let mut name = self.get_command_name(&*input.borrow()); - if input.borrow().has_parameter_option( - PhpMixed::from(vec![ - PhpMixed::from("--help".to_string()), - PhpMixed::from("-h".to_string()), - ]), - true, - ) { - if name.is_none() { - name = Some("help".to_string()); - input = Rc::new(RefCell::new(ArrayInput::new( - vec![( - PhpMixed::from("command_name".to_string()), - PhpMixed::from(self.default_command.clone()), - )], - None, - )?)); - } else { - self.want_helps = true; - } - } - - let name = match name { - Some(name) => name, - None => { - let name = self.default_command.clone(); - let definition = self.get_definition(); - let command_description = definition - .borrow() - .get_argument(&PhpMixed::from("command".to_string()))? - .get_description() - .to_string(); - let _new_command_argument = InputArgument::new( - "command".to_string(), - Some(InputArgument::OPTIONAL), - command_description, - PhpMixed::from(name.clone()), - )?; - // $definition->setArguments(array_merge($definition->getArguments(), - // ['command' => new InputArgument('command', InputArgument::OPTIONAL, ...)])) - // TODO(review): get_arguments() yields Rc<InputArgument> (shared, non-Clone) while - // set_arguments() consumes owned InputArgument values. Re-building the merged - // argument list requires an InputArgument clone/ownership strategy not yet present. - definition.borrow_mut().set_arguments(todo!( - "merge existing arguments with the new 'command' argument" - ))?; - - name - } - }; - - let command: Rc<RefCell<dyn Command>>; - let find_result = (|| -> anyhow::Result<Rc<RefCell<dyn Command>>> { - self.running_command = None; - // the command name MUST be the first element of the input - self.find(&name) - })(); - - match find_result { - Ok(c) => { - command = c; - } - Err(e) => { - // if (!($e instanceof CommandNotFoundException && !$e instanceof NamespaceNotFoundException) - // || 1 !== count($alternatives = $e->getAlternatives()) || !$input->isInteractive()) - let alternatives: Option<Vec<String>> = downcast_command_not_found(&e) - .filter(|_| !is_namespace_not_found(&e)) - .map(|cnf| cnf.get_alternatives().clone()); - - let single_alternative = match &alternatives { - Some(alts) if alts.len() == 1 => Some(alts[0].clone()), - _ => None, - }; - - if single_alternative.is_none() || !input.borrow().is_interactive() { - let mut e = e; - if self.dispatcher.is_some() { - // TODO(plugin): dispatch ConsoleErrorEvent so listeners can handle/replace the error. - let _event = ConsoleErrorEvent::new( - todo!("wrap input as Box<dyn InputInterface> for the event"), - todo!("wrap output as Box<dyn OutputInterface> for the event"), - todo!("wrap anyhow::Error as Box<dyn Error> for the event"), - None, - ); - let event: ConsoleErrorEvent = _event; - self.dispatcher - .as_ref() - .unwrap() - .borrow_mut() - .dispatch(todo!("event object"), ConsoleEvents::ERROR); - - if event.get_exit_code() == 0 { - return Ok(0); - } - - e = todo!("event.get_error() converted back to anyhow::Error"); - } - - return Err(e); - } - - let alternative = single_alternative.unwrap(); - - let mut style = SymfonyStyle::new(input.clone(), output.clone()); - output - .borrow() - .writeln(&["".to_string()], output_interface::OUTPUT_NORMAL); - let formatted_block = FormatterHelper::default().format_block( - FormatBlockMessages::String(format!( - "Command \"{}\" is not defined.", - PhpMixed::from(name.clone()), - )), - "error", - true, - ); - output - .borrow() - .writeln(&[formatted_block], output_interface::OUTPUT_NORMAL); - if !style.confirm( - &format!( - "Do you want to run \"{}\" instead? ", - PhpMixed::from(alternative.clone()), - ), - false, - ) { - if self.dispatcher.is_some() { - // TODO(plugin): dispatch ConsoleErrorEvent for the declined-alternative case. - let event = ConsoleErrorEvent::new( - todo!("wrap input as Box<dyn InputInterface>"), - todo!("wrap output as Box<dyn OutputInterface>"), - todo!("wrap error as Box<dyn Error>"), - None, - ); - self.dispatcher - .as_ref() - .unwrap() - .borrow_mut() - .dispatch(todo!("event object"), ConsoleEvents::ERROR); - - return Ok(event.get_exit_code()); - } - - return Ok(1); - } - - command = self.find(&alternative)?; - } - } - - // if ($command instanceof LazyCommand) $command = $command->getCommand(); - // TODO(review): LazyCommand is a distinct type from Command here; PHP unwraps the real - // command. The `commands` map stores Rc<RefCell<dyn Command>>, so the LazyCommand-unwrap path - // needs a design decision about how lazy commands are represented. - let _ = std::marker::PhantomData::<LazyCommand>; - - self.running_command = Some(command.clone()); - let exit_code = self.do_run_command(command.clone(), input.clone(), output.clone())?; - self.running_command = None; - - Ok(exit_code) - } - - pub fn reset(&mut self) {} - - pub fn set_helper_set(&mut self, helper_set: Rc<RefCell<HelperSet>>) { - self.helper_set = Some(helper_set); - } - - /// Get the helper set associated with the command. - pub fn get_helper_set(&mut self) -> Rc<RefCell<HelperSet>> { - if self.helper_set.is_none() { - self.helper_set = Some(self.get_default_helper_set()); - } - - self.helper_set.as_ref().unwrap().clone() - } - - pub fn set_definition(&mut self, definition: Rc<RefCell<InputDefinition>>) { - self.definition = Some(definition); - } - - /// Gets the InputDefinition related to this Application. - pub fn get_definition(&mut self) -> Rc<RefCell<InputDefinition>> { - if self.definition.is_none() { - self.definition = Some(Rc::new(RefCell::new(self.get_default_input_definition()))); - } + namespace: Option<&str>, + ) -> anyhow::Result<IndexMap<String, Rc<RefCell<dyn Command>>>>; - if self.single_command { - let input_definition = self.definition.as_ref().unwrap().clone(); - input_definition - .borrow_mut() - .set_arguments(Vec::new()) - .unwrap(); + fn find(&mut self, name: &str) -> anyhow::Result<Rc<RefCell<dyn Command>>>; - return input_definition; - } + fn get_definition(&mut self) -> Rc<RefCell<InputDefinition>>; - self.definition.as_ref().unwrap().clone() - } + fn get_helper_set(&mut self) -> Rc<RefCell<HelperSet>>; - /// Adds suggestions to `suggestions` for the current completion input (e.g. option or argument). - pub fn complete( + fn complete( &mut self, input: &CompletionInput, suggestions: &mut CompletionSuggestions, - ) -> anyhow::Result<()> { - if CompletionInput::TYPE_ARGUMENT_VALUE == input.get_completion_type() - && input.get_completion_name().as_deref() == Some("command") - { - let mut command_names: Vec<PhpMixed> = Vec::new(); - for (name, command) in self.all(None)? { - // skip hidden commands and aliased commands as they already get added below - if command.borrow().is_hidden() || command.borrow().get_name() != Some(name.clone()) - { - continue; - } - command_names.push(PhpMixed::from( - command.borrow().get_name().unwrap_or_default(), - )); - for name in command.borrow().get_aliases() { - command_names.push(PhpMixed::from(name)); - } - } - // array_filter($commandNames) - let filtered: Vec<crate::symfony::console::completion::completion_suggestions::StringOrSuggestion> = - command_names - .into_iter() - .filter(|n| shirabe_php_shim::php_truthy(n)) - .map(|n| { - crate::symfony::console::completion::completion_suggestions::StringOrSuggestion::String( - shirabe_php_shim::php_to_string(&n), - ) - }) - .collect(); - suggestions.suggest_values(filtered); - - return Ok(()); - } - - if CompletionInput::TYPE_OPTION_NAME == input.get_completion_type() { - // $suggestions->suggestOptions($this->getDefinition()->getOptions()); - // TODO(review): get_options() yields Rc<InputOption> (shared, non-Clone) while - // suggest_options() consumes owned InputOption values; an ownership/clone strategy - // for InputOption is needed. - suggestions.suggest_options(todo!("owned options from get_definition().get_options()")); - - return Ok(()); - } - - Ok(()) - } - - /// Gets the help message. - pub fn get_help(&self) -> String { - self.get_long_version() - } - - /// Gets whether to catch exceptions or not during commands execution. - pub fn are_exceptions_caught(&self) -> bool { - self.catch_exceptions - } - - /// Sets whether to catch exceptions or not during commands execution. - pub fn set_catch_exceptions(&mut self, boolean: bool) { - self.catch_exceptions = boolean; - } - - /// Gets whether to automatically exit after a command execution or not. - pub fn is_auto_exit_enabled(&self) -> bool { - self.auto_exit - } - - /// Sets whether to automatically exit after a command execution or not. - pub fn set_auto_exit(&mut self, boolean: bool) { - self.auto_exit = boolean; - } - - /// Gets the name of the application. - pub fn get_name(&self) -> String { - self.name.clone() - } - - /// Sets the application name. - pub fn set_name(&mut self, name: &str) { - self.name = name.to_string(); - } - - /// Gets the application version. - pub fn get_version(&self) -> String { - self.version.clone() - } - - /// Sets the application version. - pub fn set_version(&mut self, version: &str) { - self.version = version.to_string(); - } - - /// Returns the long version of the application. - pub fn get_long_version(&self) -> String { - if "UNKNOWN" != self.get_name() { - if "UNKNOWN" != self.get_version() { - return format!("{} <info>{}</info>", self.get_name(), self.get_version()); - } - - return self.get_name(); - } - - "Console Tool".to_string() - } - - /// Adds an array of command objects. - /// - /// If a Command is not enabled it will not be added. - pub fn add_commands(&mut self, commands: Vec<Rc<RefCell<dyn Command>>>) -> anyhow::Result<()> { - for command in commands { - self.add(command)?; - } - Ok(()) - } - - /// Adds a command object. - /// - /// If a command with the same name already exists, it will be overridden. - /// If the command is not enabled it will not be added. - pub fn add( - &mut self, - command: Rc<RefCell<dyn Command>>, - ) -> anyhow::Result<Option<Rc<RefCell<dyn Command>>>> { - self.init()?; - - // TODO(review): $command->setApplication($this) needs an Rc<RefCell<Application>> to the - // current instance. Application is held by value here; the self-reference required to set - // the command's back-pointer needs the shared-ownership design (Phase C). - command - .borrow_mut() - .set_application(todo!("Rc<RefCell<Application>> of self")); - - if !command.borrow().is_enabled() { - command.borrow_mut().set_application(None); - - return Ok(None); - } - - // if (!$command instanceof LazyCommand) { $command->getDefinition(); } - // TODO(review): LazyCommand vs Command type distinction; eager definition probe omitted - // pending lazy-command representation decision. - command.borrow().get_definition(); - - if command.borrow().get_name().is_none() { - return Err(LogicException(shirabe_php_shim::LogicException { - message: format!( - "The command defined in \"{}\" cannot have an empty name.", - PhpMixed::from(shirabe_php_shim::get_debug_type_obj(&command,)), - ), - code: 0, - }) - .into()); - } - - let name = command.borrow().get_name().unwrap(); - self.commands.insert(name, command.clone()); - - for alias in command.borrow().get_aliases() { - self.commands.insert(alias, command.clone()); - } - - Ok(Some(command)) - } - - /// Returns a registered command by name or alias. - /// - /// Throws CommandNotFoundException when given command name does not exist. - pub fn get(&mut self, name: &str) -> anyhow::Result<Rc<RefCell<dyn Command>>> { - self.init()?; - - if !self.has(name) { - return Err(CommandNotFoundException::new( - format!( - "The command \"{}\" does not exist.", - PhpMixed::from(name.to_string()), - ), - Vec::new(), - 0, - ) - .into()); - } - - // When the command has a different name than the one used at the command loader level - if !self.commands.contains_key(name) { - return Err(CommandNotFoundException::new( - format!( - "The \"{}\" command cannot be found because it is registered under multiple names. Make sure you don't set a different name via constructor or \"setName()\".", - PhpMixed::from(name.to_string()), - ), - Vec::new(), - 0, - ) - .into()); - } - - let command = self.commands[name].clone(); - - if self.want_helps { - self.want_helps = false; - - let help_command = self.get("help")?; - // $helpCommand->setCommand($command); - // TODO(review): setCommand() is defined on HelpCommand, not on the concrete `Command` - // struct; calling it through the Rc<RefCell<dyn Command>> needs the Command-subclass - // representation decision (downcast to HelpCommand). - let _ = &command; - todo!("help_command.set_command(command)"); - - #[allow(unreachable_code)] - return Ok(help_command); - } - - Ok(command) - } - - /// Returns true if the command exists, false otherwise. - pub fn has(&mut self, name: &str) -> bool { - self.init().unwrap(); - - if self.commands.contains_key(name) { - return true; - } - - if let Some(command_loader) = &self.command_loader { - if command_loader.has(name) { - let command = command_loader.get(name); - // $this->add($this->commandLoader->get($name)) - // TODO(review): command_loader.get() returns Box<dyn Command> while add() expects - // Rc<RefCell<dyn Command>>; the loader return type needs reconciliation. - let _ = command; - return self - .add(todo!( - "Rc<RefCell<dyn Command>> from command_loader.get(name)" - )) - .map(|c| c.is_some()) - .unwrap_or(false); - } - } - - false - } - - /// Returns an array of all unique namespaces used by currently registered commands. - /// - /// It does not return the global namespace which always exists. - pub fn get_namespaces(&mut self) -> anyhow::Result<Vec<String>> { - let mut namespaces: Vec<Vec<String>> = Vec::new(); - for command in self.all(None)?.values() { - if command.borrow().is_hidden() { - continue; - } - - namespaces.push( - self.extract_all_namespaces(&command.borrow().get_name().unwrap_or_default()), - ); - - for alias in command.borrow().get_aliases() { - namespaces.push(self.extract_all_namespaces(&alias)); - } - } - - // array_values(array_unique(array_filter(array_merge([], ...$namespaces)))) - let mut merged: Vec<String> = Vec::new(); - for ns in namespaces { - merged.extend(ns); - } - let merged: Vec<String> = merged.into_iter().filter(|s| !s.is_empty()).collect(); - let mut seen = std::collections::HashSet::new(); - let unique: Vec<String> = merged - .into_iter() - .filter(|s| seen.insert(s.clone())) - .collect(); - - Ok(unique) - } - - /// Finds a registered namespace by a name or an abbreviation. - /// - /// Throws NamespaceNotFoundException when namespace is incorrect or ambiguous. - pub fn find_namespace(&mut self, namespace: &str) -> anyhow::Result<String> { - let all_namespaces = self.get_namespaces()?; - // implode('[^:]*:', array_map('preg_quote', explode(':', $namespace))).'[^:]*' - let parts: Vec<String> = shirabe_php_shim::explode(":", namespace) - .into_iter() - .map(|p| shirabe_php_shim::preg_quote(&p, None)) - .collect(); - let expr = format!("{}{}", shirabe_php_shim::implode("[^:]*:", &parts), "[^:]*"); - let namespaces = shirabe_php_shim::preg_grep(&format!("{{^{}}}", expr), &all_namespaces); - - if namespaces.is_empty() { - let mut message = format!( - "There are no commands defined in the \"{}\" namespace.", - PhpMixed::from(namespace.to_string()), - ); - - let alternatives = self.find_alternatives(namespace, &all_namespaces); - if !alternatives.is_empty() { - if alternatives.len() == 1 { - message.push_str("\n\nDid you mean this?\n "); - } else { - message.push_str("\n\nDid you mean one of these?\n "); - } - - message.push_str(&shirabe_php_shim::implode("\n ", &alternatives)); - } - - return Err(NamespaceNotFoundException(CommandNotFoundException::new( - message, - alternatives, - 0, - )) - .into()); - } - - let exact = namespaces.iter().any(|n| n == namespace); - if namespaces.len() > 1 && !exact { - return Err(NamespaceNotFoundException(CommandNotFoundException::new( - format!( - "The namespace \"{}\" is ambiguous.\nDid you mean one of these?\n{}.", - PhpMixed::from(namespace.to_string()), - PhpMixed::from(self.get_abbreviation_suggestions(&namespaces)), - ), - namespaces.clone(), - 0, - )) - .into()); - } - - // $exact ? $namespace : reset($namespaces) - if exact { - Ok(namespace.to_string()) - } else { - Ok(namespaces[0].clone()) - } - } - - /// Finds a command by name or alias. - /// - /// Contrary to get, this command tries to find the best match if you give it an - /// abbreviation of a name or alias. - /// - /// Throws CommandNotFoundException when command name is incorrect or ambiguous. - pub fn find(&mut self, name: &str) -> anyhow::Result<Rc<RefCell<dyn Command>>> { - self.init()?; - - let mut aliases: IndexMap<String, String> = IndexMap::new(); - - let commands_snapshot: Vec<Rc<RefCell<dyn Command>>> = - self.commands.values().cloned().collect(); - for command in &commands_snapshot { - for alias in command.borrow().get_aliases() { - if !self.has(&alias) { - self.commands.insert(alias, command.clone()); - } - } - } - - if self.has(name) { - return self.get(name); - } - - // $allCommands = commandLoader ? array_merge(loader->getNames(), array_keys(commands)) : array_keys(commands) - let all_commands: Vec<String> = match &self.command_loader { - Some(command_loader) => { - let mut all = command_loader.get_names(); - all.extend(self.commands.keys().cloned()); - all - } - None => self.commands.keys().cloned().collect(), - }; - - let parts: Vec<String> = shirabe_php_shim::explode(":", name) - .into_iter() - .map(|p| shirabe_php_shim::preg_quote(&p, None)) - .collect(); - let expr = format!("{}{}", shirabe_php_shim::implode("[^:]*:", &parts), "[^:]*"); - let mut commands = shirabe_php_shim::preg_grep(&format!("{{^{}}}", expr), &all_commands); - - if commands.is_empty() { - commands = shirabe_php_shim::preg_grep(&format!("{{^{}}}i", expr), &all_commands); - } - - // if no commands matched or we just matched namespaces - if commands.is_empty() - || shirabe_php_shim::preg_grep(&format!("{{^{}$}}i", expr), &commands).len() < 1 - { - if let Some(pos) = shirabe_php_shim::strrpos(name, ":") { - // check if a namespace exists and contains commands - self.find_namespace(&name[..pos as usize])?; - } - - let mut message = format!( - "Command \"{}\" is not defined.", - PhpMixed::from(name.to_string()), - ); - - let mut alternatives = self.find_alternatives(name, &all_commands); - if !alternatives.is_empty() { - // remove hidden commands - let mut filtered: Vec<String> = Vec::new(); - for alt in alternatives { - if !self.get(&alt)?.borrow().is_hidden() { - filtered.push(alt); - } - } - alternatives = filtered; - - if alternatives.len() == 1 { - message.push_str("\n\nDid you mean this?\n "); - } else { - message.push_str("\n\nDid you mean one of these?\n "); - } - message.push_str(&shirabe_php_shim::implode("\n ", &alternatives)); - } - - return Err(CommandNotFoundException::new(message, alternatives, 0).into()); - } - - // filter out aliases for commands which are already on the list - if commands.len() > 1 { - // $commandList = commandLoader ? array_merge(array_flip(loader->getNames()), commands) : commands - // TODO(review): $commandList mixes flipped loader names (string => int) with Command - // instances; this heterogeneous PHP array needs a typed representation. The alias - // de-duplication and the loader->get() lazy materialization are left to design. - let mut command_list: IndexMap<String, Rc<RefCell<dyn Command>>> = - self.commands.clone(); - - let commands_clone = commands.clone(); - let mut new_commands: Vec<String> = Vec::new(); - let mut seen = std::collections::HashSet::new(); - for name_or_alias in commands { - if !command_list.contains_key(&name_or_alias) { - let loaded = self.command_loader.as_ref().unwrap().get(&name_or_alias); - let _ = loaded; - command_list.insert( - name_or_alias.clone(), - todo!("Rc<RefCell<dyn Command>> from command_loader.get(name_or_alias)"), - ); - } - - let command_name = command_list[&name_or_alias] - .borrow() - .get_name() - .unwrap_or_default(); - - aliases.insert(name_or_alias.clone(), command_name.clone()); - - let keep = command_name == name_or_alias || !commands_clone.contains(&command_name); - if keep && seen.insert(name_or_alias.clone()) { - new_commands.push(name_or_alias); - } - } - commands = new_commands; - - if commands.len() > 1 { - let usable_width = self.terminal.get_width() - 10; - let abbrevs: Vec<String> = commands.clone(); - let mut max_len: i64 = 0; - for abbrev in &abbrevs { - max_len = std::cmp::max(Helper::width(abbrev), max_len); - } - let mut formatted_abbrevs: Vec<PhpMixed> = Vec::new(); - for cmd in commands.clone() { - if command_list[&cmd].borrow().is_hidden() { - // unset($commands[array_search($cmd, $commands)]) - if let Some(idx) = commands.iter().position(|c| *c == cmd) { - commands.remove(idx); - } - formatted_abbrevs.push(PhpMixed::Bool(false)); - continue; - } - - let abbrev = format!( - "{} {}", - shirabe_php_shim::str_pad( - &cmd, - max_len as usize, - " ", - shirabe_php_shim::STR_PAD_LEFT - ), - command_list[&cmd].borrow().get_description() - ); - - if Helper::width(&abbrev) > usable_width { - formatted_abbrevs.push(PhpMixed::from(format!( - "{}...", - Helper::substr(&abbrev, 0, Some(usable_width - 3)) - ))); - } else { - formatted_abbrevs.push(PhpMixed::from(abbrev)); - } - } - - if commands.len() > 1 { - let filtered: Vec<String> = formatted_abbrevs - .iter() - .filter(|a| shirabe_php_shim::php_truthy(a)) - .map(|a| shirabe_php_shim::php_to_string(a)) - .collect(); - let suggestions = self.get_abbreviation_suggestions(&filtered); - - return Err(CommandNotFoundException::new( - format!( - "Command \"{}\" is ambiguous.\nDid you mean one of these?\n{}.", - PhpMixed::from(name.to_string()), - PhpMixed::from(suggestions), - ), - commands.clone(), - 0, - ) - .into()); - } - } - } - - // $command = $this->get(reset($commands)); - let command = self.get(&commands[0])?; - - if command.borrow().is_hidden() { - return Err(CommandNotFoundException::new( - format!( - "The command \"{}\" does not exist.", - PhpMixed::from(name.to_string()), - ), - Vec::new(), - 0, - ) - .into()); - } - - Ok(command) - } - - /// Gets the commands (registered in the given namespace if provided). - /// - /// The array keys are the full names and the values the command instances. - pub fn all( - &mut self, - namespace: Option<&str>, - ) -> anyhow::Result<IndexMap<String, Rc<RefCell<dyn Command>>>> { - self.init()?; - - if namespace.is_none() { - if self.command_loader.is_none() { - return Ok(self.commands.clone()); - } - - let mut commands = self.commands.clone(); - let names = self.command_loader.as_ref().unwrap().get_names(); - for name in names { - if !commands.contains_key(&name) && self.has(&name) { - commands.insert(name.clone(), self.get(&name)?); - } - } - - return Ok(commands); - } - - let namespace = namespace.unwrap(); - let mut commands: IndexMap<String, Rc<RefCell<dyn Command>>> = IndexMap::new(); - let entries: Vec<(String, Rc<RefCell<dyn Command>>)> = self - .commands - .iter() - .map(|(k, v)| (k.clone(), v.clone())) - .collect(); - for (name, command) in entries { - if namespace - == self.extract_namespace( - &name, - Some(shirabe_php_shim::substr_count(namespace, ":") + 1), - ) - { - commands.insert(name, command); - } - } - - if self.command_loader.is_some() { - let names = self.command_loader.as_ref().unwrap().get_names(); - for name in names { - if !commands.contains_key(&name) - && namespace - == self.extract_namespace( - &name, - Some(shirabe_php_shim::substr_count(namespace, ":") + 1), - ) - && self.has(&name) - { - commands.insert(name.clone(), self.get(&name)?); - } - } - } - - Ok(commands) - } - - /// Returns an array of possible abbreviations given a set of names. - pub fn get_abbreviations(names: Vec<String>) -> IndexMap<String, Vec<String>> { - let mut abbrevs: IndexMap<String, Vec<String>> = IndexMap::new(); - for name in names { - let mut len = shirabe_php_shim::strlen(&name); - while len > 0 { - let abbrev = shirabe_php_shim::substr(&name, 0, Some(len)); - abbrevs.entry(abbrev).or_default().push(name.clone()); - len -= 1; - } - } - - abbrevs - } - - pub fn render_throwable(&self, e: &anyhow::Error, output: Rc<RefCell<dyn OutputInterface>>) { - output - .borrow() - .writeln(&["".to_string()], output_interface::VERBOSITY_QUIET); - - self.do_render_throwable(e, output.clone()); - - if let Some(running_command) = &self.running_command { - output.borrow().writeln( - &[format!( - "<info>{}</info>", - PhpMixed::from( - OutputFormatter::escape(&shirabe_php_shim::sprintf( - &running_command.borrow_mut().get_synopsis(false), - &[PhpMixed::from(self.get_name())], - )) - .unwrap(), - ), - )], - output_interface::VERBOSITY_QUIET, - ); - output - .borrow() - .writeln(&["".to_string()], output_interface::VERBOSITY_QUIET); - } - } - - pub fn do_render_throwable(&self, e: &anyhow::Error, output: Rc<RefCell<dyn OutputInterface>>) { - // do { ... } while ($e = $e->getPrevious()); - // TODO(review): PHP walks the exception chain via getPrevious() and reads getMessage(), - // getCode(), getFile(), getLine(), getTrace(). anyhow::Error exposes a source() chain but - // not file/line/trace; faithful rendering of the trace needs a Throwable-equivalent. - let _ = output; - let _ = e; - todo!("render exception chain (getMessage/getCode/getFile/getLine/getTrace/getPrevious)") - } - - /// Configures the input and output instances based on the user arguments and options. - pub fn configure_io( - &self, - input: &Rc<RefCell<dyn InputInterface>>, - output: &Rc<RefCell<dyn OutputInterface>>, - ) -> anyhow::Result<()> { - if input.borrow().has_parameter_option( - PhpMixed::from(vec![PhpMixed::from("--ansi".to_string())]), - true, - ) { - output.borrow().set_decorated(true); - } else if input.borrow().has_parameter_option( - PhpMixed::from(vec![PhpMixed::from("--no-ansi".to_string())]), - true, - ) { - output.borrow().set_decorated(false); - } - - if input.borrow().has_parameter_option( - PhpMixed::from(vec![ - PhpMixed::from("--no-interaction".to_string()), - PhpMixed::from("-n".to_string()), - ]), - true, - ) { - input.borrow_mut().set_interactive(false); - } - - let mut shell_verbosity = shirabe_php_shim::getenv("SHELL_VERBOSITY").unwrap_or_default(); - let shell_verbosity_int: i64 = shell_verbosity.parse().unwrap_or(0); - let mut shell_verbosity: i64 = shell_verbosity_int; - match shell_verbosity_int { - -1 => { - output - .borrow() - .set_verbosity(output_interface::VERBOSITY_QUIET); - } - 1 => { - output - .borrow() - .set_verbosity(output_interface::VERBOSITY_VERBOSE); - } - 2 => { - output - .borrow() - .set_verbosity(output_interface::VERBOSITY_VERY_VERBOSE); - } - 3 => { - output - .borrow() - .set_verbosity(output_interface::VERBOSITY_DEBUG); - } - _ => { - shell_verbosity = 0; - } - } - - if input.borrow().has_parameter_option( - PhpMixed::from(vec![ - PhpMixed::from("--quiet".to_string()), - PhpMixed::from("-q".to_string()), - ]), - true, - ) { - output - .borrow() - .set_verbosity(output_interface::VERBOSITY_QUIET); - shell_verbosity = -1; - } else if input - .borrow() - .has_parameter_option(PhpMixed::from("-vvv".to_string()), true) - || input - .borrow() - .has_parameter_option(PhpMixed::from("--verbose=3".to_string()), true) - || input.borrow().get_parameter_option( - PhpMixed::from("--verbose".to_string()), - PhpMixed::Bool(false), - true, - ) == PhpMixed::from(3i64) - { - output - .borrow() - .set_verbosity(output_interface::VERBOSITY_DEBUG); - shell_verbosity = 3; - } else if input - .borrow() - .has_parameter_option(PhpMixed::from("-vv".to_string()), true) - || input - .borrow() - .has_parameter_option(PhpMixed::from("--verbose=2".to_string()), true) - || input.borrow().get_parameter_option( - PhpMixed::from("--verbose".to_string()), - PhpMixed::Bool(false), - true, - ) == PhpMixed::from(2i64) - { - output - .borrow() - .set_verbosity(output_interface::VERBOSITY_VERY_VERBOSE); - shell_verbosity = 2; - } else if input - .borrow() - .has_parameter_option(PhpMixed::from("-v".to_string()), true) - || input - .borrow() - .has_parameter_option(PhpMixed::from("--verbose=1".to_string()), true) - || input - .borrow() - .has_parameter_option(PhpMixed::from("--verbose".to_string()), true) - || shirabe_php_shim::php_truthy(&input.borrow().get_parameter_option( - PhpMixed::from("--verbose".to_string()), - PhpMixed::Bool(false), - true, - )) - { - output - .borrow() - .set_verbosity(output_interface::VERBOSITY_VERBOSE); - shell_verbosity = 1; - } - - if shell_verbosity == -1 { - input.borrow_mut().set_interactive(false); - } - - if shirabe_php_shim::function_exists("putenv") { - shirabe_php_shim::putenv(&format!("SHELL_VERBOSITY={}", shell_verbosity)); - } - shirabe_php_shim::env_set("SHELL_VERBOSITY", shell_verbosity.to_string()); - shirabe_php_shim::server_set("SHELL_VERBOSITY", shell_verbosity.to_string()); - - let _ = &mut shell_verbosity; - - Ok(()) - } - - /// Runs the current command. - /// - /// If an event dispatcher has been attached to the application, events are also - /// dispatched during the life-cycle of the command. - /// - /// Returns 0 if everything went fine, or an error code. - pub fn do_run_command( - &mut self, - command: Rc<RefCell<dyn Command>>, - input: Rc<RefCell<dyn InputInterface>>, - output: Rc<RefCell<dyn OutputInterface>>, - ) -> anyhow::Result<i64> { - if let Some(helper_set) = command.borrow().get_helper_set() { - for (_alias, helper) in helper_set.borrow().get_iterator() { - // if ($helper instanceof InputAwareInterface) $helper->setInput($input); - // TODO(review): downcasting a HelperInterface to InputAwareInterface is not - // expressible without a typed mechanism; needs design. - let _ = helper; - let _ = std::marker::PhantomData::<dyn InputAwareInterface>; - } - } - - if !self.signals_to_dispatch_event.is_empty() { - // $commandSignals = $command instanceof SignalableCommandInterface ? $command->getSubscribedSignals() : [] - // TODO(review): Command is not a SignalableCommandInterface here; downcast needed. - let command_signals: Vec<i64> = Vec::new(); - let _ = std::marker::PhantomData::<dyn SignalableCommandInterface>; - - if !command_signals.is_empty() || self.dispatcher.is_some() { - if self.signal_registry.is_none() { - return Err(RuntimeException(shirabe_php_shim::RuntimeException { - message: "Unable to subscribe to signal events. Make sure that the `pcntl` extension is installed and that \"pcntl_*\" functions are not disabled by your php.ini's \"disable_functions\" directive.".to_string(), - code: 0, - }) - .into()); - } - - if Terminal::has_stty_available() { - // TODO: registers SIGINT/SIGTERM handlers that restore the stty mode via - // shell_exec('stty ...'). pcntl signal handlers have no faithful Rust - // equivalent in Phase A. - let _stty_mode = shirabe_php_shim::shell_exec("stty -g"); - for _signal in [shirabe_php_shim::SIGINT, shirabe_php_shim::SIGTERM] { - todo!("register signal handler to restore stty mode"); - } - } - } - - if self.dispatcher.is_some() { - // TODO(plugin): for each signal, register a handler that dispatches ConsoleSignalEvent. - for &signal in &self.signals_to_dispatch_event.clone() { - let _event = ConsoleSignalEvent::new( - todo!("Box<dyn Command>"), - todo!("Box<dyn InputInterface>"), - todo!("Box<dyn OutputInterface>"), - signal, - ); - todo!("register signal handler dispatching ConsoleEvents::SIGNAL"); - } - } - - for _signal in command_signals { - // $this->signalRegistry->register($signal, [$command, 'handleSignal']); - todo!("register command->handle_signal as signal handler"); - } - } - - if self.dispatcher.is_none() { - return command.borrow_mut().run( - &mut *borrow_input_mut(&input), - &mut *borrow_output_mut(&output), - ); - } - - // bind before the console.command event, so the listeners have access to input options/arguments - match (|| -> anyhow::Result<()> { - command.borrow_mut().merge_application_definition(true); - input.borrow_mut().bind(command.borrow().get_definition())?; - Ok(()) - })() { - Ok(()) => {} - Err(e) => { - // ignore invalid options/arguments for now, to allow the event listeners to customize the InputDefinition - if !is_exception_interface(&e) { - return Err(e); - } - } - } - - // TODO(plugin): the whole dispatcher block below drives ConsoleCommandEvent / - // ConsoleErrorEvent / ConsoleTerminateEvent. The event objects require Box<dyn ...> - // wrappers for input/output/command and the dispatcher's dispatch() contract; their - // construction is left to the plugin/event design. - let _ = ConsoleCommandEvent::RETURN_CODE_DISABLED; - let _ = std::marker::PhantomData::<( - ConsoleCommandEvent, - ConsoleErrorEvent, - ConsoleTerminateEvent, - )>; - todo!("dispatcher-driven command run (console.command / console.error / console.terminate)") - } - - /// Gets the name of the command based on input. - pub fn get_command_name(&self, input: &dyn InputInterface) -> Option<String> { - if self.single_command { - Some(self.default_command.clone()) - } else { - input.get_first_argument() - } - } - - /// Gets the default input definition. - pub fn get_default_input_definition(&self) -> InputDefinition { - use crate::symfony::console::input::input_definition::DefinitionItem; - InputDefinition::new(vec![ - DefinitionItem::InputArgument( - InputArgument::new( - "command".to_string(), - Some(InputArgument::REQUIRED), - "The command to execute".to_string(), - PhpMixed::Null, - ) - .unwrap(), - ), - DefinitionItem::InputOption( - InputOption::new( - "--help", - PhpMixed::from("-h".to_string()), - Some(InputOption::VALUE_NONE), - format!( - "Display help for the given command. When no command is given display help for the <info>{}</info> command", - self.default_command - ), - PhpMixed::Null, - ) - .unwrap(), - ), - DefinitionItem::InputOption( - InputOption::new( - "--quiet", - PhpMixed::from("-q".to_string()), - Some(InputOption::VALUE_NONE), - "Do not output any message".to_string(), - PhpMixed::Null, - ) - .unwrap(), - ), - DefinitionItem::InputOption( - InputOption::new( - "--verbose", - PhpMixed::from("-v|vv|vvv".to_string()), - Some(InputOption::VALUE_NONE), - "Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug".to_string(), - PhpMixed::Null, - ) - .unwrap(), - ), - DefinitionItem::InputOption( - InputOption::new( - "--version", - PhpMixed::from("-V".to_string()), - Some(InputOption::VALUE_NONE), - "Display this application version".to_string(), - PhpMixed::Null, - ) - .unwrap(), - ), - DefinitionItem::InputOption( - InputOption::new( - "--ansi", - PhpMixed::from("".to_string()), - Some(InputOption::VALUE_NEGATABLE), - "Force (or disable --no-ansi) ANSI output".to_string(), - PhpMixed::Null, - ) - .unwrap(), - ), - DefinitionItem::InputOption( - InputOption::new( - "--no-interaction", - PhpMixed::from("-n".to_string()), - Some(InputOption::VALUE_NONE), - "Do not ask any interactive question".to_string(), - PhpMixed::Null, - ) - .unwrap(), - ), - ]) - .unwrap() - } - - /// Gets the default commands that should always be available. - pub fn get_default_commands(&self) -> Vec<Rc<RefCell<dyn Command>>> { - // return [new HelpCommand(), new ListCommand(), new CompleteCommand(), new DumpCompletionCommand()]; - // TODO(review): HelpCommand/ListCommand/CompleteCommand/DumpCompletionCommand are ported as - // distinct structs (not subtypes of the concrete `Command` struct), so they cannot populate - // a Vec<Rc<RefCell<dyn Command>>>. Reconciling Command subclassing with Rust requires the - // command-hierarchy design decision (see also `add`/`find`/LazyCommand handling). - let _ = std::marker::PhantomData::<( - HelpCommand, - ListCommand, - CompleteCommand, - DumpCompletionCommand, - )>; - todo!("construct default commands once Command-subclass representation is decided") - } - - /// Gets the default helper set with the helpers that should always be available. - pub fn get_default_helper_set(&self) -> Rc<RefCell<HelperSet>> { - use crate::symfony::console::helper::helper_interface::HelperInterface; - let helper_set = Rc::new(RefCell::new(HelperSet::default())); - let helpers: IndexMap< - crate::symfony::console::helper::helper_set::HelperSetKey, - Rc<RefCell<dyn HelperInterface>>, - > = { - let mut m: IndexMap< - crate::symfony::console::helper::helper_set::HelperSetKey, - Rc<RefCell<dyn HelperInterface>>, - > = IndexMap::new(); - m.insert( - crate::symfony::console::helper::helper_set::HelperSetKey::Int(0), - Rc::new(RefCell::new(FormatterHelper::default())), - ); - m.insert( - crate::symfony::console::helper::helper_set::HelperSetKey::Int(1), - Rc::new(RefCell::new(DebugFormatterHelper::default())), - ); - m.insert( - crate::symfony::console::helper::helper_set::HelperSetKey::Int(2), - Rc::new(RefCell::new(ProcessHelper::default())), - ); - m.insert( - crate::symfony::console::helper::helper_set::HelperSetKey::Int(3), - Rc::new(RefCell::new(QuestionHelper::default())), - ); - m - }; - HelperSet::new(&helper_set, helpers); - helper_set - } - - /// Returns abbreviated suggestions in string format. - fn get_abbreviation_suggestions(&self, abbrevs: &[String]) -> String { - format!(" {}", shirabe_php_shim::implode("\n ", abbrevs)) - } - - /// Returns the namespace part of the command name. - /// - /// This method is not part of public API and should not be used directly. - pub fn extract_namespace(&self, name: &str, limit: Option<i64>) -> String { - // $parts = explode(':', $name, -1); - let parts = shirabe_php_shim::explode_limit(":", name, -1); - - // implode(':', null === $limit ? $parts : array_slice($parts, 0, $limit)) - match limit { - None => shirabe_php_shim::implode(":", &parts), - Some(limit) => { - let sliced: Vec<String> = parts.into_iter().take(limit.max(0) as usize).collect(); - shirabe_php_shim::implode(":", &sliced) - } - } - } - - /// Finds alternative of $name among $collection, if nothing is found in - /// $collection, try in $abbrevs. - fn find_alternatives(&self, name: &str, collection: &[String]) -> Vec<String> { - let threshold = 1e3; - let mut alternatives: IndexMap<String, f64> = IndexMap::new(); - - let mut collection_parts: IndexMap<String, Vec<String>> = IndexMap::new(); - for item in collection { - collection_parts.insert(item.clone(), shirabe_php_shim::explode(":", item)); - } - - for (i, subname) in shirabe_php_shim::explode(":", name).into_iter().enumerate() { - for (collection_name, parts) in &collection_parts { - let exists = alternatives.contains_key(collection_name); - if parts.get(i).is_none() && exists { - *alternatives.get_mut(collection_name).unwrap() += threshold; - continue; - } else if parts.get(i).is_none() { - continue; - } - - let lev = shirabe_php_shim::levenshtein(&subname, &parts[i]) as f64; - if lev <= shirabe_php_shim::strlen(&subname) as f64 / 3.0 - || (!subname.is_empty() && parts[i].contains(&subname)) - { - let v = if exists { - alternatives[collection_name] + lev - } else { - lev - }; - alternatives.insert(collection_name.clone(), v); - } else if exists { - *alternatives.get_mut(collection_name).unwrap() += threshold; - } - } - } - - for item in collection { - let lev = shirabe_php_shim::levenshtein(name, item) as f64; - if lev <= shirabe_php_shim::strlen(name) as f64 / 3.0 || item.contains(name) { - let v = if alternatives.contains_key(item) { - alternatives[item] - lev - } else { - lev - }; - alternatives.insert(item.clone(), v); - } - } - - // array_filter($alternatives, fn($lev) => $lev < 2 * $threshold) - alternatives.retain(|_, lev| *lev < 2.0 * threshold); - // ksort($alternatives, SORT_NATURAL | SORT_FLAG_CASE) - let mut keys: Vec<String> = alternatives.keys().cloned().collect(); - shirabe_php_shim::sort_natural_flag_case(&mut keys); - - keys - } - - /// Sets the default Command name. - pub fn set_default_command( - &mut self, - command_name: &str, - is_single_command: bool, - ) -> anyhow::Result<&mut Self> { - // $this->defaultCommand = explode('|', ltrim($commandName, '|'))[0]; - let trimmed = shirabe_php_shim::ltrim(command_name, Some("|")); - self.default_command = shirabe_php_shim::explode("|", &trimmed) - .into_iter() - .next() - .unwrap_or_default(); - - if is_single_command { - // Ensure the command exist - self.find(command_name)?; - - self.single_command = true; - } - - Ok(self) - } - - /// @internal - pub fn is_single_command(&self) -> bool { - self.single_command - } - - fn split_string_by_width(&self, string: &str, width: i64) -> Vec<String> { - // str_split is not suitable for multi-byte characters, we should use preg_split to get char array properly. - let encoding = match shirabe_php_shim::mb_detect_encoding(string, None, true) { - None => return shirabe_php_shim::str_split(string, width), - Some(encoding) => encoding, - }; - - let utf8_string = shirabe_php_shim::mb_convert_encoding(string.into(), "utf8", &encoding); - let mut lines: Vec<String> = Vec::new(); - let mut line = String::new(); - - let mut offset = 0i64; - let mut m: Vec<String> = Vec::new(); - while shirabe_php_shim::preg_match_offset(r"/.{1,10000}/u", &utf8_string, &mut m, 0, offset) - { - offset += shirabe_php_shim::strlen(&m[0]); - - for char in shirabe_php_shim::preg_split_chars(r"//u", &m[0]) { - // test if $char could be appended to current line - if shirabe_php_shim::mb_strwidth(&format!("{}{}", line, char), Some("utf8")) - <= width - { - line.push_str(&char); - continue; - } - // if not, push current line to array and make new line - lines.push(shirabe_php_shim::str_pad( - &line, - width as usize, - " ", - shirabe_php_shim::STR_PAD_LEFT, - )); - line = char; - } - } - - lines.push(if !lines.is_empty() { - shirabe_php_shim::str_pad(&line, width as usize, " ", shirabe_php_shim::STR_PAD_LEFT) - } else { - line.clone() - }); - - shirabe_php_shim::mb_convert_variables(&encoding, "utf8", &mut lines); - - lines - } - - /// Returns all namespaces of the command name. - fn extract_all_namespaces(&self, name: &str) -> Vec<String> { - // -1 as third argument is needed to skip the command short name when exploding - let parts = shirabe_php_shim::explode_limit(":", name, -1); - let mut namespaces: Vec<String> = Vec::new(); - - for part in parts { - if !namespaces.is_empty() { - let last = namespaces.last().unwrap().clone(); - namespaces.push(format!("{}:{}", last, part)); - } else { - namespaces.push(part); - } - } - - namespaces - } - - fn init(&mut self) -> anyhow::Result<()> { - if self.initialized { - return Ok(()); - } - self.initialized = true; - - for command in self.get_default_commands() { - self.add(command)?; - } - - Ok(()) - } -} - -impl ResetInterface for Application { - fn reset(&mut self) { - Application::reset(self) - } -} - -/// Helper mirroring PHP's `$e instanceof ExceptionInterface`. -fn is_exception_interface(e: &anyhow::Error) -> bool { - // anyhow::Error stores concrete error types; enumerate the console exceptions - // that implement ExceptionInterface (PHP's `$e instanceof ExceptionInterface`). - e.downcast_ref::<CommandNotFoundException>().is_some() - || e.downcast_ref::<NamespaceNotFoundException>().is_some() - || e.downcast_ref::<LogicException>().is_some() - || e.downcast_ref::<RuntimeException>().is_some() -} - -/// Helper mirroring PHP's `$e instanceof CommandNotFoundException`. -fn downcast_command_not_found(e: &anyhow::Error) -> Option<&CommandNotFoundException> { - if let Some(cnf) = e.downcast_ref::<CommandNotFoundException>() { - return Some(cnf); - } - e.downcast_ref::<NamespaceNotFoundException>().map(|n| &n.0) -} - -/// Helper mirroring PHP's `$e instanceof NamespaceNotFoundException`. -fn is_namespace_not_found(e: &anyhow::Error) -> bool { - e.downcast_ref::<NamespaceNotFoundException>().is_some() -} - -/// Borrows the shared input as a mutable `dyn InputInterface` for passing to -/// `Command::run`, which takes `&mut dyn InputInterface`. -fn borrow_input_mut( - input: &Rc<RefCell<dyn InputInterface>>, -) -> std::cell::RefMut<'_, dyn InputInterface> { - input.borrow_mut() -} - -/// Borrows the shared output as a mutable `dyn OutputInterface` for passing to -/// `Command::run`, which takes `&mut dyn OutputInterface`. -fn borrow_output_mut( - output: &Rc<RefCell<dyn OutputInterface>>, -) -> std::cell::RefMut<'_, dyn OutputInterface> { - output.borrow_mut() + ) -> anyhow::Result<()>; } diff --git a/crates/shirabe-external-packages/src/symfony/console/command/command.rs b/crates/shirabe-external-packages/src/symfony/console/command/command.rs index 1af9c59..1640689 100644 --- a/crates/shirabe-external-packages/src/symfony/console/command/command.rs +++ b/crates/shirabe-external-packages/src/symfony/console/command/command.rs @@ -20,7 +20,7 @@ use std::rc::Rc; /// (defined below) and this concrete `BaseCommand` struct holding the base-class /// state and behavior. Subclasses embed a `BaseCommand` and implement `Command`. pub struct BaseCommand { - application: Option<Rc<RefCell<Application>>>, + application: Option<Rc<RefCell<dyn Application>>>, name: Option<String>, process_title: Option<String>, aliases: Vec<String>, @@ -130,7 +130,7 @@ impl BaseCommand { self.ignore_validation_errors = true; } - pub fn set_application(&mut self, application: Option<Rc<RefCell<Application>>>) { + pub fn set_application(&mut self, application: Option<Rc<RefCell<dyn Application>>>) { self.application = application.clone(); if let Some(application) = application { self.set_helper_set(application.borrow_mut().get_helper_set()); @@ -151,7 +151,7 @@ impl BaseCommand { } /// Gets the application instance for this command. - pub fn get_application(&self) -> Option<Rc<RefCell<Application>>> { + pub fn get_application(&self) -> Option<Rc<RefCell<dyn Application>>> { self.application.clone() } @@ -709,11 +709,11 @@ pub trait Command: std::fmt::Debug + shirabe_php_shim::AsAny { todo!() } - fn set_application(&mut self, _application: Option<Rc<RefCell<Application>>>) { + fn set_application(&mut self, _application: Option<Rc<RefCell<dyn Application>>>) { todo!() } - fn get_application(&self) -> Option<Rc<RefCell<Application>>> { + fn get_application(&self) -> Option<Rc<RefCell<dyn Application>>> { todo!() } diff --git a/crates/shirabe-external-packages/src/symfony/console/command/complete_command.rs b/crates/shirabe-external-packages/src/symfony/console/command/complete_command.rs index 7a986ec..e8a319e 100644 --- a/crates/shirabe-external-packages/src/symfony/console/command/complete_command.rs +++ b/crates/shirabe-external-packages/src/symfony/console/command/complete_command.rs @@ -396,14 +396,14 @@ impl Command for CompleteCommand { fn set_application( &mut self, - application: Option<Rc<RefCell<crate::symfony::console::application::Application>>>, + application: Option<Rc<RefCell<dyn crate::symfony::console::application::Application>>>, ) { self.inner.set_application(application); } fn get_application( &self, - ) -> Option<Rc<RefCell<crate::symfony::console::application::Application>>> { + ) -> Option<Rc<RefCell<dyn crate::symfony::console::application::Application>>> { self.inner.get_application() } diff --git a/crates/shirabe-external-packages/src/symfony/console/command/dump_completion_command.rs b/crates/shirabe-external-packages/src/symfony/console/command/dump_completion_command.rs index 51287c7..1929c64 100644 --- a/crates/shirabe-external-packages/src/symfony/console/command/dump_completion_command.rs +++ b/crates/shirabe-external-packages/src/symfony/console/command/dump_completion_command.rs @@ -230,14 +230,14 @@ impl Command for DumpCompletionCommand { fn set_application( &mut self, - application: Option<Rc<RefCell<crate::symfony::console::application::Application>>>, + application: Option<Rc<RefCell<dyn crate::symfony::console::application::Application>>>, ) { self.inner.set_application(application); } fn get_application( &self, - ) -> Option<Rc<RefCell<crate::symfony::console::application::Application>>> { + ) -> Option<Rc<RefCell<dyn crate::symfony::console::application::Application>>> { self.inner.get_application() } diff --git a/crates/shirabe-external-packages/src/symfony/console/command/help_command.rs b/crates/shirabe-external-packages/src/symfony/console/command/help_command.rs index 0a26931..ec2de93 100644 --- a/crates/shirabe-external-packages/src/symfony/console/command/help_command.rs +++ b/crates/shirabe-external-packages/src/symfony/console/command/help_command.rs @@ -162,14 +162,14 @@ impl Command for HelpCommand { fn set_application( &mut self, - application: Option<Rc<RefCell<crate::symfony::console::application::Application>>>, + application: Option<Rc<RefCell<dyn crate::symfony::console::application::Application>>>, ) { self.inner.set_application(application); } fn get_application( &self, - ) -> Option<Rc<RefCell<crate::symfony::console::application::Application>>> { + ) -> Option<Rc<RefCell<dyn crate::symfony::console::application::Application>>> { self.inner.get_application() } diff --git a/crates/shirabe-external-packages/src/symfony/console/command/lazy_command.rs b/crates/shirabe-external-packages/src/symfony/console/command/lazy_command.rs index 47f73bf..924d4bd 100644 --- a/crates/shirabe-external-packages/src/symfony/console/command/lazy_command.rs +++ b/crates/shirabe-external-packages/src/symfony/console/command/lazy_command.rs @@ -78,7 +78,7 @@ impl LazyCommand { self.get_command().ignore_validation_errors(); } - pub fn set_application(&mut self, application: Option<Rc<RefCell<Application>>>) { + pub fn set_application(&mut self, application: Option<Rc<RefCell<dyn Application>>>) { // if ($this->command instanceof parent) if let LazyCommandInner::Command(command) = &mut self.command { command.set_application(application.clone()); @@ -288,11 +288,11 @@ impl Command for LazyCommand { todo!() } - fn set_application(&mut self, application: Option<Rc<RefCell<Application>>>) { + fn set_application(&mut self, application: Option<Rc<RefCell<dyn Application>>>) { LazyCommand::set_application(self, application); } - fn get_application(&self) -> Option<Rc<RefCell<Application>>> { + fn get_application(&self) -> Option<Rc<RefCell<dyn Application>>> { self.inner.get_application() } diff --git a/crates/shirabe-external-packages/src/symfony/console/command/list_command.rs b/crates/shirabe-external-packages/src/symfony/console/command/list_command.rs index 7581c90..5203be1 100644 --- a/crates/shirabe-external-packages/src/symfony/console/command/list_command.rs +++ b/crates/shirabe-external-packages/src/symfony/console/command/list_command.rs @@ -162,14 +162,14 @@ impl Command for ListCommand { fn set_application( &mut self, - application: Option<Rc<RefCell<crate::symfony::console::application::Application>>>, + application: Option<Rc<RefCell<dyn crate::symfony::console::application::Application>>>, ) { self.inner.set_application(application); } fn get_application( &self, - ) -> Option<Rc<RefCell<crate::symfony::console::application::Application>>> { + ) -> Option<Rc<RefCell<dyn crate::symfony::console::application::Application>>> { self.inner.get_application() } diff --git a/crates/shirabe-external-packages/src/symfony/console/descriptor/application_description.rs b/crates/shirabe-external-packages/src/symfony/console/descriptor/application_description.rs index 3454b78..ff14c22 100644 --- a/crates/shirabe-external-packages/src/symfony/console/descriptor/application_description.rs +++ b/crates/shirabe-external-packages/src/symfony/console/descriptor/application_description.rs @@ -9,7 +9,7 @@ use std::rc::Rc; /// @internal #[derive(Debug)] pub struct ApplicationDescription { - application: Rc<RefCell<Application>>, + application: Rc<RefCell<dyn Application>>, namespace: Option<String>, show_hidden: bool, @@ -28,7 +28,7 @@ impl ApplicationDescription { pub const GLOBAL_NAMESPACE: &'static str = "_global"; pub fn new( - application: Rc<RefCell<Application>>, + application: Rc<RefCell<dyn Application>>, namespace: Option<String>, show_hidden: bool, ) -> Self { diff --git a/crates/shirabe-external-packages/src/symfony/console/descriptor/descriptor.rs b/crates/shirabe-external-packages/src/symfony/console/descriptor/descriptor.rs index d4aa575..d086b28 100644 --- a/crates/shirabe-external-packages/src/symfony/console/descriptor/descriptor.rs +++ b/crates/shirabe-external-packages/src/symfony/console/descriptor/descriptor.rs @@ -48,7 +48,7 @@ pub trait Descriptor: DescriptorInterface { } // case $object instanceof Application: _ if todo!("$object instanceof Application") => { - let application: std::rc::Rc<std::cell::RefCell<Application>> = + let application: std::rc::Rc<std::cell::RefCell<dyn Application>> = todo!("downcast object to Application"); self.describe_application(application, options)?; } @@ -113,7 +113,7 @@ pub trait Descriptor: DescriptorInterface { /// Describes an Application instance. fn describe_application( &mut self, - application: std::rc::Rc<std::cell::RefCell<Application>>, + application: std::rc::Rc<std::cell::RefCell<dyn Application>>, options: IndexMap<String, PhpMixed>, ) -> anyhow::Result<()>; } diff --git a/crates/shirabe-external-packages/src/symfony/console/descriptor/json_descriptor.rs b/crates/shirabe-external-packages/src/symfony/console/descriptor/json_descriptor.rs index b651618..a74fc4b 100644 --- a/crates/shirabe-external-packages/src/symfony/console/descriptor/json_descriptor.rs +++ b/crates/shirabe-external-packages/src/symfony/console/descriptor/json_descriptor.rs @@ -62,7 +62,7 @@ impl JsonDescriptor { fn describe_application( &mut self, - application: std::rc::Rc<std::cell::RefCell<Application>>, + application: std::rc::Rc<std::cell::RefCell<dyn Application>>, options: IndexMap<String, PhpMixed>, ) -> anyhow::Result<()> { let described_namespace = match options.get("namespace") { @@ -418,7 +418,7 @@ impl Descriptor for JsonDescriptor { fn describe_application( &mut self, - application: std::rc::Rc<std::cell::RefCell<Application>>, + application: std::rc::Rc<std::cell::RefCell<dyn Application>>, options: IndexMap<String, PhpMixed>, ) -> anyhow::Result<()> { JsonDescriptor::describe_application(self, application, options) diff --git a/crates/shirabe-external-packages/src/symfony/console/descriptor/markdown_descriptor.rs b/crates/shirabe-external-packages/src/symfony/console/descriptor/markdown_descriptor.rs index 1fec085..90cad25 100644 --- a/crates/shirabe-external-packages/src/symfony/console/descriptor/markdown_descriptor.rs +++ b/crates/shirabe-external-packages/src/symfony/console/descriptor/markdown_descriptor.rs @@ -214,7 +214,7 @@ impl MarkdownDescriptor { fn describe_application( &mut self, - application: std::rc::Rc<std::cell::RefCell<Application>>, + application: std::rc::Rc<std::cell::RefCell<dyn Application>>, options: IndexMap<String, PhpMixed>, ) -> anyhow::Result<()> { let described_namespace = match options.get("namespace") { @@ -223,7 +223,7 @@ impl MarkdownDescriptor { }; let mut description = ApplicationDescription::new(application.clone(), described_namespace, false); - let title = self.get_application_title(&application.borrow()); + let title = self.get_application_title(&*application.borrow()); self.write( &format!( @@ -289,7 +289,7 @@ impl MarkdownDescriptor { Ok(()) } - fn get_application_title(&self, application: &Application) -> String { + fn get_application_title(&self, application: &dyn Application) -> String { if "UNKNOWN" != application.get_name() { if "UNKNOWN" != application.get_version() { return format!( @@ -376,7 +376,7 @@ impl Descriptor for MarkdownDescriptor { fn describe_application( &mut self, - application: std::rc::Rc<std::cell::RefCell<Application>>, + application: std::rc::Rc<std::cell::RefCell<dyn Application>>, options: IndexMap<String, PhpMixed>, ) -> anyhow::Result<()> { MarkdownDescriptor::describe_application(self, application, options) diff --git a/crates/shirabe-external-packages/src/symfony/console/descriptor/text_descriptor.rs b/crates/shirabe-external-packages/src/symfony/console/descriptor/text_descriptor.rs index b5faaae..323b284 100644 --- a/crates/shirabe-external-packages/src/symfony/console/descriptor/text_descriptor.rs +++ b/crates/shirabe-external-packages/src/symfony/console/descriptor/text_descriptor.rs @@ -262,7 +262,7 @@ impl TextDescriptor { fn describe_application( &mut self, - application: std::rc::Rc<std::cell::RefCell<Application>>, + application: std::rc::Rc<std::cell::RefCell<dyn Application>>, options: IndexMap<String, PhpMixed>, ) -> anyhow::Result<()> { let described_namespace = match options.get("namespace") { @@ -597,7 +597,7 @@ impl Descriptor for TextDescriptor { fn describe_application( &mut self, - application: std::rc::Rc<std::cell::RefCell<Application>>, + application: std::rc::Rc<std::cell::RefCell<dyn Application>>, options: IndexMap<String, PhpMixed>, ) -> anyhow::Result<()> { TextDescriptor::describe_application(self, application, options) diff --git a/crates/shirabe-external-packages/src/symfony/console/descriptor/xml_descriptor.rs b/crates/shirabe-external-packages/src/symfony/console/descriptor/xml_descriptor.rs index 209964a..2ed8f17 100644 --- a/crates/shirabe-external-packages/src/symfony/console/descriptor/xml_descriptor.rs +++ b/crates/shirabe-external-packages/src/symfony/console/descriptor/xml_descriptor.rs @@ -106,7 +106,7 @@ impl XmlDescriptor { pub fn get_application_document( &self, - application: std::rc::Rc<std::cell::RefCell<Application>>, + application: std::rc::Rc<std::cell::RefCell<dyn Application>>, namespace: Option<String>, short: bool, ) -> DOMDocument { @@ -209,7 +209,7 @@ impl XmlDescriptor { fn describe_application( &mut self, - application: std::rc::Rc<std::cell::RefCell<Application>>, + application: std::rc::Rc<std::cell::RefCell<dyn Application>>, options: IndexMap<String, PhpMixed>, ) -> anyhow::Result<()> { let namespace = match options.get("namespace") { @@ -420,7 +420,7 @@ impl Descriptor for XmlDescriptor { fn describe_application( &mut self, - application: std::rc::Rc<std::cell::RefCell<Application>>, + application: std::rc::Rc<std::cell::RefCell<dyn Application>>, options: IndexMap<String, PhpMixed>, ) -> anyhow::Result<()> { XmlDescriptor::describe_application(self, application, options) diff --git a/crates/shirabe-external-packages/src/symfony/console/formatter/output_formatter_style_stack.rs b/crates/shirabe-external-packages/src/symfony/console/formatter/output_formatter_style_stack.rs index 2b290db..71474bc 100644 --- a/crates/shirabe-external-packages/src/symfony/console/formatter/output_formatter_style_stack.rs +++ b/crates/shirabe-external-packages/src/symfony/console/formatter/output_formatter_style_stack.rs @@ -1,7 +1,6 @@ use crate::symfony::console::exception::invalid_argument_exception::InvalidArgumentException; use crate::symfony::console::formatter::output_formatter_style::OutputFormatterStyle; use crate::symfony::console::formatter::output_formatter_style_interface::OutputFormatterStyleInterface; -use crate::symfony::contracts::service::reset_interface::ResetInterface; #[derive(Debug)] pub struct OutputFormatterStyleStack { @@ -98,11 +97,8 @@ impl OutputFormatterStyleStack { pub fn get_empty_style(&self) -> &dyn OutputFormatterStyleInterface { self.empty_style.as_ref() } -} -impl ResetInterface for OutputFormatterStyleStack { - /// Resets stack (ie. empty internal arrays). - fn reset(&mut self) { + pub fn reset(&mut self) { self.styles = vec![]; } } diff --git a/crates/shirabe-external-packages/src/symfony/contracts/mod.rs b/crates/shirabe-external-packages/src/symfony/contracts/mod.rs index ad1b417..36260a8 100644 --- a/crates/shirabe-external-packages/src/symfony/contracts/mod.rs +++ b/crates/shirabe-external-packages/src/symfony/contracts/mod.rs @@ -1,5 +1,3 @@ pub mod event_dispatcher; -pub mod service; pub use event_dispatcher::*; -pub use service::*; diff --git a/crates/shirabe-external-packages/src/symfony/contracts/service/mod.rs b/crates/shirabe-external-packages/src/symfony/contracts/service/mod.rs deleted file mode 100644 index 5dbf535..0000000 --- a/crates/shirabe-external-packages/src/symfony/contracts/service/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod reset_interface; - -pub use reset_interface::*; diff --git a/crates/shirabe-external-packages/src/symfony/contracts/service/reset_interface.rs b/crates/shirabe-external-packages/src/symfony/contracts/service/reset_interface.rs deleted file mode 100644 index cdfe2cf..0000000 --- a/crates/shirabe-external-packages/src/symfony/contracts/service/reset_interface.rs +++ /dev/null @@ -1,4 +0,0 @@ -/// Mirror of Symfony's `ResetInterface`. -pub trait ResetInterface { - fn reset(&mut self); -} diff --git a/crates/shirabe-php-shim/src/lib.rs b/crates/shirabe-php-shim/src/lib.rs index 2142a04..59626ea 100644 --- a/crates/shirabe-php-shim/src/lib.rs +++ b/crates/shirabe-php-shim/src/lib.rs @@ -991,8 +991,20 @@ pub trait JsonSerializable { fn json_serialize(&self) -> PhpMixed; } -pub fn in_array(_needle: PhpMixed, _haystack: &PhpMixed, _strict: bool) -> bool { - todo!() +pub fn in_array(needle: PhpMixed, haystack: &PhpMixed, strict: bool) -> bool { + let values: Vec<&PhpMixed> = match haystack { + PhpMixed::List(items) => items.iter().map(|item| item.as_ref()).collect(), + PhpMixed::Array(map) => map.values().map(|item| item.as_ref()).collect(), + _ => return false, + }; + + if !strict { + // TODO(phase-c): non-strict in_array needs PHP's loose `==` comparison semantics. Only the + // strict path is implemented; loose comparison is deferred rather than approximated. + todo!("non-strict in_array (PHP loose comparison)"); + } + + values.iter().any(|value| **value == needle) } // TODO(phase-c): takes &Path and returns Option<PathBuf> @@ -1963,8 +1975,8 @@ pub fn mb_strlen(_s: &str, _encoding: &str) -> i64 { todo!() } -pub fn stream_isatty(_stream: PhpMixed) -> bool { - todo!() +pub fn stream_isatty(stream: PhpResource) -> bool { + stream_isatty_resource(&stream) } pub fn posix_getuid() -> i64 { @@ -1979,11 +1991,11 @@ pub fn posix_getpwuid(_uid: i64) -> PhpMixed { todo!() } -pub fn posix_isatty(_stream: PhpMixed) -> bool { +pub fn posix_isatty(_stream: PhpResource) -> bool { todo!() } -pub fn fstat(_stream: PhpMixed) -> PhpMixed { +pub fn fstat(_stream: PhpResource) -> PhpMixed { todo!() } @@ -2010,6 +2022,7 @@ pub fn putenv(setting: &str) -> bool { /// PHP superglobal $_SERVER access. In the CLI SAPI $_SERVER is populated from /// the environment, which is the only source modeled here. pub fn server_get(name: &str) -> Option<String> { + // TODO: is var_os() better? std::env::var(name).ok() } @@ -2023,9 +2036,10 @@ pub fn server_contains_key(name: &str) -> bool { std::env::var_os(name).is_some() } -/// PHP superglobal $_ENV access -pub fn env_get(_name: &str) -> Option<String> { - todo!() +/// PHP superglobal $_ENV access. +pub fn env_get(name: &str) -> Option<String> { + // TODO: is var_os() better? + std::env::var(name).ok() } // TODO(php-runtime): modify the real PHP's $_ENV. @@ -2034,8 +2048,8 @@ pub fn env_set(_name: &str, _value: String) {} // TODO(php-runtime): modify the real PHP's $_ENV. pub fn env_unset(_name: &str) {} -pub fn env_contains_key(_name: &str) -> bool { - todo!() +pub fn env_contains_key(name: &str) -> bool { + std::env::var_os(name).is_some() } pub fn trim(_s: &str, _chars: Option<&str>) -> String { @@ -2518,10 +2532,6 @@ pub fn round(_value: f64, _precision: i64) -> f64 { todo!() } -pub fn stdin_handle() -> PhpMixed { - todo!() -} - pub fn composer_dev_warning_time() -> i64 { todo!() } diff --git a/crates/shirabe/src/console/application.rs b/crates/shirabe/src/console/application.rs index 810e283..b97a5d5 100644 --- a/crates/shirabe/src/console/application.rs +++ b/crates/shirabe/src/console/application.rs @@ -5,21 +5,51 @@ use indexmap::IndexMap; use shirabe_external_packages::composer::xdebug_handler::XdebugHandler; use shirabe_external_packages::seld::json_lint::ParsingException; -use shirabe_external_packages::symfony::console::Application as BaseApplication; -use shirabe_external_packages::symfony::console::command::Command; +use shirabe_external_packages::symfony::console::application::Application as BaseApplication; +use shirabe_external_packages::symfony::console::command::Command as SymfonyCommand; +use shirabe_external_packages::symfony::console::command::lazy_command::LazyCommand; +use shirabe_external_packages::symfony::console::command::signalable_command_interface::SignalableCommandInterface; +use shirabe_external_packages::symfony::console::command_loader::command_loader_interface::CommandLoaderInterface; +use shirabe_external_packages::symfony::console::completion::completion_input::CompletionInput; +use shirabe_external_packages::symfony::console::completion::completion_suggestions::CompletionSuggestions; +use shirabe_external_packages::symfony::console::console_events::ConsoleEvents; +use shirabe_external_packages::symfony::console::event::console_command_event::ConsoleCommandEvent; +use shirabe_external_packages::symfony::console::event::console_error_event::ConsoleErrorEvent; +use shirabe_external_packages::symfony::console::event::console_signal_event::ConsoleSignalEvent; +use shirabe_external_packages::symfony::console::event::console_terminate_event::ConsoleTerminateEvent; use shirabe_external_packages::symfony::console::exception::CommandNotFoundException; use shirabe_external_packages::symfony::console::exception::ExceptionInterface; +use shirabe_external_packages::symfony::console::exception::logic_exception::LogicException as ConsoleLogicException; +use shirabe_external_packages::symfony::console::exception::namespace_not_found_exception::NamespaceNotFoundException; +use shirabe_external_packages::symfony::console::exception::runtime_exception::RuntimeException as ConsoleRuntimeException; +use shirabe_external_packages::symfony::console::formatter::output_formatter::OutputFormatter; use shirabe_external_packages::symfony::console::helper::HelperInterface; use shirabe_external_packages::symfony::console::helper::HelperSet; use shirabe_external_packages::symfony::console::helper::HelperSetKey; use shirabe_external_packages::symfony::console::helper::QuestionHelper; +use shirabe_external_packages::symfony::console::helper::debug_formatter_helper::DebugFormatterHelper; +use shirabe_external_packages::symfony::console::helper::formatter_helper::{ + FormatBlockMessages, FormatterHelper, +}; +use shirabe_external_packages::symfony::console::helper::helper::Helper; +use shirabe_external_packages::symfony::console::helper::process_helper::ProcessHelper; use shirabe_external_packages::symfony::console::input::InputDefinition; use shirabe_external_packages::symfony::console::input::InputInterface; use shirabe_external_packages::symfony::console::input::InputOption; +use shirabe_external_packages::symfony::console::input::argv_input::ArgvInput; +use shirabe_external_packages::symfony::console::input::array_input::ArrayInput; +use shirabe_external_packages::symfony::console::input::input_argument::InputArgument; +use shirabe_external_packages::symfony::console::input::input_aware_interface::InputAwareInterface; use shirabe_external_packages::symfony::console::output::ConsoleOutputInterface; +use shirabe_external_packages::symfony::console::output::console_output::ConsoleOutput; use shirabe_external_packages::symfony::console::output::output_interface::{ self as output_interface, OutputInterface, }; +use shirabe_external_packages::symfony::console::signal_registry::signal_registry::SignalRegistry; +use shirabe_external_packages::symfony::console::style::style_interface::StyleInterface; +use shirabe_external_packages::symfony::console::style::symfony_style::SymfonyStyle; +use shirabe_external_packages::symfony::console::terminal::Terminal; +use shirabe_external_packages::symfony::contracts::event_dispatcher::event_dispatcher_interface::EventDispatcherInterface; use shirabe_external_packages::symfony::process::exception::ProcessTimedOutException; use shirabe_php_shim::{ LogicException as ShimLogicException, PHP_BINARY, PHP_VERSION, PHP_VERSION_ID, PhpMixed, @@ -87,9 +117,29 @@ use crate::util::HttpDownloader; use crate::util::Platform; use crate::util::Silencer; +/// The PHP `Composer\Console\Application` and `Symfony\Component\Console\Application` are +/// flattened into a single struct. Methods that are overridden by subclass and called via +/// `parent::` are prefixed by `base_`. #[derive(Debug)] pub struct Application { - inner: BaseApplication, + commands: IndexMap<String, std::rc::Rc<std::cell::RefCell<dyn SymfonyCommand>>>, + want_helps: bool, + running_command: Option<std::rc::Rc<std::cell::RefCell<dyn SymfonyCommand>>>, + name: String, + version: String, + command_loader: Option<Box<dyn CommandLoaderInterface>>, + catch_exceptions: bool, + auto_exit: bool, + definition: Option<std::rc::Rc<std::cell::RefCell<InputDefinition>>>, + helper_set: Option<std::rc::Rc<std::cell::RefCell<HelperSet>>>, + dispatcher: Option<std::rc::Rc<std::cell::RefCell<dyn EventDispatcherInterface>>>, + terminal: Terminal, + default_command: String, + single_command: bool, + initialized: bool, + signal_registry: Option<SignalRegistry>, + signals_to_dispatch_event: Vec<i64>, + pub(crate) composer: Option<PartialComposerHandle>, pub(crate) io: std::rc::Rc<std::cell::RefCell<dyn IOInterface>>, has_plugin_commands: bool, @@ -103,11 +153,6 @@ impl Application { const LOGO: &'static str = " ______\n / ____/___ ____ ___ ____ ____ ________ _____\n / / / __ \\/ __ `__ \\/ __ \\/ __ \\/ ___/ _ \\/ ___/\n/ /___/ /_/ / / / / / / /_/ / /_/ (__ ) __/ /\n\\____/\\____/_/ /_/ /_/ .___/\\____/____/\\___/_/\n /_/\n"; pub fn new(name: String, mut version: String) -> Self { - // PHP: if (method_exists($this, 'setCatchErrors')) { $this->setCatchErrors(true); } - // This Symfony Console port does not provide `setCatchErrors`, so the guarded call - // is skipped, matching `method_exists` evaluating to false. - - // PHP: static $shutdownRegistered = false; — register only once globally static SHUTDOWN_REGISTERED: std::sync::OnceLock<()> = std::sync::OnceLock::new(); if version == "" { version = composer::get_version(); @@ -147,18 +192,41 @@ impl Application { let initial_working_directory = getcwd(); - // PHP: parent::__construct($name, $version); - let inner = BaseApplication::__construct(&name, &version); - - Self { - inner, + let mut this = Self { + commands: IndexMap::new(), + want_helps: false, + running_command: None, + name, + version, + command_loader: None, + catch_exceptions: true, + auto_exit: true, + definition: None, + helper_set: None, + dispatcher: None, + terminal: Terminal::new(), + default_command: "list".to_string(), + single_command: false, + initialized: false, + signal_registry: None, + signals_to_dispatch_event: Vec::new(), composer: None, io, has_plugin_commands: false, disable_plugins_by_default: false, disable_scripts_by_default: false, initial_working_directory, + }; + if defined("SIGINT") && SignalRegistry::is_supported() { + this.signal_registry = Some(SignalRegistry::new()); + this.signals_to_dispatch_event = vec![ + shirabe_php_shim::SIGINT, + shirabe_php_shim::SIGTERM, + shirabe_php_shim::SIGUSR1, + shirabe_php_shim::SIGUSR2, + ]; } + this } pub fn run( @@ -174,7 +242,7 @@ impl Application { ), }; - self.inner.run(input, output) + self.base_run(input, output) } pub fn do_run( @@ -189,26 +257,14 @@ impl Application { .borrow() .has_parameter_option(PhpMixed::from(vec!["--no-scripts"]), false); - // PHP: static $stdin = null; — cached across doRun calls so the php://stdin handle is - // opened once. - // TODO(phase-c): faithfully caching the stdin resource across calls needs a real - // file-handle/resource model (PhpMixed currently boxes the handle opaquely) plus a - // function-static store. Recomputing per call is observably equivalent for a single - // invocation but differs if doRun is re-entered; deferred until the resource model lands. - let stdin: PhpMixed = if defined("STDIN") { - shirabe_php_shim::stdin_handle() - } else { - shirabe_php_shim::fopen("php://stdin", "r") - }; + let stdin = shirabe_php_shim::STDIN; if Platform::get_env("COMPOSER_TESTS_ARE_RUNNING").as_deref() != Some("1") && (Platform::get_env("COMPOSER_NO_INTERACTION").is_some() - || matches!(stdin, PhpMixed::Null) || !Platform::is_tty(Some(stdin))) { input.borrow_mut().set_interactive(false); } - // PHP: $this->io = new ConsoleIO($input, $output, new HelperSet([new QuestionHelper()])); let mut helpers: IndexMap< HelperSetKey, std::rc::Rc<std::cell::RefCell<dyn HelperInterface>>, @@ -270,7 +326,7 @@ impl Application { let mut command_name: Option<String> = Some(String::new()); let raw_command_name = self.get_command_name_before_binding(input.clone()); if let Some(ref raw) = raw_command_name { - match self.inner.find(raw) { + match self.find(raw) { Ok(cmd) => { // TODO(phase-c): the Symfony Application stub keeps its command registry as // PhpMixed with a todo!() find(), per the "Symfony stays todo!()" policy. @@ -460,7 +516,7 @@ impl Application { match (|| -> anyhow::Result<()> { for command in self.get_plugin_commands()? { let cmd_name = command.get_name().unwrap_or_default(); - if self.inner.has(&cmd_name) { + if self.has(&cmd_name) { // TODO(plugin): PHP uses get_class($command) for the skipped-command class // name. Plugin command discovery (get_plugin_commands) is unimplemented, so // this loop never runs; wire the concrete class name with the plugin API. @@ -512,7 +568,7 @@ impl Application { // determine command name to be executed incl plugin commands, and check if it's a proxy command let is_proxy_command = false; if let Some(ref name) = self.get_command_name_before_binding(input.clone()) { - if let Ok(command) = self.inner.find(name) { + if let Ok(command) = self.find(name) { // TODO(phase-c): same blocker as the earlier find() call — the Symfony command // registry is a PhpMixed/todo!() stub, so the resolved command's name and its // isProxyCommand() flag cannot be recovered until the typed-command registry is @@ -625,7 +681,7 @@ impl Application { str_replace("-", "_", &strtoupper(script)) ); if !defined(&script_event_const) { - if self.inner.has(script) { + if self.has(script) { self.io.write_error(&format!("<warning>A script named {} would override a Composer command and has been skipped</warning>", script)); } else { let mut description = format!( @@ -687,7 +743,7 @@ impl Application { loader.register(false); } - // if the command is not an array of commands, and points to a valid Command subclass, import its details directly + // if the command is not an array of commands, and points to a valid SymfonyCommand subclass, import its details directly let dummy_str = dummy.as_string().unwrap_or("").to_string(); let cmd: PhpMixed = if is_string(dummy) && shirabe_php_shim::class_exists(&dummy_str) @@ -709,14 +765,14 @@ impl Application { ); // TODO(phase-c): the script's command class is built by // reflection (instantiate_class) and stays PhpMixed; the - // SingleCommandApplication / Command typed registry it + // SingleCommandApplication / SymfonyCommand typed registry it // belongs to is an external-package todo!() stub. // let _ = SingleCommandApplication::new; // makes sure the command is find()'able by the name defined in composer.json, and the name isn't overridden in its configure() // TODO(phase-c): cmd is the PhpMixed result of reflection // instantiation; reading/overriding its - // name/description requires the typed Command model that + // name/description requires the typed SymfonyCommand model that // the Symfony stub does not yet provide. let _ = description.clone(); let _ = &mut cmd; @@ -736,7 +792,8 @@ impl Application { }; // Compatibility layer for symfony/console <7.4 - // TODO(phase-c): self.inner.add() takes Rc<RefCell<dyn Command>> + // TODO(phase-c): Application::add() takes Rc<RefCell<dyn + // SymfonyCommand>> // but `cmd` here is the PhpMixed result of reflection-based // plugin command instantiation; registering it as a typed // command instance is blocked on the Symfony command-registry @@ -768,7 +825,7 @@ impl Application { let _ = start_time.unwrap(); } - let result: i64 = self.inner.do_run(input.clone(), output.clone())?; + let result: i64 = self.base_do_run(input.clone(), output.clone())?; if input .borrow() @@ -783,28 +840,28 @@ impl Application { ); } - Ok(result) - })(); + // chdir back to oldWorkingDir if set + if let Some(ref owd) = old_working_dir { + if !owd.is_empty() { + let owd = owd.clone(); + let _ = Silencer::call(|| { + chdir(&owd); + Ok(()) + }); + } + } - // chdir back to oldWorkingDir if set — runs regardless of result - if let Some(ref owd) = old_working_dir { - if !owd.is_empty() { - let owd = owd.clone(); - let _ = Silencer::call(|| { - chdir(&owd); - Ok(()) - }); + if let Some(st) = start_time { + self.io.write_error(&format!( + "<info>Memory usage: {}MiB (peak: {}MiB), time: {}s</info>", + round((memory_get_usage() as f64) / 1024.0 / 1024.0, 2), + round((memory_get_peak_usage(false) as f64) / 1024.0 / 1024.0, 2), + round(microtime(true) - st, 2) + )); } - } - if let Some(st) = start_time { - self.io.write_error(&format!( - "<info>Memory usage: {}MiB (peak: {}MiB), time: {}s</info>", - round((memory_get_usage() as f64) / 1024.0 / 1024.0, 2), - round((memory_get_peak_usage(true) as f64) / 1024.0 / 1024.0, 2), - round(microtime(true) - st, 2) - )); - } + Ok(result) + })(); let outcome = match result_outcome { Ok(r) => Ok(r), @@ -1059,7 +1116,7 @@ impl Application { } else { if required { self.io.write_error(&e.to_string()); - if self.inner.are_exceptions_caught() { + if self.are_exceptions_caught() { std::process::exit(1); } return Err(e); @@ -1081,27 +1138,25 @@ impl Application { } } - /// Delegates to the underlying BaseApplication's `find` method (PHP Symfony Console). - pub fn find(&self, _name: &str) -> anyhow::Result<shirabe_php_shim::PhpMixed> { - todo!() - } - pub fn get_io(&self) -> std::rc::Rc<std::cell::RefCell<dyn IOInterface>> { self.io.clone() } pub fn get_help(&self) -> String { - format!("{}{}", Self::LOGO, self.inner.get_help()) + format!("{}{}", Self::LOGO, self.base_get_help()) } /// Initializes all the composer commands. - pub(crate) fn get_default_commands(&self) -> Vec<Box<dyn Command>> { + pub(crate) fn get_default_commands( + &self, + ) -> Vec<std::rc::Rc<std::cell::RefCell<dyn SymfonyCommand>>> { // PHP: array_merge(parent::getDefaultCommands(), [new AboutCommand(), ...]). // TODO(phase-c): the composer commands implement the shirabe BaseCommand trait, not the - // Symfony Command trait, and the orphan rule forbids a blanket - // `impl<C: HasBaseCommandData> Command for C`. Each command needs its own `impl Command` - // (or a wrapper) before they can be returned as `Box<dyn Command>`; the parent list is - // likewise a Symfony stub (get_default_commands returns Vec<PhpMixed>/todo!()). + // Symfony SymfonyCommand trait, and the orphan rule forbids a blanket + // `impl<C: HasBaseCommandData> SymfonyCommand for C`. Each command needs its own `impl + // SymfonyCommand` + // (or a wrapper) before they can be merged with `base_get_default_commands()` and returned; + // the parent list (`base_get_default_commands`) is likewise a stub. vec![] } @@ -1133,15 +1188,15 @@ impl Application { format!( "<info>{}</info> version <comment>{}{}</comment> {}", - self.inner.get_name(), - self.inner.get_version(), + self.get_name(), + self.get_version(), branch_alias_string, composer::RELEASE_DATE, ) } pub(crate) fn get_default_input_definition(&self) -> anyhow::Result<InputDefinition> { - let mut definition = self.inner.get_default_input_definition(); + let mut definition = self.base_get_default_input_definition(); definition.add_option(InputOption::new( "--profile", PhpMixed::Null, @@ -1181,9 +1236,9 @@ impl Application { Ok(definition) } - fn get_plugin_commands(&mut self) -> anyhow::Result<Vec<Box<dyn Command>>> { + fn get_plugin_commands(&mut self) -> anyhow::Result<Vec<Box<dyn SymfonyCommand>>> { // TODO(plugin): plugin command discovery is part of the plugin API - let commands: Vec<Box<dyn Command>> = vec![]; + let commands: Vec<Box<dyn SymfonyCommand>> = vec![]; // TODO(phase-c): discovering plugin-provided commands walks the PluginManager and // downcasts each plugin's CommandProvider capability — this is the Plugin API surface, @@ -1228,3 +1283,1675 @@ impl Application { function_exists("posix_getuid") && posix_getuid() == 0 } } + +/// Methods inherited from `Symfony\Component\Console\Application`. They live in the same `impl` +/// surface as the Composer overrides above so that polymorphic `self.*` calls dispatch to the +/// Composer version when one exists. Methods that Composer overrides while still calling `parent::` +/// are carried here under a `base_` prefix (`base_run`, `base_do_run`, `base_get_help`, +/// `base_get_default_input_definition`, `base_get_default_commands`). +impl Application { + /// @final + pub fn set_dispatcher( + &mut self, + dispatcher: std::rc::Rc<std::cell::RefCell<dyn EventDispatcherInterface>>, + ) { + // TODO(plugin): the event dispatcher drives ConsoleEvents listeners (plugins). + self.dispatcher = Some(dispatcher); + } + + pub fn set_command_loader(&mut self, command_loader: Box<dyn CommandLoaderInterface>) { + self.command_loader = Some(command_loader); + } + + pub fn get_signal_registry(&self) -> anyhow::Result<&SignalRegistry> { + match &self.signal_registry { + None => Err(ConsoleRuntimeException(shirabe_php_shim::RuntimeException { + message: "Signals are not supported. Make sure that the `pcntl` extension is installed and that \"pcntl_*\" functions are not disabled by your php.ini's \"disable_functions\" directive.".to_string(), + code: 0, + }) + .into()), + Some(signal_registry) => Ok(signal_registry), + } + } + + pub fn set_signals_to_dispatch_event(&mut self, signals_to_dispatch_event: Vec<i64>) { + self.signals_to_dispatch_event = signals_to_dispatch_event; + } + + /// Runs the current application (Symfony base; `parent::run`). + pub fn base_run( + &mut self, + input: Option<std::rc::Rc<std::cell::RefCell<dyn InputInterface>>>, + output: Option<std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>>, + ) -> anyhow::Result<i64> { + if shirabe_php_shim::function_exists("putenv") { + shirabe_php_shim::putenv(&format!("LINES={}", self.terminal.get_height())); + shirabe_php_shim::putenv(&format!("COLUMNS={}", self.terminal.get_width())); + } + + let input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>> = match input { + None => std::rc::Rc::new(std::cell::RefCell::new(ArgvInput::new(None, None)?)), + Some(input) => input, + }; + + let output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>> = match output { + None => std::rc::Rc::new(std::cell::RefCell::new(ConsoleOutput::new( + None, None, None, + )?)), + Some(output) => output, + }; + + // TODO: PHP installs a temporary `set_exception_handler($renderException)` and cooperates + // with Symfony's ErrorHandler to keep/restore it. PHP's process-global exception handler + // stack has no Rust equivalent; the rendering itself is invoked directly in the catch + // branch below. Review needed for the handler save/restore dance. + let render_exception = + |this: &Application, + e: &anyhow::Error, + output: &std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>| { + // if ($output instanceof ConsoleOutputInterface) render to its error output + // TODO(review): downcasting a `dyn OutputInterface` to `ConsoleOutputInterface` + // is not directly expressible; the ConsoleOutputInterface branch needs design. + this.render_throwable(e, output.clone()); + }; + + let result = (|| -> anyhow::Result<i64> { + self.configure_io(&input, &output)?; + + let exit_code = self.do_run(input.clone(), output.clone())?; + + Ok(exit_code) + })(); + + let mut exit_code = match result { + Ok(exit_code) => exit_code, + Err(e) => { + if !self.catch_exceptions { + return Err(e); + } + + render_exception(self, &e, &output); + + // $exitCode = $e->getCode(); + // is_numeric($exitCode) ? max(1, (int) $exitCode) : 1 + // TODO(review): anyhow::Error has no PHP-style getCode(); the exit code derived + // from the exception's `code` field needs the downcast strategy decided. + let exit_code = shirabe_php_shim::php_exception_get_code(&e); + if shirabe_php_shim::is_numeric_string(&exit_code.to_string()) { + let exit_code = exit_code; + if exit_code <= 0 { 1 } else { exit_code } + } else { + 1 + } + } + }; + + // finally: handler restore. See TODO above; no-op here. + + if self.auto_exit { + if exit_code > 255 { + exit_code = 255; + } + + shirabe_php_shim::exit(exit_code); + } + + Ok(exit_code) + } + + /// Runs the current application (Symfony base; `parent::doRun`). + pub fn base_do_run( + &mut self, + input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>, + output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, + ) -> anyhow::Result<i64> { + if input.borrow().has_parameter_option( + PhpMixed::from(vec![ + PhpMixed::from("--version".to_string()), + PhpMixed::from("-V".to_string()), + ]), + true, + ) { + output + .borrow() + .writeln(&[self.get_long_version()], output_interface::OUTPUT_NORMAL); + + return Ok(0); + } + + // Makes ArgvInput::getFirstArgument() able to distinguish an option from an argument. + match input.borrow_mut().bind(&self.get_definition().borrow()) { + Ok(()) => {} + Err(e) => { + // Errors must be ignored, full binding/validation happens later when the command is known. + if !is_exception_interface(&e) { + return Err(e); + } + } + } + + let mut input = input; + let mut name = self.get_command_name(&*input.borrow()); + if input.borrow().has_parameter_option( + PhpMixed::from(vec![ + PhpMixed::from("--help".to_string()), + PhpMixed::from("-h".to_string()), + ]), + true, + ) { + if name.is_none() { + name = Some("help".to_string()); + input = std::rc::Rc::new(std::cell::RefCell::new(ArrayInput::new( + vec![( + PhpMixed::from("command_name".to_string()), + PhpMixed::from(self.default_command.clone()), + )], + None, + )?)); + } else { + self.want_helps = true; + } + } + + let name = match name { + Some(name) => name, + None => { + let name = self.default_command.clone(); + let definition = self.get_definition(); + let command_description = definition + .borrow() + .get_argument(&PhpMixed::from("command".to_string()))? + .get_description() + .to_string(); + let _new_command_argument = InputArgument::new( + "command".to_string(), + Some(InputArgument::OPTIONAL), + command_description, + PhpMixed::from(name.clone()), + )?; + // $definition->setArguments(array_merge($definition->getArguments(), + // ['command' => new InputArgument('command', InputArgument::OPTIONAL, ...)])) + // TODO(review): get_arguments() yields Rc<InputArgument> (shared, non-Clone) while + // set_arguments() consumes owned InputArgument values. Re-building the merged + // argument list requires an InputArgument clone/ownership strategy not yet present. + definition.borrow_mut().set_arguments(todo!( + "merge existing arguments with the new 'command' argument" + ))?; + + name + } + }; + + let command: std::rc::Rc<std::cell::RefCell<dyn SymfonyCommand>>; + let find_result = + (|| -> anyhow::Result<std::rc::Rc<std::cell::RefCell<dyn SymfonyCommand>>> { + self.running_command = None; + // the command name MUST be the first element of the input + self.find(&name) + })(); + + match find_result { + Ok(c) => { + command = c; + } + Err(e) => { + // if (!($e instanceof CommandNotFoundException && !$e instanceof NamespaceNotFoundException) + // || 1 !== count($alternatives = $e->getAlternatives()) || !$input->isInteractive()) + let alternatives: Option<Vec<String>> = downcast_command_not_found(&e) + .filter(|_| !is_namespace_not_found(&e)) + .map(|cnf| cnf.get_alternatives().clone()); + + let single_alternative = match &alternatives { + Some(alts) if alts.len() == 1 => Some(alts[0].clone()), + _ => None, + }; + + if single_alternative.is_none() || !input.borrow().is_interactive() { + let mut e = e; + if self.dispatcher.is_some() { + // TODO(plugin): dispatch ConsoleErrorEvent so listeners can handle/replace the error. + let _event = ConsoleErrorEvent::new( + todo!("wrap input as Box<dyn InputInterface> for the event"), + todo!("wrap output as Box<dyn OutputInterface> for the event"), + todo!("wrap anyhow::Error as Box<dyn Error> for the event"), + None, + ); + let event: ConsoleErrorEvent = _event; + self.dispatcher + .as_ref() + .unwrap() + .borrow_mut() + .dispatch(todo!("event object"), ConsoleEvents::ERROR); + + if event.get_exit_code() == 0 { + return Ok(0); + } + + e = todo!("event.get_error() converted back to anyhow::Error"); + } + + return Err(e); + } + + let alternative = single_alternative.unwrap(); + + let mut style = SymfonyStyle::new(input.clone(), output.clone()); + output + .borrow() + .writeln(&["".to_string()], output_interface::OUTPUT_NORMAL); + let formatted_block = FormatterHelper::default().format_block( + FormatBlockMessages::String(format!( + "Command \"{}\" is not defined.", + PhpMixed::from(name.clone()), + )), + "error", + true, + ); + output + .borrow() + .writeln(&[formatted_block], output_interface::OUTPUT_NORMAL); + if !style.confirm( + &format!( + "Do you want to run \"{}\" instead? ", + PhpMixed::from(alternative.clone()), + ), + false, + ) { + if self.dispatcher.is_some() { + // TODO(plugin): dispatch ConsoleErrorEvent for the declined-alternative case. + let event = ConsoleErrorEvent::new( + todo!("wrap input as Box<dyn InputInterface>"), + todo!("wrap output as Box<dyn OutputInterface>"), + todo!("wrap error as Box<dyn Error>"), + None, + ); + self.dispatcher + .as_ref() + .unwrap() + .borrow_mut() + .dispatch(todo!("event object"), ConsoleEvents::ERROR); + + return Ok(event.get_exit_code()); + } + + return Ok(1); + } + + command = self.find(&alternative)?; + } + } + + // if ($command instanceof LazyCommand) $command = $command->getCommand(); + // TODO(review): LazyCommand is a distinct type from SymfonyCommand here; PHP unwraps the real + // command. The `commands` map stores Rc<RefCell<dyn SymfonyCommand>>, so the LazyCommand-unwrap path + // needs a design decision about how lazy commands are represented. + let _ = std::marker::PhantomData::<LazyCommand>; + + self.running_command = Some(command.clone()); + let exit_code = self.do_run_command(command.clone(), input.clone(), output.clone())?; + self.running_command = None; + + Ok(exit_code) + } + + pub fn set_helper_set(&mut self, helper_set: std::rc::Rc<std::cell::RefCell<HelperSet>>) { + self.helper_set = Some(helper_set); + } + + /// Get the helper set associated with the command. + pub fn get_helper_set(&mut self) -> std::rc::Rc<std::cell::RefCell<HelperSet>> { + if self.helper_set.is_none() { + self.helper_set = Some(self.get_default_helper_set()); + } + + self.helper_set.as_ref().unwrap().clone() + } + + pub fn set_definition(&mut self, definition: std::rc::Rc<std::cell::RefCell<InputDefinition>>) { + self.definition = Some(definition); + } + + /// Gets the InputDefinition related to this Application. + pub fn get_definition(&mut self) -> std::rc::Rc<std::cell::RefCell<InputDefinition>> { + if self.definition.is_none() { + // `get_default_input_definition` is the Composer override (returns a Result because the + // Rust `InputOption::new` is fallible); the option modes are constants that cannot fail, + // so unwrapping mirrors the PHP call that never throws here. + self.definition = Some(std::rc::Rc::new(std::cell::RefCell::new( + self.get_default_input_definition().unwrap(), + ))); + } + + if self.single_command { + let input_definition = self.definition.as_ref().unwrap().clone(); + input_definition + .borrow_mut() + .set_arguments(Vec::new()) + .unwrap(); + + return input_definition; + } + + self.definition.as_ref().unwrap().clone() + } + + /// Adds suggestions to `suggestions` for the current completion input (e.g. option or argument). + pub fn complete( + &mut self, + input: &CompletionInput, + suggestions: &mut CompletionSuggestions, + ) -> anyhow::Result<()> { + if CompletionInput::TYPE_ARGUMENT_VALUE == input.get_completion_type() + && input.get_completion_name().as_deref() == Some("command") + { + let mut command_names: Vec<PhpMixed> = Vec::new(); + for (name, command) in self.all(None)? { + // skip hidden commands and aliased commands as they already get added below + if command.borrow().is_hidden() || command.borrow().get_name() != Some(name.clone()) + { + continue; + } + command_names.push(PhpMixed::from( + command.borrow().get_name().unwrap_or_default(), + )); + for name in command.borrow().get_aliases() { + command_names.push(PhpMixed::from(name)); + } + } + // array_filter($commandNames) + let filtered: Vec<shirabe_external_packages::symfony::console::completion::completion_suggestions::StringOrSuggestion> = + command_names + .into_iter() + .filter(|n| shirabe_php_shim::php_truthy(n)) + .map(|n| { + shirabe_external_packages::symfony::console::completion::completion_suggestions::StringOrSuggestion::String( + shirabe_php_shim::php_to_string(&n), + ) + }) + .collect(); + suggestions.suggest_values(filtered); + + return Ok(()); + } + + if CompletionInput::TYPE_OPTION_NAME == input.get_completion_type() { + // $suggestions->suggestOptions($this->getDefinition()->getOptions()); + // TODO(review): get_options() yields Rc<InputOption> (shared, non-Clone) while + // suggest_options() consumes owned InputOption values; an ownership/clone strategy + // for InputOption is needed. + suggestions.suggest_options(todo!("owned options from get_definition().get_options()")); + + return Ok(()); + } + + Ok(()) + } + + /// Gets the help message (Symfony base; `parent::getHelp`). + pub fn base_get_help(&self) -> String { + self.get_long_version() + } + + /// Gets whether to catch exceptions or not during commands execution. + pub fn are_exceptions_caught(&self) -> bool { + self.catch_exceptions + } + + /// Sets whether to catch exceptions or not during commands execution. + pub fn set_catch_exceptions(&mut self, boolean: bool) { + self.catch_exceptions = boolean; + } + + /// Gets whether to automatically exit after a command execution or not. + pub fn is_auto_exit_enabled(&self) -> bool { + self.auto_exit + } + + /// Sets whether to automatically exit after a command execution or not. + pub fn set_auto_exit(&mut self, boolean: bool) { + self.auto_exit = boolean; + } + + /// Gets the name of the application. + pub fn get_name(&self) -> String { + self.name.clone() + } + + /// Sets the application name. + pub fn set_name(&mut self, name: &str) { + self.name = name.to_string(); + } + + /// Gets the application version. + pub fn get_version(&self) -> String { + self.version.clone() + } + + /// Sets the application version. + pub fn set_version(&mut self, version: &str) { + self.version = version.to_string(); + } + + /// Adds an array of command objects. + /// + /// If a SymfonyCommand is not enabled it will not be added. + pub fn add_commands( + &mut self, + commands: Vec<std::rc::Rc<std::cell::RefCell<dyn SymfonyCommand>>>, + ) -> anyhow::Result<()> { + for command in commands { + self.add(command)?; + } + Ok(()) + } + + /// Adds a command object. + /// + /// If a command with the same name already exists, it will be overridden. + /// If the command is not enabled it will not be added. + pub fn add( + &mut self, + command: std::rc::Rc<std::cell::RefCell<dyn SymfonyCommand>>, + ) -> anyhow::Result<Option<std::rc::Rc<std::cell::RefCell<dyn SymfonyCommand>>>> { + self.init()?; + + // TODO(review): $command->setApplication($this) needs an Rc<RefCell<Application>> to the + // current instance. Application is held by value here; the self-reference required to set + // the command's back-pointer needs the shared-ownership design (Phase C). + command + .borrow_mut() + .set_application(todo!("Rc<RefCell<Application>> of self")); + + if !command.borrow().is_enabled() { + command.borrow_mut().set_application(None); + + return Ok(None); + } + + // if (!$command instanceof LazyCommand) { $command->getDefinition(); } + // TODO(review): LazyCommand vs SymfonyCommand type distinction; eager definition probe omitted + // pending lazy-command representation decision. + command.borrow().get_definition(); + + if command.borrow().get_name().is_none() { + return Err(ConsoleLogicException(shirabe_php_shim::LogicException { + message: format!( + "The command defined in \"{}\" cannot have an empty name.", + PhpMixed::from(shirabe_php_shim::get_debug_type_obj(&command,)), + ), + code: 0, + }) + .into()); + } + + let name = command.borrow().get_name().unwrap(); + self.commands.insert(name, command.clone()); + + for alias in command.borrow().get_aliases() { + self.commands.insert(alias, command.clone()); + } + + Ok(Some(command)) + } + + /// Returns a registered command by name or alias. + /// + /// Throws CommandNotFoundException when given command name does not exist. + pub fn get( + &mut self, + name: &str, + ) -> anyhow::Result<std::rc::Rc<std::cell::RefCell<dyn SymfonyCommand>>> { + self.init()?; + + if !self.has(name) { + return Err(CommandNotFoundException::new( + format!( + "The command \"{}\" does not exist.", + PhpMixed::from(name.to_string()), + ), + Vec::new(), + 0, + ) + .into()); + } + + // When the command has a different name than the one used at the command loader level + if !self.commands.contains_key(name) { + return Err(CommandNotFoundException::new( + format!( + "The \"{}\" command cannot be found because it is registered under multiple names. Make sure you don't set a different name via constructor or \"setName()\".", + PhpMixed::from(name.to_string()), + ), + Vec::new(), + 0, + ) + .into()); + } + + let command = self.commands[name].clone(); + + if self.want_helps { + self.want_helps = false; + + let help_command = self.get("help")?; + // $helpCommand->setCommand($command); + // TODO(review): setCommand() is defined on HelpCommand, not on the concrete + // `SymfonyCommand` + // struct; calling it through the Rc<RefCell<dyn SymfonyCommand>> needs the + // SymfonyCommand-subclass + // representation decision (downcast to HelpCommand). + let _ = &command; + todo!("help_command.set_command(command)"); + + #[allow(unreachable_code)] + return Ok(help_command); + } + + Ok(command) + } + + /// Returns true if the command exists, false otherwise. + pub fn has(&mut self, name: &str) -> bool { + self.init().unwrap(); + + if self.commands.contains_key(name) { + return true; + } + + if let Some(command_loader) = &self.command_loader { + if command_loader.has(name) { + let command = command_loader.get(name); + // $this->add($this->commandLoader->get($name)) + // TODO(review): command_loader.get() returns Box<dyn SymfonyCommand> while add() expects + // Rc<RefCell<dyn SymfonyCommand>>; the loader return type needs reconciliation. + let _ = command; + return self + .add(todo!( + "Rc<RefCell<dyn SymfonyCommand>> from command_loader.get(name)" + )) + .map(|c| c.is_some()) + .unwrap_or(false); + } + } + + false + } + + /// Returns an array of all unique namespaces used by currently registered commands. + /// + /// It does not return the global namespace which always exists. + pub fn get_namespaces(&mut self) -> anyhow::Result<Vec<String>> { + let mut namespaces: Vec<Vec<String>> = Vec::new(); + for command in self.all(None)?.values() { + if command.borrow().is_hidden() { + continue; + } + + namespaces.push( + self.extract_all_namespaces(&command.borrow().get_name().unwrap_or_default()), + ); + + for alias in command.borrow().get_aliases() { + namespaces.push(self.extract_all_namespaces(&alias)); + } + } + + // array_values(array_unique(array_filter(array_merge([], ...$namespaces)))) + let mut merged: Vec<String> = Vec::new(); + for ns in namespaces { + merged.extend(ns); + } + let merged: Vec<String> = merged.into_iter().filter(|s| !s.is_empty()).collect(); + let mut seen = std::collections::HashSet::new(); + let unique: Vec<String> = merged + .into_iter() + .filter(|s| seen.insert(s.clone())) + .collect(); + + Ok(unique) + } + + /// Finds a registered namespace by a name or an abbreviation. + /// + /// Throws NamespaceNotFoundException when namespace is incorrect or ambiguous. + pub fn find_namespace(&mut self, namespace: &str) -> anyhow::Result<String> { + let all_namespaces = self.get_namespaces()?; + // implode('[^:]*:', array_map('preg_quote', explode(':', $namespace))).'[^:]*' + let parts: Vec<String> = shirabe_php_shim::explode(":", namespace) + .into_iter() + .map(|p| shirabe_php_shim::preg_quote(&p, None)) + .collect(); + let expr = format!("{}{}", shirabe_php_shim::implode("[^:]*:", &parts), "[^:]*"); + let namespaces = shirabe_php_shim::preg_grep(&format!("{{^{}}}", expr), &all_namespaces); + + if namespaces.is_empty() { + let mut message = format!( + "There are no commands defined in the \"{}\" namespace.", + PhpMixed::from(namespace.to_string()), + ); + + let alternatives = self.find_alternatives(namespace, &all_namespaces); + if !alternatives.is_empty() { + if alternatives.len() == 1 { + message.push_str("\n\nDid you mean this?\n "); + } else { + message.push_str("\n\nDid you mean one of these?\n "); + } + + message.push_str(&shirabe_php_shim::implode("\n ", &alternatives)); + } + + return Err(NamespaceNotFoundException(CommandNotFoundException::new( + message, + alternatives, + 0, + )) + .into()); + } + + let exact = namespaces.iter().any(|n| n == namespace); + if namespaces.len() > 1 && !exact { + return Err(NamespaceNotFoundException(CommandNotFoundException::new( + format!( + "The namespace \"{}\" is ambiguous.\nDid you mean one of these?\n{}.", + PhpMixed::from(namespace.to_string()), + PhpMixed::from(self.get_abbreviation_suggestions(&namespaces)), + ), + namespaces.clone(), + 0, + )) + .into()); + } + + // $exact ? $namespace : reset($namespaces) + if exact { + Ok(namespace.to_string()) + } else { + Ok(namespaces[0].clone()) + } + } + + /// Finds a command by name or alias. + /// + /// Contrary to get, this command tries to find the best match if you give it an + /// abbreviation of a name or alias. + /// + /// Throws CommandNotFoundException when command name is incorrect or ambiguous. + pub fn find( + &mut self, + name: &str, + ) -> anyhow::Result<std::rc::Rc<std::cell::RefCell<dyn SymfonyCommand>>> { + self.init()?; + + let mut aliases: IndexMap<String, String> = IndexMap::new(); + + let commands_snapshot: Vec<std::rc::Rc<std::cell::RefCell<dyn SymfonyCommand>>> = + self.commands.values().cloned().collect(); + for command in &commands_snapshot { + for alias in command.borrow().get_aliases() { + if !self.has(&alias) { + self.commands.insert(alias, command.clone()); + } + } + } + + if self.has(name) { + return self.get(name); + } + + // $allCommands = commandLoader ? array_merge(loader->getNames(), array_keys(commands)) : array_keys(commands) + let all_commands: Vec<String> = match &self.command_loader { + Some(command_loader) => { + let mut all = command_loader.get_names(); + all.extend(self.commands.keys().cloned()); + all + } + None => self.commands.keys().cloned().collect(), + }; + + let parts: Vec<String> = shirabe_php_shim::explode(":", name) + .into_iter() + .map(|p| shirabe_php_shim::preg_quote(&p, None)) + .collect(); + let expr = format!("{}{}", shirabe_php_shim::implode("[^:]*:", &parts), "[^:]*"); + let mut commands = shirabe_php_shim::preg_grep(&format!("{{^{}}}", expr), &all_commands); + + if commands.is_empty() { + commands = shirabe_php_shim::preg_grep(&format!("{{^{}}}i", expr), &all_commands); + } + + // if no commands matched or we just matched namespaces + if commands.is_empty() + || shirabe_php_shim::preg_grep(&format!("{{^{}$}}i", expr), &commands).len() < 1 + { + if let Some(pos) = shirabe_php_shim::strrpos(name, ":") { + // check if a namespace exists and contains commands + self.find_namespace(&name[..pos as usize])?; + } + + let mut message = format!( + "SymfonyCommand \"{}\" is not defined.", + PhpMixed::from(name.to_string()), + ); + + let mut alternatives = self.find_alternatives(name, &all_commands); + if !alternatives.is_empty() { + // remove hidden commands + let mut filtered: Vec<String> = Vec::new(); + for alt in alternatives { + if !self.get(&alt)?.borrow().is_hidden() { + filtered.push(alt); + } + } + alternatives = filtered; + + if alternatives.len() == 1 { + message.push_str("\n\nDid you mean this?\n "); + } else { + message.push_str("\n\nDid you mean one of these?\n "); + } + message.push_str(&shirabe_php_shim::implode("\n ", &alternatives)); + } + + return Err(CommandNotFoundException::new(message, alternatives, 0).into()); + } + + // filter out aliases for commands which are already on the list + if commands.len() > 1 { + // $commandList = commandLoader ? array_merge(array_flip(loader->getNames()), commands) : commands + // TODO(review): $commandList mixes flipped loader names (string => int) with + // SymfonyCommand + // instances; this heterogeneous PHP array needs a typed representation. The alias + // de-duplication and the loader->get() lazy materialization are left to design. + let mut command_list: IndexMap< + String, + std::rc::Rc<std::cell::RefCell<dyn SymfonyCommand>>, + > = self.commands.clone(); + + let commands_clone = commands.clone(); + let mut new_commands: Vec<String> = Vec::new(); + let mut seen = std::collections::HashSet::new(); + for name_or_alias in commands { + if !command_list.contains_key(&name_or_alias) { + let loaded = self.command_loader.as_ref().unwrap().get(&name_or_alias); + let _ = loaded; + command_list.insert( + name_or_alias.clone(), + todo!( + "Rc<RefCell<dyn SymfonyCommand>> from command_loader.get(name_or_alias)" + ), + ); + } + + let command_name = command_list[&name_or_alias] + .borrow() + .get_name() + .unwrap_or_default(); + + aliases.insert(name_or_alias.clone(), command_name.clone()); + + let keep = command_name == name_or_alias || !commands_clone.contains(&command_name); + if keep && seen.insert(name_or_alias.clone()) { + new_commands.push(name_or_alias); + } + } + commands = new_commands; + + if commands.len() > 1 { + let usable_width = self.terminal.get_width() - 10; + let abbrevs: Vec<String> = commands.clone(); + let mut max_len: i64 = 0; + for abbrev in &abbrevs { + max_len = std::cmp::max(Helper::width(abbrev), max_len); + } + let mut formatted_abbrevs: Vec<PhpMixed> = Vec::new(); + for cmd in commands.clone() { + if command_list[&cmd].borrow().is_hidden() { + // unset($commands[array_search($cmd, $commands)]) + if let Some(idx) = commands.iter().position(|c| *c == cmd) { + commands.remove(idx); + } + formatted_abbrevs.push(PhpMixed::Bool(false)); + continue; + } + + let abbrev = format!( + "{} {}", + shirabe_php_shim::str_pad( + &cmd, + max_len as usize, + " ", + shirabe_php_shim::STR_PAD_RIGHT + ), + command_list[&cmd].borrow().get_description() + ); + + if Helper::width(&abbrev) > usable_width { + formatted_abbrevs.push(PhpMixed::from(format!( + "{}...", + Helper::substr(&abbrev, 0, Some(usable_width - 3)) + ))); + } else { + formatted_abbrevs.push(PhpMixed::from(abbrev)); + } + } + + if commands.len() > 1 { + let filtered: Vec<String> = formatted_abbrevs + .iter() + .filter(|a| shirabe_php_shim::php_truthy(a)) + .map(|a| shirabe_php_shim::php_to_string(a)) + .collect(); + let suggestions = self.get_abbreviation_suggestions(&filtered); + + return Err(CommandNotFoundException::new( + format!( + "SymfonyCommand \"{}\" is ambiguous.\nDid you mean one of these?\n{}.", + PhpMixed::from(name.to_string()), + PhpMixed::from(suggestions), + ), + commands.clone(), + 0, + ) + .into()); + } + } + } + + // $command = $this->get(reset($commands)); + let command = self.get(&commands[0])?; + + if command.borrow().is_hidden() { + return Err(CommandNotFoundException::new( + format!( + "The command \"{}\" does not exist.", + PhpMixed::from(name.to_string()), + ), + Vec::new(), + 0, + ) + .into()); + } + + Ok(command) + } + + /// Gets the commands (registered in the given namespace if provided). + /// + /// The array keys are the full names and the values the command instances. + pub fn all( + &mut self, + namespace: Option<&str>, + ) -> anyhow::Result<IndexMap<String, std::rc::Rc<std::cell::RefCell<dyn SymfonyCommand>>>> { + self.init()?; + + if namespace.is_none() { + if self.command_loader.is_none() { + return Ok(self.commands.clone()); + } + + let mut commands = self.commands.clone(); + let names = self.command_loader.as_ref().unwrap().get_names(); + for name in names { + if !commands.contains_key(&name) && self.has(&name) { + commands.insert(name.clone(), self.get(&name)?); + } + } + + return Ok(commands); + } + + let namespace = namespace.unwrap(); + let mut commands: IndexMap<String, std::rc::Rc<std::cell::RefCell<dyn SymfonyCommand>>> = + IndexMap::new(); + let entries: Vec<(String, std::rc::Rc<std::cell::RefCell<dyn SymfonyCommand>>)> = self + .commands + .iter() + .map(|(k, v)| (k.clone(), v.clone())) + .collect(); + for (name, command) in entries { + if namespace + == self.extract_namespace( + &name, + Some(shirabe_php_shim::substr_count(namespace, ":") + 1), + ) + { + commands.insert(name, command); + } + } + + if self.command_loader.is_some() { + let names = self.command_loader.as_ref().unwrap().get_names(); + for name in names { + if !commands.contains_key(&name) + && namespace + == self.extract_namespace( + &name, + Some(shirabe_php_shim::substr_count(namespace, ":") + 1), + ) + && self.has(&name) + { + commands.insert(name.clone(), self.get(&name)?); + } + } + } + + Ok(commands) + } + + /// Returns an array of possible abbreviations given a set of names. + pub fn get_abbreviations(names: Vec<String>) -> IndexMap<String, Vec<String>> { + let mut abbrevs: IndexMap<String, Vec<String>> = IndexMap::new(); + for name in names { + let mut len = shirabe_php_shim::strlen(&name); + while len > 0 { + let abbrev = shirabe_php_shim::substr(&name, 0, Some(len)); + abbrevs.entry(abbrev).or_default().push(name.clone()); + len -= 1; + } + } + + abbrevs + } + + pub fn render_throwable( + &self, + e: &anyhow::Error, + output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, + ) { + output + .borrow() + .writeln(&["".to_string()], output_interface::VERBOSITY_QUIET); + + self.do_render_throwable(e, output.clone()); + + if let Some(running_command) = &self.running_command { + output.borrow().writeln( + &[format!( + "<info>{}</info>", + PhpMixed::from( + OutputFormatter::escape(&shirabe_php_shim::sprintf( + &running_command.borrow_mut().get_synopsis(false), + &[PhpMixed::from(self.get_name())], + )) + .unwrap(), + ), + )], + output_interface::VERBOSITY_QUIET, + ); + output + .borrow() + .writeln(&["".to_string()], output_interface::VERBOSITY_QUIET); + } + } + + pub fn do_render_throwable( + &self, + e: &anyhow::Error, + output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, + ) { + // do { ... } while ($e = $e->getPrevious()); + // TODO(review): PHP walks the exception chain via getPrevious() and reads getMessage(), + // getCode(), getFile(), getLine(), getTrace(). anyhow::Error exposes a source() chain but + // not file/line/trace; faithful rendering of the trace needs a Throwable-equivalent. + let _ = output; + let _ = e; + todo!("render exception chain (getMessage/getCode/getFile/getLine/getTrace/getPrevious)") + } + + /// Configures the input and output instances based on the user arguments and options. + pub fn configure_io( + &self, + input: &std::rc::Rc<std::cell::RefCell<dyn InputInterface>>, + output: &std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, + ) -> anyhow::Result<()> { + if input.borrow().has_parameter_option( + PhpMixed::from(vec![PhpMixed::from("--ansi".to_string())]), + true, + ) { + output.borrow().set_decorated(true); + } else if input.borrow().has_parameter_option( + PhpMixed::from(vec![PhpMixed::from("--no-ansi".to_string())]), + true, + ) { + output.borrow().set_decorated(false); + } + + if input.borrow().has_parameter_option( + PhpMixed::from(vec![ + PhpMixed::from("--no-interaction".to_string()), + PhpMixed::from("-n".to_string()), + ]), + true, + ) { + input.borrow_mut().set_interactive(false); + } + + let mut shell_verbosity = shirabe_php_shim::getenv("SHELL_VERBOSITY").unwrap_or_default(); + let shell_verbosity_int: i64 = shell_verbosity.parse().unwrap_or(0); + let mut shell_verbosity: i64 = shell_verbosity_int; + match shell_verbosity_int { + -1 => { + output + .borrow() + .set_verbosity(output_interface::VERBOSITY_QUIET); + } + 1 => { + output + .borrow() + .set_verbosity(output_interface::VERBOSITY_VERBOSE); + } + 2 => { + output + .borrow() + .set_verbosity(output_interface::VERBOSITY_VERY_VERBOSE); + } + 3 => { + output + .borrow() + .set_verbosity(output_interface::VERBOSITY_DEBUG); + } + _ => { + shell_verbosity = 0; + } + } + + if input.borrow().has_parameter_option( + PhpMixed::from(vec![ + PhpMixed::from("--quiet".to_string()), + PhpMixed::from("-q".to_string()), + ]), + true, + ) { + output + .borrow() + .set_verbosity(output_interface::VERBOSITY_QUIET); + shell_verbosity = -1; + } else if input + .borrow() + .has_parameter_option(PhpMixed::from("-vvv".to_string()), true) + || input + .borrow() + .has_parameter_option(PhpMixed::from("--verbose=3".to_string()), true) + || input.borrow().get_parameter_option( + PhpMixed::from("--verbose".to_string()), + PhpMixed::Bool(false), + true, + ) == PhpMixed::from(3i64) + { + output + .borrow() + .set_verbosity(output_interface::VERBOSITY_DEBUG); + shell_verbosity = 3; + } else if input + .borrow() + .has_parameter_option(PhpMixed::from("-vv".to_string()), true) + || input + .borrow() + .has_parameter_option(PhpMixed::from("--verbose=2".to_string()), true) + || input.borrow().get_parameter_option( + PhpMixed::from("--verbose".to_string()), + PhpMixed::Bool(false), + true, + ) == PhpMixed::from(2i64) + { + output + .borrow() + .set_verbosity(output_interface::VERBOSITY_VERY_VERBOSE); + shell_verbosity = 2; + } else if input + .borrow() + .has_parameter_option(PhpMixed::from("-v".to_string()), true) + || input + .borrow() + .has_parameter_option(PhpMixed::from("--verbose=1".to_string()), true) + || input + .borrow() + .has_parameter_option(PhpMixed::from("--verbose".to_string()), true) + || shirabe_php_shim::php_truthy(&input.borrow().get_parameter_option( + PhpMixed::from("--verbose".to_string()), + PhpMixed::Bool(false), + true, + )) + { + output + .borrow() + .set_verbosity(output_interface::VERBOSITY_VERBOSE); + shell_verbosity = 1; + } + + if shell_verbosity == -1 { + input.borrow_mut().set_interactive(false); + } + + if shirabe_php_shim::function_exists("putenv") { + shirabe_php_shim::putenv(&format!("SHELL_VERBOSITY={}", shell_verbosity)); + } + shirabe_php_shim::env_set("SHELL_VERBOSITY", shell_verbosity.to_string()); + shirabe_php_shim::server_set("SHELL_VERBOSITY", shell_verbosity.to_string()); + + let _ = &mut shell_verbosity; + + Ok(()) + } + + /// Runs the current command. + /// + /// If an event dispatcher has been attached to the application, events are also + /// dispatched during the life-cycle of the command. + /// + /// Returns 0 if everything went fine, or an error code. + pub fn do_run_command( + &mut self, + command: std::rc::Rc<std::cell::RefCell<dyn SymfonyCommand>>, + input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>, + output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, + ) -> anyhow::Result<i64> { + if let Some(helper_set) = command.borrow().get_helper_set() { + for (_alias, helper) in helper_set.borrow().get_iterator() { + // if ($helper instanceof InputAwareInterface) $helper->setInput($input); + // TODO(review): downcasting a HelperInterface to InputAwareInterface is not + // expressible without a typed mechanism; needs design. + let _ = helper; + let _ = std::marker::PhantomData::<dyn InputAwareInterface>; + } + } + + if !self.signals_to_dispatch_event.is_empty() { + // $commandSignals = $command instanceof SignalableCommandInterface ? $command->getSubscribedSignals() : [] + // TODO(review): SymfonyCommand is not a SignalableCommandInterface here; downcast needed. + let command_signals: Vec<i64> = Vec::new(); + let _ = std::marker::PhantomData::<dyn SignalableCommandInterface>; + + if !command_signals.is_empty() || self.dispatcher.is_some() { + if self.signal_registry.is_none() { + return Err(ConsoleRuntimeException(shirabe_php_shim::RuntimeException { + message: "Unable to subscribe to signal events. Make sure that the `pcntl` extension is installed and that \"pcntl_*\" functions are not disabled by your php.ini's \"disable_functions\" directive.".to_string(), + code: 0, + }) + .into()); + } + + if Terminal::has_stty_available() { + // TODO: registers SIGINT/SIGTERM handlers that restore the stty mode via + // shell_exec('stty ...'). pcntl signal handlers have no faithful Rust + // equivalent in Phase A. + let _stty_mode = shirabe_php_shim::shell_exec("stty -g"); + for _signal in [shirabe_php_shim::SIGINT, shirabe_php_shim::SIGTERM] { + todo!("register signal handler to restore stty mode"); + } + } + } + + if self.dispatcher.is_some() { + // TODO(plugin): for each signal, register a handler that dispatches ConsoleSignalEvent. + for &signal in &self.signals_to_dispatch_event.clone() { + let _event = ConsoleSignalEvent::new( + todo!("Box<dyn SymfonyCommand>"), + todo!("Box<dyn InputInterface>"), + todo!("Box<dyn OutputInterface>"), + signal, + ); + todo!("register signal handler dispatching ConsoleEvents::SIGNAL"); + } + } + + for _signal in command_signals { + // $this->signalRegistry->register($signal, [$command, 'handleSignal']); + todo!("register command->handle_signal as signal handler"); + } + } + + if self.dispatcher.is_none() { + return command.borrow_mut().run( + &mut *borrow_input_mut(&input), + &mut *borrow_output_mut(&output), + ); + } + + // bind before the console.command event, so the listeners have access to input options/arguments + match (|| -> anyhow::Result<()> { + command.borrow_mut().merge_application_definition(true); + input.borrow_mut().bind(command.borrow().get_definition())?; + Ok(()) + })() { + Ok(()) => {} + Err(e) => { + // ignore invalid options/arguments for now, to allow the event listeners to customize the InputDefinition + if !is_exception_interface(&e) { + return Err(e); + } + } + } + + // TODO(plugin): the whole dispatcher block below drives ConsoleCommandEvent / + // ConsoleErrorEvent / ConsoleTerminateEvent. The event objects require Box<dyn ...> + // wrappers for input/output/command and the dispatcher's dispatch() contract; their + // construction is left to the plugin/event design. + let _ = ConsoleCommandEvent::RETURN_CODE_DISABLED; + let _ = std::marker::PhantomData::<( + ConsoleCommandEvent, + ConsoleErrorEvent, + ConsoleTerminateEvent, + )>; + todo!("dispatcher-driven command run (console.command / console.error / console.terminate)") + } + + /// Gets the name of the command based on input. + pub fn get_command_name(&self, input: &dyn InputInterface) -> Option<String> { + if self.single_command { + Some(self.default_command.clone()) + } else { + input.get_first_argument() + } + } + + /// Gets the default input definition (Symfony base; `parent::getDefaultInputDefinition`). + pub fn base_get_default_input_definition(&self) -> InputDefinition { + use shirabe_external_packages::symfony::console::input::input_definition::DefinitionItem; + InputDefinition::new(vec![ + DefinitionItem::InputArgument( + InputArgument::new( + "command".to_string(), + Some(InputArgument::REQUIRED), + "The command to execute".to_string(), + PhpMixed::Null, + ) + .unwrap(), + ), + DefinitionItem::InputOption( + InputOption::new( + "--help", + PhpMixed::from("-h".to_string()), + Some(InputOption::VALUE_NONE), + format!( + "Display help for the given command. When no command is given display help for the <info>{}</info> command", + self.default_command + ), + PhpMixed::Null, + ) + .unwrap(), + ), + DefinitionItem::InputOption( + InputOption::new( + "--quiet", + PhpMixed::from("-q".to_string()), + Some(InputOption::VALUE_NONE), + "Do not output any message".to_string(), + PhpMixed::Null, + ) + .unwrap(), + ), + DefinitionItem::InputOption( + InputOption::new( + "--verbose", + PhpMixed::from("-v|vv|vvv".to_string()), + Some(InputOption::VALUE_NONE), + "Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug".to_string(), + PhpMixed::Null, + ) + .unwrap(), + ), + DefinitionItem::InputOption( + InputOption::new( + "--version", + PhpMixed::from("-V".to_string()), + Some(InputOption::VALUE_NONE), + "Display this application version".to_string(), + PhpMixed::Null, + ) + .unwrap(), + ), + DefinitionItem::InputOption( + InputOption::new( + "--ansi", + PhpMixed::from("".to_string()), + Some(InputOption::VALUE_NEGATABLE), + "Force (or disable --no-ansi) ANSI output".to_string(), + PhpMixed::Null, + ) + .unwrap(), + ), + DefinitionItem::InputOption( + InputOption::new( + "--no-interaction", + PhpMixed::from("-n".to_string()), + Some(InputOption::VALUE_NONE), + "Do not ask any interactive question".to_string(), + PhpMixed::Null, + ) + .unwrap(), + ), + ]) + .unwrap() + } + + /// Gets the default commands that should always be available (Symfony base; + /// `parent::getDefaultCommands`). + pub fn base_get_default_commands( + &self, + ) -> Vec<std::rc::Rc<std::cell::RefCell<dyn SymfonyCommand>>> { + // return [new HelpCommand(), new ListCommand(), new CompleteCommand(), new DumpCompletionCommand()]; + // TODO(review): HelpCommand/ListCommand/CompleteCommand/DumpCompletionCommand are ported as + // distinct structs (not subtypes of the concrete `SymfonyCommand` struct), so they cannot populate + // a Vec<Rc<RefCell<dyn SymfonyCommand>>>. Reconciling SymfonyCommand subclassing with Rust requires the + // command-hierarchy design decision (see also `add`/`find`/LazyCommand handling). + let _ = std::marker::PhantomData::<( + shirabe_external_packages::symfony::console::command::help_command::HelpCommand, + shirabe_external_packages::symfony::console::command::list_command::ListCommand, + shirabe_external_packages::symfony::console::command::complete_command::CompleteCommand, + shirabe_external_packages::symfony::console::command::dump_completion_command::DumpCompletionCommand, + )>; + todo!("construct default commands once SymfonyCommand-subclass representation is decided") + } + + /// Gets the default helper set with the helpers that should always be available. + pub fn get_default_helper_set(&self) -> std::rc::Rc<std::cell::RefCell<HelperSet>> { + let helper_set = std::rc::Rc::new(std::cell::RefCell::new(HelperSet::default())); + let helpers: IndexMap<HelperSetKey, std::rc::Rc<std::cell::RefCell<dyn HelperInterface>>> = { + let mut m: IndexMap< + HelperSetKey, + std::rc::Rc<std::cell::RefCell<dyn HelperInterface>>, + > = IndexMap::new(); + m.insert( + HelperSetKey::Int(0), + std::rc::Rc::new(std::cell::RefCell::new(FormatterHelper::default())), + ); + m.insert( + HelperSetKey::Int(1), + std::rc::Rc::new(std::cell::RefCell::new(DebugFormatterHelper::default())), + ); + m.insert( + HelperSetKey::Int(2), + std::rc::Rc::new(std::cell::RefCell::new(ProcessHelper::default())), + ); + m.insert( + HelperSetKey::Int(3), + std::rc::Rc::new(std::cell::RefCell::new(QuestionHelper::default())), + ); + m + }; + HelperSet::new(&helper_set, helpers); + helper_set + } + + /// Returns abbreviated suggestions in string format. + fn get_abbreviation_suggestions(&self, abbrevs: &[String]) -> String { + format!(" {}", shirabe_php_shim::implode("\n ", abbrevs)) + } + + /// Returns the namespace part of the command name. + /// + /// This method is not part of public API and should not be used directly. + pub fn extract_namespace(&self, name: &str, limit: Option<i64>) -> String { + // $parts = explode(':', $name, -1); + let parts = shirabe_php_shim::explode_limit(":", name, -1); + + // implode(':', null === $limit ? $parts : array_slice($parts, 0, $limit)) + match limit { + None => shirabe_php_shim::implode(":", &parts), + Some(limit) => { + let sliced: Vec<String> = parts.into_iter().take(limit.max(0) as usize).collect(); + shirabe_php_shim::implode(":", &sliced) + } + } + } + + /// Finds alternative of $name among $collection, if nothing is found in + /// $collection, try in $abbrevs. + fn find_alternatives(&self, name: &str, collection: &[String]) -> Vec<String> { + let threshold = 1e3; + let mut alternatives: IndexMap<String, f64> = IndexMap::new(); + + let mut collection_parts: IndexMap<String, Vec<String>> = IndexMap::new(); + for item in collection { + collection_parts.insert(item.clone(), shirabe_php_shim::explode(":", item)); + } + + for (i, subname) in shirabe_php_shim::explode(":", name).into_iter().enumerate() { + for (collection_name, parts) in &collection_parts { + let exists = alternatives.contains_key(collection_name); + if parts.get(i).is_none() && exists { + *alternatives.get_mut(collection_name).unwrap() += threshold; + continue; + } else if parts.get(i).is_none() { + continue; + } + + let lev = shirabe_php_shim::levenshtein(&subname, &parts[i]) as f64; + if lev <= shirabe_php_shim::strlen(&subname) as f64 / 3.0 + || (!subname.is_empty() && parts[i].contains(&subname)) + { + let v = if exists { + alternatives[collection_name] + lev + } else { + lev + }; + alternatives.insert(collection_name.clone(), v); + } else if exists { + *alternatives.get_mut(collection_name).unwrap() += threshold; + } + } + } + + for item in collection { + let lev = shirabe_php_shim::levenshtein(name, item) as f64; + if lev <= shirabe_php_shim::strlen(name) as f64 / 3.0 || item.contains(name) { + let v = if alternatives.contains_key(item) { + alternatives[item] - lev + } else { + lev + }; + alternatives.insert(item.clone(), v); + } + } + + // array_filter($alternatives, fn($lev) => $lev < 2 * $threshold) + alternatives.retain(|_, lev| *lev < 2.0 * threshold); + // ksort($alternatives, SORT_NATURAL | SORT_FLAG_CASE) + let mut keys: Vec<String> = alternatives.keys().cloned().collect(); + shirabe_php_shim::sort_natural_flag_case(&mut keys); + + keys + } + + /// Sets the default SymfonyCommand name. + pub fn set_default_command( + &mut self, + command_name: &str, + is_single_command: bool, + ) -> anyhow::Result<&mut Self> { + // $this->defaultCommand = explode('|', ltrim($commandName, '|'))[0]; + let trimmed = shirabe_php_shim::ltrim(command_name, Some("|")); + self.default_command = shirabe_php_shim::explode("|", &trimmed) + .into_iter() + .next() + .unwrap_or_default(); + + if is_single_command { + // Ensure the command exist + self.find(command_name)?; + + self.single_command = true; + } + + Ok(self) + } + + pub fn is_single_command(&self) -> bool { + self.single_command + } + + fn split_string_by_width(&self, string: &str, width: i64) -> Vec<String> { + // str_split is not suitable for multi-byte characters, we should use preg_split to get char array properly. + let encoding = match shirabe_php_shim::mb_detect_encoding(string, None, true) { + None => return shirabe_php_shim::str_split(string, width), + Some(encoding) => encoding, + }; + + let utf8_string = shirabe_php_shim::mb_convert_encoding(string.into(), "utf8", &encoding); + let mut lines: Vec<String> = Vec::new(); + let mut line = String::new(); + + let mut offset = 0i64; + let mut m: Vec<String> = Vec::new(); + while shirabe_php_shim::preg_match_offset(r"/.{1,10000}/u", &utf8_string, &mut m, 0, offset) + { + offset += shirabe_php_shim::strlen(&m[0]); + + for char in shirabe_php_shim::preg_split_chars(r"//u", &m[0]) { + // test if $char could be appended to current line + if shirabe_php_shim::mb_strwidth(&format!("{}{}", line, char), Some("utf8")) + <= width + { + line.push_str(&char); + continue; + } + // if not, push current line to array and make new line + lines.push(shirabe_php_shim::str_pad( + &line, + width as usize, + " ", + shirabe_php_shim::STR_PAD_RIGHT, + )); + line = char; + } + } + + lines.push(if !lines.is_empty() { + shirabe_php_shim::str_pad(&line, width as usize, " ", shirabe_php_shim::STR_PAD_RIGHT) + } else { + line.clone() + }); + + shirabe_php_shim::mb_convert_variables(&encoding, "utf8", &mut lines); + + lines + } + + /// Returns all namespaces of the command name. + fn extract_all_namespaces(&self, name: &str) -> Vec<String> { + // -1 as third argument is needed to skip the command short name when exploding + let parts = shirabe_php_shim::explode_limit(":", name, -1); + let mut namespaces: Vec<String> = Vec::new(); + + for part in parts { + if !namespaces.is_empty() { + let last = namespaces.last().unwrap().clone(); + namespaces.push(format!("{}:{}", last, part)); + } else { + namespaces.push(part); + } + } + + namespaces + } + + fn init(&mut self) -> anyhow::Result<()> { + if self.initialized { + return Ok(()); + } + self.initialized = true; + + for command in self.get_default_commands() { + self.add(command)?; + } + + Ok(()) + } +} + +impl BaseApplication for Application { + fn get_name(&self) -> String { + Application::get_name(self) + } + + fn get_version(&self) -> String { + Application::get_version(self) + } + + fn get_help(&self) -> String { + Application::get_help(self) + } + + fn is_single_command(&self) -> bool { + Application::is_single_command(self) + } + + fn extract_namespace(&self, name: &str, limit: Option<i64>) -> String { + Application::extract_namespace(self, name, limit) + } + + fn find_namespace(&mut self, namespace: &str) -> anyhow::Result<String> { + Application::find_namespace(self, namespace) + } + + fn all( + &mut self, + namespace: Option<&str>, + ) -> anyhow::Result<IndexMap<String, std::rc::Rc<std::cell::RefCell<dyn SymfonyCommand>>>> { + Application::all(self, namespace) + } + + fn find( + &mut self, + name: &str, + ) -> anyhow::Result<std::rc::Rc<std::cell::RefCell<dyn SymfonyCommand>>> { + Application::find(self, name) + } + + fn get_definition(&mut self) -> std::rc::Rc<std::cell::RefCell<InputDefinition>> { + Application::get_definition(self) + } + + fn get_helper_set(&mut self) -> std::rc::Rc<std::cell::RefCell<HelperSet>> { + Application::get_helper_set(self) + } + + fn complete( + &mut self, + input: &CompletionInput, + suggestions: &mut CompletionSuggestions, + ) -> anyhow::Result<()> { + Application::complete(self, input, suggestions) + } +} + +/// Helper mirroring PHP's `$e instanceof ExceptionInterface`. +fn is_exception_interface(e: &anyhow::Error) -> bool { + // anyhow::Error stores concrete error types; enumerate the console exceptions + // that implement ExceptionInterface (PHP's `$e instanceof ExceptionInterface`). + e.downcast_ref::<CommandNotFoundException>().is_some() + || e.downcast_ref::<NamespaceNotFoundException>().is_some() + || e.downcast_ref::<ConsoleLogicException>().is_some() + || e.downcast_ref::<ConsoleRuntimeException>().is_some() +} + +/// Helper mirroring PHP's `$e instanceof CommandNotFoundException`. +fn downcast_command_not_found(e: &anyhow::Error) -> Option<&CommandNotFoundException> { + if let Some(cnf) = e.downcast_ref::<CommandNotFoundException>() { + return Some(cnf); + } + e.downcast_ref::<NamespaceNotFoundException>().map(|n| &n.0) +} + +/// Helper mirroring PHP's `$e instanceof NamespaceNotFoundException`. +fn is_namespace_not_found(e: &anyhow::Error) -> bool { + e.downcast_ref::<NamespaceNotFoundException>().is_some() +} + +/// Borrows the shared input as a mutable `dyn InputInterface` for passing to +/// `SymfonyCommand::run`, which takes `&mut dyn InputInterface`. +fn borrow_input_mut( + input: &std::rc::Rc<std::cell::RefCell<dyn InputInterface>>, +) -> std::cell::RefMut<'_, dyn InputInterface> { + input.borrow_mut() +} + +/// Borrows the shared output as a mutable `dyn OutputInterface` for passing to +/// `SymfonyCommand::run`, which takes `&mut dyn OutputInterface`. +fn borrow_output_mut( + output: &std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, +) -> std::cell::RefMut<'_, dyn OutputInterface> { + output.borrow_mut() +} diff --git a/crates/shirabe/src/event_dispatcher/event_dispatcher.rs b/crates/shirabe/src/event_dispatcher/event_dispatcher.rs index c54df20..f0ef82f 100644 --- a/crates/shirabe/src/event_dispatcher/event_dispatcher.rs +++ b/crates/shirabe/src/event_dispatcher/event_dispatcher.rs @@ -3,9 +3,6 @@ use indexmap::IndexMap; use shirabe_external_packages::composer::pcre::{CaptureKey, Preg}; -use shirabe_external_packages::symfony::console::Application; -use shirabe_external_packages::symfony::console::input::StringInput; -use shirabe_external_packages::symfony::console::output::ConsoleOutput; use shirabe_external_packages::symfony::process::ExecutableFinder; use shirabe_external_packages::symfony::process::PhpExecutableFinder; use shirabe_php_shim::{ @@ -573,64 +570,28 @@ impl EventDispatcher { continue; } - let mut app = Application::__construct("UNKNOWN", "UNKNOWN"); - app.set_catch_exceptions(false); - // PHP: if (method_exists($app, 'setCatchErrors')) { $app->setCatchErrors(false); } - // This Symfony Console port does not provide `setCatchErrors`, so the - // guarded call is skipped, matching `method_exists` evaluating to false. - app.set_auto_exit(false); - // TODO(plugin): instantiate command class dynamically: `new $className($event->getName())` + // PHP hosts the user's Command class in a throwaway, bare + // `Symfony\Component\Console\Application` (NOT Composer's Application): + // $app = new Application(); + // $app->setCatchExceptions(false); + // $app->setAutoExit(false); + // $cmd = new $className($event->getName()); + // $app->add($cmd); + // $app->setDefaultCommand((string) $cmd->getName(), true); + // $return = $app->run(new StringInput(...), $output); + // + // TODO(plugin): a `scripts` entry naming a Symfony Command subclass is run by + // hosting it in a bare Symfony console Application. This requires the PHP + // runtime — both the dynamic `new $className(...)` instantiation and the real + // Symfony Application. It will be implemented by generating a PHP bootstrap + // (the boilerplate above) parameterized by the class name, event name and + // args, then executing it via the PHP runtime with the child process + // inheriting STDOUT/STDERR in place of reusing the in-memory output. No + // Rust-side Symfony Application is involved, so none is constructed here. + let _ = &additional_args; todo!( - "plugin: CommandBase::new — dynamic plugin command instantiation not supported" + "plugin: run a `scripts` Command class via the PHP runtime (bare Symfony Application host)" ); - let result = (|| -> anyhow::Result<i64> { - let args = additional_args - .iter() - .map(|arg| ProcessExecutor::escape(arg)) - .collect::<Vec<_>>() - .join(" "); - // reusing the output from $this->io is mostly needed for tests, but generally speaking - // it does not hurt to keep the same stream as the current Application - let is_console_io = self.io.borrow().as_any().is::<ConsoleIO>(); - let output: ConsoleOutput = if is_console_io { - // TODO(plugin): \ReflectionProperty to read private `output` from ConsoleIO - // is required by the original PHP — needs user-decided porting strategy. - let _refl_php_version_gate = PHP_VERSION_ID < 80100; - todo!("\\ReflectionProperty on ConsoleIO::$output") - } else { - ConsoleOutput::new(None, None, None)? - }; - let input_str = event - .get_flags() - .get("script-alias-input") - .and_then(|v| v.as_string()) - .unwrap_or(&args) - .to_string(); - let input = StringInput::new(&input_str)?; - let output = output; - Ok(app.run( - Some(std::rc::Rc::new(std::cell::RefCell::new(input))), - Some(std::rc::Rc::new(std::cell::RefCell::new(output))), - )?) - })(); - match result { - Ok(v) => r#return = v, - Err(e) => { - self.io.write_error3( - &format!( - "<error>{}</error>", - format!( - "Script {} handling the {} event terminated with an exception", - PhpMixed::String(callable_str.clone()), - PhpMixed::String(event.get_name().to_string()), - ) - ), - true, - crate::io::QUIET, - ); - return Err(e); - } - } } Callable::String(callable_str) => { let args = additional_args diff --git a/crates/shirabe/src/util/platform.rs b/crates/shirabe/src/util/platform.rs index ecf2338..5155e8f 100644 --- a/crates/shirabe/src/util/platform.rs +++ b/crates/shirabe/src/util/platform.rs @@ -5,8 +5,8 @@ use std::sync::Mutex; use anyhow::Result; use shirabe_external_packages::composer::pcre::Preg; use shirabe_php_shim::{ - PhpMixed, RuntimeException, defined, env_contains_key, env_get, env_set, env_unset, - file_exists, file_get_contents, fopen, fstat, function_exists, getcwd, getenv, in_array, + PhpMixed, PhpResource, RuntimeException, defined, env_contains_key, env_get, env_set, + env_unset, file_exists, file_get_contents, fstat, function_exists, getcwd, getenv, in_array, ini_get, is_array, is_readable, mb_strlen, php_os_family, posix_geteuid, posix_getpwuid, posix_getuid, posix_isatty, putenv, realpath, server_contains_key, server_get, server_set, server_unset, stream_isatty, stripos, strlen, strtoupper, substr, usleep, @@ -288,21 +288,12 @@ impl Platform { } /// @param ?resource $fd Open file descriptor or null to default to STDOUT - pub fn is_tty(fd: Option<PhpMixed>) -> bool { + pub fn is_tty(fd: Option<PhpResource>) -> bool { let fd = match fd { Some(f) => f, None => { - if defined("STDOUT") { - // TODO(phase-c): map the STDOUT constant to a runtime stdout resource; depends - // on the unmodeled PHP stream/resource layer. - todo!("STDOUT constant") - } else { - let fd = fopen("php://stdout", "w"); - if matches!(fd, PhpMixed::Bool(false)) { - return false; - } - fd - } + // TODO(phase-c): STDOUT is not yet modeled as a `PhpResource` constant. + todo!("STDOUT resource constant") } }; |
