Skip to content

Fix #1073: resolve Strauss-prefixed classes on a Composer dependency install - #1261

Open
Miraeld wants to merge 2 commits into
developfrom
fix/1073-composer-dependency-install
Open

Fix #1073: resolve Strauss-prefixed classes on a Composer dependency install#1261
Miraeld wants to merge 2 commits into
developfrom
fix/1073-composer-dependency-install

Conversation

@Miraeld

@Miraeld Miraeld commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

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 three League\Container classes - see the last section.

Type of change

  • New feature (non-breaking change which adds functionality).
  • Bug fix (non-breaking change which fixes an issue).
  • Enhancement (non-breaking change which improves an existing functionality).
  • Breaking change (fix or feature that would cause existing functionality to not work as before).
  • Sub-task of #(issue number)
  • Chore
  • Release

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.

Symbol the bootstrap needs Before After
Imagify\Dependencies\League\Container\Container MISSING OK
Imagify\Dependencies\League\Container\ServiceProvider\AbstractServiceProvider MISSING OK
Imagify\Dependencies\WPMedia\PluginFamily\Controller\PluginFamily MISSING OK
Imagify\Dependencies\WPMedia\Mixpanel\Optin MISSING OK
Imagify\Dependencies\WPMedia\Mixpanel\TrackingPlugin MISSING OK
Imagify_WP_Background_Process MISSING OK

6/6 missing before, 0/6 after. The same probe asserts the over-reach guard: Imagify_WP_Retina_2x, Imagify_WP_Time_Capsule and Imagify_Settings are 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

mkdir -p /tmp/dep/root && cd /tmp/dep/root
cat > composer.json <<'JSON'
{
  "repositories": [ { "type": "path", "url": "../pkg", "options": { "symlink": false } } ],
  "require": { "wp-media/imagify-plugin": "*" },
  "config": { "allow-plugins": true },
  "minimum-stability": "dev", "prefer-stable": true
}
JSON
# ../pkg = a copy of this branch WITHOUT vendor/, so Strauss cannot run.
composer install

Then load vendor/autoload.php, require the plugin's inc/functions/dependencies.php, call imagify_register_dependencies_fallback_autoloader(), and check class_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 require and activate the plugin.

Regression check on a normal install: composer install in 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

  • Plugin bootstrap (inc/main.php) - the fallback is registered right after Composer's autoloader.
  • Composer-based installs only. A normal install (zip from WordPress.org, or composer install in the plugin directory) never reaches the new code path.
  • No behaviour change to any feature, setting, or endpoint.

Technical description

Documentation

composer.json's extra.strauss prefixes four packages into Imagify\Dependencies\, with classmap_prefix: "Imagify_" for global classes. That runs from post-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, while inc/main.php and classes/Plugin.php still use the prefixed names.

The originals are available, since all four packages are in require and install into the root project's vendor directory. So the fix maps prefixed to unprefixed lazily:

spl_autoload_register( function ( $class_name ) {
    $original = imagify_unprefix_dependency_class( $class_name );
    // ... alias only when the original really exists
} );

The two mapping rules are deliberately asymmetric:

  • Namespaces: generic. 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.
  • Global classes: explicit allowlist (Imagify_WP_Async_Request, Imagify_WP_Background_Process). This is the important asymmetry. Imagify uses the Imagify_ prefix for its own global classes as well, and blindly stripping it would make Imagify_WP_Retina_2x resolve to another plugin's WP_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

  • Version skew. In dependency mode Imagify now binds to whatever league/container the 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.2 constraint from require.
  • Aliasing the wrong class. Mitigated by the allowlist above and covered by a unit test plus the live probe.
  • CI does not cover this scenario. Every workflow job runs a root-package 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-family and wp-media/wp-mixpanel are used unprefixed in classes/Admin/ServiceProvider.php, classes/Admin/PluginFamilySubscriber.php, classes/Tracking/ServiceProvider.php, classes/Tracking/BaseTracking.php, classes/Tracking/Notices.php and inc/classes/class-imagify-views.php - all wired into Plugin::init(). Aliasing only League\Container clears the fatal in the issue and then hits the identical one on Imagify\Dependencies\WPMedia\PluginFamily\Controller\PluginFamily moments 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

  • I validated all the Acceptance Criteria. If possible, provide screenshots or videos.
  • I triggered all changed lines of code at least once without new errors/warnings/notices.
  • I implemented built-in tests to cover the new/changed code.

Code style

  • I wrote a self-explanatory code about what it does.
  • I protected entry points against unexpected inputs.
  • I did not introduce unnecessary complexity.
  • Output messages (errors, notices, logs) are explicit enough for users to understand the issue and are actionnable.

Unticked items justification

N/A.

Additional Checks

  • In the case of complex code, I wrote comments to explain it.
  • When possible, I prepared ways to observe the implemented system (logs, data, etc.)
  • I added error handling logic when using functions that could throw errors (HTTP/API request, filesystem, etc.)

…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.
@codacy-production

codacy-production Bot commented Aug 25, 2026

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 10 complexity · 0 duplication

Metric Results
Complexity 10
Duplication 0

View in Codacy

🟢 Coverage 91.30% diff coverage

Metric Results
Coverage variation Report missing for 07216be1
Diff coverage 91.30% diff coverage (50.00%)

View coverage diff in Codacy

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.
@Miraeld
Miraeld requested a review from remyperona August 25, 2026 10:44
@Miraeld Miraeld self-assigned this Aug 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Installation via Composer causes errors after update to 2.2.8

2 participants