From 3f525f228052ad04e6f9da6ff160bef803d079bd Mon Sep 17 00:00:00 2001 From: nsfisis Date: Thu, 6 Aug 2026 20:04:25 +0900 Subject: 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) --- .../php/runtime/Shirabe/MaterializedValue.php | 114 +++++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 crates/shirabe-php-rpc/php/runtime/Shirabe/MaterializedValue.php (limited to 'crates/shirabe-php-rpc/php/runtime/Shirabe/MaterializedValue.php') 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 @@ +$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 $args + * @return array{__pnew: string, __args: list, __calls: list}>} + */ + 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); + } +} -- cgit v1.3.1