aboutsummaryrefslogtreecommitdiffhomepage
path: root/docs/dev
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-08-07 05:11:55 +0900
committernsfisis <nsfisis@gmail.com>2026-08-07 05:11:55 +0900
commit9a393adc0ace86cac788723b524e83c63dfc91c1 (patch)
treefadac10efa6518c1a5d4d16f829da6969229ce09 /docs/dev
parent14a8a474120ef4289625f05ba5d87fa9e9d19542 (diff)
downloadphp-shirabe-9a393adc0ace86cac788723b524e83c63dfc91c1.tar.gz
php-shirabe-9a393adc0ace86cac788723b524e83c63dfc91c1.tar.zst
php-shirabe-9a393adc0ace86cac788723b524e83c63dfc91c1.zip
feat(php-rpc): cross materialized values as PHP object records
A materialized value used to cross as a constructor call: the class name, the arguments, and any post-construction setter. Describing a real instance that way needed a ReflectionProperty read for every field the class exposes no getter for, and state no constructor takes (an unset pretty string, a Link built without a pretty constraint) had no faithful call to describe it at all. The value now crosses as the object record serialize() writes for it, which unserialize() revives without running a constructor, so both sides transfer the state itself instead of a recipe for rebuilding it. The PHP half keeps only the class list (also the allowed_classes list of every frame payload) and the UTC rebasing of dates; describe(), build() and the reflection are gone. The wire codec gains O: records (PluginValue::PhpObject) and r: back references, whose resolution reproduces PHP's numbering of every value in a payload; a cyclic object graph and a PHP reference (R:) are rejected. Two behaviours change with it: a date crosses carrying timezone_type 3 "UTC" rather than a +00:00 offset, which is what ArrayLoader builds a release date as, and a Link subclass crosses as a P-table entity instead of being silently downgraded to a plain Link. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'docs/dev')
-rw-r--r--docs/dev/php-rpc.md45
1 files changed, 26 insertions, 19 deletions
diff --git a/docs/dev/php-rpc.md b/docs/dev/php-rpc.md
index 77d0d365..392b318e 100644
--- a/docs/dev/php-rpc.md
+++ b/docs/dev/php-rpc.md
@@ -65,31 +65,38 @@ Payloads are encoded with a Rust reimplementation of the PHP `serialize()` gramm
(`src/value.rs`), byte-compatible with the PHP core implementation under
`serialize_precision=-1` (the float formatting itself is ported in `shirabe-php-src`). The value
model is `PluginValue`: PHP scalars, byte strings (`Vec<u8>` — non-UTF-8 round-trips
-losslessly), lists, ordered maps, and three handle descriptor kinds encoded as reserved arrays:
+losslessly), lists, ordered maps, object records, and three handle descriptor kinds encoded as
+reserved arrays:
- `{__rhandle, __class, __epoch[, __snapshot]}` — entity lives on the Rust side
- `{__phandle, __class, __implements}` — entity lives in the PHP child
- `{__pclass}` — a PHP class name
-- `{__pnew, __args[, __calls]}` — a materialized value (below)
-An immutable value has no entity to point at, so it crosses in neither table: the descriptor
-names the real class plus the constructor arguments (and any post-construction calls) needed to
-rebuild it, and each side builds a genuine instance of its own. `Composer\Package\Link` travels
-this way, together with the `composer/semver` constraint it holds — encoded structurally rather
-than re-parsed from its string form, which would lose the pretty strings and the conjunctive
-flag. The `\DateTimeInterface` release date uses the same shape. The two halves are
+An immutable value has no entity to point at, so it crosses in neither table: it travels as the
+object record `serialize()` writes for it (`PluginValue::PhpObject` — the class name and the
+property table, property names carrying PHP's visibility mangling), and each side rebuilds its
+own value from those fields. `unserialize()` revives the PHP one without running a constructor,
+which is what makes the two directions equivalent: a value whose state a constructor cannot
+express — an unset pretty string, a `Link` built without a pretty constraint — has no faithful
+constructor call, and no field has to be read back out through reflection.
+`Composer\Package\Link` travels this way, together with the `composer/semver` constraint it
+holds, and so does the `\DateTimeInterface` release date, rebased on UTC because the Rust side
+carries no timezone database. The two halves are
`crates/shirabe/src/plugin/php_plugin_value.rs` and `Shirabe\MaterializedValue`; the set of
-classes that may cross is a closed list on both sides, so a descriptor can never name an
-arbitrary class.
+classes that may cross is a closed list on both sides — the PHP one is the `allowed_classes`
+list of every `unserialize()` — so a record can never name an arbitrary class.
-`PluginValue::Object` is encode-only: the wire erases the array/object distinction and object
-revival is banned (`unserialize(..., ['allowed_classes' => false])` is enforced on the PHP
-side), so the decoder only produces `List` (contiguous 0-based int keys) or `Array`. The
-decoder is iterative (input nesting never becomes call-stack depth) and additionally rejects
-payloads nested deeper than 512 levels.
+`PluginValue::Object` is encode-only: the wire erases the array/object distinction for a
+class-less object, so the decoder only produces `List` (contiguous 0-based int keys) or `Array`
+for an `a:` record. A repeated object instance arrives as PHP's `r:` back-reference, which the
+decoder resolves by copying the value it names (identity means nothing to a value on this side);
+a cyclic object graph and a PHP reference (`R:`) are both rejected. The decoder is iterative
+(input nesting never becomes call-stack depth) and additionally rejects payloads nested deeper
+than 512 levels.
The codec is verified against the real PHP `serialize()`/`unserialize()` by oracle tests
-(`tests/oracle.rs`), with floats, non-UTF-8 byte strings and deep nesting as focus areas.
+(`tests/oracle.rs`), with floats, non-UTF-8 byte strings, object records and deep nesting as
+focus areas.
## Concurrency and reentrancy
@@ -184,9 +191,9 @@ by `scripts/plugin-stub-generator/generate-stubs` and must not be edited by hand
surface (`getIO()`/`getComposer()`/...) answers from the Rust handoff.
- `Shirabe\RustCommandStub` — the reverse stub for built-in commands registered into that
application.
-- `Shirabe\MaterializedValue` — the PHP half of the materialized-value codec: it builds the
- real value classes from a `__pnew` descriptor and describes such instances back in the same
- shape.
+- `Shirabe\MaterializedValue` — the PHP half of the materialized-value codec: the closed list of
+ classes `unserialize()` may revive, and the hook that hands such an instance to `serialize()`
+ in place of a handle descriptor.
- `Composer\EventDispatcher\Event` — dual-mode: revived from a Rust handle (through
`__shirabeBind`) it proxies like a generated stub, while a natively-constructed instance (real
Composer code in the worker does `new PreCommandRunEvent(...)`, whose parent constructor lands