aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe-php-rpc/php/runtime/Shirabe/MaterializedValue.php
blob: eee48266018c5da36ccd96140e569a786fee18d4 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
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);
    }
}