Repository navigation
Pass the plugin's identity and keep only contract in the global namespace - #34
Merged
Merged
Conversation
The template locked the framework from before its plugin identity and slug-rooted hooks, and WooCommerce 11.2, while WooCommerce 11.3.0 is the newest stable. The Composer lock moves the framework to its eb1473c split, WooCommerce and its stubs to 11.3.0, PHPUnit to 13.4.7 and phpstorm-stubs to 2026.3; the npm lock takes Playwright 1.65.0 and the other releases past npm's seven-day cooldown, keeping the two transitive packages whose newer versions the cooldown hides. The entry's WooCommerce headers and constraints rise to 11.3 (X ruling 374). The framework move forces these changes: the composition root builds one PluginIdentity, hands it to the logger, the scheduler, the boot boundary, the WooCommerce check, the migration runner and the notices, and defines it in the container; uninstall.php builds its own after the engine's check; the runner derives dws_tpl_revision itself, so the revision constant goes and the footprint names its hook, dws_plugin_template/migration/run; the tests, the safeguard recipe and the architecture document read the framework's hooks under the slug; and the fill-in sweeps that root and refuses it as a leftover. Assisted-by: Claude Code:claude-opus-5-5
The template held no check that every shipped directory carries the silence-is-golden stub, and its fresh-request helper ignored any output before a row's last line, so a warning or notice a request printed went unnoticed. The plugins wrote both first, in the form the template takes (X ruling 371). DirectoryStubsTest and RunsRequestsTrait are copied from the plugins unchanged but for the namespace. The rows whose fixtures provoke a diagnostic, a missing file's warning, the build error's log line or a boot listener's throw, name it through request_provoking(). WooCommerce queries two of its tables on a new network site before installing them, so MultisiteTest first sends that site a request without the plugin, as the plugins' row does (X ruling 385). Assisted-by: Claude Code:claude-opus-5-5
X ruling 373 had the template drop its entry's Tested up to as the plugins did, because Plugin Check refuses the header beside a readme.txt. The template has no readme.txt, so Plugin Check never reaches that check, while the bundled updater reads the header at the release tag for each update offer; without it, WordPress's Updates screen reads "Not tested" for every release (X ruling 407). The entry keeps the header, and an EntryHeadersTest row holds it to the locked WordPress core's major and minor, as the WC tested up to row does for WooCommerce. The wordpress.org recipe's first step deletes the header and its row along with Update URI. Assisted-by: Claude Code:claude-opus-5-5
The framework roots every hook it fires for a plugin in the plugin's slug, which survives a change of owner, while the template's recipes and its AGENTS.md still named the hooks a plugin fires itself under the dws_tpl storage prefix, and its README and the fill-in workflow's inputs gave the prefix as the root of every hook (X ruling 377). The recipes' safeguard filter, its relaxation and the hook-renaming example, and AGENTS.md's hook-name rule, read dws_plugin_template/, which the fill-in sweeps to the new plugin's slug in snake case. The README and the workflow's inputs name the slug as the hook root and the prefix as that of the globals and the stored data. Assisted-by: Claude Code:claude-opus-5-5
The template's safeguards are an include file that registers named global callbacks, which any code can remove by name, and its entry reports an incomplete build through two global helpers, one of which logs the error once an hour, while functions.php catches a composition root that cannot be built (X rulings 393 and 394). PluginBootTest expects the entry to require Safeguards.php before the bootstrap package, and drops the boot-listener row, whose catch goes. PartialDeployTest finds the entry's notice among the admin notices' callbacks and checks, for a missing bootstrap package, blocks manifest, assets build or autoloader and for an include that throws, that nothing boots, that a plugin manager sees the notice and a visitor does not, and that no log line is printed; the log and deactivation rows go. SafeguardRecipeTest loads the recipe's Safeguards class under another name and removes its safeguard by the closure's class. Assisted-by: Claude Code:claude-opus-5-5
A named global safeguard callback can be removed with remove_filter(), which defeats a restriction meant to relax only through its own filter, and the entry's two build-error helpers, the hourly log line and functions.php's catches around the composition root added globals and code for failures the entry already sees (X rulings 393 and 394). Safeguards.php at the plugin root is a final class the entry requires by path before the floor gate and calls once, an empty slot whose recipe adds static-closure safeguards and the protected relaxation helper. The entry wraps the bootstrap, the floor gate, the build check, the autoloader and functions.php in one try, whose catch rethrows in local and development environments and otherwise hooks one static-closure notice. functions.php boots, declares the WooCommerce compatibility and unschedules on deactivation from static closures without catches, leaving the composition's failures to PluginBoot. includes/safeguards.php goes; the safeguards' PHPStan profile, Quality and the fill-in parse Safeguards.php on PHP 7.4 in its place, and the profile no longer ignores wp_get_environment_type(), which the catch guards with function_exists(). Assisted-by: Claude Code:claude-opus-5-5
The template's option keys, defaults, typed readers, translated text and validator, its migration steps and its footprint are prefixed globals in includes/, which any code can call and come to rely on, and its string reader returns the greeting's translated default, mixing presentation into the data read (X rulings 391, 392 and 408). The tests read the keys, defaults and readers from Options, the steps from Migrations::get_steps() and the footprint from Plugin::FOOTPRINT, which FootprintTest checks against Options' constants. A missing greeting reads as an empty string, untranslated, while the account dashboard still prints the translated default. The settings field's placeholder is the greeting's default message, and the validator's rows run through the field's validate callable. Assisted-by: Claude Code:claude-opus-5-5
The option keys, defaults, readers, translated text and validator, the migration steps and the footprint were prefixed globals in includes/, so any code could call them and come to depend on them, and the string reader returned the greeting's translated default, mixing what the greeting shows into the data read (X rulings 391, 392 and 408). Options names the stored settings and their defaults as constants and reads each as its type through generic, untranslated statics. Migrations lists its protected static steps as first-class callables, and the composition root holds the footprint as a constant, which deactivation and uninstall.php read. The greeting's component owns its translated default message, which it prints for an empty stored message and the settings field shows as its placeholder; the validator is a protected static of Settings, handed to its field. The three includes go, and the Unit suite loads the classes through the autoloader. Assisted-by: Claude Code:claude-opus-5-5
The template carried its own registrars for a wp-scripts build, global functions every plugin copied, while core's Assets registers a plugin's built scripts and styles from the same asset files, falls back to the file's time where none exists and swaps in a right-to-left stylesheet only where its file exists (X rulings 395, 398, 399 and 406). The composition root defines Assets over the plugin's identity, and Blocks and the greeting's component take it by constructor. The include and its test go, with their fixture, since core's suite covers Assets. Assisted-by: Claude Code:claude-opus-5-5
Any code can call a global function or read a global constant, so a plugin built from the template comes to support whatever it declares there, while functions.php still declared the accessor without @api (X rulings 384 and 397). GlobalsTest reads functions.php and every file in includes/ and expects each top-level function and constant they declare to carry @api. Assisted-by: Claude Code:claude-opus-5-5
The accessor is the one global through which other code reaches the plugin, yet carried no @api, so nothing told an integration that it may rely on the function while the composition root it returns stays internal (X ruling 397). dws_tpl_plugin() carries @api, and its docblock states that only the function is contract and the root's members stay internal unless tagged. Assisted-by: Claude Code:claude-opus-5-5
The nightly on WordPress trunk timed out clicking the example block's Outlined style, since WordPress 7.2 renders block styles as radios in a Styles radiogroup, while 7.1, the floor CI also tests, renders them as buttons (X ruling 371). The editor spec clicks the Outlined radio, or the Outlined button where none exists, within the Editor settings region. Assisted-by: Claude Code:claude-opus-5-5
The agent guide, the architecture, the scaffold notes, the test guide and every recipe still named the global helpers and constants that moved into Safeguards, Options, Migrations and Plugin::FOOTPRINT, the dws_tpl hook names that moved under the slug, and the framework constructors that now take the plugin's identity, so a plugin following them would rebuild the anatomy the template left. Describe the entry's one catch, the static-closure hooks, the class homes of options, migrations and the footprint, core's Assets and the identity, and port each recipe's code to them. Assisted-by: Claude Code:claude-opus-5-5
Quality rehearsed the fill-in at the longest prefix the script accepted and checked it against PHPCS alone, while the framework's PluginIdentity refuses a prefix longer than 32 characters. No step of the rehearsal built an identity, so a script that accepted more than the framework would seed a plugin that throws when it boots or uninstalls. Expect the plugin's slug and prefix to make an identity the framework accepts, and rehearse at a 28-character segment, the longest the identity leaves after dws_, which the script's cap of 22 still refuses. Assisted-by: Claude Code:claude-opus-5-5
The fill-in capped the prefix's segment at 22 characters, because a longer one pushed the template's longest option constants past PHPCS's alignment. Those names are now class constants built from Plugin::PREFIX, so the bound that holds is the framework's PluginIdentity, which takes a prefix of at most 32 characters. Accept a segment of up to 28 characters, the most that identity leaves after dws_, and give its bound as the reason. Assisted-by: Claude Code:claude-opus-5-5
Another plugin that needs one of the template's settings can only read the stored option by its key, and the template shows no function a plugin would offer for that, although an integration may need one (X rulings 384, 391 and 413). GreetingFeatureTest expects dws_tpl_is_greeting_enabled() to report the stored toggle, on, off and unset, as the greeting's gate reads it. Assisted-by: Claude Code:claude-opus-5-5
The template gave another plugin no way to read one of its settings but the stored option itself, and showed no function of the kind a plugin offers when an integration needs a setting (X rulings 384, 391 and 413). includes/greeting.php declares dws_tpl_is_greeting_enabled(), tagged @api, which reads the toggle through Options, as the greeting's gate does, and adds no filter of its own. Assisted-by: Claude Code:claude-opus-5-5
The recipes showed no way to offer a setting to another plugin, the architecture, the scaffold notes and the agent guide listed only the peer getters in includes/, and the add-on convention named only those getters, so a plugin built from the template would neither find the new function nor know to delete it with the greeting (X ruling 413). A recipe adds one narrowly named, typed @api function per setting an integration needs, delegating to Options; the add-on convention reads a setting through such a function, else through the base plugin's options; the architecture and AGENTS.md's layout name includes/greeting.php, and the scaffold README and the without-WooCommerce recipe delete it with the greeting. Assisted-by: Claude Code:claude-opus-5-5
The option keys and the network option the footprint names were built from Plugin::PREFIX, so no stored name appeared whole in the code and a search for a contract string found nothing, while one edit to the prefix silently renamed every stored key (X ruling 414). Options' keys and the footprint's network option hold their whole names, as do the recipes' option keys and their two meta keys, which become constants on the classes that store them; the footprint's prefixes stay built from the prefix. FootprintTest already fails a key outside the prefix family. Assisted-by: Claude Code:claude-opus-5-5
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.
The template declared its option keys, defaults and readers, its migration steps, its footprint, its asset registrars, its safeguards and its boot callbacks as prefixed globals, which any code could call or remove by name, and handed the framework its entry file, slug and prefix as loose strings. Its own hooks took their root from the storage prefix, while the framework now roots every hook in the slug, and the framework it locked predated both.
Changes
eb1473csplit, with WooCommerce 11.3 as its floor and tested version. The composition root builds onePluginIdentityfromSLUGandPREFIXand passes it to the logger, the scheduler, the boot boundary, the WooCommerce check, the migration runner, the admin notices and the assets;uninstall.phpbuilds its own once the engine is present.functions.phpdeclares the@apiaccessor and registers the boot, deactivation and order-tables hooks as static closures, andincludes/loads only the@apipeer getters. A Unit test expects every global function and constant there to carry@api.Optionsnames the stored settings and their defaults and reads them through generic, untranslated readers;Migrationslists its steps;Plugin::FOOTPRINTholds the footprint. The greeting's component owns its translated default.Safeguards.php, an empty slot the recipe fills with static closures. The entry handles a missing bootstrap package, build tree or required file in onetry, whosecatchrethrows in local and development environments and otherwise shows the build notice.Assetsregisters the built scripts and styles, in place of the template's own registrars.dws_plugin_template/, which the fill-in sweeps.Tested up to, which the bundled updater reads for each offer, held to the locked WordPress by a Unit row.Testing
Composer validation, the changelog check, PHPCS, PHPStan, Unit and Integration, the multisite group, ESLint, Stylelint, the type check, the end-to-end suite on WordPress 7.1.4 and the editor spec on 7.2-beta5, the fill-in rehearsal at the longest prefix and at
dws_ao, the PHP 7.4 parse of the entry,Safeguards.phpanduninstall.php, actionlint, zizmor and both audits, all passing locally.