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']);