← Back to blog

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.

Projects

CactusPlay - Game shop run by a vetted contractor network

CactusPlay - Game shop run by a vetted contractor network

Before: CIS buyers compared PlayStation and Xbox prices by hand across scattered sellers, and every order had to be filled by the founders themselves. After: a shop with a live catalogue and a CRM any vetted contractor with Turkish payment details could join to take orders and earn on them. Result: 20,000+ positions grabbed hourly, price changes reaching the shop and Yandex.Market untouched, and fulfilment no longer capped by one pair of hands.

Posts

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