aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe/src/plugin
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-08-07 20:51:59 +0900
committernsfisis <nsfisis@gmail.com>2026-08-07 20:51:59 +0900
commit18a37a098157a98edbc8473e9c0d8dff3e8a88fa (patch)
tree471682a4493fc8ddc1577a6f1e3c3d9c203ebf2c /crates/shirabe/src/plugin
parentcd9e4a2b67cdea258e1daa2d9d830b7643bc19bb (diff)
downloadphp-shirabe-18a37a098157a98edbc8473e9c0d8dff3e8a88fa.tar.gz
php-shirabe-18a37a098157a98edbc8473e9c0d8dff3e8a88fa.tar.zst
php-shirabe-18a37a098157a98edbc8473e9c0d8dff3e8a88fa.zip
fix(plugin): make proxy stub property access an explicit error
A proxy stub declares none of the real class's instance properties, and the `__get`/`__set` forwarders were emitted only for classes that declare a public one. Every other property access therefore got PHP's own answer for an undeclared property — null on a read, a dynamic property on a write — so plugin code reading state the entity holds ran on with null and failed somewhere else entirely, or not at all. Emit the forwarders on every root stub, add `__isset`/`__unset` alongside them so `isset()` cannot answer false silently either, and serve all four from one dispatcher that answers the state the Rust-side entity exposes and rejects every other name. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'crates/shirabe/src/plugin')
-rw-r--r--crates/shirabe/src/plugin/php_plugin_proxy.rs73
1 files changed, 47 insertions, 26 deletions
diff --git a/crates/shirabe/src/plugin/php_plugin_proxy.rs b/crates/shirabe/src/plugin/php_plugin_proxy.rs
index bd40b358..541718af 100644
--- a/crates/shirabe/src/plugin/php_plugin_proxy.rs
+++ b/crates/shirabe/src/plugin/php_plugin_proxy.rs
@@ -305,6 +305,12 @@ impl RustMethodDispatcher for PluginRpcDispatcher<'_> {
None => Err(runtime_throw(format!("unknown Rust handle {rhandle}"))),
};
}
+ if matches!(method_name, "__get" | "__set" | "__isset" | "__unset") {
+ return match entity {
+ Some(entity) => dispatch_property_access(&entity, method_name, &args),
+ None => Err(runtime_throw(format!("unknown Rust handle {rhandle}"))),
+ };
+ }
match entity {
Some(RustEntity::Io(io)) => dispatch_io_method(&io, method_name, &args),
Some(RustEntity::Composer(composer)) => {
@@ -458,6 +464,47 @@ fn clone_entity(entity: &RustEntity) -> Result<PluginValue, PhpThrow> {
]))
}
+/// Serves the `__get`/`__set`/`__isset`/`__unset` forwarders every proxy stub carries.
+fn dispatch_property_access(
+ entity: &RustEntity,
+ method_name: &str,
+ args: &[PluginValue],
+) -> Result<PluginValue, PhpThrow> {
+ let property = required_string_arg(method_name, args.first())?;
+ // `BasePackage::$id` is the one instance property the entities expose so far.
+ if let RustEntity::Package(package) = entity
+ && property == "id"
+ {
+ return match method_name {
+ "__get" => Ok(PluginValue::Int(
+ package.borrow().as_package_interface().get_id(),
+ )),
+ "__isset" => Ok(PluginValue::Bool(true)),
+ "__set" => {
+ let id = match args.get(1) {
+ Some(PluginValue::Int(id)) => *id,
+ other => {
+ return Err(runtime_throw(format!(
+ "the package property `id` takes an int, got {other:?}"
+ )));
+ }
+ };
+ package.borrow_mut().as_package_interface_mut().set_id(id);
+ Ok(PluginValue::Null)
+ }
+ _ => Err(runtime_throw(
+ "the package property `id` cannot be unset over RPC".to_string(),
+ )),
+ };
+ }
+ // TODO(plugin): the instance properties the proxied classes expose are widened on demand,
+ // driven by these explicit errors from real plugins. Each one has to decide how the state
+ // the real class keeps in that property is served from the Rust-side entity.
+ Err(runtime_throw(format!(
+ "the property `{property}` is not available over RPC yet"
+ )))
+}
+
fn dispatch_composer_method(
composer: &ComposerHandle,
method_name: &str,
@@ -1552,32 +1599,6 @@ fn dispatch_package_method(
}
return Ok(PluginValue::Null);
}
- // `BasePackage::$id` is the one public property of the package classes, so the stub's
- // property forwarders only ever carry it.
- "__get" | "__set" => {
- let property = required_string_arg(method_name, args.first())?;
- if property != "id" {
- return Err(runtime_throw(format!(
- "the package property `{property}` is not available over RPC"
- )));
- }
- return if method_name == "__get" {
- Ok(PluginValue::Int(
- package.borrow().as_package_interface().get_id(),
- ))
- } else {
- let id = match args.get(1) {
- Some(PluginValue::Int(id)) => *id,
- other => {
- return Err(runtime_throw(format!(
- "the package property `id` takes an int, got {other:?}"
- )));
- }
- };
- package.borrow_mut().as_package_interface_mut().set_id(id);
- Ok(PluginValue::Null)
- };
- }
"equals" => {
let other = package_from_arg(method_name, args.first())?;
let this = PackageInterfaceHandle::from_rc_unchecked(package.clone());