Strauss: prefix bundled Composer dependencies
WordPress loads every active plugin into one PHP process, and for any given class name, whichever plugin's Composer autoloader registers first wins. If your plugin bundles Guzzle 7 and another plugin bundles Guzzle 6, one of you silently runs the other's version — best case a subtle bug, worst case a fatal. Strauss solves this by copying your runtime dependencies into vendor-prefixed/ and rewriting their namespaces, classnames, and constants so they are unique to your plugin (GuzzleHttp\Client becomes My_Plugin\GuzzleHttp\Client). This skill wires Strauss into the Composer lifecycle and the release build. It is about bundling isolation, not autoloading your own plugin code.
When to use this skill
- A distributed plugin ships any Composer runtime dependency (
require, notrequire-dev) — HTTP clients, SDKs, loggers, parsers. - Two plugins fatal or misbehave together with "Cannot redeclare class", wrong-version method signatures, or
Uncaught Error: Call to undefined methodon a vendor class. composer.jsoncontainsextra.straussorextra.mozart(reviewing or extending an existing setup).- Migrating off Mozart (Strauss began as a Mozart fork and reads the Mozart config for a seamless migration).
- Setting up the release/CI build for a plugin whose
vendor/is gitignored.
Install: pinned phar via composer scripts (recommended)
The upstream-recommended install is the phar, downloaded on demand and run from composer scripts — not composer require --dev brianhenryie/strauss. The phar keeps Strauss's own dependency tree (composer/composer, symfony/console, nikic/php-parser, monolog, …) out of your dev autoloader, where it could conflict with your own dev tools.
Create bin/ (with a .gitkeep), add bin/strauss.phar to .gitignore, then:
"scripts": {
"prefix-namespaces": [
"sh -c 'test -f ./bin/strauss.phar || curl -o bin/strauss.phar -L -C - https://github.com/BrianHenryIE/strauss/releases/latest/download/strauss.phar'",
"@php bin/strauss.phar",
"@composer dump-autoload"
],
"post-install-cmd": [
"@prefix-namespaces"
],
"post-update-cmd": [
"@prefix-namespaces"
],
"post-autoload-dump": [
"@php bin/strauss.phar include-autoloader"
]
}
Every composer install / composer update now regenerates the prefixed tree automatically — nobody has to remember a manual step.
For reproducible CI builds, pin a release instead of latest:
https://github.com/BrianHenryIE/strauss/releases/download/0.28.1/strauss.phar
Strauss follows WordPress's PHP floor ("this project will not increase its minimum required PHP version ahead of WordPress"); it runs on PHP 7.4+.
Configure (composer.json → extra.strauss)
Strauss works zero-config — namespace_prefix and classmap_prefix are inferred from your composer.json name/autoload, and packages defaults to everything in require. Set them explicitly anyway so the prefix is deliberate:
"extra": {
"strauss": {
"target_directory": "vendor-prefixed",
"namespace_prefix": "My_Plugin\\Vendor\\",
"classmap_prefix": "My_Plugin_Vendor_",
"constant_prefix": "MYPLUGIN_",
"packages": [],
"update_call_sites": false,
"delete_vendor_packages": false
}
}
Key options and their defaults:
| Option | Default | What it does |
|---|---|---|
target_directory |
vendor-prefixed |
Where prefixed copies land. |
namespace_prefix |
inferred | Prepended to every namespace (GuzzleHttp\ → My_Plugin\Vendor\GuzzleHttp\). |
classmap_prefix |
inferred | Prepended to global (non-namespaced) classes. |
constant_prefix |
empty | Prepended to define()d constants. Empty = constants are not prefixed. |
packages |
all of require |
Which packages to process. Dev tools in require-dev are never touched — that's correct, don't add them. |
update_call_sites |
false |
true / false / array of paths. true rewrites references to the prefixed classes in your own autoload paths; an array (e.g. ["includes", "templates"]) scopes it. CLI: --updateCallSites=includes,templates. |
delete_vendor_packages |
false |
Removes the originals from vendor/ after copying, so the unprefixed classes can't load by accident (and don't ship twice). |
override_autoload |
{} |
Replace a package's broken/missing autoload key so Strauss can process it. |
exclude_from_copy / exclude_from_prefix |
empty | Each accepts packages, namespaces, file_patterns (regex). Use exclude_from_prefix.namespaces only for deliberately shared interop namespaces. |
exclude_constants |
empty | Same shape plus a literal constants list. |
namespace_replacement_patterns |
{} |
Regex-based custom renames when a plain prefix isn't enough. |
include_root_autoload |
false |
Makes the Strauss autoloader also load your project's root autoload, so you require only one file. |
Autoloading after prefixing
Strauss generates its own autoloader in the target directory. Two equivalent wirings:
// Option A — require it directly in the main plugin file:
require_once __DIR__ . '/vendor-prefixed/autoload.php';
Option B — the post-autoload-dump script above runs strauss include-autoloader, which appends the include to vendor/autoload.php, so your existing single require vendor/autoload.php keeps working.
With delete_vendor_packages: true, Strauss writes vendor/composer/autoload_aliases.php so the old class names still resolve during development (your test suite, existing references). That file is a dev convenience only — never include it in production code: shipping it re-creates exactly the global-name collision you ran Strauss to avoid.
Your own code must use the prefixed names
Prefixing rewrites the library; something must also rewrite (or hand-write) your references to it:
// WRONG — resolves to whichever plugin's Guzzle registered first
use GuzzleHttp\Client;
// RIGHT — your isolated copy
use My_Plugin\Vendor\GuzzleHttp\Client;
- Either write the prefixed names directly (new code), or set
update_call_sitesand let Strauss rewrite your call sites (existing code). - Grep for string class references afterwards:
class_exists( 'GuzzleHttp\Client' ), class names in config arrays,::class-less factory strings. Static analysis and rewriters see these unreliably — review them by hand. - Objects that cross plugin boundaries don't match prefixed type-hints. If your public API accepts, say, a PSR-3 logger from another plugin,
My_Plugin\Vendor\Psr\Log\LoggerInterfacewill reject the caller's unprefixedPsr\Log\LoggerInterface. That boundary must either use unprefixed interfaces (exclude_from_prefix.namespaces) — accepting the shared-version risk for that one namespace — or not type-hint the vendor interface at all.
Release / CI workflow
vendor/gitignored (typical): gitignorevendor-prefixed/too. CI runscomposer install(the scripts build the prefixed tree), then zips the plugin withvendor-prefixed/— and withvendor/only if the autoloader wiring needs it (Option B) ordelete_vendor_packagestrimmed it to safe remnants.- Test the result before shipping:
php bin/strauss.phar --dry-runpreviews without writing;--info/--debugraise verbosity. - The distributed zip must never contain an unprefixed copy of a runtime dependency — that is the whole point.
delete_vendor_packages: trueis the cleanest guarantee.
Migrating from Mozart
Strauss is a Mozart fork and reads your existing Mozart configuration from composer.json, so the swap is: replace the Mozart package/phar with Strauss, run it, verify the output, then move the config to extra.strauss at leisure. What you gain over Mozart: a single output directory mirroring vendor structure, a generated autoload.php, files autoloader support, constant prefixing, license-respecting file headers, and active maintenance.
PHP-Scoper is the other alternative: it scopes an entire codebase (built for shipping phars) with PHP config files, while Strauss prefixes only the bundled dependencies in place, configured from composer.json — for the WordPress-plugin case, Strauss is the direct fit.
Critical rules
- Any distributed plugin that bundles Composer runtime dependencies must prefix them. Unprefixed
vendor/in a public plugin is a collision waiting for the second plugin that bundles the same library. - Run Strauss from composer scripts (
post-install-cmd/post-update-cmd), not by hand — a manually skipped run ships unprefixed classes. - Pin the phar release in CI for reproducible builds;
latestis fine for local dev. - Wire the autoloader deliberately — either
require vendor-prefixed/autoload.phporstrauss include-autoloaderonpost-autoload-dump. Missing wiring fails only at runtime, on the first vendor class. - Never ship
autoload_aliases.phpor any unprefixed duplicate of a processed package. Preferdelete_vendor_packages: truein release builds. - Prefix only
require(the default): PHPUnit, PHPCS, PHPStan and friends stay unprefixed invendor/. - Audit string class references and plugin-boundary type-hints after prefixing — the two places a mechanical rewrite can't decide for you.
Cross-references
- Run
wp-plugin-bootstrapfor where the autoloader require belongs in the main plugin file and load order. - Run
wp-phpcs-coding-standardsand excludevendor-prefixed/from the ruleset — generated copies of third-party code are not yours to lint. - Run
wp-phpunit-test-setupto slot the prefix-namespaces script into the same composer-scripts QA pipeline and CI.
What this skill does NOT cover
- Autoloading or namespacing your own plugin code (that is plain PSR-4 in
composer.jsonautoload). - Site-internal/bespoke plugins where you control every installed plugin — prefixing is still good hygiene there, but the conflict risk that forces it is a distribution problem.
- PHP-Scoper configuration details.
References
- Strauss repository and README: https://github.com/BrianHenryIE/strauss
- Releases (phar downloads): https://github.com/BrianHenryIE/strauss/releases
- Mozart (predecessor): https://github.com/coenjacobs/mozart
- PHP-Scoper (whole-codebase alternative): https://github.com/humbug/php-scoper