Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 23 additions & 12 deletions docs/bootstrap-inventory.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`)

Expand Down Expand Up @@ -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 |
Expand Down Expand Up @@ -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`
Expand All @@ -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`

Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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`
Expand Down
2 changes: 1 addition & 1 deletion docs/capabilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
2 changes: 1 addition & 1 deletion docs/pages/capability-comparison.html
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ <h1>PHP capability comparison</h1>
This is <em>not</em> a full php-src inventory — only items we measure in
<a href="https://github.com/PurHur/php-compiler/blob/master/docs/capabilities-syntax.md"><code>capabilities-syntax.md</code></a>
and <a href="https://github.com/PurHur/php-compiler/blob/master/docs/capabilities.md"><code>capabilities.md</code></a>.
Generated 2026-07-25 (matrix mtime).
Generated 2026-07-26 (matrix mtime).
</p>
</header>

Expand Down
2 changes: 2 additions & 0 deletions ext/soap/BuiltinClasses.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
18 changes: 18 additions & 0 deletions ext/soap/SoapExtensionPolicy.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down
95 changes: 95 additions & 0 deletions ext/soap/VmSoapOpaque.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
<?php

declare(strict_types=1);

namespace PHPCompiler\ext\soap;

use PHPCompiler\VM\ClassEntry;
use PHPCompiler\VM\Context;
use PHPCompiler\VM\ObjectEntry;
use PHPCompiler\VM\Variable;

/**
* Soap\Url / Soap\Sdl — PHP 8.4 resource→object opaque types (php-src ext/soap/soap.stub.php; #23230).
*
* Final internal classes; userland `new` rejected via {@see \PHPCompiler\VM\ReservedBuiltinClass}.
*/
final class VmSoapOpaque
{
public const URL_CLASS = 'Soap\\Url';

public const URL_CLASS_LC = 'soap\\url';

public const SDL_CLASS = 'Soap\\Sdl';

public const SDL_CLASS_LC = 'soap\\sdl';

public static function register(Context $ctx): void
{
if (!SoapExtensionPolicy::advertisesOpaqueUrlSdlTypes()) {
return;
}

self::registerOne($ctx, self::URL_CLASS, self::URL_CLASS_LC);
self::registerOne($ctx, self::SDL_CLASS, self::SDL_CLASS_LC);
}

private static function registerOne(Context $ctx, string $name, string $lc): void
{
if (isset($ctx->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;
}
}
3 changes: 3 additions & 0 deletions lib/VM/ReservedBuiltinClass.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string> lc => display name — runtime implements guard (#13327, #15445, #18781) */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -22,3 +24,5 @@ use_soap_error_handler=0
SoapClient=0
SoapServer=0
SoapFault=0
SoapUrl=0
SoapSdl=0
32 changes: 32 additions & 0 deletions test/compliance/cases/stdlib/soap_url_sdl_classes.phpt
Original file line number Diff line number Diff line change
@@ -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--
<?php
declare(strict_types=1);

echo 'url=', (int) class_exists('Soap\\Url', false), "\n";
echo 'sdl=', (int) class_exists('Soap\\Sdl', false), "\n";
$ru = new ReflectionClass('Soap\\Url');
$rs = new ReflectionClass('Soap\\Sdl');
echo 'url_final=', (int) $ru->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
2 changes: 2 additions & 0 deletions test/repro/extension_loaded_soap_phantom.php
Original file line number Diff line number Diff line change
Expand Up @@ -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";
32 changes: 32 additions & 0 deletions test/repro/maintainer_gap_soap_url_sdl_84.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php
// repro #23230 — Soap\Url / Soap\Sdl on PROFILE=8.4+ (ext/soap/soap.stub.php)
declare(strict_types=1);

if (!class_exists('SoapClient', false)) {
fwrite(STDERR, "SoapClient missing (need PROFILE>=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";
1 change: 1 addition & 0 deletions test/selfhost/compiler_lib_spine_smoke/main.php
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
39 changes: 39 additions & 0 deletions test/unit/SoapExtensionPolicyTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
}
}
Loading