Two plugins, one library, one broken site: namespacing bundled dependencies
- Published on
- • 4 mins read•--- views
Every WordPress plugin that uses Composer ships its own vendor/ directory. On a site with twenty plugins, that is twenty autoloaders registered against one PHP process.
For distinct libraries this is merely wasteful. For a library two plugins both depend on, it is a bug, and one whose symptoms appear in somebody else's code.
The mechanics
PHP class names are global. When plugin A's autoloader registers first and loads Vendor\Library\Client from its own bundled copy, plugin B's autoloader never runs for that class: the class already exists, so B silently gets A's version.
If A bundles v2 and B expects v3, B breaks. The failure is a fatal error about a missing method, in plugin B, on a line of code that has been correct for two years. Neither plugin's author can reproduce it, because neither has the other installed.
Captcha libraries hit this constantly. So do HTTP clients, PSR implementations, and anything from the Symfony or Illuminate families, precisely the packages a plugin is most likely to pull in without thinking.
The fixes that do not work
"Just require the same version." You cannot. You do not control the other plugin, its release schedule, or whether it is still maintained.
"Check if the class exists first." class_exists() tells you a class is loaded, not that it is the version you need. Guarding your own definition simply makes you the plugin that silently uses the wrong code.
"Load our vendor directory first." Now you win the race, and the other plugin breaks instead. This is not a fix, it is choosing who suffers.
Rename the dependency at build time
The only durable answer is to stop sharing the name. PHP-Scoper rewrites the namespaces of everything in vendor/ during the build, so your bundled copy becomes YourPrefix\Vendor\Library\Client. Two plugins can now ship two versions of the same library and neither can see the other's.
The configuration is mostly about what not to rename:
$wp_classes = json_decode(file_get_contents(__DIR__ .
'/vendor/sniccowp/php-scoper-wordpress-excludes/generated/exclude-wordpress-classes.json'), true);
$wp_functions = json_decode(file_get_contents(__DIR__ .
'/vendor/sniccowp/php-scoper-wordpress-excludes/generated/exclude-wordpress-functions.json'), true);
$wp_constants = json_decode(file_get_contents(__DIR__ .
'/vendor/sniccowp/php-scoper-wordpress-excludes/generated/exclude-wordpress-constants.json'), true);
WordPress's own symbols must stay untouched. Prefixing add_action produces a plugin that loads and does nothing. The sniccowp/php-scoper-wordpress-excludes package maintains those lists so you do not have to.
Interfaces and contracts usually need excluding too:
$excluded = [
'/^Psr\\\\/',
'/^Symfony\\\\Contracts\\\\/',
'/^Illuminate\\\\Contracts\\\\/',
];
The reasoning: a PSR interface is a shared vocabulary. If you prefix Psr\Log\LoggerInterface, your logger no longer satisfies anyone else's type hint, and interoperability, the entire purpose of the standard, is gone. Prefix implementations; leave the contracts alone.
Costs, honestly
Scoping is not free.
The build gains a step. Development runs unscoped, production runs scoped, and the two are not byte-identical. A bug that only reproduces in the scoped build is a genuinely unpleasant afternoon.
Dynamic class names break. new $className and call_user_func('Vendor\Thing::method') are strings, so the scoper cannot see them, and they need patching or exclusion. Stack traces also get uglier, since every frame carries the prefix.
Weigh those against the alternative, which is a bug you cannot reproduce, reported by a user who will blame your plugin for breaking a different one.
The general rule
If your code will run in a process you do not control, do not rely on having a name to yourself. WordPress plugins are the obvious case, but the same reasoning covers browser extensions sharing a page, libraries loaded into a host application, and anything injected into a runtime somebody else also injects into.
Global names are a shared resource. Bundling a dependency means claiming part of that namespace, and the polite thing to do is also the durable one: claim a part nobody else will want.
Open for contract collaboration
I am available for contract-based collaboration. If you have an interesting project idea, schedule a call via Calendly.
Schedule a 30-min call