aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe-php-rpc
diff options
context:
space:
mode:
Diffstat (limited to 'crates/shirabe-php-rpc')
-rw-r--r--crates/shirabe-php-rpc/php/runtime/Shirabe/MaterializedThrowable.php70
-rw-r--r--crates/shirabe-php-rpc/php/worker.php15
-rw-r--r--crates/shirabe-php-rpc/src/frame.rs68
-rw-r--r--crates/shirabe-php-rpc/src/lib.rs17
4 files changed, 165 insertions, 5 deletions
diff --git a/crates/shirabe-php-rpc/php/runtime/Shirabe/MaterializedThrowable.php b/crates/shirabe-php-rpc/php/runtime/Shirabe/MaterializedThrowable.php
new file mode 100644
index 00000000..7227a5f3
--- /dev/null
+++ b/crates/shirabe-php-rpc/php/runtime/Shirabe/MaterializedThrowable.php
@@ -0,0 +1,70 @@
+<?php
+
+// The PHP half of the Throw frame's exception codec. A Rust-side failure carries the class it
+// was thrown as, so `catch (TransportException $e)` in plugin code catches what it would catch
+// under Composer, plus the state that class declares beyond message and code.
+
+namespace Shirabe;
+
+final class MaterializedThrowable
+{
+ /**
+ * Rebuilds the exception a Throw frame describes. A class the child cannot construct from a
+ * message and a code keeps the RuntimeException shape, which is all the frame guarantees.
+ *
+ * @param array<string, mixed> $properties
+ */
+ public static function revive(string $class, string $message, int $code, array $properties): \Throwable
+ {
+ $exception = self::instantiate($class, $message, $code);
+ foreach ($properties as $name => $value) {
+ // A name the class does not declare is a Shirabe bug rather than a plugin one, and
+ // ReflectionProperty reports it as such instead of dropping the state silently.
+ $property = new \ReflectionProperty($exception, $name);
+ $property->setAccessible(true);
+ $property->setValue($exception, $value);
+ }
+
+ return $exception;
+ }
+
+ private static function instantiate(string $class, string $message, int $code): \Throwable
+ {
+ if ($class === '' || !class_exists($class) || !is_a($class, \Throwable::class, true)) {
+ return new \RuntimeException($message, $code);
+ }
+ $constructor = (new \ReflectionClass($class))->getConstructor();
+ if ($constructor === null || !self::acceptsMessageAndCode($constructor)) {
+ return new \RuntimeException($message, $code);
+ }
+
+ return new $class($message, $code);
+ }
+
+ /**
+ * Whether the constructor has \Exception's shape as far as the frame fills it in: a string
+ * message, an int code, and nothing else required.
+ */
+ private static function acceptsMessageAndCode(\ReflectionMethod $constructor): bool
+ {
+ $parameters = $constructor->getParameters();
+ if (count($parameters) < 2) {
+ return false;
+ }
+ foreach ($parameters as $position => $parameter) {
+ $type = $parameter->getType();
+ $name = $type instanceof \ReflectionNamedType ? $type->getName() : null;
+ if ($position === 0 && $name !== 'string') {
+ return false;
+ }
+ if ($position === 1 && $name !== 'int') {
+ return false;
+ }
+ if ($position >= 2 && !$parameter->isOptional()) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+}
diff --git a/crates/shirabe-php-rpc/php/worker.php b/crates/shirabe-php-rpc/php/worker.php
index bc10c2de..defa4fb5 100644
--- a/crates/shirabe-php-rpc/php/worker.php
+++ b/crates/shirabe-php-rpc/php/worker.php
@@ -337,10 +337,13 @@ final class ShirabeRpcRuntime
$outParams = self::fromWire($fields[1] ?? []);
return self::fromWire($fields[0]);
}
- [$class, $message, $code] = $fields;
- // TODO(plugin): reconstruct the original exception class instead of collapsing
- // everything to RuntimeException.
- throw new RuntimeException($message, (int) $code);
+ [$class, $message, $code, $properties] = $fields;
+ throw \Shirabe\MaterializedThrowable::revive(
+ $class,
+ $message,
+ (int) $code,
+ self::fromWire($properties)
+ );
}
self::dispatchRequest($tag, $inId, $payload);
}
@@ -435,7 +438,9 @@ final class ShirabeRpcRuntime
self::writeFrame(
SHIRABE_TAG_THROW,
$corrId,
- serialize([get_class($e), $e->getMessage(), (int) $e->getCode()])
+ // TODO(plugin): the properties field is empty in this direction; no Rust-side
+ // consumer rebuilds a ported exception from a Throw frame yet.
+ serialize([get_class($e), $e->getMessage(), (int) $e->getCode(), []])
);
}
}
diff --git a/crates/shirabe-php-rpc/src/frame.rs b/crates/shirabe-php-rpc/src/frame.rs
index 3a355c86..fe7752bd 100644
--- a/crates/shirabe-php-rpc/src/frame.rs
+++ b/crates/shirabe-php-rpc/src/frame.rs
@@ -66,6 +66,9 @@ pub enum Frame {
exception_class: String,
message: String,
code: i64,
+ /// The state the exception carries beyond `message` and `code`, keyed by the property
+ /// names its class declares. Empty for an exception that carries none.
+ properties: IndexMap<String, PluginValue>,
},
ReleaseRustHandle {
rhandle: u64,
@@ -180,11 +183,18 @@ impl Frame {
exception_class,
message,
code,
+ properties,
..
} => vec![
PluginValue::string(exception_class.clone()),
PluginValue::string(message.clone()),
PluginValue::Int(*code),
+ PluginValue::Array(
+ properties
+ .iter()
+ .map(|(name, v)| (name.clone().into_bytes(), v.clone()))
+ .collect(),
+ ),
],
Frame::ReleaseRustHandle { rhandle } => vec![int_value(*rhandle)],
Frame::ReleasePhpHandle { phandle } => vec![int_value(*phandle)],
@@ -302,6 +312,7 @@ fn decode_frame(tag: u8, corr_id: u64, payload: &[u8]) -> Frame {
panic!("PHP RPC: protocol violation — Throw code is not an int: {other:?}")
}
},
+ properties: expect_properties(next()),
},
TAG_RELEASE_RUST_HANDLE => Frame::ReleaseRustHandle {
rhandle: expect_id(next()),
@@ -354,6 +365,27 @@ fn expect_positions(value: PluginValue) -> Vec<u32> {
.collect()
}
+fn expect_properties(value: PluginValue) -> IndexMap<String, PluginValue> {
+ match value {
+ PluginValue::List(items) if items.is_empty() => IndexMap::new(),
+ PluginValue::Array(map) => map
+ .into_iter()
+ .map(|(key, item)| {
+ let name = String::from_utf8(key).unwrap_or_else(|error| {
+ panic!(
+ "PHP RPC: protocol violation — exception property name is not UTF-8: {:?}",
+ String::from_utf8_lossy(error.as_bytes())
+ )
+ });
+ (name, item)
+ })
+ .collect(),
+ other => {
+ panic!("PHP RPC: protocol violation — exception properties is not an array: {other:?}")
+ }
+ }
+}
+
fn expect_out_params(value: PluginValue) -> IndexMap<u32, PluginValue> {
match value {
PluginValue::List(items) if items.is_empty() => IndexMap::new(),
@@ -416,6 +448,42 @@ mod tests {
}
#[test]
+ fn frame_roundtrip_throw_with_properties() {
+ let frame = roundtrip(Frame::Throw {
+ corr_id: 11,
+ exception_class: "Composer\\Downloader\\TransportException".to_string(),
+ message: "The \"https://example.org\" file could not be downloaded".to_string(),
+ code: 401,
+ properties: [
+ ("statusCode".to_string(), PluginValue::Int(401)),
+ ("response".to_string(), PluginValue::Null),
+ ]
+ .into_iter()
+ .collect(),
+ });
+ match frame {
+ Frame::Throw {
+ corr_id,
+ exception_class,
+ message,
+ code,
+ properties,
+ } => {
+ assert_eq!(corr_id, 11);
+ assert_eq!(exception_class, "Composer\\Downloader\\TransportException");
+ assert_eq!(
+ message,
+ "The \"https://example.org\" file could not be downloaded"
+ );
+ assert_eq!(code, 401);
+ assert_eq!(properties.get("statusCode"), Some(&PluginValue::Int(401)));
+ assert_eq!(properties.get("response"), Some(&PluginValue::Null));
+ }
+ other => panic!("unexpected frame: {other:?}"),
+ }
+ }
+
+ #[test]
fn frame_roundtrip_return_with_out_params() {
let frame = roundtrip(Frame::Return {
corr_id: 9,
diff --git a/crates/shirabe-php-rpc/src/lib.rs b/crates/shirabe-php-rpc/src/lib.rs
index dfd8fa67..c898fb82 100644
--- a/crates/shirabe-php-rpc/src/lib.rs
+++ b/crates/shirabe-php-rpc/src/lib.rs
@@ -621,6 +621,15 @@ pub struct PhpThrow {
pub exception_class: String,
pub message: String,
pub code: i64,
+ /// The state the exception carries beyond `message` and `code`, keyed by the property names
+ /// its class declares. The child revives them onto the instance it rebuilds.
+ ///
+ /// TODO(plugin): populated only Rust to PHP. An exception a plugin throws crosses with its
+ /// class, message and code alone, because no Rust-side consumer rebuilds a ported exception
+ /// from a `PhpThrow` yet.
+ ///
+ /// Boxed so a `Result<_, PhpThrow>` stays small; every dispatcher returns one.
+ pub properties: Box<IndexMap<String, PluginValue>>,
}
impl PhpThrow {
@@ -629,6 +638,7 @@ impl PhpThrow {
exception_class: "RuntimeException".to_string(),
message,
code: 0,
+ properties: Box::new(IndexMap::new()),
}
}
}
@@ -805,11 +815,13 @@ fn rpc_call(
exception_class,
message,
code,
+ properties,
} if corr_id == my_id => {
return Ok(Err(PhpThrow {
exception_class,
message,
code,
+ properties: Box::new(properties),
}));
}
Frame::CallRustMethod {
@@ -846,6 +858,7 @@ fn rpc_call(
exception_class: throw.exception_class,
message: throw.message,
code: throw.code,
+ properties: *throw.properties,
},
};
send_frame(&reply)?;
@@ -1042,6 +1055,10 @@ const RUNTIME_FILES: &[(&str, &str)] = &[
include_str!("../php/runtime/Composer/EventDispatcher/Event.php"),
),
(
+ "Shirabe/MaterializedThrowable.php",
+ include_str!("../php/runtime/Shirabe/MaterializedThrowable.php"),
+ ),
+ (
"Shirabe/MaterializedValue.php",
include_str!("../php/runtime/Shirabe/MaterializedValue.php"),
),