From 6a6ec1b8f8a5c70d21f3772ce637b763e8ab21ea Mon Sep 17 00:00:00 2001 From: nsfisis Date: Mon, 31 Aug 2026 00:15:52 +0900 Subject: feat(plugin): carry Http\Response across as a materialized value MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A response is built for one request and the graph never retains it, so there is no entity for a handle to point at. The child holds a real instance instead, revived from the object record the wire carries, and collect() frees the copy each world holds — which is what that method is for. The value-object rule rejects the class only because collect() assigns to $this, so the category comes from an overrides.list entry. HttpDownloader::get() and copy() answer with one. Two gaps stay: decodeJson() reaches Composer\Json\JsonFile, which a guard shadows, and Composer answers a curl request with the CurlResponse subclass where this port flattens the value into a Response. Co-Authored-By: Claude Opus 5 (1M context) --- .../php/guards/Composer/Util/Http/CurlResponse.php | 40 +++++++++++++++ .../php/guards/Composer/Util/Http/Response.php | 57 ---------------------- .../php/runtime/Shirabe/MaterializedValue.php | 2 + crates/shirabe-php-rpc/src/value.rs | 47 ++++++++++++++++++ 4 files changed, 89 insertions(+), 57 deletions(-) delete mode 100644 crates/shirabe-php-rpc/php/guards/Composer/Util/Http/Response.php (limited to 'crates/shirabe-php-rpc') diff --git a/crates/shirabe-php-rpc/php/guards/Composer/Util/Http/CurlResponse.php b/crates/shirabe-php-rpc/php/guards/Composer/Util/Http/CurlResponse.php index ae229589..ffb000f3 100644 --- a/crates/shirabe-php-rpc/php/guards/Composer/Util/Http/CurlResponse.php +++ b/crates/shirabe-php-rpc/php/guards/Composer/Util/Http/CurlResponse.php @@ -19,4 +19,44 @@ class CurlResponse extends Response { \ShirabeUnsupportedClass::fail(self::class, 'getCurlInfo'); } + + public function getStatusCode(): int + { + \ShirabeUnsupportedClass::fail(self::class, 'getStatusCode'); + } + + public function getStatusMessage(): ?string + { + \ShirabeUnsupportedClass::fail(self::class, 'getStatusMessage'); + } + + public function getHeaders(): array + { + \ShirabeUnsupportedClass::fail(self::class, 'getHeaders'); + } + + public function getHeader(string $name): ?string + { + \ShirabeUnsupportedClass::fail(self::class, 'getHeader'); + } + + public function getBody(): ?string + { + \ShirabeUnsupportedClass::fail(self::class, 'getBody'); + } + + public function decodeJson() + { + \ShirabeUnsupportedClass::fail(self::class, 'decodeJson'); + } + + public function collect(): void + { + \ShirabeUnsupportedClass::fail(self::class, 'collect'); + } + + public static function findHeaderValue(array $headers, string $name): ?string + { + \ShirabeUnsupportedClass::fail(self::class, 'findHeaderValue'); + } } diff --git a/crates/shirabe-php-rpc/php/guards/Composer/Util/Http/Response.php b/crates/shirabe-php-rpc/php/guards/Composer/Util/Http/Response.php deleted file mode 100644 index 61eebb7f..00000000 --- a/crates/shirabe-php-rpc/php/guards/Composer/Util/Http/Response.php +++ /dev/null @@ -1,57 +0,0 @@ -\0name`. The declaring class is + /// the one whose body holds the `private` declaration, which is not `self.class` once a + /// subclass inherits it. + pub fn private(&self, declaring_class: &str, name: &str) -> Option<&PluginValue> { + self.props + .get(private_key(declaring_class, name).as_slice()) + } + + pub fn set_private(&mut self, declaring_class: &str, name: &str, value: PluginValue) { + self.props.insert(private_key(declaring_class, name), value); + } } fn protected_key(name: &str) -> Vec { @@ -78,6 +90,14 @@ fn protected_key(name: &str) -> Vec { key } +fn private_key(declaring_class: &str, name: &str) -> Vec { + let mut key = vec![0]; + key.extend_from_slice(declaring_class.as_bytes()); + key.push(0); + key.extend_from_slice(name.as_bytes()); + key +} + /// The value model of the plugin RPC boundary: PHP scalars, arrays, object records, and handle /// descriptors. /// @@ -795,6 +815,19 @@ mod tests { b"O:45:\"Composer\\Semver\\Constraint\\MatchAllConstraint\":1:{s:15:\"\0*\0prettyString\";N;}".as_slice(), ); + // A private property carries the declaring class rather than `*`, which is what lets a + // subclass hold its own property of the same name. + let mut response = PhpObject::new("Composer\\Util\\Http\\Response"); + response.set_private( + "Composer\\Util\\Http\\Response", + "code", + PluginValue::Int(200), + ); + assert_eq!( + serialize(&PluginValue::PhpObject(response)), + b"O:27:\"Composer\\Util\\Http\\Response\":1:{s:33:\"\0Composer\\Util\\Http\\Response\0code\";i:200;}".as_slice(), + ); + let mut date = PhpObject::new("DateTimeImmutable"); date.set_public("date", PluginValue::string("2026-08-07 12:34:56.123456")); date.set_public("timezone_type", PluginValue::Int(3)); @@ -819,6 +852,20 @@ mod tests { assert_eq!(inner.protected("operator"), Some(&PluginValue::Int(4))); assert_eq!(inner.public("operator"), None); assert_eq!(outer.protected("prettyConstraint"), None); + + let mut response = PhpObject::new("Composer\\Util\\Http\\Response"); + response.set_private( + "Composer\\Util\\Http\\Response", + "body", + PluginValue::string("{}"), + ); + roundtrip(PluginValue::PhpObject(response.clone())); + assert_eq!( + response.private("Composer\\Util\\Http\\Response", "body"), + Some(&PluginValue::string("{}")) + ); + assert_eq!(response.protected("body"), None); + assert_eq!(response.public("body"), None); } /// PHP numbers every value of a payload, including the ones inside an object and the -- cgit v1.3.1-4-g156e