From c644aa4df0dfcd58e3f0a1372611921678ed6c6d Mon Sep 17 00:00:00 2001 From: nsfisis Date: Tue, 23 Jun 2026 02:26:56 +0900 Subject: refactor(console): close HelperSet to a fixed set of four helpers Replace the dynamic, string-keyed HelperSet (set/has/get/get_iterator, HelperSetKey, deprecated set_command/get_command) with a closed set of FormatterHelper, DebugFormatterHelper, ProcessHelper and QuestionHelper instantiated by an argument-less constructor and exposed through typed getters (get_formatter/get_debug_formatter/get_process/get_question). The typed getters let ProcessHelper, QuestionHelper::write_error and InitCommand::interact drop their downcast/placeholder todo!() stubs. Dynamic registration of plugin-provided helpers is intentionally dropped for now and tracked via TODO(plugin) comments. Co-Authored-By: Claude Opus 4.8 --- .../src/symfony/console/command/command.rs | 8 +- .../src/symfony/console/helper/helper_set.rs | 143 ++++++++------------- .../src/symfony/console/helper/process_helper.rs | 38 ++---- .../src/symfony/console/helper/question_helper.rs | 26 ++-- crates/shirabe/src/command/init_command.rs | 25 ++-- crates/shirabe/src/console/application.rs | 49 ++----- 6 files changed, 98 insertions(+), 191 deletions(-) (limited to 'crates') 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 2d94ad2..59aa915 100644 --- a/crates/shirabe-external-packages/src/symfony/console/command/command.rs +++ b/crates/shirabe-external-packages/src/symfony/console/command/command.rs @@ -822,9 +822,11 @@ impl Command for CommandData { 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). + // TODO(plugin): PHP's Command::getHelper($name) looks a helper up by string via + // HelperSet::get($name). The HelperSet is now a closed set exposing only typed getters + // (get_formatter/get_question/...), so a string-keyed lookup no longer exists. Callers + // should use the typed getters on the HelperSet directly; restoring name-based lookup is + // deferred until the plugin API (which is the only source of dynamically named helpers). let _ = helper_set; todo!() } diff --git a/crates/shirabe-external-packages/src/symfony/console/helper/helper_set.rs b/crates/shirabe-external-packages/src/symfony/console/helper/helper_set.rs index e73a53e..d72b627 100644 --- a/crates/shirabe-external-packages/src/symfony/console/helper/helper_set.rs +++ b/crates/shirabe-external-packages/src/symfony/console/helper/helper_set.rs @@ -1,117 +1,76 @@ //! ref: composer/vendor/symfony/console/Helper/HelperSet.php -use crate::symfony::console::command::command::Command; -use crate::symfony::console::exception::invalid_argument_exception::InvalidArgumentException; +use crate::symfony::console::helper::debug_formatter_helper::DebugFormatterHelper; +use crate::symfony::console::helper::formatter_helper::FormatterHelper; use crate::symfony::console::helper::helper_interface::HelperInterface; -use indexmap::IndexMap; +use crate::symfony::console::helper::process_helper::ProcessHelper; +use crate::symfony::console::helper::question_helper::QuestionHelper; use std::cell::RefCell; use std::rc::Rc; /// HelperSet represents a set of helpers to be used with a command. /// -/// @implements \IteratorAggregate -#[derive(Debug, Default, Clone)] +/// Symfony lets arbitrary helpers be registered by name, but Composer only ever uses the four +/// helpers `Application::getDefaultHelperSet()` installs. This port closes the set to exactly those +/// four, instantiates them in the argument-less constructor, and exposes them through typed getters +/// instead of Symfony's string-keyed `get()`/`has()`/`set()`. +/// +/// TODO(plugin): a plugin-defined custom command may register extra helpers dynamically via +/// `getApplication()->getHelperSet()`. Restoring that path (a `set()` equivalent plus name-based +/// lookup) is deferred until the plugin API is implemented. +#[derive(Debug)] pub struct HelperSet { - helpers: IndexMap>>, - command: Option>>, + formatter_helper: Rc>, + debug_formatter_helper: Rc>, + process_helper: Rc>, + question_helper: Rc>, } impl HelperSet { - /// @param Helper[] $helpers An array of helper - pub fn new( - this: &Rc>, - helpers: IndexMap>>, - ) { - for (alias, helper) in helpers { - let alias = match alias { - HelperSetKey::Int(_) => None, - HelperSetKey::String(alias) => Some(alias), - }; - Self::set(this, helper, alias.as_deref()); - } - } + /// Builds the fixed set of helpers and wires each one's back-reference to the owning set, + /// mirroring the `$helper->setHelperSet($this)` call PHP's `HelperSet::set()` performs. + pub fn new() -> Rc> { + let formatter_helper = Rc::new(RefCell::new(FormatterHelper::default())); + let debug_formatter_helper = Rc::new(RefCell::new(DebugFormatterHelper::default())); + let process_helper = Rc::new(RefCell::new(ProcessHelper::default())); + let question_helper = Rc::new(RefCell::new(QuestionHelper::default())); - pub fn set( - this: &Rc>, - helper: Rc>, - alias: Option<&str>, - ) { - let name = helper.borrow().get_name(); - this.borrow_mut().helpers.insert(name, helper.clone()); - if let Some(alias) = alias { - this.borrow_mut() - .helpers - .insert(alias.to_string(), helper.clone()); - } + let this = Rc::new(RefCell::new(HelperSet { + formatter_helper: formatter_helper.clone(), + debug_formatter_helper: debug_formatter_helper.clone(), + process_helper: process_helper.clone(), + question_helper: question_helper.clone(), + })); - helper.borrow_mut().set_helper_set(Some(this.clone())); - } + formatter_helper + .borrow_mut() + .set_helper_set(Some(this.clone())); + debug_formatter_helper + .borrow_mut() + .set_helper_set(Some(this.clone())); + process_helper + .borrow_mut() + .set_helper_set(Some(this.clone())); + question_helper + .borrow_mut() + .set_helper_set(Some(this.clone())); - /// Returns true if the helper if defined. - pub fn has(&self, name: &str) -> bool { - self.helpers.contains_key(name) + this } - /// Gets a helper value. - /// - /// @throws InvalidArgumentException if the helper is not defined - pub fn get( - &self, - name: &str, - ) -> Result>, InvalidArgumentException> { - if !self.has(name) { - return Err(InvalidArgumentException( - shirabe_php_shim::InvalidArgumentException { - message: format!( - "The helper \"{}\" is not defined.", - shirabe_php_shim::PhpMixed::String(name.to_string()), - ), - code: 0, - }, - )); - } - - Ok(self.helpers[name].clone()) + pub fn get_formatter(&self) -> Rc> { + self.formatter_helper.clone() } - /// @deprecated since Symfony 5.4 - pub fn set_command(&mut self, command: Option>>) { - shirabe_php_shim::trigger_deprecation( - "symfony/console", - "5.4", - "Method \"%s()\" is deprecated.", - "HelperSet::setCommand", - ); - - self.command = command; + pub fn get_debug_formatter(&self) -> Rc> { + self.debug_formatter_helper.clone() } - /// Gets the command associated with this helper set. - /// - /// @deprecated since Symfony 5.4 - pub fn get_command(&self) -> Option>> { - shirabe_php_shim::trigger_deprecation( - "symfony/console", - "5.4", - "Method \"%s()\" is deprecated.", - "HelperSet::getCommand", - ); - - self.command.clone() + pub fn get_process(&self) -> Rc> { + self.process_helper.clone() } - /// @return \Traversable - pub fn get_iterator( - &self, - ) -> impl Iterator>)> { - self.helpers.iter() + pub fn get_question(&self) -> Rc> { + self.question_helper.clone() } } - -/// PHP array keys are either integers or strings; the HelperSet constructor -/// distinguishes them via `\is_int($alias)`. -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub enum HelperSetKey { - Int(i64), - String(String), -} 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 index e62b9ac..f141bee 100644 --- a/crates/shirabe-external-packages/src/symfony/console/helper/process_helper.rs +++ b/crates/shirabe-external-packages/src/symfony/console/helper/process_helper.rs @@ -57,12 +57,11 @@ impl ProcessHelper { let output: Rc> = todo!("$output instanceof ConsoleOutputInterface redirect to error output"); - let formatter: Rc> = self + let formatter: Rc> = self .get_helper_set() .unwrap() .borrow() - .get("debug_formatter") - .unwrap(); + .get_debug_formatter(); // Normalize $cmd: a single Process becomes a one-element array. let mut cmd = match cmd { @@ -234,12 +233,11 @@ impl ProcessHelper { let output: Rc> = todo!("$output instanceof ConsoleOutputInterface redirect to error output"); - let formatter: Rc> = self + let formatter: Rc> = self .get_helper_set() .unwrap() .borrow() - .get("debug_formatter") - .unwrap(); + .get_debug_formatter(); let object_hash = shirabe_php_shim::spl_object_hash_process(process); @@ -268,46 +266,30 @@ impl ProcessHelper { 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>, - ) -> Rc> { - todo!("downcast HelperInterface handle to DebugFormatterHelper (Phase C)") - } - fn formatter_start( - formatter: &Rc>, + formatter: &Rc>, id: &str, message: &str, ) -> String { - Self::debug_formatter(formatter) - .borrow_mut() - .start(id, message, "RUN") + formatter.borrow_mut().start(id, message, "RUN") } fn formatter_stop( - formatter: &Rc>, + formatter: &Rc>, id: &str, message: &str, successful: bool, ) -> String { - Self::debug_formatter(formatter) - .borrow_mut() - .stop(id, message, successful, "RES") + formatter.borrow_mut().stop(id, message, successful, "RES") } fn formatter_progress( - formatter: &Rc>, + formatter: &Rc>, id: &str, buffer: &str, error: bool, ) -> String { - Self::debug_formatter(formatter) + formatter .borrow_mut() .progress(id, buffer, error, "OUT", "ERR") } diff --git a/crates/shirabe-external-packages/src/symfony/console/helper/question_helper.rs b/crates/shirabe-external-packages/src/symfony/console/helper/question_helper.rs index db9c094..f2c2a00 100644 --- a/crates/shirabe-external-packages/src/symfony/console/helper/question_helper.rs +++ b/crates/shirabe-external-packages/src/symfony/console/helper/question_helper.rs @@ -5,6 +5,7 @@ use crate::symfony::console::exception::missing_input_exception::MissingInputExc use crate::symfony::console::exception::runtime_exception::RuntimeException; use crate::symfony::console::formatter::output_formatter::OutputFormatter; use crate::symfony::console::formatter::output_formatter_style::OutputFormatterStyle; +use crate::symfony::console::helper::formatter_helper::FormatBlockMessages; use crate::symfony::console::helper::helper::Helper; use crate::symfony::console::helper::helper_interface::HelperInterface; use crate::symfony::console::helper::helper_set::HelperSet; @@ -316,22 +317,17 @@ impl QuestionHelper { output: Rc>, error: &shirabe_php_shim::Exception, ) { - let message; - if let Some(helper_set) = self.get_helper_set() { - if helper_set.borrow().has("formatter") { - // PHP: `$this->getHelperSet()->get('formatter')->formatBlock(...)`. - // HelperSet::get yields `dyn HelperInterface`, which does not have - // `AsAny` as a supertrait, so it cannot be downcast to - // FormatterHelper. Resolved once HelperInterface gains AsAny (see - // report). - let _formatter = helper_set.borrow().get("formatter").unwrap(); - todo!("downcast dyn HelperInterface to FormatterHelper for format_block"); - } else { - message = format!("{}", error.message); - } + let message = if let Some(helper_set) = self.get_helper_set() { + let formatter = helper_set.borrow().get_formatter(); + let message = formatter.borrow().format_block( + FormatBlockMessages::String(error.message.clone()), + "error", + false, + ); + message } else { - message = format!("{}", error.message); - } + format!("{}", error.message) + }; output .borrow() diff --git a/crates/shirabe/src/command/init_command.rs b/crates/shirabe/src/command/init_command.rs index f63014c..a04836d 100644 --- a/crates/shirabe/src/command/init_command.rs +++ b/crates/shirabe/src/command/init_command.rs @@ -7,7 +7,6 @@ use shirabe_external_packages::composer::pcre::{CaptureKey, Preg}; use shirabe_external_packages::composer::spdx_licenses::SpdxLicenses; use shirabe_external_packages::symfony::console::command::command::Command; use shirabe_external_packages::symfony::console::helper::FormatBlockMessages; -use shirabe_external_packages::symfony::console::helper::FormatterHelper; use shirabe_external_packages::symfony::console::input::ArrayInput; use shirabe_external_packages::symfony::console::input::InputInterface; use shirabe_external_packages::symfony::console::output::OutputInterface; @@ -435,17 +434,16 @@ impl Command for InitCommand { fn interact( &mut self, input: Rc>, - _output: Rc>, + output: Rc>, ) { let _ = (|| -> anyhow::Result<()> { let io = self.get_io(); // @var FormatterHelper $formatter — PHP: $this->getHelperSet()->get('formatter') - // TODO(phase-c): get_helper_set returns PhpMixed and HelperSet::get is a todo!() stub (per - // the "Symfony stays todo!()" policy), so the typed FormatterHelper cannot be retrieved - // until the Helper trait + typed HelperSet are modelled. - let formatter: FormatterHelper = todo!(); - let _ = &formatter; - let _ = self.get_helper_set(); + let formatter = self + .get_helper_set() + .expect("the init command is registered on an application before interact() runs") + .borrow() + .get_formatter(); // initialize repos if configured let repositories: Vec = input @@ -523,7 +521,7 @@ impl Command for InitCommand { io.write_error3( &format!( "\n{}\n", - formatter.format_block( + formatter.borrow().format_block( FormatBlockMessages::String( "Welcome to the Composer config generator".to_string(), ), @@ -695,6 +693,7 @@ impl Command for InitCommand { }), None, minimum_stability_default + .clone() .map(PhpMixed::String) .unwrap_or(PhpMixed::Null), )?; @@ -796,8 +795,8 @@ impl Command for InitCommand { let requirements = if (require.len() as i64) > 0 || io.ask_confirmation(question, true) { self.determine_requirements( - input, - _output, + input.clone(), + output.clone(), require, platform_repo.as_ref(), &preferred_stability, @@ -826,8 +825,8 @@ impl Command for InitCommand { let dev_requirements = if (require_dev.len() as i64) > 0 || io.ask_confirmation(question, true) { self.determine_requirements( - input, - _output, + input.clone(), + output.clone(), require_dev, platform_repo.as_ref(), &preferred_stability, diff --git a/crates/shirabe/src/console/application.rs b/crates/shirabe/src/console/application.rs index c3287e7..8112e18 100644 --- a/crates/shirabe/src/console/application.rs +++ b/crates/shirabe/src/console/application.rs @@ -22,23 +22,18 @@ use shirabe_external_packages::symfony::console::exception::missing_input_except 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::{ @@ -1633,33 +1628,10 @@ impl Application { } /// Gets the default helper set with the helpers that should always be available. + /// + /// The closed set of four helpers is instantiated by `HelperSet::new()` itself. pub fn get_default_helper_set(&self) -> std::rc::Rc> { - let helper_set = std::rc::Rc::new(std::cell::RefCell::new(HelperSet::default())); - let helpers: IndexMap>> = { - let mut m: IndexMap< - HelperSetKey, - std::rc::Rc>, - > = 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 + HelperSet::new() } /// Returns abbreviated suggestions in string format. @@ -2887,15 +2859,12 @@ impl ApplicationHandle { output: std::rc::Rc>, ) -> anyhow::Result { let application = &self.0; - 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::; - } - } + // PHP: foreach ($command->getHelperSet() as $helper) { if ($helper instanceof + // InputAwareInterface) $helper->setInput($input); } + // TODO(plugin): the HelperSet is now a closed set of four helpers, none of which implement + // InputAwareInterface, so this loop is a no-op. Plugin-registered InputAware helpers cannot + // be reached until dynamic helper registration is restored (see HelperSet). + let _ = command.borrow().get_helper_set(); if !application.borrow().signals_to_dispatch_event.is_empty() { // $commandSignals = $command instanceof SignalableCommandInterface ? $command->getSubscribedSignals() : [] -- cgit v1.3.1