aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe-external-packages/src
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-08-01 20:51:40 +0900
committernsfisis <nsfisis@gmail.com>2026-08-01 20:51:40 +0900
commite5e6cc6807c9fbbef883ffe0eec9eed477d5bee3 (patch)
tree8d5f1ff760e56b370c53ac0311c219ee7a7988c2 /crates/shirabe-external-packages/src
parent06d2c5c884eee0b5b663730f4d379a7ceae3a8e4 (diff)
downloadphp-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')
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/helper/question_helper.rs213
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/helper/symfony_question_helper.rs56
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/style/output_style.rs15
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/style/symfony_style.rs177
4 files changed, 261 insertions, 200 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 {
diff --git a/crates/shirabe-external-packages/src/symfony/console/helper/symfony_question_helper.rs b/crates/shirabe-external-packages/src/symfony/console/helper/symfony_question_helper.rs
index 086f9c1c..cae3434d 100644
--- a/crates/shirabe-external-packages/src/symfony/console/helper/symfony_question_helper.rs
+++ b/crates/shirabe-external-packages/src/symfony/console/helper/symfony_question_helper.rs
@@ -1,10 +1,11 @@
//! ref: composer/vendor/symfony/console/Helper/SymfonyQuestionHelper.php
use crate::symfony::console::formatter::output_formatter::OutputFormatter;
-use crate::symfony::console::helper::question_helper::QuestionHelper;
+use crate::symfony::console::helper::question_helper::{QuestionHelper, QuestionHelperInterface};
use crate::symfony::console::output::output_interface;
use crate::symfony::console::output::output_interface::OutputInterface;
use crate::symfony::console::question::QuestionInterface;
+use crate::symfony::console::style::style_interface::StyleInterface;
use crate::symfony::console::style::symfony_style::SymfonyStyle;
use shirabe_php_shim::PhpMixed;
use std::ops::{Deref, DerefMut};
@@ -20,7 +21,26 @@ impl SymfonyQuestionHelper {
Self::default()
}
- pub(crate) fn write_prompt(
+ fn get_eof_shortcut(&self) -> String {
+ if shirabe_php_shim::php_os_family() == "Windows" {
+ return "<comment>Ctrl+Z</comment> then <comment>Enter</comment>".to_string();
+ }
+
+ "<comment>Ctrl+D</comment>".to_string()
+ }
+}
+
+impl QuestionHelperInterface for SymfonyQuestionHelper {
+ fn inner(&self) -> &QuestionHelper {
+ &self.inner
+ }
+
+ fn inner_mut(&mut self) -> &mut QuestionHelper {
+ &mut self.inner
+ }
+
+ /// {@inheritdoc}
+ fn write_prompt(
&self,
output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>,
question: &impl QuestionInterface,
@@ -108,35 +128,23 @@ impl SymfonyQuestionHelper {
.write(&[prompt], false, output_interface::OUTPUT_NORMAL);
}
- pub(crate) fn write_error(
+ /// {@inheritdoc}
+ fn write_error(
&self,
output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>,
error: &shirabe_php_shim::Exception,
) {
- let is_symfony_style = {
- let borrowed = output.borrow();
- (*borrowed)
- .as_any()
- .downcast_ref::<SymfonyStyle>()
- .is_some()
- };
- if is_symfony_style {
- // $output->newLine(); $output->error($error->getMessage());
- // SymfonyStyle's newLine()/error() require mutable access to the
- // concrete type; mutable downcasting through the trait object is
- // resolved in a later phase.
- todo!("SymfonyStyle newLine()/error() require &mut SymfonyStyle");
- }
-
- self.inner.write_error(output, error);
- }
+ {
+ let mut borrowed = output.borrow_mut();
+ if let Some(style) = (*borrowed).as_any_mut().downcast_mut::<SymfonyStyle>() {
+ style.new_line(1);
+ style.error(PhpMixed::String(error.message.clone()));
- fn get_eof_shortcut(&self) -> String {
- if shirabe_php_shim::php_os_family() == "Windows" {
- return "<comment>Ctrl+Z</comment> then <comment>Enter</comment>".to_string();
+ return;
+ }
}
- "<comment>Ctrl+D</comment>".to_string()
+ self.inner.write_error(output, error);
}
}
diff --git a/crates/shirabe-external-packages/src/symfony/console/style/output_style.rs b/crates/shirabe-external-packages/src/symfony/console/style/output_style.rs
index 7d68ceaf..8d28e76d 100644
--- a/crates/shirabe-external-packages/src/symfony/console/style/output_style.rs
+++ b/crates/shirabe-external-packages/src/symfony/console/style/output_style.rs
@@ -41,7 +41,6 @@ impl OutputStyle {
Self::as_console_output_interface(&self.output)
.unwrap()
- .borrow()
.get_error_output()
}
@@ -55,10 +54,18 @@ impl OutputStyle {
.is_some()
}
+ /// PHP casts to `ConsoleOutputInterface`; `ConsoleOutput` being its only implementor, a
+ /// borrow of the concrete type serves as the cast result.
fn as_console_output_interface(
- _output: &std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>,
- ) -> Option<std::rc::Rc<std::cell::RefCell<dyn ConsoleOutputInterface>>> {
- todo!()
+ output: &std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>,
+ ) -> Option<std::cell::Ref<'_, crate::symfony::console::output::console_output::ConsoleOutput>>
+ {
+ std::cell::Ref::filter_map(output.borrow(), |output| {
+ output
+ .as_any()
+ .downcast_ref::<crate::symfony::console::output::console_output::ConsoleOutput>()
+ })
+ .ok()
}
}
diff --git a/crates/shirabe-external-packages/src/symfony/console/style/symfony_style.rs b/crates/shirabe-external-packages/src/symfony/console/style/symfony_style.rs
index 69cd4191..0cc7cf25 100644
--- a/crates/shirabe-external-packages/src/symfony/console/style/symfony_style.rs
+++ b/crates/shirabe-external-packages/src/symfony/console/style/symfony_style.rs
@@ -1,7 +1,6 @@
//! ref: composer/vendor/symfony/console/Style/SymfonyStyle.php
use crate::symfony::console::exception::invalid_argument_exception::InvalidArgumentException;
-use crate::symfony::console::exception::runtime_exception::RuntimeException;
use crate::symfony::console::formatter::OutputFormatter;
use crate::symfony::console::formatter::OutputFormatterInterface;
use crate::symfony::console::helper::Helper;
@@ -9,6 +8,8 @@ use crate::symfony::console::helper::ProgressBar;
use crate::symfony::console::helper::SymfonyQuestionHelper;
use crate::symfony::console::helper::Table;
use crate::symfony::console::helper::TableCell;
+use crate::symfony::console::helper::TableSeparator;
+use crate::symfony::console::helper::question_helper::QuestionHelperInterface;
use crate::symfony::console::helper::table::{Cell, Row};
use crate::symfony::console::input::InputInterface;
use crate::symfony::console::output::ConsoleOutputInterface;
@@ -39,6 +40,16 @@ pub struct SymfonyStyle {
pub const MAX_LINE_LENGTH: i64 = 120;
+/// A `definition_list` entry. PHP types it as `string|array|TableSeparator`; any other type is
+/// rejected with an `InvalidArgumentException` (a `LogicException`), which this enum makes
+/// unrepresentable.
+#[derive(Debug)]
+pub enum DefinitionListItem {
+ String(String),
+ Array(indexmap::IndexMap<String, PhpMixed>),
+ TableSeparator(TableSeparator),
+}
+
impl SymfonyStyle {
pub fn new(
input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>,
@@ -126,11 +137,11 @@ impl SymfonyStyle {
}
/// Formats a horizontal table.
- pub fn horizontal_table(&mut self, headers: Vec<PhpMixed>, rows: Vec<PhpMixed>) {
+ pub fn horizontal_table(&mut self, headers: Vec<Cell>, rows: Vec<Row>) {
self.create_table()
.set_horizontal(true)
- .set_headers(headers.into_iter().map(Cell::from).collect())
- .set_rows(rows.into_iter().map(Row::from).collect())
+ .set_headers(headers)
+ .set_rows(rows)
.render();
self.new_line(1);
@@ -142,76 +153,66 @@ impl SymfonyStyle {
/// * 'A title'
/// * ['key' => 'value']
/// * new TableSeparator()
- pub fn definition_list(&mut self, list: Vec<PhpMixed>) {
- let mut headers: Vec<PhpMixed> = Vec::new();
- let mut row: Vec<PhpMixed> = Vec::new();
+ pub fn definition_list(&mut self, list: Vec<DefinitionListItem>) {
+ let mut headers: Vec<Cell> = Vec::new();
+ let mut row: Vec<Cell> = Vec::new();
for value in list {
- if Self::is_table_separator(&value) {
- headers.push(value.clone());
- row.push(value);
- continue;
- }
- if shirabe_php_shim::is_string(&value) {
- // TODO: store a `TableCell` (with colspan => 2) into the mixed array.
- let _table_cell = TableCell::new(&Self::php_string(&value), {
- let mut options = indexmap::IndexMap::new();
- options.insert(
- "colspan".to_string(),
- crate::symfony::console::helper::table_cell::TableCellOption::Int(2),
- );
- options
- });
- let _ = _table_cell;
- todo!();
- }
- if !shirabe_php_shim::is_array(&value) {
- // TODO(plugin): recoverable error path.
- let _ = InvalidArgumentException(shirabe_php_shim::InvalidArgumentException {
- message: "Value should be an array, string, or an instance of TableSeparator."
- .to_string(),
- code: 0,
- });
- todo!()
- }
- // $headers[] = key($value); $row[] = current($value);
- let (first_key, first_value) = match &value {
- PhpMixed::Array(entries) => (
- entries
+ match value {
+ DefinitionListItem::TableSeparator(separator) => {
+ headers.push(Cell::Separator(separator.clone()));
+ row.push(Cell::Separator(separator));
+ }
+ DefinitionListItem::String(value) => {
+ headers.push(Cell::Cell(
+ TableCell::new(&value, {
+ let mut options = indexmap::IndexMap::new();
+ options.insert(
+ "colspan".to_string(),
+ crate::symfony::console::helper::table_cell::TableCellOption::Int(
+ 2,
+ ),
+ );
+ options
+ })
+ .expect("colspan is a valid TableCell option"),
+ ));
+ row.push(Cell::Null);
+ }
+ DefinitionListItem::Array(value) => {
+ // $headers[] = key($value); $row[] = current($value);
+ let first_key = value
.keys()
.next()
.map(|k| PhpMixed::String(k.clone()))
- .unwrap_or(PhpMixed::Null),
- entries
+ .unwrap_or(PhpMixed::Null);
+ let first_value = value
.values()
.next()
.cloned()
- .unwrap_or(PhpMixed::Bool(false)),
- ),
- PhpMixed::List(items) => (
- if items.is_empty() {
- PhpMixed::Null
- } else {
- PhpMixed::Int(0)
- },
- items.first().cloned().unwrap_or(PhpMixed::Bool(false)),
- ),
- _ => unreachable!("value is an array past the is_array guard"),
- };
- headers.push(first_key);
- row.push(first_value);
+ .unwrap_or(PhpMixed::Bool(false));
+ headers.push(Cell::from(first_key));
+ row.push(Cell::from(first_value));
+ }
+ }
}
- self.horizontal_table(headers, vec![PhpMixed::List(row.into_iter().collect())]);
+ self.horizontal_table(headers, vec![Row::Cells(row)]);
}
+ /// @see ProgressBar::iterate()
+ ///
+ /// PHP returns a generator (`yield from`); this port evaluates eagerly, following
+ /// `ProgressBar::iterate`.
pub fn progress_iterate(
&mut self,
- _iterable: Vec<PhpMixed>,
- _max: Option<i64>,
- ) -> Vec<PhpMixed> {
- // TODO(phase-c/d): PHP uses `yield from`; porting the generator semantics of
- // ProgressBar::iterate() requires a streaming design not yet in place.
- todo!()
+ iterable: Vec<(PhpMixed, PhpMixed)>,
+ max: Option<i64>,
+ ) -> anyhow::Result<Vec<(PhpMixed, PhpMixed)>> {
+ let yielded = self.create_progress_bar(0).iterate(iterable, max)?;
+
+ self.new_line(2);
+
+ Ok(yielded)
}
pub fn ask_question(&mut self, question: &impl QuestionInterface) -> PhpMixed {
@@ -223,7 +224,9 @@ impl SymfonyStyle {
self.question_helper = Some(SymfonyQuestionHelper::new());
}
- // TODO(plugin): pass `self` as the OutputInterface to the question helper.
+ // TODO(phase-c): PHP passes `$this` as the OutputInterface, so SymfonyQuestionHelper's
+ // write_error renders through SymfonyStyle::error; SymfonyStyle is not an OutputInterface
+ // trait object here, so the raw output is passed instead.
let answer = {
let input = self.input.clone();
let mut input = input.borrow_mut();
@@ -253,15 +256,14 @@ impl SymfonyStyle {
}
pub fn create_table(&mut self) -> Table {
- // TODO(plugin): ConsoleOutputInterface::section() requires runtime type info.
- let output = if Self::is_console_output_interface(&self.output) {
- Self::as_console_output_interface(&self.output)
- .unwrap()
- .borrow()
- .section() as std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>
- } else {
- self.output.clone()
- };
+ let output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>> =
+ if Self::is_console_output_interface(&self.output) {
+ Self::as_console_output_interface(&self.output)
+ .unwrap()
+ .section()
+ } else {
+ self.output.clone()
+ };
let mut style = Table::get_style_definition("symfony-style-guide".to_string())
.expect("style definition lookup")
.expect("undefined style definition");
@@ -290,16 +292,12 @@ impl SymfonyStyle {
}
fn get_progress_bar(&mut self) -> &mut ProgressBar {
- if self.progress_bar.is_none() {
- // TODO(plugin): recoverable error path.
- let _ = RuntimeException(shirabe_php_shim::RuntimeException {
- message: "The ProgressBar is not started.".to_string(),
- code: 0,
- });
- todo!()
- }
-
- self.progress_bar.as_mut().unwrap()
+ // PHP throws RuntimeException('The ProgressBar is not started.'). Reaching this without a
+ // prior progress_start() call is a caller bug, and the StyleInterface signatures carry no
+ // Result, so panic.
+ self.progress_bar
+ .as_mut()
+ .expect("The ProgressBar is not started.")
}
fn auto_prepend_block(&mut self) {
@@ -447,16 +445,15 @@ impl SymfonyStyle {
.is_some()
}
+ /// PHP casts to `ConsoleOutputInterface`; `ConsoleOutput` being its only implementor, a
+ /// borrow of the concrete type serves as the cast result.
fn as_console_output_interface(
- _output: &std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>,
- ) -> Option<std::rc::Rc<std::cell::RefCell<dyn ConsoleOutputInterface>>> {
- todo!()
- }
-
- // TODO(phase-c/d): `PhpMixed` cannot carry a `TableSeparator` object, so the
- // `$value instanceof TableSeparator` check has no faithful representation yet.
- fn is_table_separator(_value: &PhpMixed) -> bool {
- todo!()
+ output: &std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>,
+ ) -> Option<std::cell::Ref<'_, ConsoleOutput>> {
+ std::cell::Ref::filter_map(output.borrow(), |output| {
+ output.as_any().downcast_ref::<ConsoleOutput>()
+ })
+ .ok()
}
fn php_string(value: &PhpMixed) -> String {