aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe/src/plugin/plugin_manager.rs
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-08-04 06:33:32 +0900
committernsfisis <nsfisis@gmail.com>2026-08-04 06:33:32 +0900
commit11cdaae87c37e479fea6c39447041c312410b101 (patch)
tree06e67edf1f437850f6f319c02f1304d130081fab /crates/shirabe/src/plugin/plugin_manager.rs
parent901cbbf285ed4e9c1f7bf75b413643c117a6105a (diff)
downloadphp-shirabe-11cdaae87c37e479fea6c39447041c312410b101.tar.gz
php-shirabe-11cdaae87c37e479fea6c39447041c312410b101.tar.zst
php-shirabe-11cdaae87c37e479fea6c39447041c312410b101.zip
feat(plugin): instantiate plugin capabilities through the PHP RPC worker
getPluginCapability was a no-op returning None. Now it runs the real flow: class_exists in the worker, new $capabilityClass($ctorArgs) with the plugin's own phandle spliced in as $ctorArgs['plugin'], both instanceof checks answered by is_a in the child, and a per-interface Rust adapter over the resulting entity (CommandProvider and the plain Capability marker; anything else is an explicit error). getPluginCapabilities now propagates errors instead of swallowing them. Capable::get_capabilities widens from IndexMap<String, String> to IndexMap<String, PhpMixed>: upstream casts the return with (array) and validates only the queried key, so the narrow type both rejected maps Composer accepts and made the invalidImplementationClassNames data provider unrepresentable. The old code also returned ' 0 ' (trimmed to the falsy '0') as a valid class name where upstream throws; the rewritten validation follows upstream's empty/is_string/trim sequence. CommandProvider::get_commands returns BaseCommand adapters whose name is read back over RPC after the PHP constructor ran configure(); executing one still needs the PHP-side Symfony Application and stays an explicit error. The is_array / instanceof BaseCommand checks that upstream's Application::getPluginCommands performs on the raw getCommands value live in the adapter, because Vec<Box<dyn BaseCommand>> asserts every element up front. Ports testCommandProviderCapability (plugin-v8 end to end against the real worker) and testQueryingWithInvalidCapabilityClassNameThrows (all eight provider cases); the two tests that pass a PHPUnit mock plugin into PHP stay ignored — a Rust-native mock has no PHP-side entity to cross the boundary as $ctorArgs['plugin']. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Diffstat (limited to 'crates/shirabe/src/plugin/plugin_manager.rs')
-rw-r--r--crates/shirabe/src/plugin/plugin_manager.rs138
1 files changed, 102 insertions, 36 deletions
diff --git a/crates/shirabe/src/plugin/plugin_manager.rs b/crates/shirabe/src/plugin/plugin_manager.rs
index d720bcfe..9e52c01f 100644
--- a/crates/shirabe/src/plugin/plugin_manager.rs
+++ b/crates/shirabe/src/plugin/plugin_manager.rs
@@ -14,7 +14,9 @@ use crate::package::base_package::{self};
use crate::package::version::VersionParser;
use crate::plugin::PluginBlockedException;
use crate::plugin::capability::Capability;
-use crate::plugin::php_plugin_proxy::{PhpPluginProxy, PluginRpcDispatcher};
+use crate::plugin::php_plugin_proxy::{
+ PhpCapabilityProxy, PhpCommandProviderProxy, PhpPluginProxy, PluginRpcDispatcher, php_is_a,
+};
use crate::plugin::plugin_interface::{self, PluginInterface};
use crate::repository::InstalledRepository;
use crate::repository::RepositoryInterfaceHandle;
@@ -25,9 +27,9 @@ use indexmap::IndexMap;
use shirabe_external_packages::composer::pcre::Preg;
use shirabe_php_rpc::{PluginValue, call_function_with_dispatcher};
use shirabe_php_shim::{
- E_USER_DEPRECATED, PhpMixed, RuntimeException, UnexpectedValueException, array_key_exists,
- dirname, file_get_contents, implode, ksort, php_regex, preg_quote, strrpos, strtr_array,
- substr, trigger_error, trim, var_export_str, version_compare,
+ E_USER_DEPRECATED, PhpMixed, RuntimeException, UnexpectedValueException, dirname, empty,
+ file_get_contents, implode, ksort, php_regex, preg_quote, strrpos, strtr_array, substr,
+ trigger_error, trim, var_export, var_export_str, version_compare,
};
use shirabe_semver::constraint::SimpleConstraint;
@@ -966,36 +968,26 @@ impl PluginManager {
let capabilities = capable.get_capabilities()?;
// PHP: !empty($capabilities[$capability]) && is_string($capabilities[$capability]) && trim($capabilities[$capability])
- if let Some(s) = capabilities.get(capability) {
+ if let Some(value) = capabilities.get(capability)
+ && !empty(value)
+ && let PhpMixed::String(s) = value
+ {
let trimmed = trim(s, Some(" \t\n\r\0\u{0B}"));
- if !s.is_empty() && s != "0" && !trimmed.is_empty() {
+ // PHP evaluates trim(...) in boolean context: "" and "0" are falsy.
+ if !trimmed.is_empty() && trimmed != "0" {
return Ok(Some(trimmed));
}
}
- // PHP: empty($capabilities[$capability]) — true for null, false, 0, "", "0", [], or missing key.
- // In Rust the values are typed as String, so we only need to consider "", "0".
- let cap_is_empty = match capabilities.get(capability) {
- None => true,
- Some(s) if s.is_empty() || s == "0" => true,
- _ => false,
- };
- if array_key_exists(capability, &capabilities)
- && (cap_is_empty
- || trim(
- capabilities
- .get(capability)
- .map(|s| s.as_str())
- .unwrap_or(""),
- Some(" \t\n\r\0\u{0B}"),
- )
- .is_empty())
- {
+ // PHP: array_key_exists($capability, $capabilities) && (empty(...) || !is_string(...)
+ // || !trim(...)). Once the first branch has declined, a present key always fails one
+ // of the three disjuncts, so a present key unconditionally throws here.
+ if let Some(value) = capabilities.get(capability) {
return Err(UnexpectedValueException {
message: format!(
"Plugin {} provided invalid capability class name(s), got {}",
plugin.get_class_name(),
- var_export_str(capabilities.get(capability).unwrap(), true)
+ var_export(value, true)
),
code: 0,
}
@@ -1009,36 +1001,110 @@ impl PluginManager {
&self,
plugin: &dyn PluginInterface,
capability_class_name: &str,
- _ctor_args: IndexMap<String, PhpMixed>,
+ ctor_args: IndexMap<String, PluginValue>,
) -> anyhow::Result<Option<Box<dyn Capability>>> {
- // TODO(plugin): instantiate plugin capability via runtime class lookup
- let _capability_class =
+ let capability_class =
match self.get_capability_implementation_class_name(plugin, capability_class_name)? {
Some(c) => c,
None => return Ok(None),
};
- // PHP: requires class_exists / new $capabilityClass($ctorArgs); cannot be performed in Rust without a runtime registry.
- Ok(None)
+
+ // PHP: if (!class_exists($capabilityClass))
+ let exists = unwrap_php_result(call_function_with_dispatcher(
+ "class_exists",
+ vec![PluginValue::string(capability_class.clone())],
+ Some(&mut PluginRpcDispatcher::default()),
+ ))?;
+ if !matches!(exists, PluginValue::Bool(true)) {
+ return Err(RuntimeException {
+ message: format!(
+ "Cannot instantiate Capability, as class {} from plugin {} does not exist.",
+ capability_class,
+ plugin.get_class_name()
+ ),
+ code: 0,
+ }
+ .into());
+ }
+
+ // PHP: $ctorArgs['plugin'] = $plugin; the capability constructor receives the plugin
+ // instance itself, so a plugin with no PHP-side entity cannot be represented.
+ let plugin_value = match plugin.__as_php_plugin_proxy() {
+ Some(proxy) => PluginValue::PhpHandle(shirabe_php_rpc::PhpObjHandle {
+ phandle: proxy.phandle,
+ class: proxy.class.clone(),
+ implements: proxy.implements.clone(),
+ }),
+ None => anyhow::bail!(
+ "cannot instantiate capability {capability_class}: plugin {} has no PHP-side entity to pass as $ctorArgs['plugin']",
+ plugin.get_class_name()
+ ),
+ };
+ let mut ctor_args = ctor_args;
+ ctor_args.insert("plugin".to_string(), plugin_value);
+ let args_value = PluginValue::Array(
+ ctor_args
+ .into_iter()
+ .map(|(k, v)| (k.into_bytes(), v))
+ .collect(),
+ );
+
+ // PHP: $capabilityObj = new $capabilityClass($ctorArgs);
+ let capability_obj = unwrap_php_result(shirabe_php_rpc::new_object(
+ &capability_class,
+ vec![args_value],
+ Some(&mut PluginRpcDispatcher::default()),
+ ))?;
+ let handle = match capability_obj {
+ PluginValue::PhpHandle(handle) => handle,
+ other => anyhow::bail!(
+ "worker returned a non-object for new {capability_class}(...): {other:?}"
+ ),
+ };
+
+ // PHP: if (!$capabilityObj instanceof Capability || !$capabilityObj instanceof $capabilityClassName)
+ if !php_is_a(&handle, "Composer\\Plugin\\Capability\\Capability")?
+ || !php_is_a(&handle, capability_class_name)?
+ {
+ return Err(RuntimeException {
+ message: format!(
+ "Class {capability_class} must implement both Composer\\Plugin\\Capability\\Capability and {capability_class_name}."
+ ),
+ code: 0,
+ }
+ .into());
+ }
+
+ match capability_class_name {
+ "Composer\\Plugin\\Capability\\CommandProvider" => {
+ Ok(Some(Box::new(PhpCommandProviderProxy::new(handle))))
+ }
+ "Composer\\Plugin\\Capability\\Capability" => {
+ Ok(Some(Box::new(PhpCapabilityProxy::new(handle))))
+ }
+ // A capability interface outside composer-plugin-api that the instanceof checks
+ // accepted (the plugin ships its own): no Rust adapter exists for its methods.
+ other => anyhow::bail!("capability {other} is recognized but not yet adapted"),
+ }
}
pub fn get_plugin_capabilities(
&self,
capability_class_name: &str,
- ctor_args: IndexMap<String, PhpMixed>,
- ) -> Vec<Box<dyn Capability>> {
- // TODO(plugin): aggregate capabilities across all loaded plugins
+ ctor_args: IndexMap<String, PluginValue>,
+ ) -> anyhow::Result<Vec<Box<dyn Capability>>> {
let mut capabilities: Vec<Box<dyn Capability>> = vec![];
for plugin in self.get_plugins() {
- if let Ok(Some(capability)) = self.get_plugin_capability(
+ if let Some(capability) = self.get_plugin_capability(
&*plugin.borrow(),
capability_class_name,
ctor_args.clone(),
- ) {
+ )? {
capabilities.push(capability);
}
}
- capabilities
+ Ok(capabilities)
}
fn parse_allowed_plugins(