diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-08-01 20:51:40 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-08-01 20:51:40 +0900 |
| commit | e5e6cc6807c9fbbef883ffe0eec9eed477d5bee3 (patch) | |
| tree | 8d5f1ff760e56b370c53ac0311c219ee7a7988c2 /crates/shirabe-external-packages/src/symfony/console/helper/question_helper.rs | |
| parent | 06d2c5c884eee0b5b663730f4d379a7ceae3a8e4 (diff) | |
| download | php-shirabe-e5e6cc6807c9fbbef883ffe0eec9eed477d5bee3.tar.gz php-shirabe-e5e6cc6807c9fbbef883ffe0eec9eed477d5bee3.tar.zst php-shirabe-e5e6cc6807c9fbbef883ffe0eec9eed477d5bee3.zip | |
feat(symfony-console): implement interactive question/style helpers
Resolve the remaining todo!()s in SymfonyStyle, OutputStyle,
QuestionHelper and SymfonyQuestionHelper:
* Wire up the virtual dispatch PHP performs for the protected
writePrompt()/writeError() overrides, following the codebase's
established inheritance idiom (Command, ArchiveDownloader): the base
class becomes a trait (QuestionHelperInterface, named after the
QuestionInterface precedent) whose provided methods ask/do_ask/
validate_attempts carry the template logic and late-bind the
write_prompt/write_error hooks through Self, with inner()/inner_mut()
reaching the base-class state. SymfonyQuestionHelper overrides the
hooks as plain trait-impl methods, mirroring PHP's protected-method
overriding, so SymfonyStyle-driven questions now render the Symfony
Style Guide prompt.
* Type definition_list input as an enum (string|array|TableSeparator)
because PhpMixed intentionally cannot carry objects; the
InvalidArgumentException branch (a LogicException) becomes
unrepresentable. horizontal_table now takes typed Cells/Rows.
* Propagate the MissingInputException thrown inside autocomplete()
through a Result instead of aborting.
* Implement as_console_output_interface via Ref::filter_map on
ConsoleOutput, the interface's only implementor.
* Port progressIterate eagerly, following ProgressBar::iterate.
* Map __FILE__ to current_exe(): a native binary never runs from a
phar, so the hiddeninput.exe relocation branch correctly never fires.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Diffstat (limited to 'crates/shirabe-external-packages/src/symfony/console/helper/question_helper.rs')
| -rw-r--r-- | crates/shirabe-external-packages/src/symfony/console/helper/question_helper.rs | 213 |
1 files changed, 131 insertions, 82 deletions
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 f6ef31da..fd716aaa 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 @@ -35,13 +35,22 @@ static STTY: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new( /// self::$stdinIsInteractive static STDIN_IS_INTERACTIVE: std::sync::Mutex<Option<bool>> = std::sync::Mutex::new(None); -impl QuestionHelper { +/// PHP dispatches `$this->writePrompt()` / `$this->writeError()` virtually, and +/// `SymfonyQuestionHelper` overrides both protected methods. The embedded-super port expresses +/// that late binding as a trait: the template methods are provided here and reach the overridable +/// hooks through `Self`, while `inner()` reaches the base-class state. PHP has no such interface; +/// the invented name follows the `QuestionInterface` precedent. +pub trait QuestionHelperInterface { + fn inner(&self) -> &QuestionHelper; + + fn inner_mut(&mut self) -> &mut QuestionHelper; + /// Asks a question to the user. /// /// @return mixed The user answer /// /// @throws RuntimeException If there is no data to read in the input stream - pub fn ask( + fn ask( &mut self, input: &mut dyn InputInterface, output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, @@ -60,13 +69,13 @@ impl QuestionHelper { } if !input.is_interactive() { - return Ok(Ok(self.get_default_answer(question))); + return Ok(Ok(self.inner().get_default_answer(question))); } if let Some(streamable) = input.as_streamable() && let Some(stream) = streamable.get_stream() { - self.input_stream = Some(stream); + self.inner_mut().input_stream = Some(stream); } let result: anyhow::Result<Result<PhpMixed, MissingInputException>> = (|| { @@ -85,7 +94,7 @@ impl QuestionHelper { Err(exception) => { input.set_interactive(false); - let fallback_output = self.get_default_answer(question); + let fallback_output = self.inner().get_default_answer(question); if matches!(fallback_output, PhpMixed::Null) { return Ok(Err(exception)); } @@ -95,16 +104,7 @@ impl QuestionHelper { } } - pub fn get_name(&self) -> String { - "question".to_string() - } - - /// Prevents usage of stty. - pub fn disable_stty() { - STTY.store(false, std::sync::atomic::Ordering::SeqCst); - } - - /// Asks the question to the user. + /// Asks the question to the user (PHP private; on the trait so it can late-bind the hooks). /// /// @return mixed /// @@ -117,6 +117,7 @@ impl QuestionHelper { self.write_prompt(std::rc::Rc::clone(&output), question); let input_stream = self + .inner() .input_stream .clone() .unwrap_or_else(shirabe_php_shim::stdin); @@ -132,12 +133,15 @@ impl QuestionHelper { // The autocompleter callback yields an iterable (Option here); PHP // treats a null result as an empty list of suggestions. let callback = move |input: &str| callback(input).unwrap_or_default(); - let autocomplete = self.autocomplete( + let autocomplete = match self.inner().autocomplete( std::rc::Rc::clone(&output), question, &input_stream, &callback, - ); + ) { + Ok(value) => value, + Err(exception) => return Ok(Err(exception)), + }; ret = PhpMixed::String(if question.is_trimmable() { shirabe_php_shim::trim(&autocomplete, None) } else { @@ -146,7 +150,7 @@ impl QuestionHelper { } else { let mut r: PhpMixed = PhpMixed::Bool(false); if question.is_hidden() { - match self.get_hidden_response( + match self.inner().get_hidden_response( std::rc::Rc::clone(&output), &input_stream, question.is_trimmable(), @@ -176,7 +180,7 @@ impl QuestionHelper { shirabe_php_shim::stream_set_blocking(&input_stream, true); } - let read = self.read_input(&input_stream, question); + let read = self.inner().read_input(&input_stream, question); if !shirabe_php_shim::boolval(&is_blocked) { shirabe_php_shim::stream_set_blocking(&input_stream, false); @@ -221,6 +225,99 @@ impl QuestionHelper { Ok(Ok(ret)) } + /// Validates an attempt (PHP private; on the trait so it can late-bind the hooks). + /// + /// @param callable $interviewer A callable that will ask for a question and return the result + /// + /// @return mixed The validated response + /// + /// @throws \Exception In case the max number of attempts has been reached and no valid response has been given + fn validate_attempts( + &self, + interviewer: &dyn Fn() -> anyhow::Result<Result<PhpMixed, MissingInputException>>, + output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, + question: &impl QuestionInterface, + ) -> anyhow::Result<Result<PhpMixed, MissingInputException>> { + let mut error: Option<shirabe_php_shim::Exception> = None; + let mut attempts = question.get_max_attempts(); + + loop { + // while (null === $attempts || $attempts--) + match attempts { + None => {} + Some(0) => break, + Some(n) => attempts = Some(n - 1), + } + + if let Some(ref error) = error { + self.write_error(std::rc::Rc::clone(&output), error); + } + + let interviewed = match interviewer()? { + Ok(value) => value, + Err(missing) => return Ok(Err(missing)), + }; + + match question.get_validator().unwrap()(Some(interviewed)) { + Ok(value) => return Ok(Ok(value)), + Err(e) => { + // PHP: `catch (RuntimeException $e) { throw $e; } catch (\Exception $error) {}`. + // The validator return type is fixed to InvalidArgumentException here, so the + // RuntimeException rethrow branch is statically unreachable; record the error + // and retry. + error = Some(shirabe_php_shim::Exception { + message: e.0.message.clone(), + code: e.0.code, + }); + } + } + } + + // throw $error; + Err(anyhow::Error::msg( + error.map(|e| e.message).unwrap_or_default(), + )) + } + + /// Outputs the question prompt (PHP protected; the overridable hook). + fn write_prompt( + &self, + output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, + question: &impl QuestionInterface, + ) { + self.inner().write_prompt(output, question); + } + + /// Outputs an error message (PHP protected; the overridable hook). + fn write_error( + &self, + output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, + error: &shirabe_php_shim::Exception, + ) { + self.inner().write_error(output, error); + } +} + +impl QuestionHelperInterface for QuestionHelper { + fn inner(&self) -> &QuestionHelper { + self + } + + fn inner_mut(&mut self) -> &mut QuestionHelper { + self + } +} + +impl QuestionHelper { + pub fn get_name(&self) -> String { + "question".to_string() + } + + /// Prevents usage of stty. + pub fn disable_stty() { + STTY.store(false, std::sync::atomic::Ordering::SeqCst); + } + /// @return mixed fn get_default_answer(&self, question: &impl QuestionInterface) -> PhpMixed { let default = question.get_default(); @@ -344,7 +441,7 @@ impl QuestionHelper { question: &impl QuestionInterface, input_stream: &shirabe_php_shim::PhpResource, autocomplete: &dyn Fn(&str) -> Vec<PhpMixed>, - ) -> String { + ) -> Result<String, MissingInputException> { let cursor = Cursor::new(std::rc::Rc::clone(&output), Some(input_stream.clone())); let mut full_choice = String::new(); @@ -400,10 +497,12 @@ impl QuestionHelper { && matches!(question.get_default(), PhpMixed::Null)) { shirabe_php_shim::shell_exec(&format!("stty {}", stty_mode)); - // throw new MissingInputException('Aborted.'); - // autocomplete() returns string in PHP; this throw aborts the - // whole read. Faithful exception propagation is resolved later. - todo!("MissingInputException('Aborted.') thrown inside autocomplete"); + return Err(MissingInputException(RuntimeException( + shirabe_php_shim::RuntimeException { + message: "Aborted.".to_string(), + code: 0, + }, + ))); } else if c.as_deref() == Some("\u{7f}") { // Backspace Character if 0 == num_matches && 0 != i { @@ -570,7 +669,7 @@ impl QuestionHelper { // Reset stty so it behaves normally again shirabe_php_shim::shell_exec(&format!("stty {}", stty_mode)); - full_choice + Ok(full_choice) } fn most_recently_entered_value(&self, entered: &str) -> String { @@ -671,60 +770,6 @@ impl QuestionHelper { Ok(Ok(value)) } - /// Validates an attempt. - /// - /// @param callable $interviewer A callable that will ask for a question and return the result - /// - /// @return mixed The validated response - /// - /// @throws \Exception In case the max number of attempts has been reached and no valid response has been given - fn validate_attempts( - &self, - interviewer: &dyn Fn() -> anyhow::Result<Result<PhpMixed, MissingInputException>>, - output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, - question: &impl QuestionInterface, - ) -> anyhow::Result<Result<PhpMixed, MissingInputException>> { - let mut error: Option<shirabe_php_shim::Exception> = None; - let mut attempts = question.get_max_attempts(); - - loop { - // while (null === $attempts || $attempts--) - match attempts { - None => {} - Some(0) => break, - Some(n) => attempts = Some(n - 1), - } - - if let Some(ref error) = error { - self.write_error(std::rc::Rc::clone(&output), error); - } - - let interviewed = match interviewer()? { - Ok(value) => value, - Err(missing) => return Ok(Err(missing)), - }; - - match question.get_validator().unwrap()(Some(interviewed)) { - Ok(value) => return Ok(Ok(value)), - Err(e) => { - // PHP: `catch (RuntimeException $e) { throw $e; } catch (\Exception $error) {}`. - // The validator return type is fixed to InvalidArgumentException here, so the - // RuntimeException rethrow branch is statically unreachable; record the error - // and retry. - error = Some(shirabe_php_shim::Exception { - message: e.0.message.clone(), - code: e.0.code, - }); - } - } - } - - // throw $error; - Err(anyhow::Error::msg( - error.map(|e| e.message).unwrap_or_default(), - )) - } - fn is_interactive_input(&self, input_stream: &shirabe_php_shim::PhpResource) -> bool { let uri = shirabe_php_shim::stream_get_meta_data(input_stream) .get("uri") @@ -871,10 +916,14 @@ impl QuestionHelper { } } -// PHP `__FILE__` magic constant. The shim's `file()` is PHP's file() function, -// not the magic constant, and there is no `__FILE__` shim yet (see report). +/// PHP `__FILE__` magic constant. The executing code lives in the Shirabe binary itself, which is +/// the closest analogue for a native executable; it never carries the `phar:` scheme, so the +/// phar-relocation branch in `get_hidden_response` correctly never triggers. fn magic_file() -> String { - todo!("magic_file: shim needs a __FILE__ magic-constant equivalent") + std::env::current_exe() + .expect("current executable path") + .display() + .to_string() } impl HelperInterface for QuestionHelper { |
