From c3b367ab27de325b7be20298b45891e9bb7b7644 Mon Sep 17 00:00:00 2001 From: PurHur Date: Sun, 26 Jul 2026 00:37:11 +0000 Subject: [PATCH] Stdlib: register Soap\Url / Soap\Sdl on PROFILE=8.4+ (#23230) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PHP 8.4 resource→object opaque types from ext/soap/soap.stub.php — final internal classes with userland new rejected; withheld on reference 8.2. Co-authored-by: Cursor --- docs/bootstrap-inventory.md | 35 ++++--- docs/capabilities.md | 2 +- docs/pages/capability-comparison.html | 2 +- ext/soap/BuiltinClasses.php | 2 + ext/soap/SoapExtensionPolicy.php | 18 ++++ ext/soap/VmSoapOpaque.php | 95 +++++++++++++++++++ lib/VM/ReservedBuiltinClass.php | 3 + .../stdlib/extension_loaded_soap_phantom.phpt | 4 + .../cases/stdlib/soap_url_sdl_classes.phpt | 32 +++++++ test/repro/extension_loaded_soap_phantom.php | 2 + test/repro/maintainer_gap_soap_url_sdl_84.php | 32 +++++++ .../compiler_lib_spine_smoke/main.php | 1 + test/unit/SoapExtensionPolicyTest.php | 39 ++++++++ 13 files changed, 253 insertions(+), 14 deletions(-) create mode 100644 ext/soap/VmSoapOpaque.php create mode 100644 test/compliance/cases/stdlib/soap_url_sdl_classes.phpt create mode 100644 test/repro/maintainer_gap_soap_url_sdl_84.php diff --git a/docs/bootstrap-inventory.md b/docs/bootstrap-inventory.md index cd87308f08..6e124816e9 100644 --- a/docs/bootstrap-inventory.md +++ b/docs/bootstrap-inventory.md @@ -8,11 +8,11 @@ Regenerate: `php script/bootstrap-inventory.php` | Metric | Count | |--------|------:| -| PHP files on vm.php path | 6561 | -| Phase A inventory files (M2 ratio SSOT) | 6561 | +| PHP files on vm.php path | 6562 | +| Phase A inventory files (M2 ratio SSOT) | 6562 | | Phase A ratio-deferred paths | 0 | | Source constructs flagged (blockers) | 0 | -| Source constructs flagged (warnings) | 21094 | +| Source constructs flagged (warnings) | 21100 | ## Compiler CFG gaps (`lib/Compiler.php`) @@ -1859,6 +1859,7 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag | `ext/soap/SoapFaultConstruct.php` | 0 | 1 | | `ext/soap/VmSoapClient.php` | 0 | 60 | | `ext/soap/VmSoapEncoding.php` | 0 | 32 | +| `ext/soap/VmSoapOpaque.php` | 0 | 6 | | `ext/soap/VmSoapServer.php` | 0 | 47 | | `ext/soap/bootstrap_soapfault.php` | 0 | 1 | | `ext/soap/is_soap_fault.php` | 0 | 2 | @@ -21588,14 +21589,14 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag ### `ext/soap/BuiltinClasses.php` **Warnings** (review for bootstrap subset): -- new Variable (line 47) -- new Variable (line 48) - new Variable (line 49) -- new ClassProperty (line 57) -- new ClassProperty (line 60) -- new ClassProperty (line 63) -- new ClassProperty (line 66) -- new SoapFaultConstruct (line 69) +- new Variable (line 50) +- new Variable (line 51) +- new ClassProperty (line 59) +- new ClassProperty (line 62) +- new ClassProperty (line 65) +- new ClassProperty (line 68) +- new SoapFaultConstruct (line 71) - 2 class method(s) ### `ext/soap/Module.php` @@ -21618,7 +21619,7 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag ### `ext/soap/SoapExtensionPolicy.php` **Warnings** (review for bootstrap subset): -- 8 class method(s) +- 9 class method(s) ### `ext/soap/SoapFaultConstruct.php` @@ -21725,6 +21726,16 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag - new ArgumentCountError (line 220) - 10 class method(s) +### `ext/soap/VmSoapOpaque.php` + +**Warnings** (review for bootstrap subset): +- new ClassEntry (line 46) +- new ObjectEntry (line 60) +- new ObjectEntry (line 74) +- new Variable (line 82) +- new Variable (line 90) +- 6 class method(s) + ### `ext/soap/VmSoapServer.php` **Warnings** (review for bootstrap subset): @@ -52709,7 +52720,7 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag ### `lib/VM/ReservedBuiltinClass.php` **Warnings** (review for bootstrap subset): -- new Error (line 47) +- new Error (line 50) - 4 class method(s) ### `lib/VM/ResourceSupport.php` diff --git a/docs/capabilities.md b/docs/capabilities.md index 735a336526..6dd2f7e901 100644 --- a/docs/capabilities.md +++ b/docs/capabilities.md @@ -1200,7 +1200,7 @@ Auto-generated by `script/capability-matrix.php`. Do not edit by hand. | `str_starts_with` | yes | yes | yes | standard | JIT PHPT; AOT PHPT | | `str_word_count` | yes | yes | yes | standard | JIT PHPT; AOT PHPT | | `strcasecmp` | yes | yes | yes | standard | JIT PHPT; AOT PHPT | -| `strchr` | yes | yes | yes | standard | | +| `strchr` | yes | yes | yes | standard | JIT PHPT; AOT PHPT | | `strcmp` | yes | yes | yes | standard | JIT PHPT; AOT PHPT | | `strcoll` | yes | yes | yes | standard | JIT PHPT | | `strcspn` | yes | yes | yes | standard | empty $characters: PHP 8.4 full byte length (GH-12592, #7088); JIT PHPT; AOT PHPT | diff --git a/docs/pages/capability-comparison.html b/docs/pages/capability-comparison.html index 28b79a9310..ab4cffd531 100644 --- a/docs/pages/capability-comparison.html +++ b/docs/pages/capability-comparison.html @@ -52,7 +52,7 @@

PHP capability comparison

This is not a full php-src inventory — only items we measure in capabilities-syntax.md and capabilities.md. - Generated 2026-07-25 (matrix mtime). + Generated 2026-07-26 (matrix mtime).

diff --git a/ext/soap/BuiltinClasses.php b/ext/soap/BuiltinClasses.php index 9c1056f4d7..864e0ce14b 100644 --- a/ext/soap/BuiltinClasses.php +++ b/ext/soap/BuiltinClasses.php @@ -23,6 +23,8 @@ public static function register(Context $ctx): void VmSoapEncoding::register($ctx); VmSoapServer::registerClass($ctx); VmSoapClient::registerClass($ctx); + // PHP 8.4+ Soap\Url / Soap\Sdl opaque types (#23230). + VmSoapOpaque::register($ctx); foreach (\array_diff(\array_keys($ctx->classes), $before) as $lc) { $ctx->classes[$lc]->isInternal = true; } diff --git a/ext/soap/SoapExtensionPolicy.php b/ext/soap/SoapExtensionPolicy.php index 46125a4540..2e6ae1f223 100644 --- a/ext/soap/SoapExtensionPolicy.php +++ b/ext/soap/SoapExtensionPolicy.php @@ -36,6 +36,24 @@ public static function advertisesExceptionClass(): bool return self::advertisesExtension(); } + /** + * Soap\Url / Soap\Sdl — php-src 8.4+ only (soap.stub.php resource→object; #23230). + * + * SoapClient may be advertised via host php-soap on 8.2; these types stay withheld until + * the effective language profile (or host PHP) is ≥ 8.4. + */ + public static function advertisesOpaqueUrlSdlTypes(): bool + { + if (!self::advertisesExtension()) { + return false; + } + if (\version_compare(\PHP_VERSION, '8.4.0', '>=')) { + return true; + } + + return \version_compare(\PHPCompiler\CompilerVersion::languageProfileVersion(), '8.4.0', '>='); + } + /** Compliance filenames that exercise SoapClient / SoapServer / SoapFault. */ public static function isSoapComplianceCase(string $testFileName): bool { diff --git a/ext/soap/VmSoapOpaque.php b/ext/soap/VmSoapOpaque.php new file mode 100644 index 0000000000..0c6416c42f --- /dev/null +++ b/ext/soap/VmSoapOpaque.php @@ -0,0 +1,95 @@ +classes[$lc])) { + $ctx->classes[$lc]->isInternal = true; + $ctx->classes[$lc]->isFinal = true; + + return; + } + + $entry = new ClassEntry($name); + $entry->isInternal = true; + $entry->isFinal = true; + $ctx->classes[$lc] = $entry; + } + + /** Factory for SoapClient::$httpurl / related wiring (php-src soap_url_object_create). */ + public static function newUrlObject(Context $ctx): ObjectEntry + { + self::register($ctx); + $class = $ctx->classes[self::URL_CLASS_LC] ?? null; + if (null === $class) { + throw new \LogicException('Soap\\Url is not registered in this compiler build'); + } + $object = new ObjectEntry($class); + $object->constructed = true; + + return $object; + } + + /** Factory for SoapClient::$sdl (php-src soap_sdl_object_create). */ + public static function newSdlObject(Context $ctx): ObjectEntry + { + self::register($ctx); + $class = $ctx->classes[self::SDL_CLASS_LC] ?? null; + if (null === $class) { + throw new \LogicException('Soap\\Sdl is not registered in this compiler build'); + } + $object = new ObjectEntry($class); + $object->constructed = true; + + return $object; + } + + public static function urlVariable(Context $ctx): Variable + { + $var = new Variable(Variable::TYPE_OBJECT); + $var->object(self::newUrlObject($ctx)); + + return $var; + } + + public static function sdlVariable(Context $ctx): Variable + { + $var = new Variable(Variable::TYPE_OBJECT); + $var->object(self::newSdlObject($ctx)); + + return $var; + } +} diff --git a/lib/VM/ReservedBuiltinClass.php b/lib/VM/ReservedBuiltinClass.php index 9312c2230c..00af8f49c1 100644 --- a/lib/VM/ReservedBuiltinClass.php +++ b/lib/VM/ReservedBuiltinClass.php @@ -25,6 +25,9 @@ final class ReservedBuiltinClass 'pgsql\\connection' => 'Cannot directly construct PgSql\Connection, use pg_connect() or pg_pconnect() instead', 'pgsql\\result' => 'Cannot directly construct PgSql\Result, use a dedicated function instead', 'pgsql\\lob' => 'Cannot directly construct PgSql\Lob, use pg_lo_open() instead', + // php-src ext/soap/soap.c soap_url/sdl_object_get_constructor (#23230) + 'soap\\url' => 'Cannot directly construct Soap\\Url', + 'soap\\sdl' => 'Cannot directly construct Soap\\Sdl', ]; /** @var array lc => display name — runtime implements guard (#13327, #15445, #18781) */ diff --git a/test/compliance/cases/stdlib/extension_loaded_soap_phantom.phpt b/test/compliance/cases/stdlib/extension_loaded_soap_phantom.phpt index fbeed54a7f..c2f0efade1 100644 --- a/test/compliance/cases/stdlib/extension_loaded_soap_phantom.phpt +++ b/test/compliance/cases/stdlib/extension_loaded_soap_phantom.phpt @@ -12,6 +12,8 @@ echo 'use_soap_error_handler=', (int) function_exists('use_soap_error_handler'), echo 'SoapClient=', (int) class_exists('SoapClient', false), "\n"; echo 'SoapServer=', (int) class_exists('SoapServer', false), "\n"; echo 'SoapFault=', (int) class_exists('SoapFault', false), "\n"; +echo 'SoapUrl=', (int) class_exists('Soap\\Url', false), "\n"; +echo 'SoapSdl=', (int) class_exists('Soap\\Sdl', false), "\n"; ?> --EXPECT-- loaded=0 @@ -22,3 +24,5 @@ use_soap_error_handler=0 SoapClient=0 SoapServer=0 SoapFault=0 +SoapUrl=0 +SoapSdl=0 diff --git a/test/compliance/cases/stdlib/soap_url_sdl_classes.phpt b/test/compliance/cases/stdlib/soap_url_sdl_classes.phpt new file mode 100644 index 0000000000..ef6179fe22 --- /dev/null +++ b/test/compliance/cases/stdlib/soap_url_sdl_classes.phpt @@ -0,0 +1,32 @@ +--TEST-- +Stdlib: Soap\Url / Soap\Sdl final internal classes on soap PROFILE=8.4+ (#23230, ext/soap/soap.stub.php) +--FILE-- +isFinal(), ' url_internal=', (int) $ru->isInternal(), "\n"; +echo 'sdl_final=', (int) $rs->isFinal(), ' sdl_internal=', (int) $rs->isInternal(), "\n"; +try { + new Soap\Url(); + echo "url_ctor=ok\n"; +} catch (Error $e) { + echo 'url_ctor=', $e->getMessage(), "\n"; +} +try { + new Soap\Sdl(); + echo "sdl_ctor=ok\n"; +} catch (Error $e) { + echo 'sdl_ctor=', $e->getMessage(), "\n"; +} +?> +--EXPECT-- +url=1 +sdl=1 +url_final=1 url_internal=1 +sdl_final=1 sdl_internal=1 +url_ctor=Cannot directly construct Soap\Url +sdl_ctor=Cannot directly construct Soap\Sdl diff --git a/test/repro/extension_loaded_soap_phantom.php b/test/repro/extension_loaded_soap_phantom.php index 208238bb24..0727513588 100644 --- a/test/repro/extension_loaded_soap_phantom.php +++ b/test/repro/extension_loaded_soap_phantom.php @@ -4,4 +4,6 @@ echo 'SoapClient=', class_exists('SoapClient', false) ? 'yes' : 'no', "\n"; echo 'SoapServer=', class_exists('SoapServer', false) ? 'yes' : 'no', "\n"; echo 'SoapFault=', class_exists('SoapFault', false) ? 'yes' : 'no', "\n"; +echo 'SoapUrl=', class_exists('Soap\\Url', false) ? 'yes' : 'no', "\n"; +echo 'SoapSdl=', class_exists('Soap\\Sdl', false) ? 'yes' : 'no', "\n"; echo 'is_soap_fault=', function_exists('is_soap_fault') ? 'yes' : 'no', "\n"; diff --git a/test/repro/maintainer_gap_soap_url_sdl_84.php b/test/repro/maintainer_gap_soap_url_sdl_84.php new file mode 100644 index 0000000000..641d7e3b96 --- /dev/null +++ b/test/repro/maintainer_gap_soap_url_sdl_84.php @@ -0,0 +1,32 @@ +=8.4 or host soap)\n"); + exit(1); +} + +foreach (['Soap\\Url', 'Soap\\Sdl'] as $name) { + if (!class_exists($name, false)) { + fwrite(STDERR, "missing $name\n"); + exit(1); + } + $r = new ReflectionClass($name); + if (!$r->isFinal() || !$r->isInternal()) { + fwrite(STDERR, "$name final=".((int) $r->isFinal())." internal=".((int) $r->isInternal())."\n"); + exit(1); + } + try { + new $name(); + fwrite(STDERR, "$name construct allowed\n"); + exit(1); + } catch (Error $e) { + if (!str_contains($e->getMessage(), 'Cannot directly construct')) { + fwrite(STDERR, "$name bad ctor: ".$e->getMessage()."\n"); + exit(1); + } + } +} + +echo "ok\n"; diff --git a/test/selfhost/compiler_lib_spine_smoke/main.php b/test/selfhost/compiler_lib_spine_smoke/main.php index d3c01fba5e..8d3421e66f 100644 --- a/test/selfhost/compiler_lib_spine_smoke/main.php +++ b/test/selfhost/compiler_lib_spine_smoke/main.php @@ -1646,6 +1646,7 @@ require_once __DIR__.'/../../../ext/soap/SoapFaultConstruct.php'; require_once __DIR__.'/../../../ext/soap/VmSoapClient.php'; require_once __DIR__.'/../../../ext/soap/VmSoapEncoding.php'; +require_once __DIR__.'/../../../ext/soap/VmSoapOpaque.php'; require_once __DIR__.'/../../../ext/soap/VmSoapServer.php'; require_once __DIR__.'/../../../ext/soap/bootstrap_soapfault.php'; require_once __DIR__.'/../../../ext/soap/is_soap_fault.php'; diff --git a/test/unit/SoapExtensionPolicyTest.php b/test/unit/SoapExtensionPolicyTest.php index 670ce41411..966743f09c 100644 --- a/test/unit/SoapExtensionPolicyTest.php +++ b/test/unit/SoapExtensionPolicyTest.php @@ -35,5 +35,44 @@ public function testWithholdsOnReferenceWithoutHostSoap(): void self::assertFalse( ext\standard\VmReflection::classExists($runtime->vmContext, 'SoapFault') ); + self::assertFalse( + ext\standard\VmReflection::classExists($runtime->vmContext, 'Soap\\Url') + ); + self::assertFalse( + ext\standard\VmReflection::classExists($runtime->vmContext, 'Soap\\Sdl') + ); + self::assertFalse(SoapExtensionPolicy::advertisesOpaqueUrlSdlTypes()); + } + + public function testSoapUrlSdlOnForwardProfile84(): void + { + $prev = getenv('PHP_COMPILER_PROFILE'); + putenv('PHP_COMPILER_PROFILE=8.4'); + try { + self::assertTrue(SoapExtensionPolicy::advertisesExtension()); + self::assertTrue(SoapExtensionPolicy::advertisesOpaqueUrlSdlTypes()); + + $runtime = new Runtime(); + self::assertTrue( + ext\standard\VmReflection::classExists($runtime->vmContext, 'Soap\\Url') + ); + self::assertTrue( + ext\standard\VmReflection::classExists($runtime->vmContext, 'Soap\\Sdl') + ); + $url = $runtime->vmContext->classes['soap\\url'] ?? null; + $sdl = $runtime->vmContext->classes['soap\\sdl'] ?? null; + self::assertNotNull($url); + self::assertNotNull($sdl); + self::assertTrue($url->isFinal); + self::assertTrue($url->isInternal); + self::assertTrue($sdl->isFinal); + self::assertTrue($sdl->isInternal); + } finally { + if (false === $prev || null === $prev) { + putenv('PHP_COMPILER_PROFILE'); + } else { + putenv('PHP_COMPILER_PROFILE='.$prev); + } + } } }