//! ref: composer/vendor/symfony/console/Command/Command.php use crate::symfony::console::application::Application; use crate::symfony::console::completion::completion_input::CompletionInput; use crate::symfony::console::completion::completion_suggestions::CompletionSuggestions; use crate::symfony::console::exception::invalid_argument_exception::InvalidArgumentException; use crate::symfony::console::helper::helper_set::HelperSet; use crate::symfony::console::input::input_argument::InputArgument; 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::output_interface::{self, OutputInterface}; use indexmap::IndexMap; use shirabe_php_shim::PhpMixed; use std::cell::RefCell; use std::rc::Rc; /// The base-class state of the PHP `Command` class. /// /// The PHP `Command` class is split into the polymorphic [`Command`] trait (the /// methods callers invoke on a command of unknown concrete type) and this struct, /// which holds the base-class fields and provides their canonical behavior via /// `impl Command for CommandData`. Subclasses embed a `CommandData` (directly, or /// transitively through `BaseCommandData`) and forward the state methods to it. pub struct CommandData { application: Option>>, name: Option, process_title: Option, aliases: Vec, definition: Option, hidden: bool, help: String, description: String, full_definition: Option, ignore_validation_errors: bool, // A callable(InputInterface, OutputInterface) -> i64. code: Option PhpMixed>>, synopsis: IndexMap, usages: Vec, helper_set: Option>>, } impl CommandData { // see https://tldp.org/LDP/abs/html/exitcodes.html pub const SUCCESS: i64 = 0; pub const FAILURE: i64 = 1; pub const INVALID: i64 = 2; /// The default command name. // NOTE: PHP `protected static $defaultName`; static late-binding property. pub const DEFAULT_NAME: Option<&'static str> = None; /// The default command description. // NOTE: PHP `protected static $defaultDescription`; static late-binding property. pub const DEFAULT_DESCRIPTION: Option<&'static str> = None; pub fn get_default_name() -> Option { // TODO(review): PHP uses ReflectionClass to read the #[AsCommand] attribute // and ReflectionProperty to check that `$defaultName` is declared on the late-static // class itself (not inherited). Reflection-based late static binding cannot be // reproduced in Phase A; human review needed for the porting strategy. todo!() } pub fn get_default_description() -> Option { // TODO(review): same Reflection/late-static-binding concern as get_default_name(). todo!() } /// Builds the base-class state. `name` is the name of the command; passing None /// means it must be set in the subclass `configure()`. /// /// Unlike PHP's `__construct`, this does not call `configure()` — the concrete /// command's `new()` calls `configure()` after embedding the data, mirroring the /// virtual dispatch of `$this->configure()` from the parent constructor. pub fn new(name: Option) -> Self { let mut this = CommandData { application: None, name: None, process_title: None, aliases: Vec::new(), definition: Some( InputDefinition::new(Vec::new()).expect("an empty InputDefinition cannot fail"), ), hidden: false, help: String::new(), description: String::new(), full_definition: None, ignore_validation_errors: false, code: None, synopsis: IndexMap::new(), usages: Vec::new(), helper_set: None, }; // PHP's __construct also derives the name from getDefaultName() when null and // sets the default description; both rely on Reflection late-static-binding // (get_default_name/get_default_description are todo!()), and concrete commands // always set their name in configure(), so only an explicit name is honored here. if let Some(name) = name { this.name = Some(name); } this } /// Applies a `$defaultName`-style name (PHP `Command::__construct` when `$name` is null and a /// `static $defaultName` exists). The string is `|`-separated; a leading empty segment marks the /// command hidden, the next segment is the name, and the rest are aliases. pub fn apply_default_name(&mut self, default_name: &str) -> anyhow::Result<()> { let mut aliases: Vec = default_name.split('|').map(|s| s.to_string()).collect(); let mut name = aliases.remove(0); if name.is_empty() { self.set_hidden(true); name = if aliases.is_empty() { String::new() } else { aliases.remove(0) }; } self.set_name(&name)?; self.set_aliases(aliases)?; Ok(()) } /// Validates a command name. /// /// It must be non-empty and parts can optionally be separated by ":". /// /// Throws InvalidArgumentException when the name is invalid. fn validate_name(&self, name: &str) -> anyhow::Result> { let mut matches: Vec> = Vec::new(); if !shirabe_php_shim::preg_match(r"/^[^\:]++(\:[^\:]++)*$/", name, &mut matches) { return Ok(Err(InvalidArgumentException( shirabe_php_shim::InvalidArgumentException { message: format!("Command name \"{}\" is invalid.", name), code: 0, }, ))); } Ok(Ok(())) } /// Sets an array of argument and option instances (the Symfony-typed entry point; /// `BaseCommand::set_definition` adapts the Composer-typed arguments to this). pub fn set_definition(&mut self, definition: SetDefinitionArg) -> &mut Self { match definition { SetDefinitionArg::Definition(definition) => { self.definition = Some(definition); } SetDefinitionArg::Array(definition) => { let _ = self.definition.as_mut().unwrap().set_definition(definition); } } self.full_definition = None; self } /// Adds an argument (Symfony-typed entry point). /// /// Throws InvalidArgumentException when argument mode is not valid. pub fn add_argument( &mut self, name: &str, mode: Option, description: &str, default: PhpMixed, ) -> anyhow::Result<&mut Self> { self.definition .as_mut() .unwrap() .add_argument(InputArgument::new( name.to_string(), mode, description.to_string(), default.clone(), )?)?; if let Some(full_definition) = self.full_definition.as_mut() { full_definition.add_argument(InputArgument::new( name.to_string(), mode, description.to_string(), default, )?)?; } Ok(self) } /// Adds an option (Symfony-typed entry point). /// /// Throws InvalidArgumentException if option mode is invalid or incompatible. pub fn add_option( &mut self, name: &str, shortcut: PhpMixed, mode: Option, description: &str, default: PhpMixed, ) -> anyhow::Result<&mut Self> { self.definition .as_mut() .unwrap() .add_option(InputOption::new( name, shortcut.clone(), mode, description.to_string(), default.clone(), )?)?; if let Some(full_definition) = self.full_definition.as_mut() { full_definition.add_option(InputOption::new( name, shortcut, mode, description.to_string(), default, )?)?; } Ok(self) } } /// The argument of `CommandData::set_definition()`, which accepts either an array of /// argument/option instances or an InputDefinition. #[derive(Debug)] pub enum SetDefinitionArg { Array(Vec), Definition(InputDefinition), } /// Forwards a single trait method to an embedded field that already implements the /// method (the "inner" command-state holder). /// /// Each `Command`/`BaseCommand` implementer spells out the methods it delegates, one /// `delegate_to_inner!` per method, alongside the few methods it overrides by hand. /// The first argument names the field to forward to; the second is the method's /// signature. Fluent setters returning `&mut Self` (optionally wrapped in /// `anyhow::Result`) are handled specially so the returned reference is re-rooted at /// the outer `self` rather than the inner field. #[macro_export] macro_rules! delegate_to_inner { // fluent fallible: -> anyhow::Result<&mut Self> ($field:ident, fn $name:ident(&mut self $(, $arg:ident : $ty:ty )* $(,)?) -> anyhow::Result<&mut Self>) => { fn $name(&mut self $(, $arg: $ty)*) -> anyhow::Result<&mut Self> { self.$field.$name($($arg),*)?; Ok(self) } }; // fluent infallible: -> &mut Self ($field:ident, fn $name:ident(&mut self $(, $arg:ident : $ty:ty )* $(,)?) -> &mut Self) => { fn $name(&mut self $(, $arg: $ty)*) -> &mut Self { self.$field.$name($($arg),*); self } }; // &self with a return type ($field:ident, fn $name:ident(&self $(, $arg:ident : $ty:ty )* $(,)?) -> $ret:ty) => { fn $name(&self $(, $arg: $ty)*) -> $ret { self.$field.$name($($arg),*) } }; // &self without a return type ($field:ident, fn $name:ident(&self $(, $arg:ident : $ty:ty )* $(,)?)) => { fn $name(&self $(, $arg: $ty)*) { self.$field.$name($($arg),*) } }; // &mut self with a return type ($field:ident, fn $name:ident(&mut self $(, $arg:ident : $ty:ty )* $(,)?) -> $ret:ty) => { fn $name(&mut self $(, $arg: $ty)*) -> $ret { self.$field.$name($($arg),*) } }; // &mut self without a return type ($field:ident, fn $name:ident(&mut self $(, $arg:ident : $ty:ty )* $(,)?)) => { fn $name(&mut self $(, $arg: $ty)*) { self.$field.$name($($arg),*) } }; } /// Forwards every `Command` state method (the setters/getters whose canonical impl lives on /// `CommandData` and which no subclass overrides) to an embedded field. Each command invokes /// this once inside its `impl Command` block and spells out by hand only the behavior hooks it /// overrides (`configure`/`execute`/`initialize`/...). The single argument names the field to /// forward to (`inner` for Symfony commands, `base_command_data` for Composer commands). #[macro_export] macro_rules! delegate_command_trait_impls_to_inner { ($field:ident) => { $crate::delegate_to_inner!($field, fn is_enabled(&self) -> bool); $crate::delegate_to_inner!($field, fn set_application(&mut self, application: Option>>)); $crate::delegate_to_inner!($field, fn get_application(&self) -> Option>>); $crate::delegate_to_inner!($field, fn set_helper_set(&mut self, helper_set: std::rc::Rc>)); $crate::delegate_to_inner!($field, fn get_helper_set(&self) -> Option>>); $crate::delegate_to_inner!($field, fn merge_application_definition(&mut self, merge_args: bool)); $crate::delegate_to_inner!($field, fn get_definition(&self) -> &$crate::symfony::console::input::input_definition::InputDefinition); $crate::delegate_to_inner!($field, fn get_native_definition(&self) -> &$crate::symfony::console::input::input_definition::InputDefinition); $crate::delegate_to_inner!($field, fn set_name(&mut self, name: &str) -> anyhow::Result<()>); $crate::delegate_to_inner!($field, fn get_name(&self) -> Option); $crate::delegate_to_inner!($field, fn set_process_title(&mut self, title: &str)); $crate::delegate_to_inner!($field, fn get_process_title(&self) -> Option); $crate::delegate_to_inner!($field, fn set_hidden(&mut self, hidden: bool)); $crate::delegate_to_inner!($field, fn is_hidden(&self) -> bool); $crate::delegate_to_inner!($field, fn set_description(&mut self, description: &str)); $crate::delegate_to_inner!($field, fn get_description(&self) -> String); $crate::delegate_to_inner!($field, fn set_help(&mut self, help: &str)); $crate::delegate_to_inner!($field, fn get_help(&self) -> String); $crate::delegate_to_inner!($field, fn get_processed_help(&self) -> String); $crate::delegate_to_inner!($field, fn set_aliases(&mut self, aliases: Vec) -> anyhow::Result<()>); $crate::delegate_to_inner!($field, fn get_aliases(&self) -> Vec); $crate::delegate_to_inner!($field, fn get_synopsis(&mut self, short: bool) -> String); $crate::delegate_to_inner!($field, fn add_usage(&mut self, usage: &str)); $crate::delegate_to_inner!($field, fn get_usages(&self) -> Vec); $crate::delegate_to_inner!($field, fn get_helper(&self, name: &str) -> anyhow::Result>); $crate::delegate_to_inner!($field, fn set_code(&mut self, code: Box shirabe_php_shim::PhpMixed>)); $crate::delegate_to_inner!($field, fn get_code(&self) -> Option<&Box shirabe_php_shim::PhpMixed>>); $crate::delegate_to_inner!($field, fn ignore_validation_errors(&mut self)); $crate::delegate_to_inner!($field, fn get_ignore_validation_errors(&self) -> bool); }; } /// Polymorphic interface for all commands (PHP's `Command` base class as seen by /// callers that hold a command of unknown concrete type). /// /// The canonical behavior lives in `impl Command for CommandData`; subclasses forward /// the state methods there and override the behavior hooks (`configure`/`execute`/...). /// Object-safe so `dyn Command` works; the fluent `where Self: Sized` setters are only /// called from `configure()` on a concrete command, never through `dyn Command`. pub trait Command: std::fmt::Debug + shirabe_php_shim::AsAny { fn clone_box(&self) -> Box { todo!() } // --- behavior hooks (PHP-overridable; defaults match the PHP `Command` class) --- /// Configures the current command. fn configure(&mut self) -> anyhow::Result<()> { Ok(()) } /// Executes the current command, returning 0 or an exit code. /// /// Concrete commands override this; reaching the default means a command class /// forgot to implement it (PHP throws LogicException — a programming error here). fn execute( &mut self, _input: Rc>, _output: Rc>, ) -> anyhow::Result { panic!("You must override the execute() method in the concrete command class."); } /// Interacts with the user before the InputDefinition is validated. fn interact( &mut self, _input: Rc>, _output: Rc>, ) { } /// Initializes the command after the input has been bound and before it is validated. fn initialize( &mut self, _input: Rc>, _output: Rc>, ) -> anyhow::Result<()> { Ok(()) } /// Adds suggestions to `suggestions` for the current completion input. fn complete(&self, _input: &CompletionInput, _suggestions: &mut CompletionSuggestions) {} /// Whether this command proxies to another application/command (Composer's /// `BaseCommand::isProxyCommand`). Exposed here so the `dyn Command` registry can detect proxy /// commands without downcasting to the Composer `BaseCommand` trait; defaults to `false` and is /// overridden by Composer proxy commands such as `GlobalCommand`. fn is_proxy_command(&self) -> bool { false } /// Runs the command. /// /// Template method: it calls `self.initialize()`, `self.interact()` and /// `self.execute()`, which dispatch to the concrete command's overrides. It must /// not be overridden (except by proxy commands like `GlobalCommand`) nor delegated, /// or that late binding breaks. fn run( &mut self, input: Rc>, output: Rc>, ) -> anyhow::Result { self.base_run(input, output) } /// The base-class (`Command`) body of `run`, as PHP's `Command::run`. Proxy commands such as /// `GlobalCommand` override `run` but still call `base_run` to delegate to the base behavior, /// matching PHP's `parent::run($input, $output)`. It must not be overridden, or the late /// binding of `initialize`/`interact`/`execute` to the concrete command breaks. fn base_run( &mut self, input: Rc>, output: Rc>, ) -> anyhow::Result { // add the application arguments and options self.merge_application_definition(true); // bind the input against the command specific arguments/options match input.borrow_mut().bind(self.get_definition()) { Ok(()) => {} Err(e) => { if !self.get_ignore_validation_errors() { return Err(e); } } } self.initialize(input.clone(), output.clone())?; if let Some(process_title) = self.get_process_title() { // TODO: PHP probes for cli_set_process_title / setproctitle availability. if shirabe_php_shim::function_exists("cli_set_process_title") { if !shirabe_php_shim::cli_set_process_title(&process_title) { if shirabe_php_shim::PHP_OS == "Darwin" { output.borrow_mut().writeln( &["Running \"cli_set_process_title\" as an unprivileged user is not supported on MacOS.".to_string()], output_interface::VERBOSITY_VERY_VERBOSE, ); } else { shirabe_php_shim::cli_set_process_title(&process_title); } } } else if shirabe_php_shim::function_exists("setproctitle") { shirabe_php_shim::setproctitle(&process_title); } else if output.borrow().get_verbosity() == output_interface::VERBOSITY_VERY_VERBOSE { output.borrow_mut().writeln( &["Install the proctitle PECL to be able to change the process title.".to_string()], output_interface::OUTPUT_NORMAL, ); } } if input.borrow().is_interactive() { self.interact(input.clone(), output.clone()); } // The command name argument is often omitted when a command is executed directly with its run() method. // It would fail the validation if we didn't make sure the command argument is present, // since it's required by the application. if input.borrow().has_argument("command") && matches!(input.borrow().get_argument("command")?, PhpMixed::Null) { let name = self.get_name(); input .borrow_mut() .set_argument("command", PhpMixed::from(name))?; } input.borrow_mut().validate()?; let status_code: PhpMixed; if let Some(code) = self.get_code() { status_code = code(&mut *input.borrow_mut(), &mut *output.borrow_mut()); } else { let executed = self.execute(input.clone(), output.clone())?; status_code = PhpMixed::from(executed); // PHP also raises \TypeError when execute() does not return int; in this // strongly-typed port execute() already returns an int, so the check is moot. } // is_numeric($statusCode) ? (int) $statusCode : 0 Ok(shirabe_php_shim::is_numeric_to_int(&status_code)) } // --- state methods (canonical impl on `CommandData`; subclasses forward there) --- fn is_enabled(&self) -> bool; fn set_application(&mut self, application: Option>>); fn get_application(&self) -> Option>>; fn set_helper_set(&mut self, helper_set: Rc>); fn get_helper_set(&self) -> Option>>; fn merge_application_definition(&mut self, merge_args: bool); fn get_definition(&self) -> &InputDefinition; fn get_native_definition(&self) -> &InputDefinition; fn set_name(&mut self, name: &str) -> anyhow::Result<()>; fn get_name(&self) -> Option; fn set_process_title(&mut self, title: &str); fn get_process_title(&self) -> Option; fn set_hidden(&mut self, hidden: bool); fn is_hidden(&self) -> bool; fn set_description(&mut self, description: &str); fn get_description(&self) -> String; fn set_help(&mut self, help: &str); fn get_help(&self) -> String; fn get_processed_help(&self) -> String; fn set_aliases(&mut self, aliases: Vec) -> anyhow::Result<()>; fn get_aliases(&self) -> Vec; fn get_synopsis(&mut self, short: bool) -> String; fn add_usage(&mut self, usage: &str); fn get_usages(&self) -> Vec; fn get_helper( &self, name: &str, ) -> anyhow::Result< Result, >; fn set_code( &mut self, code: Box PhpMixed>, ); fn get_code( &self, ) -> Option<&Box PhpMixed>>; fn ignore_validation_errors(&mut self); fn get_ignore_validation_errors(&self) -> bool; } impl Command for CommandData { fn is_enabled(&self) -> bool { true } fn set_application(&mut self, application: Option>>) { self.application = application.clone(); if let Some(application) = application { self.set_helper_set(application.borrow_mut().get_helper_set()); } else { self.helper_set = None; } self.full_definition = None; } fn get_application(&self) -> Option>> { self.application.clone() } fn set_helper_set(&mut self, helper_set: Rc>) { self.helper_set = Some(helper_set); } fn get_helper_set(&self) -> Option>> { self.helper_set.clone() } /// Merges the application definition with the command definition. fn merge_application_definition(&mut self, merge_args: bool) { let application = match &self.application { None => return, Some(application) => application.clone(), }; // InputDefinition stores its entries as `Rc` / `Rc` while the // setters take owned values, so the shared entries are cloned out (both types derive Clone). let app_definition = application.borrow_mut().get_definition(); let mut full_definition = InputDefinition::new(Vec::new()).expect("an empty InputDefinition cannot fail"); let own_options: Vec = self .definition .as_ref() .unwrap() .get_options() .values() .map(|option| (**option).clone()) .collect(); full_definition .set_options(own_options) .expect("the command's own options are already valid"); let app_options: Vec = app_definition .borrow() .get_options() .values() .map(|option| (**option).clone()) .collect(); full_definition .add_options(app_options) .expect("merging the application options cannot conflict here"); if merge_args { let app_arguments: Vec = app_definition .borrow() .get_arguments() .values() .map(|argument| (**argument).clone()) .collect(); full_definition .set_arguments(app_arguments) .expect("the application arguments are already valid"); let own_arguments: Vec = self .definition .as_ref() .unwrap() .get_arguments() .values() .map(|argument| (**argument).clone()) .collect(); full_definition .add_arguments(Some(own_arguments)) .expect("merging the command's own arguments cannot conflict here"); } else { let own_arguments: Vec = self .definition .as_ref() .unwrap() .get_arguments() .values() .map(|argument| (**argument).clone()) .collect(); full_definition .set_arguments(own_arguments) .expect("the command's own arguments are already valid"); } self.full_definition = Some(full_definition); } fn get_definition(&self) -> &InputDefinition { match &self.full_definition { Some(full_definition) => full_definition, None => self.get_native_definition(), } } fn get_native_definition(&self) -> &InputDefinition { match &self.definition { None => { // PHP throws LogicException; `definition` is set in `new()`, so None is a // programming error (forgot to call the parent constructor). panic!( "Command class is not correctly initialized. You probably forgot to call the parent constructor." ); } Some(definition) => definition, } } fn set_name(&mut self, name: &str) -> anyhow::Result<()> { if let Err(e) = self.validate_name(name)? { return Err(e.into()); } self.name = Some(name.to_string()); Ok(()) } fn get_name(&self) -> Option { self.name.clone() } fn set_process_title(&mut self, title: &str) { self.process_title = Some(title.to_string()); } fn get_process_title(&self) -> Option { self.process_title.clone() } fn set_hidden(&mut self, hidden: bool) { self.hidden = hidden; } fn is_hidden(&self) -> bool { self.hidden } fn set_description(&mut self, description: &str) { self.description = description.to_string(); } fn get_description(&self) -> String { self.description.clone() } fn set_help(&mut self, help: &str) { self.help = help.to_string(); } fn get_help(&self) -> String { self.help.clone() } fn get_processed_help(&self) -> String { let name = self.name.clone(); let is_single_command = match &self.application { Some(application) => application.borrow().is_single_command(), None => false, }; let placeholders = [ "%command.name%".to_string(), "%command.full_name%".to_string(), ]; let php_self = shirabe_php_shim::server("PHP_SELF"); let replacements = [ name.clone().unwrap_or_default(), if is_single_command { php_self.clone() } else { format!("{} {}", php_self, name.unwrap_or_default()) }, ]; let help = self.get_help(); let subject = if help.is_empty() { self.get_description() } else { help }; shirabe_php_shim::str_replace_array(&placeholders, &replacements, &subject) } fn set_aliases(&mut self, aliases: Vec) -> anyhow::Result<()> { let mut list = Vec::new(); for alias in &aliases { if let Err(e) = self.validate_name(alias)? { return Err(e.into()); } list.push(alias.clone()); } // PHP: `\is_array($aliases) ? $aliases : $list`. Here `aliases` is always an // array (Vec), so the result is `aliases`; `list` mirrors the validation loop. self.aliases = aliases; Ok(()) } fn get_aliases(&self) -> Vec { self.aliases.clone() } fn get_synopsis(&mut self, short: bool) -> String { let key = if short { "short" } else { "long" }.to_string(); if !self.synopsis.contains_key(&key) { let value = format!( "{} {}", self.name.clone().unwrap_or_default(), self.definition.as_ref().unwrap().get_synopsis(short) ) .trim() .to_string(); self.synopsis.insert(key.clone(), value); } self.synopsis[&key].clone() } fn add_usage(&mut self, usage: &str) { let mut usage = usage.to_string(); let name = self.name.clone().unwrap_or_default(); if !usage.starts_with(&name) { usage = format!("{} {}", name, usage); } self.usages.push(usage); } fn get_usages(&self) -> Vec { self.usages.clone() } fn get_helper( &self, name: &str, ) -> anyhow::Result< Result, > { let helper_set = match &self.helper_set { None => { return Ok(Err( crate::symfony::console::exception::logic_exception::LogicException( shirabe_php_shim::LogicException { message: format!( "Cannot retrieve helper \"{}\" because there is no HelperSet defined. Did you forget to add your command to the application or to set the application on the command using the setApplication() method? You can also set the HelperSet directly using the setHelperSet() method.", name ), code: 0, }, ), )); } Some(helper_set) => helper_set, }; // TODO(review): HelperSet::get() returns `Rc>`, but // Command::getHelper() is typed `mixed` (PhpMixed) here and PhpMixed cannot hold a // helper instance. The helper return modelling needs a dedicated type (Phase C). let _ = helper_set; todo!() } fn set_code( &mut self, code: Box PhpMixed>, ) { // TODO: PHP rebinds an unbound Closure's $this to the command instance via // ReflectionFunction/Closure::bind. Rust closures have no `$this` rebinding; // the closure is stored as-is. self.code = Some(code); } fn get_code( &self, ) -> Option<&Box PhpMixed>> { self.code.as_ref() } fn ignore_validation_errors(&mut self) { self.ignore_validation_errors = true; } fn get_ignore_validation_errors(&self) -> bool { self.ignore_validation_errors } } impl std::fmt::Debug for CommandData { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("CommandData") .field("name", &self.name) .field("aliases", &self.aliases) .field("hidden", &self.hidden) .field("description", &self.description) .finish_non_exhaustive() } }