diff options
Diffstat (limited to 'crates/shirabe-php-rpc/php/runtime/Shirabe')
| -rw-r--r-- | crates/shirabe-php-rpc/php/runtime/Shirabe/MaterializedValue.php | 98 |
1 files changed, 18 insertions, 80 deletions
diff --git a/crates/shirabe-php-rpc/php/runtime/Shirabe/MaterializedValue.php b/crates/shirabe-php-rpc/php/runtime/Shirabe/MaterializedValue.php index eee48266..c16b4e18 100644 --- a/crates/shirabe-php-rpc/php/runtime/Shirabe/MaterializedValue.php +++ b/crates/shirabe-php-rpc/php/runtime/Shirabe/MaterializedValue.php @@ -3,10 +3,12 @@ // 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. +// the wire carries the object record `serialize()` writes for it, which `unserialize()` revives +// without running a constructor. // -// 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. +// CLASSES is the whole vocabulary that may cross this way: it is both the set of classes handed +// to `serialize()` in place of a handle descriptor, and the `allowed_classes` list every frame +// payload is unserialized under, so a payload can never name another class. namespace Shirabe; @@ -18,7 +20,7 @@ use Composer\Semver\Constraint\MultiConstraint; final class MaterializedValue { - private const CLASSES = [ + public const CLASSES = [ Link::class, Constraint::class, MultiConstraint::class, @@ -29,86 +31,22 @@ final class MaterializedValue ]; /** - * @param array{__pnew: string, __args?: array, __calls?: array} $descriptor + * The object to serialize onto the wire in place of $value, or null when the Rust side has + * no value to rebuild it as (it then crosses as a P-table entity). */ - public static function build(array $descriptor): object + public static function forWire(object $value): ?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')], - ]; + // The Rust side holds instants in UTC and carries no timezone database, so the date + // crosses rebased on UTC. Going through the offset rather than the zone keeps the + // instant exact across an ambiguous wall clock, and drops any subclass a plugin + // brought along. + return (new \DateTimeImmutable($value->format('Y-m-d H:i:s.uP'))) + ->setTimezone(new \DateTimeZone('UTC')); } - 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); + // Not instanceof: a subclass has state and behaviour of its own that no Rust value + // carries, so it crosses as an entity instead. + return \in_array(\get_class($value), self::CLASSES, true) ? $value : null; } } |
