aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe/src
diff options
context:
space:
mode:
Diffstat (limited to 'crates/shirabe/src')
-rw-r--r--crates/shirabe/src/event_dispatcher/event_dispatcher.rs230
-rw-r--r--crates/shirabe/src/event_dispatcher/event_subscriber_interface.rs13
-rw-r--r--crates/shirabe/src/plugin/capable.rs5
-rw-r--r--crates/shirabe/src/plugin/php_plugin_proxy.rs193
-rw-r--r--crates/shirabe/src/plugin/plugin_interface.rs10
-rw-r--r--crates/shirabe/src/plugin/plugin_manager.rs32
6 files changed, 404 insertions, 79 deletions
diff --git a/crates/shirabe/src/event_dispatcher/event_dispatcher.rs b/crates/shirabe/src/event_dispatcher/event_dispatcher.rs
index f5b85ca1..2a25eac8 100644
--- a/crates/shirabe/src/event_dispatcher/event_dispatcher.rs
+++ b/crates/shirabe/src/event_dispatcher/event_dispatcher.rs
@@ -9,11 +9,13 @@ use crate::event_dispatcher::Event;
use crate::event_dispatcher::EventInterface;
use crate::event_dispatcher::EventSubscriberInterface;
use crate::event_dispatcher::ScriptExecutionException;
+use crate::event_dispatcher::SubscribedEventEntry;
use crate::installer::BinaryInstaller;
use crate::installer::InstallerEvent;
use crate::installer::PackageEvent;
use crate::io::IOInterface;
use crate::io::IOInterfaceImmutable;
+use crate::plugin::php_plugin_proxy::PluginRpcDispatcher;
use crate::repository::RepositoryInterface;
use crate::script::Event as ScriptEvent;
use crate::util::Platform;
@@ -25,7 +27,7 @@ use shirabe_external_packages::symfony::process::ExecutableFinder;
use shirabe_external_packages::symfony::process::PhpExecutableFinder;
use shirabe_php_rpc::{
PhpThrow, PluginValue, RustMethodDispatcher, RustObjHandle, call_function,
- call_function_with_dispatcher, call_static_method,
+ call_function_with_dispatcher, call_php_method, call_static_method,
};
use shirabe_php_shim::{
InvalidArgumentException, PATH_SEPARATOR, PhpMixed, RuntimeException, array_pop, array_push,
@@ -38,14 +40,14 @@ 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-based (`ArrayCallable`) listeners come from plugins and are not
-/// implemented yet — only the string forms used by composer.json `scripts` work here.
#[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.
+ ///
+ /// TODO(plugin): only listeners whose object half lives in the PHP child (`PhpMethod`) are
+ /// invocable; an `ArrayCallable` carrying a `PhpMixed` object still has no invocation path.
ArrayCallable(Box<PhpMixed>, String),
/// PHP `\Closure`, invoked with the event exactly like `$callable($event)` in
/// `EventDispatcher::doDispatch`. Today this is only produced by Composer's own commands
@@ -53,6 +55,9 @@ pub enum Callable {
/// `dependencyResolutionCompleted` tracker) — Plugin-supplied closures remain out of scope
/// pending Plugin API.
Closure(std::rc::Rc<dyn Fn(&dyn EventInterface) -> PhpMixed>),
+ /// `[$subscriber, $methodName]` array callable whose object half is a plugin entity in the
+ /// PHP child process, registered through `addSubscriber`. Invoked over RPC.
+ PhpMethod(shirabe_php_rpc::PhpObjHandle, String),
}
impl std::fmt::Debug for Callable {
@@ -65,6 +70,11 @@ impl std::fmt::Debug for Callable {
.field(method)
.finish(),
Callable::Closure(_) => f.write_str("Closure(..)"),
+ Callable::PhpMethod(handle, method) => f
+ .debug_tuple("PhpMethod")
+ .field(handle)
+ .field(method)
+ .finish(),
}
}
}
@@ -358,9 +368,87 @@ impl EventDispatcher {
} else {
0
};
+ } else if let Callable::PhpMethod(ref handle, ref method_name) = callable {
+ self.make_autoloader(event, &callable)?;
+ Self::ensure_script_autoloader()?;
+ let is_callable_value = unwrap_php_result(call_function_with_dispatcher(
+ "is_callable",
+ vec![PluginValue::List(vec![
+ PluginValue::PhpHandle(handle.clone()),
+ PluginValue::string(method_name.clone()),
+ ])],
+ Some(&mut PluginRpcDispatcher::default()),
+ ))?;
+ if !matches!(is_callable_value, PluginValue::Bool(true)) {
+ return Err(anyhow::anyhow!(RuntimeException {
+ message: format!(
+ "Subscriber {}::{} for event {} is not callable, make sure the function is defined and public",
+ handle.class,
+ method_name,
+ event.get_name()
+ ),
+ code: 0,
+ }));
+ }
+ self.io.write_error3(
+ &format!(
+ "> {}: {}->{}",
+ formatted_event_name_with_args.clone(),
+ handle.class,
+ method_name,
+ ),
+ true,
+ crate::io::VERBOSE,
+ );
+ let stub_class = Self::event_stub_class(event).ok_or_else(|| {
+ // TODO(plugin): only the base Event and Script\Event proxy stubs exist so
+ // far; installer/package/plugin events need their own stubs.
+ anyhow::anyhow!(RuntimeException {
+ message: format!(
+ "no proxy stub is available yet for the event `{}` dispatched to {}::{}",
+ event.get_name(),
+ handle.class,
+ method_name,
+ ),
+ code: 0,
+ })
+ })?;
+ let event_rhandle = shirabe_php_rpc::alloc_rhandle();
+ let mut dispatcher = PluginRpcDispatcher {
+ event: Some((event_rhandle, event)),
+ };
+ let outcome = call_php_method(
+ handle.phandle,
+ method_name,
+ vec![PluginValue::RustHandle(RustObjHandle {
+ rhandle: event_rhandle,
+ class: stub_class.to_string(),
+ epoch: 0,
+ snapshot: None,
+ })],
+ Some(&mut dispatcher),
+ )?;
+ r#return = match outcome {
+ Ok(value) => {
+ if matches!(value, PluginValue::Bool(false)) {
+ 1
+ } else {
+ 0
+ }
+ }
+ // TODO(plugin): the original exception class is collapsed to
+ // RuntimeException on this side of the boundary.
+ Err(throw) => {
+ return Err(anyhow::anyhow!(RuntimeException {
+ message: throw.message,
+ code: throw.code,
+ }));
+ }
+ };
} 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.
+ // TODO(plugin): an ArrayCallable whose object half is a PhpMixed has no
+ // invocation path; only the is_callable error lane and the verbose echo of the
+ // PHP branch are replicated here.
self.make_autoloader(event, &callable)?;
if !is_callable(&PhpMixed::Null) {
let (class_name, method) = match &callable {
@@ -1034,16 +1122,10 @@ try {{
);
}
- // The event crosses the boundary as a proxy stub: the child sees an instance of the
- // stub class (same FQCN as the real event class) whose methods call back here.
- let stub_class = if event.as_any().downcast_ref::<ScriptEvent>().is_some() {
- "Composer\\Script\\Event"
- } else if event.as_any().downcast_ref::<Event>().is_some() {
- "Composer\\EventDispatcher\\Event"
- } else {
+ let stub_class = Self::event_stub_class(event).ok_or_else(|| {
// TODO(plugin): only the base Event and Script\Event proxy stubs exist so far;
// installer/package/plugin events need their own stubs.
- return Err(anyhow::anyhow!(RuntimeException {
+ anyhow::anyhow!(RuntimeException {
message: format!(
"no proxy stub is available yet for the event `{}` dispatched to {}::{}",
event.get_name(),
@@ -1051,8 +1133,8 @@ try {{
method_name,
),
code: 0,
- }));
- };
+ })
+ })?;
Self::ensure_script_autoloader()?;
let rhandle = shirabe_php_rpc::alloc_rhandle();
@@ -1082,6 +1164,18 @@ try {{
}
}
+ /// The proxy stub class (same FQCN as the real event class) an event crosses the RPC
+ /// boundary as, or `None` when no stub exists for it yet.
+ fn event_stub_class(event: &dyn EventInterface) -> Option<&'static str> {
+ if event.as_any().downcast_ref::<ScriptEvent>().is_some() {
+ Some("Composer\\Script\\Event")
+ } else if event.as_any().downcast_ref::<Event>().is_some() {
+ Some("Composer\\EventDispatcher\\Event")
+ } else {
+ None
+ }
+ }
+
fn event_needs_to_output(&self, event: &dyn EventInterface) -> bool {
// do not output the command being run when using `composer exec` as it is fairly obvious the user is running it
if event.get_name() == "__exec_command" {
@@ -1135,12 +1229,38 @@ try {{
}
/// Adds object methods as listeners for the events in getSubscribedEvents
- pub fn add_subscriber<S: EventSubscriberInterface>(&mut self, _subscriber: &S) {
- // TODO(plugin): port full subscriber registration — depends on dynamic dispatch
- // for `[$subscriber, $methodName]` style callables.
- for (event_name, _params) in S::get_subscribed_events() {
- let _ = event_name;
+ pub fn add_subscriber(
+ &mut self,
+ subscriber: &dyn EventSubscriberInterface,
+ ) -> anyhow::Result<()> {
+ for (event_name, params) in subscriber.get_subscribed_events()? {
+ match params {
+ SubscribedEventEntry::Method(method) => {
+ self.add_listener(
+ &event_name,
+ Callable::PhpMethod(subscriber.subscriber_handle(), method),
+ 0,
+ );
+ }
+ SubscribedEventEntry::MethodWithPriority(method, priority) => {
+ self.add_listener(
+ &event_name,
+ Callable::PhpMethod(subscriber.subscriber_handle(), method),
+ priority.unwrap_or(0),
+ );
+ }
+ SubscribedEventEntry::Methods(listeners) => {
+ for (method, priority) in listeners {
+ self.add_listener(
+ &event_name,
+ Callable::PhpMethod(subscriber.subscriber_handle(), method),
+ priority.unwrap_or(0),
+ );
+ }
+ }
+ }
}
+ Ok(())
}
/// Retrieves all listeners for a given event
@@ -1358,6 +1478,8 @@ try {{
PhpMixed::String(class) => format!("{}::{}", class, method),
other => format!("{}::{}", get_class(other), method),
},
+ // PHP: get_class($callable[0]).'::'.$callable[1] — the object half's runtime class.
+ Callable::PhpMethod(handle, method) => format!("{}::{}", handle.class, method),
Callable::Closure(_) => "closure".to_string(),
};
if self.previous_listeners.contains_key(&callable_key) {
@@ -1573,31 +1695,9 @@ impl RustMethodDispatcher for ScriptRpcDispatcher<'_> {
)));
}
match self.event {
- Some((event_rhandle, event)) if event_rhandle == rhandle => match method_name {
- "getName" => Ok(PluginValue::string(event.get_name())),
- "getArguments" => Ok(PluginValue::List(
- event
- .get_arguments()
- .iter()
- .map(|arg| PluginValue::string(arg.clone()))
- .collect(),
- )),
- "getFlags" => Ok(PluginValue::from_php_mixed(&PhpMixed::Array(
- event.get_flags().clone(),
- ))),
- "isPropagationStopped" => Ok(PluginValue::Bool(event.is_propagation_stopped())),
- "isDevMode" => match event.as_any().downcast_ref::<ScriptEvent>() {
- Some(script_event) => Ok(PluginValue::Bool(script_event.is_dev_mode())),
- None => Err(runtime_throw(
- "isDevMode is only available on script events".to_string(),
- )),
- },
- // TODO(plugin): getComposer/getIO/stopPropagation and the rest need the full
- // object-graph proxying of the plugin activation milestone.
- other => Err(runtime_throw(format!(
- "the Event method `{other}` is not available over RPC yet"
- ))),
- },
+ Some((event_rhandle, event)) if event_rhandle == rhandle => {
+ dispatch_event_method(event, method_name)
+ }
_ => Err(runtime_throw(format!(
"unknown Rust handle {rhandle} (script-event handles are scoped to a single \
dispatched call)"
@@ -1606,6 +1706,39 @@ impl RustMethodDispatcher for ScriptRpcDispatcher<'_> {
}
}
+/// Serves an event proxy stub's method call, shared by the script dispatcher and the plugin
+/// dispatcher (both expose one live event handle per dispatched call).
+pub(crate) fn dispatch_event_method(
+ event: &dyn EventInterface,
+ method_name: &str,
+) -> Result<PluginValue, PhpThrow> {
+ match method_name {
+ "getName" => Ok(PluginValue::string(event.get_name())),
+ "getArguments" => Ok(PluginValue::List(
+ event
+ .get_arguments()
+ .iter()
+ .map(|arg| PluginValue::string(arg.clone()))
+ .collect(),
+ )),
+ "getFlags" => Ok(PluginValue::from_php_mixed(&PhpMixed::Array(
+ event.get_flags().clone(),
+ ))),
+ "isPropagationStopped" => Ok(PluginValue::Bool(event.is_propagation_stopped())),
+ "isDevMode" => match event.as_any().downcast_ref::<ScriptEvent>() {
+ Some(script_event) => Ok(PluginValue::Bool(script_event.is_dev_mode())),
+ None => Err(runtime_throw(
+ "isDevMode is only available on script events".to_string(),
+ )),
+ },
+ // TODO(plugin): getComposer/getIO/stopPropagation and the rest need full proxying
+ // of the object graph an event exposes, which does not exist yet.
+ other => Err(runtime_throw(format!(
+ "the Event method `{other}` is not available over RPC yet"
+ ))),
+ }
+}
+
fn runtime_throw(message: String) -> PhpThrow {
PhpThrow {
exception_class: "RuntimeException".to_string(),
@@ -1657,6 +1790,7 @@ pub trait EventDispatcherInterface: std::fmt::Debug {
transaction: Transaction,
) -> anyhow::Result<i64>;
fn add_listener(&mut self, event_name: &str, listener: Callable, priority: i64);
+ fn add_subscriber(&mut self, subscriber: &dyn EventSubscriberInterface) -> anyhow::Result<()>;
fn has_event_listeners(&mut self, event: &dyn EventInterface) -> bool;
}
@@ -1693,6 +1827,10 @@ impl EventDispatcherInterface for EventDispatcher {
self.add_listener(event_name, listener, priority);
}
+ fn add_subscriber(&mut self, subscriber: &dyn EventSubscriberInterface) -> anyhow::Result<()> {
+ self.add_subscriber(subscriber)
+ }
+
fn has_event_listeners(&mut self, event: &dyn EventInterface) -> bool {
self.has_event_listeners(event)
}
diff --git a/crates/shirabe/src/event_dispatcher/event_subscriber_interface.rs b/crates/shirabe/src/event_dispatcher/event_subscriber_interface.rs
index 41d723dc..0a5d88d5 100644
--- a/crates/shirabe/src/event_dispatcher/event_subscriber_interface.rs
+++ b/crates/shirabe/src/event_dispatcher/event_subscriber_interface.rs
@@ -1,15 +1,26 @@
//! ref: composer/src/Composer/EventDispatcher/EventSubscriberInterface.php
use indexmap::IndexMap;
+use shirabe_php_rpc::PhpObjHandle;
/// Represents one event's subscriber info: method name only, method+priority, or multiple handlers.
+#[derive(Debug)]
pub enum SubscribedEventEntry {
Method(String),
MethodWithPriority(String, Option<i64>),
Methods(Vec<(String, Option<i64>)>),
}
+// The sole implementor is the PHP plugin proxy (plugins are the only subscribers in Composer
+// itself), so the trait deviates from the PHP shape in two deliberate ways: the PHP-side static
+// `getSubscribedEvents()` takes `&self` here (the receiver carries which PHP class to call, and
+// an associated function would not be dyn-compatible), and it is fallible because the answer
+// crosses the RPC boundary.
pub trait EventSubscriberInterface {
/// Returns an array of event names this subscriber wants to listen to.
- fn get_subscribed_events() -> IndexMap<String, SubscribedEventEntry>;
+ fn get_subscribed_events(&self) -> anyhow::Result<IndexMap<String, SubscribedEventEntry>>;
+
+ /// The subscriber as it crosses the wire: PHP's `[$subscriber, $method]` array callables
+ /// capture the subscriber object itself, represented here by its P-table handle.
+ fn subscriber_handle(&self) -> PhpObjHandle;
}
diff --git a/crates/shirabe/src/plugin/capable.rs b/crates/shirabe/src/plugin/capable.rs
index 23369046..830a7dab 100644
--- a/crates/shirabe/src/plugin/capable.rs
+++ b/crates/shirabe/src/plugin/capable.rs
@@ -2,7 +2,8 @@
use indexmap::IndexMap;
-// TODO(plugin): Plugin API - interface for plugins that expose capability implementations
+// The sole implementor is the PHP plugin proxy (Composer itself never implements Capable), so
+// the trait is fallible: the answer crosses the RPC boundary.
pub trait Capable {
- fn get_capabilities(&self) -> IndexMap<String, String>;
+ fn get_capabilities(&self) -> anyhow::Result<IndexMap<String, String>>;
}
diff --git a/crates/shirabe/src/plugin/php_plugin_proxy.rs b/crates/shirabe/src/plugin/php_plugin_proxy.rs
index 2b823fa5..143af8bb 100644
--- a/crates/shirabe/src/plugin/php_plugin_proxy.rs
+++ b/crates/shirabe/src/plugin/php_plugin_proxy.rs
@@ -7,11 +7,14 @@
use crate::autoload::ClassLoader;
use crate::composer::ComposerHandle;
+use crate::event_dispatcher::event_dispatcher::dispatch_event_method;
+use crate::event_dispatcher::{EventInterface, EventSubscriberInterface, SubscribedEventEntry};
use crate::io::IOInterface;
use crate::plugin::plugin_interface::PluginInterface;
use indexmap::IndexMap;
use shirabe_php_rpc::{
- PhpThrow, PluginValue, RustMethodDispatcher, RustObjHandle, call_php_method, release_php_handle,
+ PhpObjHandle, PhpThrow, PluginValue, RustMethodDispatcher, RustObjHandle, call_php_method,
+ release_php_handle,
};
/// A Rust-side entity a PHP proxy stub points back to.
@@ -22,20 +25,39 @@ enum RustEntity {
}
thread_local! {
- /// The R table. Entries are strong references kept for the worker's lifetime.
- /// TODO(plugin): GC (dropping entries on ReleaseRustHandle) is not implemented yet;
- /// until then entities registered here are intentionally never released.
+ /// The R table. Entries are strong references held while the child-side stub lives; a
+ /// ReleaseRustHandle notification (sent by the stub's `__destruct`) removes the entry.
+ /// Handles are monotonically increasing and never reused, so a released handle can never
+ /// be confused with a later entity (no generation counter is needed).
/// TODO(plugin): thread-local while the worker and its stub intern table are
/// process-global; the session lock serializes calls, and every dispatch currently runs on
/// the thread that registered the handle, but a handle minted on one thread is invisible
/// to another.
static R_TABLE: std::cell::RefCell<IndexMap<u64, RustEntity>> =
std::cell::RefCell::new(IndexMap::new());
+
+ static RELEASE_HOOK_INSTALLED: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
+}
+
+/// Installs the ReleaseRustHandle listener that drops R-table entries when the child-side stub
+/// is destructed. Idempotent; called by every entity registration.
+fn ensure_release_hook_installed() {
+ RELEASE_HOOK_INSTALLED.with(|installed| {
+ if !installed.get() {
+ installed.set(true);
+ shirabe_php_rpc::set_release_rust_handle_hook(|rhandle| {
+ R_TABLE.with(|table| {
+ table.borrow_mut().shift_remove(&rhandle);
+ });
+ });
+ }
+ });
}
/// Registers the Composer instance in the R table, interned by shared-pointer identity so the
/// same instance always crosses the boundary as the same handle (`===` in the child).
pub(crate) fn register_composer_entity(composer: &ComposerHandle) -> u64 {
+ ensure_release_hook_installed();
R_TABLE.with(|table| {
let mut table = table.borrow_mut();
let ptr = std::rc::Rc::as_ptr(composer.as_rc()) as *const () as usize;
@@ -54,6 +76,7 @@ pub(crate) fn register_composer_entity(composer: &ComposerHandle) -> u64 {
/// Registers an IO instance in the R table, interned like [`register_composer_entity`].
pub(crate) fn register_io_entity(io: &std::rc::Rc<std::cell::RefCell<dyn IOInterface>>) -> u64 {
+ ensure_release_hook_installed();
R_TABLE.with(|table| {
let mut table = table.borrow_mut();
let ptr = std::rc::Rc::as_ptr(io) as *const () as usize;
@@ -118,10 +141,18 @@ pub(crate) fn find_file_in_registered_loaders(class: &str) -> Option<String> {
/// Serves `CallRustMethod` requests from the child while a plugin-related call is in flight:
/// rhandle 0 is the runtime service endpoint (autoload lookups), every other rhandle resolves
/// through the R table. Unsupported methods are explicit errors, never silent fallbacks.
-#[derive(Debug)]
-pub(crate) struct PluginRpcDispatcher;
+#[derive(Debug, Default)]
+pub(crate) struct PluginRpcDispatcher<'a> {
+ /// At most one live event handle exposed per dispatched listener call, alongside the
+ /// persistent R table.
+ ///
+ /// TODO(plugin): a listener that stores the event stub beyond its own call observes an
+ /// unknown handle error afterwards; keeping events in the R table needs full proxying of
+ /// the object graph an event exposes, which does not exist yet.
+ pub(crate) event: Option<(u64, &'a dyn EventInterface)>,
+}
-impl RustMethodDispatcher for PluginRpcDispatcher {
+impl RustMethodDispatcher for PluginRpcDispatcher<'_> {
fn dispatch(
&mut self,
rhandle: u64,
@@ -150,6 +181,12 @@ impl RustMethodDispatcher for PluginRpcDispatcher {
)));
}
+ if let Some((event_rhandle, event)) = self.event
+ && event_rhandle == rhandle
+ {
+ return dispatch_event_method(event, method_name);
+ }
+
// The entity is cloned out so no table borrow is held while the handler runs (a
// handler that re-enters register_*_entity would otherwise panic on the RefCell).
let entity = R_TABLE.with(|table| table.borrow().get(&rhandle).cloned());
@@ -264,11 +301,18 @@ fn runtime_throw(message: String) -> PhpThrow {
pub struct PhpPluginProxy {
pub(crate) phandle: u64,
pub(crate) class: String,
+ /// Every interface the entity's class implements (`class_implements` in the child), for
+ /// the `instanceof` checks PHP performs on plugin objects.
+ pub(crate) implements: Vec<String>,
}
impl PhpPluginProxy {
- pub fn new(phandle: u64, class: String) -> Self {
- Self { phandle, class }
+ pub fn new(phandle: u64, class: String, implements: Vec<String>) -> Self {
+ Self {
+ phandle,
+ class,
+ implements,
+ }
}
fn forward_lifecycle_call(
@@ -294,7 +338,12 @@ impl PhpPluginProxy {
snapshot: None,
}),
];
- let outcome = call_php_method(self.phandle, method, args, Some(&mut PluginRpcDispatcher))?;
+ let outcome = call_php_method(
+ self.phandle,
+ method,
+ args,
+ Some(&mut PluginRpcDispatcher::default()),
+ )?;
match outcome {
Ok(_) => Ok(()),
// TODO(plugin): the original exception class is collapsed to RuntimeException on
@@ -359,11 +408,135 @@ impl PluginInterface for PhpPluginProxy {
self.class.clone()
}
+ fn as_event_subscriber(&self) -> Option<&dyn EventSubscriberInterface> {
+ if self
+ .implements
+ .iter()
+ .any(|interface| interface == "Composer\\EventDispatcher\\EventSubscriberInterface")
+ {
+ Some(self)
+ } else {
+ None
+ }
+ }
+
fn __as_php_plugin_proxy(&self) -> Option<&PhpPluginProxy> {
Some(self)
}
}
+impl EventSubscriberInterface for PhpPluginProxy {
+ fn get_subscribed_events(&self) -> anyhow::Result<IndexMap<String, SubscribedEventEntry>> {
+ // PHP: `$subscriber->getSubscribedEvents()` (an instance call of the static method).
+ let outcome = call_php_method(
+ self.phandle,
+ "getSubscribedEvents",
+ Vec::new(),
+ Some(&mut PluginRpcDispatcher::default()),
+ )?;
+ let value = match outcome {
+ Ok(value) => value,
+ // TODO(plugin): the original exception class is collapsed to RuntimeException on
+ // this side of the boundary.
+ Err(throw) => {
+ return Err(anyhow::anyhow!(shirabe_php_shim::RuntimeException {
+ message: throw.message,
+ code: throw.code,
+ }));
+ }
+ };
+ decode_subscribed_events(&self.class, value)
+ }
+
+ fn subscriber_handle(&self) -> PhpObjHandle {
+ PhpObjHandle {
+ phandle: self.phandle,
+ class: self.class.clone(),
+ implements: self.implements.clone(),
+ }
+ }
+}
+
+/// Decodes a `getSubscribedEvents()` wire value into the three shapes `addSubscriber`
+/// distinguishes: `'method'`, `['method', priority]`, and `[['method', priority], ...]`.
+/// A shape outside these is an explicit error, never a silently dropped listener.
+fn decode_subscribed_events(
+ class: &str,
+ value: PluginValue,
+) -> anyhow::Result<IndexMap<String, SubscribedEventEntry>> {
+ let entries = match value {
+ PluginValue::Array(entries) => entries,
+ PluginValue::List(items) => {
+ // A PHP list-shaped array (integer keys) cannot map event names; an empty one is
+ // the only legal case (an empty PHP array serializes as a list).
+ if items.is_empty() {
+ IndexMap::new()
+ } else {
+ return Err(subscribed_events_shape_error(
+ class,
+ &PluginValue::List(items),
+ ));
+ }
+ }
+ other => return Err(subscribed_events_shape_error(class, &other)),
+ };
+ let mut events: IndexMap<String, SubscribedEventEntry> = IndexMap::new();
+ for (event_name, params) in entries {
+ // TODO(phase-e): lossy UTF-8; event and method names are bytes in PHP.
+ let event_name = String::from_utf8_lossy(&event_name).into_owned();
+ let entry = match &params {
+ PluginValue::String(method) => {
+ SubscribedEventEntry::Method(String::from_utf8_lossy(method).into_owned())
+ }
+ PluginValue::List(items) => match items.first() {
+ Some(PluginValue::String(method)) => SubscribedEventEntry::MethodWithPriority(
+ String::from_utf8_lossy(method).into_owned(),
+ decode_listener_priority(class, items.get(1))?,
+ ),
+ _ => {
+ let mut listeners = Vec::with_capacity(items.len());
+ for listener in items {
+ let PluginValue::List(pair) = listener else {
+ return Err(subscribed_events_shape_error(class, &params));
+ };
+ let Some(PluginValue::String(method)) = pair.first() else {
+ return Err(subscribed_events_shape_error(class, &params));
+ };
+ listeners.push((
+ String::from_utf8_lossy(method).into_owned(),
+ decode_listener_priority(class, pair.get(1))?,
+ ));
+ }
+ SubscribedEventEntry::Methods(listeners)
+ }
+ },
+ _ => return Err(subscribed_events_shape_error(class, &params)),
+ };
+ events.insert(event_name, entry);
+ }
+ Ok(events)
+}
+
+fn decode_listener_priority(
+ class: &str,
+ value: Option<&PluginValue>,
+) -> anyhow::Result<Option<i64>> {
+ match value {
+ None => Ok(None),
+ Some(PluginValue::Int(priority)) => Ok(Some(*priority)),
+ Some(other) => Err(subscribed_events_shape_error(class, other)),
+ }
+}
+
+fn subscribed_events_shape_error(class: &str, value: &PluginValue) -> anyhow::Error {
+ anyhow::anyhow!(shirabe_php_shim::RuntimeException {
+ message: format!(
+ "{class}::getSubscribedEvents() returned an unsupported shape over RPC: {value:?}"
+ ),
+ code: 0,
+ })
+}
+
impl Drop for PhpPluginProxy {
fn drop(&mut self) {
// A dead worker has nothing left to release.
diff --git a/crates/shirabe/src/plugin/plugin_interface.rs b/crates/shirabe/src/plugin/plugin_interface.rs
index 989329ea..586fb43b 100644
--- a/crates/shirabe/src/plugin/plugin_interface.rs
+++ b/crates/shirabe/src/plugin/plugin_interface.rs
@@ -1,6 +1,7 @@
//! ref: composer/src/Composer/Plugin/PluginInterface.php
use crate::composer::ComposerHandle;
+use crate::event_dispatcher::EventSubscriberInterface;
use crate::io::IOInterface;
use crate::plugin::Capable;
@@ -31,11 +32,10 @@ pub trait PluginInterface: std::fmt::Debug {
/// the name PHP would report.
fn get_class_name(&self) -> String;
- // TODO(plugin): PHP-side `instanceof` checks for EventSubscriberInterface / Capable.
- // EventSubscriberInterface is not dyn-compatible (its only method is associated, not
- // a `&self` method), so we expose a boolean predicate instead.
- fn is_event_subscriber_interface(&self) -> bool {
- false
+ // PHP-side `instanceof EventSubscriberInterface` / `instanceof Capable` checks map to
+ // these downcast accessors.
+ fn as_event_subscriber(&self) -> Option<&dyn EventSubscriberInterface> {
+ None
}
fn as_capable(&self) -> Option<&dyn Capable> {
diff --git a/crates/shirabe/src/plugin/plugin_manager.rs b/crates/shirabe/src/plugin/plugin_manager.rs
index 65c4b03f..a3810d0c 100644
--- a/crates/shirabe/src/plugin/plugin_manager.rs
+++ b/crates/shirabe/src/plugin/plugin_manager.rs
@@ -485,9 +485,12 @@ impl PluginManager {
.into());
}
let handle = self.php_runtime_new_object(&class)?;
- let plugin: std::rc::Rc<std::cell::RefCell<dyn PluginInterface>> = std::rc::Rc::new(
- std::cell::RefCell::new(PhpPluginProxy::new(handle.phandle, handle.class)),
- );
+ let plugin: std::rc::Rc<std::cell::RefCell<dyn PluginInterface>> =
+ std::rc::Rc::new(std::cell::RefCell::new(PhpPluginProxy::new(
+ handle.phandle,
+ handle.class,
+ handle.implements,
+ )));
self.add_plugin(plugin.clone(), is_global_plugin, Some(package.clone()))?;
self.registered_plugins
.entry(package.get_name().to_string())
@@ -513,7 +516,7 @@ impl PluginManager {
let value = unwrap_php_result(call_function_with_dispatcher(
function,
args,
- Some(&mut PluginRpcDispatcher),
+ Some(&mut PluginRpcDispatcher::default()),
))?;
match value {
PluginValue::Bool(value) => Ok(value),
@@ -545,7 +548,7 @@ impl PluginManager {
unwrap_php_result(call_function_with_dispatcher(
"__shirabe_eval",
vec![PluginValue::string(code)],
- Some(&mut PluginRpcDispatcher),
+ Some(&mut PluginRpcDispatcher::default()),
))?;
Ok(())
}
@@ -561,7 +564,7 @@ impl PluginManager {
PluginValue::string(file_identifier),
PluginValue::string(file),
],
- Some(&mut PluginRpcDispatcher),
+ Some(&mut PluginRpcDispatcher::default()),
))?;
Ok(())
}
@@ -571,7 +574,7 @@ impl PluginManager {
let value = unwrap_php_result(shirabe_php_rpc::new_object(
class,
vec![],
- Some(&mut PluginRpcDispatcher),
+ Some(&mut PluginRpcDispatcher::default()),
))?;
match value {
PluginValue::PhpHandle(handle) => Ok(handle),
@@ -735,12 +738,12 @@ impl PluginManager {
.borrow_mut()
.activate(self.composer_full(), self.io.clone())?;
- // TODO(plugin): if plugin is EventSubscriberInterface, hook into the event dispatcher
- // The PHP code calls $this->composer->getEventDispatcher()->addSubscriber($plugin);
- // — add_subscriber here is generic over `S: EventSubscriberInterface` and cannot
- // accept a `&dyn EventSubscriberInterface`. Skipped until subscriber dispatch is
- // implemented dynamically.
- let _ = plugin.borrow().is_event_subscriber_interface();
+ let plugin_ref = plugin.borrow();
+ if let Some(subscriber) = plugin_ref.as_event_subscriber() {
+ let event_dispatcher = self.composer_full().borrow().get_event_dispatcher();
+ let result = event_dispatcher.borrow_mut().add_subscriber(subscriber);
+ result?;
+ }
Ok(())
}
@@ -949,13 +952,12 @@ impl PluginManager {
plugin: &dyn PluginInterface,
capability: &str,
) -> anyhow::Result<Option<String>> {
- // TODO(plugin): capability lookup
let capable = match plugin.as_capable() {
Some(c) => c,
None => return Ok(None),
};
- let capabilities = capable.get_capabilities();
+ let capabilities = capable.get_capabilities()?;
// PHP: !empty($capabilities[$capability]) && is_string($capabilities[$capability]) && trim($capabilities[$capability])
if let Some(s) = capabilities.get(capability) {