[6.x] Reduce class autoloading during boot - #15381
Conversation
`ExtensionServiceProvider` was calling `::register()` on every core action, dictionary, fieldtype, scope, tag, widget, js driver and update script, which autoloads each class (and its parents and traits) just to read its handle. keying the arrays by handle lets the bindings be written straight into `statamic.extensions`, so around 200 fewer files are loaded on every boot. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ly4xenxkL1AcEdtf47qWjy
`Augmentor` already accepts closures, so wrapping the core extensions defers loading tiptap until something is actually augmented. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ly4xenxkL1AcEdtf47qWjy
`is_subclass_of` autoloads every resource class on boot, and the defaults are our own classes. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ly4xenxkL1AcEdtf47qWjy
laravel instantiates event subscribers on boot, so injecting `PresetGenerator` into their constructors pulled the whole glide server chain in with them. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ly4xenxkL1AcEdtf47qWjy
# Conflicts: # src/Providers/ExtensionServiceProvider.php
jasonvarga
left a comment
There was a problem hiding this comment.
Warning — the test file this PR edits is never executed by CI
tests/Assets/GeneratePresetImageManipulationsOnUpload.php
phpunit.dist.xml uses ./tests. This file has no Test suffix (class GeneratePresetImageManipulationsOnUpload), so it has never run in CI. That matters here because it is the only coverage for the GeneratePresetImageManipulations change, and the PR modifies it specifically to accommodate that change — so green CI gives no assurance the listener refactor works.
The naming is pre-existing, but this is the moment to fix it: I renamed the file and class in a scratch worktree at the PR's head and ran it — 7/7 pass. It is a free, verified fix.
Suggested fix: rename the file to GeneratePresetImageManipulationsOnUploadTest.php and the class to match.
(For reference, on 6.x the same file errors when invoked by path — Too few arguments to ...presets_are_generated_for_images() — because PHPUnit\Framework\Attributes\DataProvider was never imported. The PR adds that import, which is why it now passes.)
Warning — hardcoded handles duplicate handle() with nothing guarding the two in sync
src/Providers/ExtensionServiceProvider.php:29-260
The handle for every core extension now lives in two places: the array key here, and handle() / aliases() on the class. They agree today (verified exhaustively above), but nothing keeps them agreeing. There are no tests touching statamic.extensions or ExtensionServiceProvider at all.
Drift is silent and the failure modes are unpleasant:
Adding a new core fieldtype without a key (Fieldtypes\Foo::class on its own line) registers it under integer key 0 — and would clobber another 0 entry.
Renaming a class, or adding a protected static $handle, changes handle() while the registry key stays put. Lookups go through the registry so the fieldtype still resolves, but handle() is independently used at runtime for the Vue component name (Fieldtype::component()), the title translation key (statamic::fieldtypes..title), the docs URL, and FieldtypeRepository::makeSelectableInForms(self::handle()). The result is a fieldtype that half-works.
Suggested fix: add a test that walks each array and asserts the keys equal $class::handle() plus $class::aliases(). Cheap now, and it's the guard that makes the whole optimisation safe to maintain.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KyzRo7SQepA8mJeVoXKnYW
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KyzRo7SQepA8mJeVoXKnYW
|
Thanks for the review. Both addressed:
|
This pull request reduces the number of classes autoloaded while booting the app.
ExtensionServiceProviderwas calling::register()on every core action, dictionary, fieldtype, scope, tag, widget, JS driver and update script so each one could announce its handle. Reading the handle means autoloading the class, along with its parents and traits, so around 200 files were being loaded on every boot before anything had asked for a fieldtype or tag.The same pattern was repeated on a smaller scale elsewhere: the API resources were validated with
is_subclass_of, the Bard provider instantiated every tiptap extension up front, and the two Glide listeners injectedPresetGeneratorinto their constructors, which Laravel resolves when it instantiates subscribers and pulls the whole Glide server chain in with it.This PR fixes it by:
statamic.extensions.::register()is still the API for app and addon extensions, core just skips asking itself questions it already knows the answers to.Augmentoralready supports.PresetGeneratorinside the Glide listeners' handlers instead of their constructors.Measured on an M3 with opcache off, which is what tests and Artisan see:
The saving is class loading, so it lands on cold boots: the first test in a PHPUnit process (per worker when running in parallel), Artisan commands, queue workers and any request without opcache. Warm boots with everything already in memory are unchanged. The reporter's numbers came from a slow filesystem, where the same file count costs proportionally more.
References #10753