aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe/src/event_dispatcher
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-07-24 20:23:42 +0900
committernsfisis <nsfisis@gmail.com>2026-07-24 20:23:42 +0900
commit90d9b1dc0035a70dab76d520b9dcd14ec57273ad (patch)
tree30f6576fddcd4b6fb6fc883c28610b324a15b3cc /crates/shirabe/src/event_dispatcher
parent4a8f5a83b933a06974916b102da673f2a88aebe8 (diff)
downloadphp-shirabe-90d9b1dc0035a70dab76d520b9dcd14ec57273ad.tar.gz
php-shirabe-90d9b1dc0035a70dab76d520b9dcd14ec57273ad.tar.zst
php-shirabe-90d9b1dc0035a70dab76d520b9dcd14ec57273ad.zip
fix(event-dispatcher): invoke Closure listeners instead of always failing is_callable
RequireCommand registers an inline listener on InstallerEvents::PRE_OPERATIONS_EXEC to track dependency_resolution_completed, mirroring PHP's `function () use (&$dependencyResolutionCompleted) { ... }`. This is Composer's own code, not a Plugin subscriber, but it went through the shared non-string-callable path, which checked is_callable() against a hardcoded PhpMixed::Null and always failed, breaking every `require` that reaches the install step. Callable::Closure now carries the actual Rc<dyn Fn> instead of being a data-less placeholder, and is invoked directly (Closures are always callable in PHP). The ArrayCallable path used by future Plugin subscribers is untouched. Un-ignoring the two require_command_test cases that cited this bug reveals two separate, pre-existing issues (a missing ext-requirement warning message, and a RefCell re-entrancy panic in ConsoleIO::ask_question); their #[ignore] reasons are updated to describe the real current blocker instead of the now-fixed one.
Diffstat (limited to 'crates/shirabe/src/event_dispatcher')
-rw-r--r--crates/shirabe/src/event_dispatcher/event_dispatcher.rs37
1 files changed, 32 insertions, 5 deletions
diff --git a/crates/shirabe/src/event_dispatcher/event_dispatcher.rs b/crates/shirabe/src/event_dispatcher/event_dispatcher.rs
index 6deb130e..1f1d9619 100644
--- a/crates/shirabe/src/event_dispatcher/event_dispatcher.rs
+++ b/crates/shirabe/src/event_dispatcher/event_dispatcher.rs
@@ -34,16 +34,34 @@ use shirabe_php_shim::{
/// Represents a callable listener. PHP's `callable` may be a string (command, script, or
/// "Class::method"), a `[object|string, method]` pair, or a `\Closure`.
///
-/// TODO(plugin): Subscriber- and Closure-based listeners come from plugins and are not
+/// TODO(plugin): Subscriber-based (`ArrayCallable`) listeners come from plugins and are not
/// implemented yet — only the string forms used by composer.json `scripts` work here.
-#[derive(Debug, Clone)]
+#[derive(Clone)]
pub enum Callable {
String(String),
/// `[$className_or_object, $methodName]` array callable. The first element is represented
/// here as `PhpMixed` to keep parity with PHP's loose typing.
ArrayCallable(Box<PhpMixed>, String),
- /// PHP `\Closure` placeholder.
- Closure,
+ /// PHP `\Closure`, invoked with the event exactly like `$callable($event)` in
+ /// `EventDispatcher::doDispatch`. Today this is only produced by Composer's own commands
+ /// registering an inline listener on themselves (e.g. `RequireCommand`'s
+ /// `dependencyResolutionCompleted` tracker) — Plugin-supplied closures remain out of scope
+ /// pending Plugin API.
+ Closure(std::rc::Rc<dyn Fn(&dyn EventInterface) -> PhpMixed>),
+}
+
+impl std::fmt::Debug for Callable {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ Callable::String(s) => f.debug_tuple("String").field(s).finish(),
+ Callable::ArrayCallable(first, method) => f
+ .debug_tuple("ArrayCallable")
+ .field(first)
+ .field(method)
+ .finish(),
+ Callable::Closure(_) => f.write_str("Closure(..)"),
+ }
+ }
}
/// The Event Dispatcher.
@@ -326,7 +344,16 @@ impl EventDispatcher {
}
);
let is_string_callable = matches!(callable, Callable::String(_));
- if !is_string_callable {
+ if let Callable::Closure(ref closure) = callable {
+ let _ = self.make_autoloader(event, &callable);
+ // Closures are always callable in PHP (is_callable() returns true for any \Closure),
+ // so the is_callable()/RuntimeException branch below never applies here.
+ r#return = if matches!(closure(event), PhpMixed::Bool(false)) {
+ 1
+ } else {
+ 0
+ };
+ } else if !is_string_callable {
// TODO(plugin): non-string callable handling — verify is_callable, invoke,
// and replicate the get_class / write_error / is_callable error path from PHP.
let _ = self.make_autoloader(event, &callable);