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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
{
Expand Down
9 changes: 7 additions & 2 deletions src/Replacer/ClassmapReplacer.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,20 @@ class ClassmapReplacer implements ReplacerInterface
/** @var array<string> 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',
Expand Down
37 changes: 37 additions & 0 deletions tests/Unit/Replacer/ClassmapReplacerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "<?php\nclass {$className} {}\n";
$usage = "<?php\n\$x = new {$className}();\n{$className}::FOO;";

$this->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']);
Expand Down
Loading