aboutsummaryrefslogtreecommitdiffhomepage
path: root/docs/dev/plugin-stub-generation.md
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-08-30 23:01:25 +0900
committernsfisis <nsfisis@gmail.com>2026-08-30 23:09:56 +0900
commitd3bc3354c9705dfc6dc5e9b9adb5eb64d41e4c49 (patch)
treea745ecc3403104d5a34f29964362e239e8f5675b /docs/dev/plugin-stub-generation.md
parent057f3b8de26293319e265c1d86d9a1153124f3c7 (diff)
downloadphp-shirabe-d3bc3354c9705dfc6dc5e9b9adb5eb64d41e4c49.tar.gz
php-shirabe-d3bc3354c9705dfc6dc5e9b9adb5eb64d41e4c49.tar.zst
php-shirabe-d3bc3354c9705dfc6dc5e9b9adb5eb64d41e4c49.zip
feat(plugin): serve ProcessExecutor as a proxy stub
Composer reaches this class two ways: the object graph hands one out through Composer::getLoop()->getProcessExecutor(), and plugins write `new ProcessExecutor($io)` freely. Both bind to a Rust-side entity, so the timeout the run shares -- seeded from process-timeout and rewritten while the run is in flight -- has one value instead of one per world, and the executor can still be passed to the classes that take one (`new Filesystem($process)`). Three things the stub generator was missing came with it: - By-ref parameters. The call carries their positions and the answer carries what each holds afterwards; a position the answer omits was never assigned to, which is what PHP does with an untouched by-ref parameter. ProcessExecutor::execute is the only one on a proxied class. - Argument arity, reproduced where the real body reads func_num_args(). execute($cmd) forwards the child's output and execute($cmd, $out) captures it, and nothing but the argument count separates the two. - Static methods that cannot run in the worker. One that reads a static property the Rust side owns, or that reaches a guarded class, forwards through __shirabeCallStatic instead of being materialized. That also fixes Filesystem::isLocalPath and getPlatformPath, whose materialized bodies called the guarded Composer\Util\Platform. The async surface stays an explicit error. executeAsync resolves its promise with a Symfony Process, whose proc_open() resource and pipes belong to whichever process called start(), so a Rust-side spawn has none to hand back; running the real start() in the worker needs a promise representation that crosses the boundary unresolved. The fixture project drives the whole synchronous surface from plugin code and compares the trace against upstream Composer byte for byte. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'docs/dev/plugin-stub-generation.md')
-rw-r--r--docs/dev/plugin-stub-generation.md27
1 files changed, 25 insertions, 2 deletions
diff --git a/docs/dev/plugin-stub-generation.md b/docs/dev/plugin-stub-generation.md
index e2b6d848..a14d7ce1 100644
--- a/docs/dev/plugin-stub-generation.md
+++ b/docs/dev/plugin-stub-generation.md
@@ -88,11 +88,32 @@ the generator's vendor directory or the classifier report is unavailable.
the class's own public methods already cover their surface.
* Methods returning `self`/`static` perform the RPC and then `return $this;`
to preserve identity instead of round-tripping the handle.
+* **By-ref parameters** are declared `&$name` on the stub as well. The call
+ carries their positions, and the answer carries the value each of them holds
+ afterwards, which the stub assigns back (see "By-ref parameters" in
+ `docs/dev/php-rpc.md`). A position the answer omits is left alone, so a
+ parameter the callee never assigned to keeps the value it had.
+* **Arity** is reproduced when — and only when — the real method body calls
+ `func_num_args()`, in which case the stub trims the argument list to what the
+ caller actually passed instead of sending the declared defaults. Sending them
+ regardless would make the Rust side answer a call the plugin never made:
+ `ProcessExecutor::execute($cmd)` forwards the child's output, while
+ `execute($cmd, $out)` captures it, and only the argument count separates them.
+ A body calling `func_get_args()` fails generation instead.
* **Class constants, static methods and public static properties** are
materialized verbatim from the real source (they read no instance state and
run locally in the worker), together with any non-public static helpers the
methods call. Constants keep their declared visibility, so a non-public one
stays unreadable from outside the stub as it is in the real class.
+* A static method is materialized only when it can actually run in the worker.
+ One that reads a **static property** — whose value the Rust side owns, as
+ `ProcessExecutor::$timeout` does — or that reaches a class a **guard**
+ shadows there — as `ProcessExecutor::escape()` reaches `Composer\Util\Platform`
+ — forwards instead, through `__shirabeCallStatic` on handle 0, carrying a
+ comment that names the reason. The test covers the transitive closure of the
+ non-public static helpers the method calls, since those are materialized with
+ it. A forwarded static may not take by-ref parameters; that combination fails
+ generation.
* **Instance properties** are not declared on the stub, whatever their
visibility: they are entity state. Every root stub instead carries
`__get`/`__set`/`__isset`/`__unset` forwarders, so each access reaches the
@@ -137,8 +158,10 @@ Generation fails — instead of emitting something quietly wrong — on:
* a target missing from the classifier report, classified other than
`rust-proxy`/`contract`, or a report carrying violations,
-* by-ref or variadic parameters (in constructors too), static interface
- methods, magic methods other than `__toString`/`__clone`,
+* variadic parameters (in constructors too), by-ref parameters in a
+ constructor or in a forwarded static method, `func_get_args()` in a forwarded
+ body, static interface methods, magic methods other than
+ `__toString`/`__clone`,
* an omitted override diverging from the inherited stub signature,
* a subclass target listed before its base class, or extending a class that is
neither a target nor provided by `php/runtime/`,