diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-08-06 20:04:25 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-08-06 20:04:25 +0900 |
| commit | 3f525f228052ad04e6f9da6ff160bef803d079bd (patch) | |
| tree | 3fe2d078e6a30b12ce041b1b3f1e589bc47cf8f3 /crates/shirabe-php-rpc | |
| parent | d9dca94603766712b5989ea8169c9e286d27a60c (diff) | |
| download | php-shirabe-3f525f228052ad04e6f9da6ff160bef803d079bd.tar.gz php-shirabe-3f525f228052ad04e6f9da6ff160bef803d079bd.tar.zst php-shirabe-3f525f228052ad04e6f9da6ff160bef803d079bd.zip | |
feat(plugin): let links and release dates cross the RPC boundary
The link getters and setters and the release date accessors were explicit
errors for every non-empty value, because an immutable value has no entity
to point a handle at. They now cross as materialized values: the descriptor
names the real class and the constructor arguments, and each side builds a
genuine instance of its own.
The semver constraint a link holds is encoded structurally rather than
re-parsed from its string form, so the pretty strings and the conjunctive
flag survive the crossing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'crates/shirabe-php-rpc')
| -rw-r--r-- | crates/shirabe-php-rpc/php/runtime/Shirabe/MaterializedValue.php | 114 | ||||
| -rw-r--r-- | crates/shirabe-php-rpc/php/worker.php | 7 | ||||
| -rw-r--r-- | crates/shirabe-php-rpc/src/lib.rs | 4 |
3 files changed, 125 insertions, 0 deletions
diff --git a/crates/shirabe-php-rpc/php/runtime/Shirabe/MaterializedValue.php b/crates/shirabe-php-rpc/php/runtime/Shirabe/MaterializedValue.php new file mode 100644 index 00000000..eee48266 --- /dev/null +++ b/crates/shirabe-php-rpc/php/runtime/Shirabe/MaterializedValue.php @@ -0,0 +1,114 @@ +<?php + +// The PHP half of the materialized-value codec (crates/shirabe/src/plugin/php_plugin_value.rs). +// An object whose entity lives on the Rust side crosses as a handle, but an immutable value +// has no entity to point at: the child holds a genuine instance of the real class instead, and +// the wire carries the class name plus the constructor arguments needed to rebuild it. +// +// Only the classes listed in CLASSES cross this way. An unknown class name is an explicit +// error rather than a `new $class`, so the descriptor can never name an arbitrary class. + +namespace Shirabe; + +use Composer\Package\Link; +use Composer\Semver\Constraint\Constraint; +use Composer\Semver\Constraint\MatchAllConstraint; +use Composer\Semver\Constraint\MatchNoneConstraint; +use Composer\Semver\Constraint\MultiConstraint; + +final class MaterializedValue +{ + private const CLASSES = [ + Link::class, + Constraint::class, + MultiConstraint::class, + MatchAllConstraint::class, + MatchNoneConstraint::class, + \DateTimeImmutable::class, + \DateTime::class, + ]; + + /** + * @param array{__pnew: string, __args?: array, __calls?: array} $descriptor + */ + public static function build(array $descriptor): object + { + $class = $descriptor['__pnew']; + if (!\in_array($class, self::CLASSES, true)) { + throw new \RuntimeException( + "the class {$class} cannot be materialized in the plugin runtime" + ); + } + $value = new $class(...array_values($descriptor['__args'] ?? [])); + foreach ($descriptor['__calls'] ?? [] as [$method, $args]) { + $value->$method(...array_values($args)); + } + return $value; + } + + /** + * The descriptor for a value the Rust side rebuilds by value, or null when the object is + * not one of them (it then crosses as a P-table entity). + * + * @return ?array{__pnew: string, __args: array, __calls?: array} + */ + public static function describe(object $value): ?array + { + if ($value instanceof Link) { + return [ + '__pnew' => Link::class, + '__args' => [ + self::field($value, 'source'), + self::field($value, 'target'), + $value->getConstraint(), + self::field($value, 'description'), + // Not getPrettyConstraint(): that throws when the link was built without + // one, and an absent pretty constraint has to cross as absent. + self::field($value, 'prettyConstraint'), + ], + ]; + } + if ($value instanceof Constraint) { + return self::constraint($value, [$value->getOperator(), $value->getVersion()]); + } + if ($value instanceof MultiConstraint) { + return self::constraint($value, [$value->getConstraints(), $value->isConjunctive()]); + } + if ($value instanceof MatchAllConstraint || $value instanceof MatchNoneConstraint) { + return self::constraint($value, []); + } + if ($value instanceof \DateTimeInterface) { + return [ + '__pnew' => $value instanceof \DateTime ? \DateTime::class : \DateTimeImmutable::class, + // DATE_ATOM widened by the microseconds a PHP date carries, so the instant + // crosses at the full precision this side can represent. + '__args' => [$value->format('Y-m-d\TH:i:s.uP')], + ]; + } + return null; + } + + /** + * No constraint constructor takes the pretty string, and whether it was ever set is + * observable through getPrettyString(), so it travels as a post-construction call. + * + * @param list<mixed> $args + * @return array{__pnew: string, __args: list<mixed>, __calls: list<array{string, list<mixed>}>} + */ + private static function constraint(object $value, array $args): array + { + return [ + '__pnew' => \get_class($value), + '__args' => $args, + '__calls' => [['setPrettyString', [self::field($value, 'prettyString')]]], + ]; + } + + /** Reads a protected field these value classes expose no getter for. */ + private static function field(object $value, string $name) + { + $property = new \ReflectionProperty($value, $name); + (\PHP_VERSION_ID < 80100) and $property->setAccessible(true); + return $property->getValue($value); + } +} diff --git a/crates/shirabe-php-rpc/php/worker.php b/crates/shirabe-php-rpc/php/worker.php index 9a36abdd..e82c45ba 100644 --- a/crates/shirabe-php-rpc/php/worker.php +++ b/crates/shirabe-php-rpc/php/worker.php @@ -227,6 +227,10 @@ final class ShirabeRpcRuntime // A natively-constructed dual-mode instance falls through to the P table below. } if (is_object($value)) { + $materialized = \Shirabe\MaterializedValue::describe($value); + if ($materialized !== null) { + return array_map([self::class, 'toWire'], $materialized); + } return ShirabePhpObjectRegistry::descriptor($value); } if (is_resource($value)) { @@ -257,6 +261,9 @@ final class ShirabeRpcRuntime if (isset($value['__pclass']) && count($value) === 1) { return $value['__pclass']; } + if (isset($value['__pnew'])) { + return \Shirabe\MaterializedValue::build(array_map([self::class, 'fromWire'], $value)); + } return array_map([self::class, 'fromWire'], $value); } diff --git a/crates/shirabe-php-rpc/src/lib.rs b/crates/shirabe-php-rpc/src/lib.rs index 4f9ba6b5..05067420 100644 --- a/crates/shirabe-php-rpc/src/lib.rs +++ b/crates/shirabe-php-rpc/src/lib.rs @@ -661,6 +661,10 @@ const RUNTIME_FILES: &[(&str, &str)] = &[ include_str!("../php/runtime/Composer/EventDispatcher/Event.php"), ), ( + "Shirabe/MaterializedValue.php", + include_str!("../php/runtime/Shirabe/MaterializedValue.php"), + ), + ( "Shirabe/RustCommandStub.php", include_str!("../php/runtime/Shirabe/RustCommandStub.php"), ), |
