diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-08-09 11:19:03 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-08-09 11:19:03 +0900 |
| commit | 6051927c8fa32cfffa102d2a170c5a6cf747a1b9 (patch) | |
| tree | 3fe6769c90cb03ca8f1b9b825e38ad459182361e /crates/shirabe-symfony-console/src/helper/process_helper.rs | |
| parent | e3e8806aec771e482899ed3470e920f7b291fa95 (diff) | |
| download | php-shirabe-6051927c8fa32cfffa102d2a170c5a6cf747a1b9.tar.gz php-shirabe-6051927c8fa32cfffa102d2a170c5a6cf747a1b9.tar.zst php-shirabe-6051927c8fa32cfffa102d2a170c5a6cf747a1b9.zip | |
refactor(symfony-console): extract symfony/console into the shirabe-symfony-console crate
Move `Symfony\Component\Console` out of shirabe-external-packages and
into its own crate, so the path is
`shirabe_symfony_console::application::Application` instead of
`shirabe_external_packages::symfony::console::application::Application`.
The `delegate_to_inner!` and `delegate_command_trait_impls_to_inner!`
macros move with it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'crates/shirabe-symfony-console/src/helper/process_helper.rs')
| -rw-r--r-- | crates/shirabe-symfony-console/src/helper/process_helper.rs | 310 |
1 files changed, 310 insertions, 0 deletions
diff --git a/crates/shirabe-symfony-console/src/helper/process_helper.rs b/crates/shirabe-symfony-console/src/helper/process_helper.rs new file mode 100644 index 00000000..be1c1a84 --- /dev/null +++ b/crates/shirabe-symfony-console/src/helper/process_helper.rs @@ -0,0 +1,310 @@ +//! ref: composer/vendor/symfony/console/Helper/ProcessHelper.php + +use crate::helper::debug_formatter_helper::DebugFormatterHelper; +use crate::helper::helper::Helper; +use crate::helper::helper_interface::HelperInterface; +use crate::helper::helper_set::HelperSet; +use crate::output::ConsoleOutputInterface; +use crate::output::output_interface::{self, OutputInterface}; +use shirabe_symfony_process::exception::process_failed_exception::ProcessFailedException; +use shirabe_symfony_process::process::Process; + +/// The ProcessHelper class provides helpers to run external processes. +/// +/// @final +#[derive(Debug, Default)] +pub struct ProcessHelper { + inner: Helper, +} + +/// `$cmd` is either a `Process` instance or an array whose first element is a +/// binary path (string) or a `Process`, followed by extra environment entries. +#[derive(Debug)] +pub enum ProcessHelperCmd { + Process(Process), + Array(Vec<ProcessHelperCmdElement>), +} + +#[derive(Debug)] +pub enum ProcessHelperCmdElement { + String(String), + Process(Process), +} + +impl ProcessHelper { + /// Runs an external process. + /// + /// @param array|Process $cmd An instance of Process or an array of the command and arguments + /// @param callable|null $callback A PHP callback to run whenever there is some + /// output available on STDOUT or STDERR + pub fn run( + &self, + output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, + cmd: ProcessHelperCmd, + error: Option<&str>, + callback: Option<Box<dyn FnMut(&str, &str)>>, + verbosity: i64, + ) -> anyhow::Result<Process> { + // `class_exists(Process::class)` guards against the optional symfony/process + // component being absent; in this port the component is always available. + + // PHP: `if ($output instanceof ConsoleOutputInterface) { $output = + // $output->getErrorOutput(); }`. ConsoleOutput is the only OutputInterface + // implementor that also implements ConsoleOutputInterface, so the check + // reduces to a downcast to the concrete type. + let output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>> = { + let redirected = shirabe_php_shim::AsAny::as_any(&*output.borrow()) + .downcast_ref::<crate::output::console_output::ConsoleOutput>() + .map(|console| console.get_error_output()); + redirected.unwrap_or(output) + }; + + let formatter: std::rc::Rc<std::cell::RefCell<DebugFormatterHelper>> = self + .get_helper_set() + .unwrap() + .borrow() + .get_debug_formatter(); + + // Normalize $cmd: a single Process becomes a one-element array. + let mut cmd = match cmd { + ProcessHelperCmd::Process(process) => { + vec![ProcessHelperCmdElement::Process(process)] + } + ProcessHelperCmd::Array(cmd) => cmd, + }; + + // `!\is_array($cmd)` cannot happen given the enum, so the TypeError branch + // is unreachable here. + + let mut process: Process; + match cmd.first() { + Some(ProcessHelperCmdElement::String(_)) => { + let command: Vec<String> = cmd + .iter() + .map(|element| match element { + ProcessHelperCmdElement::String(s) => s.clone(), + ProcessHelperCmdElement::Process(_) => unreachable!(), + }) + .collect(); + process = Process::new( + command, + None, + None, + shirabe_php_shim::PhpMixed::Null, + Some(60.0), + )?; + cmd = vec![]; + } + Some(ProcessHelperCmdElement::Process(_)) => { + let first = cmd.remove(0); + process = match first { + ProcessHelperCmdElement::Process(process) => process, + ProcessHelperCmdElement::String(_) => unreachable!(), + }; + } + None => { + anyhow::bail!(shirabe_php_shim::InvalidArgumentException::new(format!( + "Invalid command provided to \"{}()\": the command should be an array whose first element is either the path to the binary to run or a \"Process\" object.", + shirabe_php_shim::PhpMixed::String("ProcessHelper::run".to_string()), + ))); + } + } + + if verbosity <= output.borrow().get_verbosity() { + let started = Self::formatter_start( + &formatter, + &shirabe_php_shim::spl_object_hash(&process), + &self.escape_string(&process.get_command_line()), + ); + output + .borrow() + .write(&[started], false, output_interface::OUTPUT_NORMAL); + } + + let callback = if output.borrow().is_debug() { + Some(self.wrap_callback(output.clone(), &process, callback)) + } else { + callback + }; + + // PHP passes the remaining `$cmd` array as the `$env` argument to Process::run. + let env: indexmap::IndexMap<String, shirabe_php_shim::PhpMixed> = cmd + .iter() + .enumerate() + .filter_map(|(i, element)| match element { + ProcessHelperCmdElement::String(s) => { + Some((i.to_string(), shirabe_php_shim::PhpMixed::String(s.clone()))) + } + ProcessHelperCmdElement::Process(_) => None, + }) + .collect(); + let callback: Option<Box<dyn FnMut(&str, &str) -> bool>> = callback.map(|mut cb| { + Box::new(move |r#type: &str, buffer: &str| -> bool { + cb(r#type, buffer); + false + }) as Box<dyn FnMut(&str, &str) -> bool> + }); + process.run(callback, env)?; + + if verbosity <= output.borrow().get_verbosity() { + let message = if process.is_successful() { + "Command ran successfully".to_string() + } else { + format!( + "{} Command did not run successfully", + match process.get_exit_code() { + Some(code) => shirabe_php_shim::PhpMixed::Int(code), + None => shirabe_php_shim::PhpMixed::Null, + }, + ) + }; + let stopped = Self::formatter_stop( + &formatter, + &shirabe_php_shim::spl_object_hash(&process), + &message, + process.is_successful(), + ); + output + .borrow() + .write(&[stopped], false, output_interface::OUTPUT_NORMAL); + } + + if !process.is_successful() + && let Some(error) = error + { + output.borrow().writeln( + &[format!("<error>{}</error>", self.escape_string(error))], + output_interface::OUTPUT_NORMAL, + ); + } + + Ok(process) + } + + /// Runs the process. + /// + /// This is identical to run() except that an exception is thrown if the process + /// exits with a non-zero exit code. + /// + /// @param array|Process $cmd An instance of Process or a command to run + /// @param callable|null $callback A PHP callback to run whenever there is some + /// output available on STDOUT or STDERR + /// + /// @throws ProcessFailedException + /// + /// @see run() + pub fn must_run( + &self, + output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, + cmd: ProcessHelperCmd, + error: Option<&str>, + callback: Option<Box<dyn FnMut(&str, &str)>>, + ) -> anyhow::Result<Process> { + let mut process = self.run( + output, + cmd, + error, + callback, + output_interface::VERBOSITY_VERY_VERBOSE, + )?; + + if !process.is_successful() { + anyhow::bail!(ProcessFailedException::new(&mut process)?); + } + + Ok(process) + } + + /// Wraps a Process callback to add debugging output. + pub fn wrap_callback( + &self, + output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, + process: &Process, + mut callback: Option<Box<dyn FnMut(&str, &str)>>, + ) -> Box<dyn FnMut(&str, &str)> { + // PHP: `if ($output instanceof ConsoleOutputInterface) { $output = + // $output->getErrorOutput(); }`. ConsoleOutput is the only OutputInterface + // implementor that also implements ConsoleOutputInterface, so the check + // reduces to a downcast to the concrete type. + let output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>> = { + let redirected = shirabe_php_shim::AsAny::as_any(&*output.borrow()) + .downcast_ref::<crate::output::console_output::ConsoleOutput>() + .map(|console| console.get_error_output()); + redirected.unwrap_or(output) + }; + + let formatter: std::rc::Rc<std::cell::RefCell<DebugFormatterHelper>> = self + .get_helper_set() + .unwrap() + .borrow() + .get_debug_formatter(); + + let object_hash = shirabe_php_shim::spl_object_hash(process); + + Box::new(move |r#type: &str, buffer: &str| { + let progressed = Self::formatter_progress( + &formatter, + &object_hash, + &Self::escape_string_static(buffer), + Process::ERR == r#type, + ); + output + .borrow() + .write(&[progressed], false, output_interface::OUTPUT_NORMAL); + + if let Some(callback) = callback.as_mut() { + callback(r#type, buffer); + } + }) + } + + fn escape_string(&self, str: &str) -> String { + shirabe_php_shim::str_replace("<", "\\<", str) + } + + fn escape_string_static(str: &str) -> String { + shirabe_php_shim::str_replace("<", "\\<", str) + } + + fn formatter_start( + formatter: &std::rc::Rc<std::cell::RefCell<DebugFormatterHelper>>, + id: &str, + message: &str, + ) -> String { + formatter.borrow_mut().start(id, message, "RUN") + } + + fn formatter_stop( + formatter: &std::rc::Rc<std::cell::RefCell<DebugFormatterHelper>>, + id: &str, + message: &str, + successful: bool, + ) -> String { + formatter.borrow_mut().stop(id, message, successful, "RES") + } + + fn formatter_progress( + formatter: &std::rc::Rc<std::cell::RefCell<DebugFormatterHelper>>, + id: &str, + buffer: &str, + error: bool, + ) -> String { + formatter + .borrow_mut() + .progress(id, buffer, error, "OUT", "ERR") + } +} + +impl HelperInterface for ProcessHelper { + fn set_helper_set(&mut self, helper_set: Option<std::rc::Rc<std::cell::RefCell<HelperSet>>>) { + self.inner.set_helper_set(helper_set); + } + + fn get_helper_set(&self) -> Option<std::rc::Rc<std::cell::RefCell<HelperSet>>> { + self.inner.get_helper_set() + } + + fn get_name(&self) -> String { + "process".to_string() + } +} |
