aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe-php-rpc/php
diff options
context:
space:
mode:
Diffstat (limited to 'crates/shirabe-php-rpc/php')
-rw-r--r--crates/shirabe-php-rpc/php/runtime/Shirabe/MaterializedThrowable.php70
-rw-r--r--crates/shirabe-php-rpc/php/worker.php15
2 files changed, 80 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(), []])
);
}
}