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