diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-06-12 01:01:35 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-06-12 01:01:43 +0900 |
| commit | 1e44f5723e4c0e0903d00a61f254901e612fe5e1 (patch) | |
| tree | 9c2e1eec364bbf6418a4072ee7767b04c3df0297 /crates/shirabe-external-packages/src/symfony/console/helper/process_helper.rs | |
| parent | ddf0a624145b618c05c2ee47414545b99b685f37 (diff) | |
| download | php-shirabe-1e44f5723e4c0e0903d00a61f254901e612fe5e1.tar.gz php-shirabe-1e44f5723e4c0e0903d00a61f254901e612fe5e1.tar.zst php-shirabe-1e44f5723e4c0e0903d00a61f254901e612fe5e1.zip | |
feat(symfony-console): port Symfony Console and make the workspace compile
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Diffstat (limited to 'crates/shirabe-external-packages/src/symfony/console/helper/process_helper.rs')
| -rw-r--r-- | crates/shirabe-external-packages/src/symfony/console/helper/process_helper.rs | 306 |
1 files changed, 306 insertions, 0 deletions
diff --git a/crates/shirabe-external-packages/src/symfony/console/helper/process_helper.rs b/crates/shirabe-external-packages/src/symfony/console/helper/process_helper.rs new file mode 100644 index 0000000..7652cac --- /dev/null +++ b/crates/shirabe-external-packages/src/symfony/console/helper/process_helper.rs @@ -0,0 +1,306 @@ +use crate::symfony::console::helper::debug_formatter_helper::DebugFormatterHelper; +use crate::symfony::console::helper::helper::Helper; +use crate::symfony::console::helper::helper_interface::HelperInterface; +use crate::symfony::console::helper::helper_set::HelperSet; +use crate::symfony::console::output::console_output_interface::ConsoleOutputInterface; +use crate::symfony::console::output::output_interface::{self, OutputInterface}; +use crate::symfony::process::exception::process_failed_exception::ProcessFailedException; +use crate::symfony::process::process::Process; +use std::cell::RefCell; +use std::rc::Rc; + +/// 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: Rc<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(); }`. Downcasting a shared `dyn + // OutputInterface` to `dyn ConsoleOutputInterface` is unresolved; + // deferred to Phase C. + let output: Rc<RefCell<dyn OutputInterface>> = + todo!("$output instanceof ConsoleOutputInterface redirect to error output"); + + let formatter: Rc<RefCell<dyn HelperInterface>> = self + .get_helper_set() + .unwrap() + .borrow() + .get("debug_formatter") + .unwrap(); + + // 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, None, 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 { + message: shirabe_php_shim::sprintf( + "Invalid command provided to \"%s()\": 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() + )], + ), + code: 0, + }); + } + } + + if verbosity <= output.borrow().get_verbosity() { + let started = Self::formatter_start( + &formatter, + &shirabe_php_shim::spl_object_hash_process(&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. + process.run(callback); + + if verbosity <= output.borrow().get_verbosity() { + let message = if process.is_successful() { + "Command ran successfully".to_string() + } else { + shirabe_php_shim::sprintf( + "%s 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(&process), + &message, + process.is_successful(), + ); + output + .borrow() + .write(&[stopped], false, output_interface::OUTPUT_NORMAL); + } + + if !process.is_successful() && error.is_some() { + output.borrow().writeln( + &[shirabe_php_shim::sprintf( + "<error>%s</error>", + &[shirabe_php_shim::PhpMixed::String( + self.escape_string(error.unwrap()), + )], + )], + 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: Rc<RefCell<dyn OutputInterface>>, + cmd: ProcessHelperCmd, + error: Option<&str>, + callback: Option<Box<dyn FnMut(&str, &str)>>, + ) -> anyhow::Result<Process> { + let process = self.run( + output, + cmd, + error, + callback, + output_interface::VERBOSITY_VERY_VERBOSE, + )?; + + if !process.is_successful() { + anyhow::bail!(ProcessFailedException::new(&process)); + } + + Ok(process) + } + + /// Wraps a Process callback to add debugging output. + pub fn wrap_callback( + &self, + output: Rc<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(); }`. Downcasting a shared `dyn + // OutputInterface` to `dyn ConsoleOutputInterface` is unresolved; + // deferred to Phase C. + let output: Rc<RefCell<dyn OutputInterface>> = + todo!("$output instanceof ConsoleOutputInterface redirect to error output"); + + let formatter: Rc<RefCell<dyn HelperInterface>> = self + .get_helper_set() + .unwrap() + .borrow() + .get("debug_formatter") + .unwrap(); + + let object_hash = shirabe_php_shim::spl_object_hash_process(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) + } + + /// PHP fetches `debug_formatter` from the HelperSet as a `HelperInterface` and + /// dynamically dispatches `start`/`stop`/`progress`, which are concrete + /// `DebugFormatterHelper` methods not present on the interface. Resolving the + /// dynamic helper handle back to the concrete `DebugFormatterHelper` is a + /// downcast that requires a Phase C decision (e.g. an `as_any` on + /// `HelperInterface`); deferred here. + fn debug_formatter( + _formatter: &Rc<RefCell<dyn HelperInterface>>, + ) -> Rc<RefCell<DebugFormatterHelper>> { + todo!("downcast HelperInterface handle to DebugFormatterHelper (Phase C)") + } + + fn formatter_start( + formatter: &Rc<RefCell<dyn HelperInterface>>, + id: &str, + message: &str, + ) -> String { + Self::debug_formatter(formatter) + .borrow_mut() + .start(id, message, "RUN") + } + + fn formatter_stop( + formatter: &Rc<RefCell<dyn HelperInterface>>, + id: &str, + message: &str, + successful: bool, + ) -> String { + Self::debug_formatter(formatter) + .borrow_mut() + .stop(id, message, successful, "RES") + } + + fn formatter_progress( + formatter: &Rc<RefCell<dyn HelperInterface>>, + id: &str, + buffer: &str, + error: bool, + ) -> String { + Self::debug_formatter(formatter) + .borrow_mut() + .progress(id, buffer, error, "OUT", "ERR") + } +} + +impl HelperInterface for ProcessHelper { + fn set_helper_set(&mut self, helper_set: Option<Rc<RefCell<HelperSet>>>) { + self.inner.set_helper_set(helper_set); + } + + fn get_helper_set(&self) -> Option<Rc<RefCell<HelperSet>>> { + self.inner.get_helper_set() + } + + fn get_name(&self) -> String { + "process".to_string() + } +} |
