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);
}
}
|