From 2963fb40e6159ff116d220c4e63fec03e8498b60 Mon Sep 17 00:00:00 2001 From: mostafa Date: Wed, 22 Apr 2026 22:22:32 +0330 Subject: [PATCH] Keep polyfill stub classes in the global namespace (v1.2.7) Symfony polyfill packages ship stubs that declare Normalizer, Attribute, JsonException, PhpToken, and UnhandledMatchError in the global namespace so they serve as fallbacks when the corresponding PHP extension/version is missing. ClassmapReplacer was prefixing those stubs alongside other global classes, which broke the fallback and fatalled with "Class X not found" on servers without the native implementation (e.g. no intl ext). Extend the built-in allowlist with the five polyfilled classes plus CompileError, UnitEnum, BackedEnum, SensitiveParameter, and Override so future polyfill stubs don't regress the same way. --- CHANGELOG.md | 7 ++++ composer.json | 2 +- src/Replacer/ClassmapReplacer.php | 9 +++-- tests/Unit/Replacer/ClassmapReplacerTest.php | 37 ++++++++++++++++++++ 4 files changed, 52 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fc07eba..2e50ee7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## 1.2.7 - 2026-04-22 + +### Fixed +- **Polyfill stub classes incorrectly prefixed**: Symfony polyfill packages (`polyfill-intl-normalizer`, `polyfill-php73`, `polyfill-php80`) ship stub files that declare classes in the global namespace so they act as fallbacks when the corresponding PHP extension or version is missing. `ClassmapReplacer` prefixed these stubs alongside other global classes, which broke the fallbacks and caused fatal `Class "X" not found` errors at runtime on servers that lacked the native implementation — most visibly `Normalizer` on hosts without the `intl` extension, but also `Attribute`, `JsonException`, `PhpToken`, and `UnhandledMatchError` on older PHP versions. The built-in allowlist now covers these five classes, plus the related `CompileError`, `UnitEnum`, `BackedEnum`, `SensitiveParameter`, and `Override` globals. + +--- + ## 1.2.6 - 2026-04-18 ### Fixed diff --git a/composer.json b/composer.json index 6b96630..09e67e3 100644 --- a/composer.json +++ b/composer.json @@ -2,7 +2,7 @@ "name": "veronalabs/wp-scoper", "description": "A Composer plugin that prefixes namespaces in WordPress plugin dependencies to prevent conflicts", "type": "composer-plugin", - "version": "1.2.6", + "version": "1.2.7", "license": "MIT", "authors": [ { diff --git a/src/Replacer/ClassmapReplacer.php b/src/Replacer/ClassmapReplacer.php index b7a7936..6b01cfd 100644 --- a/src/Replacer/ClassmapReplacer.php +++ b/src/Replacer/ClassmapReplacer.php @@ -15,15 +15,20 @@ class ClassmapReplacer implements ReplacerInterface /** @var array PHP built-in classes to never prefix */ private static $phpBuiltinClasses = [ 'stdClass', 'Exception', 'ErrorException', 'Error', 'TypeError', 'ValueError', - 'ArithmeticError', 'DivisionByZeroError', 'ParseError', 'Throwable', + 'ArithmeticError', 'DivisionByZeroError', 'ParseError', 'CompileError', + 'Throwable', 'UnhandledMatchError', 'RuntimeException', 'LogicException', 'InvalidArgumentException', 'BadMethodCallException', 'BadFunctionCallException', 'DomainException', 'LengthException', 'OutOfBoundsException', 'OutOfRangeException', 'OverflowException', 'RangeException', 'UnderflowException', 'UnexpectedValueException', 'Iterator', 'IteratorAggregate', 'ArrayAccess', 'Serializable', - 'Countable', 'Traversable', 'JsonSerializable', 'Stringable', + 'Countable', 'Traversable', 'JsonSerializable', 'JsonException', 'Stringable', + 'UnitEnum', 'BackedEnum', 'Generator', 'Closure', 'Fiber', + 'Attribute', 'SensitiveParameter', 'Override', + 'PhpToken', + 'Normalizer', 'DateTime', 'DateTimeImmutable', 'DateTimeInterface', 'DateTimeZone', 'DateInterval', 'DatePeriod', 'SplFileInfo', 'SplFileObject', 'SplTempFileObject', diff --git a/tests/Unit/Replacer/ClassmapReplacerTest.php b/tests/Unit/Replacer/ClassmapReplacerTest.php index a4ef4d0..098aa42 100644 --- a/tests/Unit/Replacer/ClassmapReplacerTest.php +++ b/tests/Unit/Replacer/ClassmapReplacerTest.php @@ -91,6 +91,43 @@ public function testDoesNotPrefixPhpBuiltins(): void $this->assertStringNotContainsString('WPS_stdClass', $result); } + /** + * @dataProvider polyfillStubClassProvider + */ + public function testDoesNotPrefixPolyfillStubClasses(string $className): void + { + // Symfony polyfill-* packages ship stubs that declare these classes in + // the global namespace so they act as a fallback when the corresponding + // PHP extension/version is missing. Prefixing the stub class breaks the + // fallback and causes "Class X not found" fatals on servers that lack + // the native implementation (e.g. PHP without the intl extension). + $replacer = new ClassmapReplacer('WPS_', [$className]); + $stub = "assertStringNotContainsString( + 'WPS_' . $className, + $replacer->replace($stub), + "Polyfill stub declaration for {$className} must stay in the global namespace" + ); + $this->assertStringNotContainsString( + 'WPS_' . $className, + $replacer->replace($usage), + "References to global polyfilled {$className} must not be prefixed" + ); + } + + public function polyfillStubClassProvider(): array + { + return [ + 'Attribute (PHP 8.0)' => ['Attribute'], + 'JsonException (PHP 7.3)' => ['JsonException'], + 'Normalizer (intl extension)' => ['Normalizer'], + 'PhpToken (PHP 8.0)' => ['PhpToken'], + 'UnhandledMatchError (PHP 8.0)' => ['UnhandledMatchError'], + ]; + } + public function testDoesNotDoublePrefixClass(): void { $replacer = new ClassmapReplacer('WPS_', ['MyGlobalClass']);