11: Loader boot and load path (merge after #9 and #10) - #11
Open
nikolaystrikhar wants to merge 13 commits into
Open
11: Loader boot and load path (merge after #9 and #10)#11nikolaystrikhar wants to merge 13 commits into
nikolaystrikhar wants to merge 13 commits into
Conversation
This was referenced Aug 3, 2026
nikolaystrikhar
force-pushed
the
10-notices-queue
branch
from
August 6, 2026 14:22
2f976f3 to
a8e8860
Compare
nikolaystrikhar
force-pushed
the
11-loader-load-path
branch
from
August 6, 2026 14:22
7d4435a to
4d1c627
Compare
resolve() returned object and cached whatever the container handed back, so a mis-bound interface was stored and then failed on the accessor's return type -- a TypeError blaming this library rather than the binding. reset() threw the same one, so there was no way back out of it. It throws Config_Exception at the boundary now, before the memo. The per-accessor @var annotations are gone: verified against the pinned PHPStan 1.12 that an object return narrowing to an interface is unreported at level 5 and 6 either way, so they silenced nothing. resolve() carries @template T instead. The README pointed hosts at Loader::reset() to pick up a container set late, but reset() also drops every registration -- following that advice left nothing to load, silently. It now states the real rule: set the container before the first register() call.
The memo map holds a different type per key, so it cannot be typed as T as a whole and the cache hit returned a bare object. Required by PHPStan level 8.
… spy Config lost its reset() when the state-clearing moved to a tests-only helper, so the suite reaches for Config_State::reset() instead. The registrar double becomes a named class. Reading a counter off a value typed as Registrar_Interface is reading a property the interface does not declare, which static analysis rejects; the named type carries its own. Providers become static Generators, and the container-resolution failure asserts Config_Exception rather than the base class it extends.
…path file_exists() was the gate before require_once, but it is true for a directory and for a file with no read permission, and require_once fatals on both. A deploy that loses a read bit would have taken the site down from the one function whose contract is that it never fatals. is_file() and is_readable() now. is_already_loaded() moves ahead of are_dependencies_met(). The dependency check calls an arbitrary host callable and was running first, so with the standalone active a host whose check happened to fail would queue "requirements are not met" for a plugin the admin can watch running. The defined() check is the cheapest gate and the one carrying the whole re-declaration guarantee. A missing bundled file reports through _doing_it_wrong() rather than the notice queue. Queueing a dependency notice renders the host's own dependency_notice_message, so a broken build would have told the site owner to upgrade a dependency that was fine. WordPress draws the same line. boot() detects that plugins_loaded has already passed priority 2 and loads inline. Adding an action at a passed priority is accepted and never fires, so a host who drops the ', 0' from the documented bootstrap got a site where nothing loaded at all, with no warning. load_all() and render_notices() report a missing hook prefix instead of throwing out of a core action, where it would have been a total outage over a bootstrap mistake. Tests: the fake-registrar test passed with the instanceof guard deleted, so it now binds a registrar returning mixed junk; adds the require_once dedupe case, the unreadable and directory cases, and the late-boot cases. The boot tests rewind did_action( 'plugins_loaded' ), which the harness has already fired before any test can run.
Booting from plugins_loaded at priority 2 wired the load hook into the priority bucket the dispatch was already iterating, which WP_Hook never reaches, and the exclusive comparison meant nothing was reported either. Nothing loaded and the site looked healthy. Also document that a bundled file included from a method does not get the global scope wp-settings.php gives a plugin.
…cides The two private helpers sat between public methods; they move below them, alongside the rest of the private API. The guard becomes wiring_window_has_closed(). It answers whether there is still a window in which hooks can usefully be added, which is what both the load hook and anything wired alongside it depend on -- a name tied to one priority would have to be renamed the moment a second hook shares the guard. LOAD_PRIORITY documents why the load loop runs where it does without describing hooks that do not exist yet.
reset() now removes the hooks boot() added. It already cleared the booted flag, so without this a reset Loader reported itself unbooted while its callbacks were still attached, and the re-boot test asserted a hook the first boot had left behind rather than one the second had wired. That test splits in two: one for the unwiring, one for the re-boot. Constants come from setConstant(), which is undone when the test ends, rather than define(), which lasts for the process. Config lost its reset() when state-clearing moved to a tests-only helper, so the suite uses Config_State::reset(). The late-boot priorities become a static Generator.
The queue is a site option on every install; on single site that call falls through to the plain option table, so the test helper no longer branches on is_multisite() and no longer has to describe its return as array|false. The two tests asserting an empty queue say so directly, rather than leaning on the sentinel the helper used to return. The README described hook priorities the library does not wire yet.
nikolaystrikhar
force-pushed
the
10-notices-queue
branch
from
August 11, 2026 10:47
a8e8860 to
5bd6fb0
Compare
nikolaystrikhar
force-pushed
the
11-loader-load-path
branch
from
August 11, 2026 10:47
4d1c627 to
fe01f47
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds
Loader::boot(),load_all(), the five gates in front of therequire_once, and theshould_loadfilter.Stacked on #10.
This is the library. Everything before it registers configuration and resolves objects; this is the code that decides whether a bundled plugin actually loads, and the first PR whose failure mode is a white screen rather than a wrong return value.
A sub-plugin is skipped when it is disabled, its guard constant is already defined, its dependencies are unmet, its file is missing or unreadable, or
should_loadreturns false. The filter runs last, so it is only asked about a sub-plugin that would otherwise have loaded — and returningtruecannot force a load past the guard constant., 0is not decoration.boot()wires itself atplugins_loadedpriority 2, and WordPress accepts, then never fires, a callback added at a priority the running dispatch has already reached. A lateboot()used to load nothing at all, silently; now it is reported and the load runs inline.is_already_loaded()runs beforeare_dependencies_met(). Dependencies are an arbitrary host callable, and asking it first would let the library blame dependencies for a plugin the user can plainly see running.Config_Exceptionout of a core action and fatalling the site.is_file()andis_readable()guard therequire_once.file_exists()is true for a directory and for an unreadable file, and both fatal.Documented rather than fixed: the bundled file is included from inside a method, so a top-level
$varin it is function-local instead of global. No wrapper can change that, so the README says so. A parse error in a sub-plugin still fatals, deliberately.