Skip to content

fix: plugin classes fail to load when the Composer classmap is stale - #2956

Open
lucadobrescu wants to merge 2 commits into
fix/2929-dynamic-content-globalsfrom
fix/2954-autoload-missing-class
Open

fix: plugin classes fail to load when the Composer classmap is stale#2956
lucadobrescu wants to merge 2 commits into
fix/2929-dynamic-content-globalsfrom
fix/2954-autoload-missing-class

Conversation

@lucadobrescu

@lucadobrescu lucadobrescu commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Closes #2954.

Summary

Otter initializes a fixed list of classes on init and calls new $classname() on each one. A site on 3.2.0 hit Class "...Atomic_Wind_Blocks" not found at inc/class-main.php:97, so every frontend request ended in a fatal error.

The released packages are not the cause. Every wp.org zip since 3.1.6 — the first release with this class — ships inc/plugins/class-atomic-wind-blocks.php and its entry in both generated maps, vendor/composer/autoload_classmap.php and vendor/composer/autoload_static.php. The class reaches runtime only through those generated maps, so the class becomes unloadable when the map on the site stops matching the files on disk: an interrupted or partially applied plugin update, or an OPcache entry compiled from the previous version.

This PR fixes the loading first, and keeps a guard for the case where the file is truly absent.

  • Fallback loader (inc/class-autoloader.php) — resolves ThemeIsle\GutenbergBlocks\* from the file name, following the plugin's class-kebab-case.php convention. otter-blocks.php appends it to the SPL stack after Composer, so Composer still answers first and the fallback runs only when Composer has no answer. A stale map costs nothing: the class loads and its feature keeps working.

  • Main::autoload_classes() — checks class_exists() before instantiating, and skips non-string entries. This covers what no loader can fix: a file that is missing from disk, or a third-party filter that adds a class it never ships. The request then loses one feature instead of the whole site.

Note

The fallback covers every class in the autoload list, including the 17 that otter-pro injects through the otter_blocks_autoloader filter, which carried the same fatal risk. It does not cover 15 files in inc/ whose names diverge from the convention (inc/Tracker.php, inc/server/interface-ai-backend.php, inc/integrations/providers/class-mailchimp.php, and similar). Those stay classmap-only. A unit test asserts the list stays inside the convention, so this cannot drift unnoticed.

Class loading on init

flowchart LR
    A[init, priority 9] --> B[otter_blocks_autoloader<br/>filter: Pro + 3rd party]
    B --> C{Composer<br/>classmap hit?}
    C -- Yes --> F[Instantiate,<br/>call instance]
    C -- No --> D{New: file exists<br/>for the class name?}:::added
    D -- Yes --> F
    D -- No --> E[New: skip entry,<br/>continue list]:::added
    E --> G[Request renders]
    F --> G

    classDef added fill:#1a7f37,color:#fff,stroke:#116329,stroke-width:3px
Loading

Test instructions

Steps 1 and 2 create the state a stale map puts a site in.

  1. Check out this branch and run composer install. Open vendor/composer/autoload_classmap.php and delete the line for Atomic_Wind_Blocks. Open vendor/composer/autoload_static.php and delete the same line. Composer reads the static map first, so both files must lose the entry.

    Expect: neither file contains Atomic_Wind_Blocks.

  2. Open wp-config.php. Set WP_DEBUG to true, set WP_DEBUG_LOG to true, and set WP_DEBUG_DISPLAY to false. Delete wp-content/debug.log if the file exists.

    Expect: no log file is present.

  3. Open the site home page in a browser.

    Expect: the page renders with HTTP 200, and debug.log holds no Class "...Atomic_Wind_Blocks" not found entry. On development the same request returns HTTP 500 and writes that fatal at inc/class-main.php:97.

  4. Go to WP Admin → Otter → Dashboard → Modules and turn on Enable Atomic Wind Blocks. Then run:

    wp eval 'var_export( WP_Block_Type_Registry::get_instance()->is_registered( "atomic-wind/box" ) );'

    Expect: true. The module loads through the fallback loader while the map stays stale.

  5. Run composer dump-autoload to rebuild the map. Reload the home page.

    Expect: the page renders with HTTP 200.


Checklist before the final review

  • Included E2E or unit tests for the changes in this PR.
  • Visual elements are not affected by independent changes.
  • It is at least compatible with the minimum WordPress version.
  • It loads additional script in frontend only if it is required.
  • Does not impact the Core Web Vitals.
  • In case of deprecation, old blocks are safely migrated.
  • It is usable in Widgets and FSE.
  • Copy/Paste is working if the attributes are modified.
  • PR is following the best practices

@pirate-bot pirate-bot added the pr-checklist-complete The Pull Request checklist is complete. (automatic label) label Aug 3, 2026
@lucadobrescu
lucadobrescu requested a review from Copilot August 3, 2026 08:06
@pirate-bot

Copy link
Copy Markdown
Contributor

Bundle Size Diff

Package Old Size New Size Diff
Animations 178.33 KB 178.33 KB 0 B (0.00%)
Blocks 1.65 MB 1.65 MB 0 B (0.00%)
CSS 7.83 KB 7.83 KB 0 B (0.00%)
Dashboard 172.49 KB 172.49 KB 0 B (0.00%)
Onboarding 68.14 KB 68.14 KB 0 B (0.00%)
Export Import 4.73 KB 4.73 KB 0 B (0.00%)
Pro 439.82 KB 439.82 KB 0 B (0.00%)

@pirate-bot

pirate-bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Plugin build for fb75ec3 is ready 🛎️!

@lucadobrescu lucadobrescu self-assigned this Aug 3, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Prevents frontend fatal errors when an autoload-list class is unavailable.

Changes:

  • Guards class instantiation with type and existence checks.
  • Adds PHPUnit coverage for invalid and unavailable entries.
  • Adds serialized E2E coverage for frontend and admin resilience.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated no comments.

Show a summary per file
File Description
inc/class-main.php Skips unloadable autoload entries.
tests/test-main-autoload.php Tests autoload resilience and bundled classes.
packages/e2e-tests/mu-plugins/otter-e2e-bootstrap.php Adds the broken-autoloader scenario flag.
src/blocks/test/e2e/blocks/autoloader-resilience.spec.js Verifies frontend and admin requests survive.
src/blocks/test/e2e/playwright.config.js Serializes the stateful resilience spec.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@Soare-Robert-Daniel

Copy link
Copy Markdown
Contributor

@lucadobrescu let's research if we can fix the loading. Skipping the check is more like a compromise for a broken pipeline.

@pirate-bot

pirate-bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

E2E Tests

Playwright Test Status: See serial and parallel matrix jobs

Performance Results serverResponse: {"q25":310.2,"q50":337.65,"q75":345.2,"cnt":10}, firstPaint: {"q25":1158.1,"q50":1298.9,"q75":1486.7,"cnt":10}, domContentLoaded: {"q25":3004.2,"q50":3058.4,"q75":3115.3,"cnt":10}, loaded: {"q25":3006.1,"q50":3060.1,"q75":3117,"cnt":10}, firstContentfulPaint: {"q25":3414,"q50":3459.5,"q75":3534.7,"cnt":10}, firstBlock: {"q25":12252.6,"q50":12325.85,"q75":12457.3,"cnt":10}, type: {"q25":22.39,"q50":23.59,"q75":27.02,"cnt":10}, typeWithoutInspector: {"q25":20.54,"q50":21.11,"q75":22.66,"cnt":10}, typeWithTopToolbar: {"q25":29.25,"q50":30.33,"q75":31.34,"cnt":10}, typeContainer: {"q25":15.29,"q50":15.88,"q75":17.88,"cnt":10}, focus: {"q25":123.84,"q50":125.26,"q75":128.57,"cnt":10}, inserterOpen: {"q25":40.03,"q50":40.98,"q75":41.49,"cnt":10}, inserterSearch: {"q25":15.34,"q50":16.4,"q75":17.47,"cnt":10}, inserterHover: {"q25":6.42,"q50":6.9,"q75":7.39,"cnt":20}, loadPatterns: {"q25":1556.22,"q50":1585.17,"q75":1663.37,"cnt":10}, listViewOpen: {"q25":224.78,"q50":229.7,"q75":236.99,"cnt":10}

@lucadobrescu lucadobrescu changed the title fix: frontend fatal when a class in the autoload list cannot be loaded fix: plugin classes fail to load when the Composer classmap is stale Aug 3, 2026
@lucadobrescu
lucadobrescu changed the base branch from development to fix/2929-dynamic-content-globals August 3, 2026 09:13
Luca Dobrescu and others added 2 commits August 3, 2026 12:19
Main::autoload_classes() instantiated every entry of its class list without
checking that the class is loadable. On a package with a stale Composer
classmap the Atomic Wind entry resolved to nothing and `new $classname()`
threw an uncaught Error on `init`, taking down every request (#2954).

Skip entries that are not loadable so a packaging or third-party filter
problem degrades to a missing feature instead of a site-wide fatal.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…tale

Composer exposes `inc/` through a generated classmap in `vendor/`, so a map that
does not match the files on disk — an interrupted plugin update, an OPcache entry
compiled from the previous version — makes a class that is present unloadable.

Register a fallback loader for `ThemeIsle\GutenbergBlocks\*` that resolves a class
from its file name. It is appended to the SPL stack, so Composer still answers
first and the fallback only runs when Composer has no answer. The class then
loads and its feature keeps working, instead of being skipped by the guard in
Main::autoload_classes().

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@lucadobrescu
lucadobrescu force-pushed the fix/2954-autoload-missing-class branch from 286494b to fb75ec3 Compare August 3, 2026 09:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pr-checklist-complete The Pull Request checklist is complete. (automatic label)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants