Fix #1073: resolve Strauss-prefixed classes on a Composer dependency install - #1261
Fix #1073: resolve Strauss-prefixed classes on a Composer dependency install#1261Miraeld wants to merge 2 commits into
Conversation
…install Imagify's dependencies are prefixed into `Imagify\Dependencies\` (and `Imagify_` for global classes) by Strauss, which runs from this package's own Composer install scripts. Composer does not run a dependency's scripts, so when Imagify is installed as a dependency - Bedrock, for instance - Strauss never runs and none of those prefixed symbols exist. Activation then dies with `Class "Imagify\Dependencies\League\Container\Container" not found`. The unprefixed originals are present, because every prefixed package is declared in `require` and lands in the root project's vendor directory. So register a fallback autoloader that aliases each prefixed symbol to its original on demand. Two mapping rules, deliberately asymmetric: - The `Imagify\Dependencies\` namespace is exclusively Strauss output, so it is mapped generically. Adding a package to the Strauss config later needs no change here. - Global classes use an explicit allowlist. Imagify prefixes its own global classes with `Imagify_` too (`Imagify_Settings`, `Imagify_WP_Retina_2x`, ...), and stripping that blindly would let an unrelated third-party class be aliased in their place - `Imagify_WP_Retina_2x` resolving to some other plugin's `WP_Retina_2x` would be worse than the fatal it replaced. The autoloader is registered after Composer's, and only aliases when the original genuinely exists, so a normal install never reaches it. Reproduced and verified with a real dependency-mode install (path repository, plugin source with no vendor/ so Strauss cannot run): all 6 prefixed symbols the bootstrap needs were missing before, all 6 resolve after, and Imagify's own `Imagify_`-prefixed classes are confirmed not aliased. Note this is why aliasing only the three League\Container classes is not enough: `wp-media/plugin-family` and `wp-media/wp-mixpanel` are prefixed too and are used unprefixed across the service-provider chain, so the same fatal would resurface a few lines later.
Up to standards ✅🟢 Issues
|
| Metric | Results |
|---|---|
| Complexity | 10 |
| Duplication | 0 |
🟢 Coverage 91.30% diff coverage
Metric Results Coverage variation Report missing for 07216be1 Diff coverage ✅ 91.30% diff coverage (50.00%) Coverage variation details
Coverable lines Covered lines Coverage Common ancestor commit (07216be) Report Missing Report Missing Report Missing Head commit (e76f3be) 20504 1640 8.00% Coverage variation is the difference between the coverage for the head and common ancestor commits of the pull request branch:
<coverage of head commit> - <coverage of common ancestor commit>Diff coverage details
Coverable lines Covered lines Diff coverage Pull request (#1261) 23 21 91.30% Diff coverage is the percentage of lines that are covered by tests out of the coverable lines that the pull request added or modified:
<covered lines added or modified>/<coverable lines added or modified> * 100%1 Codacy didn't receive coverage data for the commit, or there was an error processing the received data. Check your integration for errors and validate that your coverage setup is correct.
NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.
The mapping function was covered, but the spl_autoload_register() closure that actually performs the aliasing was not. Adds four tests over the live autoloader: a prefixed class resolves to the real unprefixed implementation, a prefixed interface resolves, a prefixed name with no original stays unresolved rather than erroring, and a non-allowlisted Imagify_ name is not aliased onto a same-named third-party class even when one exists.
Description
Fixes #1073
Installing Imagify with Composer (Bedrock and similar) and activating it killed the site with
Class "Imagify\Dependencies\League\Container\Container" not found. It now activates and runs.Supersedes #1074 by @faisalahammad, who identified the cause correctly. Opened separately because that PR's branch is on a fork with
maintainerCanModify: false, and because the fix needed to cover more than the threeLeague\Containerclasses - see the last section.Type of change
Detailed scenario
What was tested
Reproduced the real scenario, since CI cannot: a throwaway root project with a path repository pointing at the plugin source with no
vendor/, so Strauss cannot run - exactly a dependency-mode install.Imagify\Dependencies\League\Container\ContainerImagify\Dependencies\League\Container\ServiceProvider\AbstractServiceProviderImagify\Dependencies\WPMedia\PluginFamily\Controller\PluginFamilyImagify\Dependencies\WPMedia\Mixpanel\OptinImagify\Dependencies\WPMedia\Mixpanel\TrackingPluginImagify_WP_Background_Process6/6 missing before, 0/6 after. The same probe asserts the over-reach guard:
Imagify_WP_Retina_2x,Imagify_WP_Time_CapsuleandImagify_Settingsare confirmed not aliased.Automated - 4 unit tests on the mapping function, including the negative case that matters: Imagify's own
Imagify_-prefixed global classes must not be unprefixed.Suites: 532 unit / 1545 assertions, 148 integration / 391 assertions, 0 failures. PHPCS and PHPStan clean.
How to test
Then load
vendor/autoload.php, require the plugin'sinc/functions/dependencies.php, callimagify_register_dependencies_fallback_autoloader(), and checkclass_exists( 'Imagify\Dependencies\League\Container\Container' ). It is false without the call and true with it.Or, end to end: install into a real Bedrock site with
composer requireand activate the plugin.Regression check on a normal install:
composer installin the plugin directory (Strauss runs), then activate. Everything must behave exactly as before - the fallback autoloader is registered last and only fires for symbols nothing else resolved.Affected Features & Quality Assurance Scope
inc/main.php) - the fallback is registered right after Composer's autoloader.composer installin the plugin directory) never reaches the new code path.Technical description
Documentation
composer.json'sextra.straussprefixes four packages intoImagify\Dependencies\, withclassmap_prefix: "Imagify_"for global classes. That runs frompost-install-cmd/post-update-cmd- this package's scripts. Composer never runs a dependency's scripts, so in dependency mode the prefixing simply does not happen, whileinc/main.phpandclasses/Plugin.phpstillusethe prefixed names.The originals are available, since all four packages are in
requireand install into the root project's vendor directory. So the fix maps prefixed to unprefixed lazily:The two mapping rules are deliberately asymmetric:
Imagify\Dependencies\is exclusively Strauss output, so stripping the prefix is always correct - and a package added to the Strauss config later needs no change here.Imagify_WP_Async_Request,Imagify_WP_Background_Process). This is the important asymmetry. Imagify uses theImagify_prefix for its own global classes as well, and blindly stripping it would makeImagify_WP_Retina_2xresolve to another plugin'sWP_Retina_2x. Silently binding to an unrelated class is worse than the fatal we are fixing, so this side stays a list.Registered after Composer's autoloader and gated on the original existing, so normal installs are untouched.
New dependencies
None.
Risks
league/containerthe root project resolved, rather than a prefixed copy it controls. That is inherent to installing this way - the alternative is the current fatal - and Composer still honours the^4.2constraint fromrequire.composer install, where Strauss runs normally, so CI green says nothing about this bug either way. That is why the reproduction above was done by hand. A dependency-mode CI job would be the proper long-term guard and is worth a follow-up.Why the three-class version is not enough
Strauss prefixes four packages, not one.
wp-media/plugin-familyandwp-media/wp-mixpanelare used unprefixed inclasses/Admin/ServiceProvider.php,classes/Admin/PluginFamilySubscriber.php,classes/Tracking/ServiceProvider.php,classes/Tracking/BaseTracking.php,classes/Tracking/Notices.phpandinc/classes/class-imagify-views.php- all wired intoPlugin::init(). Aliasing onlyLeague\Containerclears the fatal in the issue and then hits the identical one onImagify\Dependencies\WPMedia\PluginFamily\Controller\PluginFamilymoments later. The "before" table above shows all six symbols missing, which is why the generic namespace mapping is the right shape.Mandatory Checklist
Code validation
Code style
Unticked items justification
N/A.
Additional Checks