Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ functions.php the @api dws_tpl_plugin(), the includes/ loader, th
uninstall.php the framework's uninstall engine over Plugin::FOOTPRINT, with Options::DELETE_DATA's consent
src/ the plugin's classes, each named for its concept
├── Plugin.php SLUG, PREFIX (dws_tpl), COMPONENTS, FOOTPRINT, the identity, the logger, the scheduler, the migration runner and the kernel
├── Options.php the option keys, their defaults and the typed readers
├── Options.php the option keys, their defaults in stored form and the typed readers
├── Migrations.php get_steps() over the protected steps, append-only
├── Settings.php the WooCommerce settings tab dws_tpl: the greeting's fields, their validator and the uninstall consent
├── Settings.php the WooCommerce settings tab dws_tpl: its registration, the uninstall consent and each feature's fields
├── Blocks.php the blocks from blocks/build/blocks-manifest.php and the block-editor script
└── Greeting/ the greeting: GreetingFeature, gated on Options::GREETING_ENABLED, and its AccountDashboardGreeting child, which owns the default message
└── Greeting/ the greeting: GreetingFeature, gated on Options::GREETING_ENABLED, its AccountDashboardGreeting child, which owns the default message, and GreetingSettings, its fields
includes/ more @api functions and constants, grouped by topic, loaded in name order, _-prefixed files skipped
├── greeting.php whether the greeting is on, for an integration that needs the setting
└── metadata.php the slug, name and version getters, the plugin's surface for a peer plugin
Expand Down Expand Up @@ -56,9 +56,9 @@ Each runs PHP 8.5 with WooCommerce from `vendor/` and its order tables on; CI ru
3. **Request context.** `is_admin()` is false on REST requests and true in admin-ajax ones, so an admin-only feature decides the request context in its callbacks, never in `should_load()`.
4. **Add-ons.** An add-on extends a base plugin only through the base plugin's documented hooks. It reads a template-built base plugin's slug, name and version only through that plugin's `{prefix}_get_plugin_*()` getters, and a setting through an `@api` function where the base plugin offers one, else through the base plugin's options. It calls the base plugin's functions from `plugins_loaded` on, since the add-on may load first, and behind `function_exists()`, since a base plugin below its floors or with a partial build may stay active without defining them.
5. **Request reads.** An integer or boolean is read as `filter_var( wp_unslash( $_POST['key'] ?? '' ), FILTER_VALIDATE_INT, FILTER_NULL_ON_FAILURE ) ?? $default` (`FILTER_VALIDATE_BOOL` for a boolean, where an absent key reads false, as an unchecked checkbox posts nothing), and a string through an `is_string()` guard and the field's sanitizer; never `filter_input()`, the `default` filter option or a cast on request input.
6. **Options and data.** Options are read only through `Options`' typed readers, which translate nothing, safeguards excepted; a translated default belongs to the component that shows it, as the greeting's message does. Everything stored lives under `dws_tpl_`, or `_dws_tpl_` for hidden meta, but for scheduled jobs, whose hooks take the hook root as every hook does and whose Action Scheduler group is the bare `dws_tpl`. The footprint covers the prefix's options, transients and user meta; object meta needs a `meta_prefixes` entry per type, and anything else, such as a scheduled hook, its own entry.
6. **Options and data.** Each setting is one autoloaded option, its key on `Options` and its default in stored form in `DEFAULTS`, which the first migration seeds, a release that adds settings seeds again, and its field reads; a feature's fields live in its folder's fields class. Outside the safeguards and the migration steps, options are read only through `Options`' typed readers, which translate nothing; a translated default belongs to the component that shows it, as the greeting's message does. Everything stored lives under `dws_tpl_`, or `_dws_tpl_` for hidden meta, but for scheduled jobs, whose hooks take the hook root as every hook does and whose Action Scheduler group is the bare `dws_tpl`. The footprint covers the prefix's options, transients and user meta; object meta needs a `meta_prefixes` entry per type, and anything else, such as a scheduled hook, its own entry.
7. **Safeguards** live in `Safeguards.php`, a final class whose `register()` hooks each safeguard as a static closure, which no code can remove by name, so a safeguard relaxes only through its own filter, through the protected `is_relaxed()` (`docs/recipes.md`); only what other classes read is public. The entry requires the class by path before the floor check, so it parses on PHP 7.4 and call only what their hook hands them, WordPress APIs present at the oldest WordPress the bootstrap package runs on and WooCommerce APIs present since 3.6, guarding anything newer with `function_exists()` or `method_exists()`. WordPress loads the plugin while WooCommerce is inactive, so a safeguard that calls WooCommerce is hooked from one closure, which `register()` runs on `woocommerce_loaded`, or at once where that has fired already, as on the request that activates the plugin; WooCommerce fires it, since 3.6, once every plugin has loaded. `phpstan.safeguards.neon` reads the class and the entry, with WooCommerce's stubs loaded, and checks the WordPress floor; review checks the WooCommerce floor, which no tool reads.
8. **Migrations** are append-only: a step converges from any partial state, verifies its writes and throws when one fails.
8. **Migrations** are append-only: a step converges from any partial state, verifies its writes and throws when one fails. A step names each stored name it touches, such as an option or meta key or a scheduled hook, and each default as a literal and reads stored rows directly, never through `Options` or a constant another class declares, so a later rename leaves what it does as it shipped; the seeding step alone reads `Options::DEFAULTS`.
9. **WordPress floor.** The plugin calls only APIs its `Requires at least` has, PHP's and JavaScript's alike: PHPStan's wp-compat reads the floor from that header, and CI runs the end-to-end suite on the floor's WordPress, which supplies the `@wordpress/*` scripts at runtime.

## Contract
Expand Down
2 changes: 1 addition & 1 deletion README.scaffold.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ The fill-in renamed the plugin and kept the template's demo, which boots and pas

| Part | Where it lives | Removing it |
| --- | --- | --- |
| The greeting | `src/Greeting/`, the Greeting section of `src/Settings.php` with its validator, the `GREETING_ENABLED`, `GREETING` and `GREETING_ORDERS` keys in `src/Options.php`, and `includes/greeting.php`, the function through which another plugin reads whether it is on | Delete `src/Greeting/` and its row in `Plugin::COMPONENTS`, `includes/greeting.php`, the Greeting section of `src/Settings.php` with `validate_greeting_orders()`, `assets/src/greeting.scss` and its `greeting=` entry in `package.json`'s `build:assets` and `start:assets`, the three keys with their defaults, their lines in `Migrations::add_settings()`, and `tests/Integration/Greeting/`, then the rows naming them in `OptionsTest`, `MigrationsTest` and `SettingsTest`, and `PluginBootTest`'s expectation that the greeting is skipped. `tests/EndToEnd/settings.spec.ts` drives the greeting's validated field, so point it at a validated field of the plugin's own. |
| The greeting | `src/Greeting/`, its fields in `GreetingSettings`, which `src/Settings.php` spreads into the tab, the `GREETING_ENABLED`, `GREETING` and `GREETING_ORDERS` keys in `src/Options.php`, and `includes/greeting.php`, the function through which another plugin reads whether it is on | Delete `src/Greeting/` and its row in `Plugin::COMPONENTS`, `includes/greeting.php`, the `GreetingSettings` spread in `src/Settings.php`, `assets/src/greeting.scss` and its `greeting=` entry in `package.json`'s `build:assets` and `start:assets`, the three keys with their defaults, and `tests/Integration/Greeting/`, then the rows naming them in `OptionsTest`, the Integration `MigrationsTest` and `SettingsTest`, `PluginBootTest`'s expectation that the greeting is skipped, and the fingerprint in the Unit `MigrationsTest`, which becomes the one its failure prints, since no release has seeded the old keys. `tests/EndToEnd/settings.spec.ts` drives the greeting's number field, so point it at a field of the plugin's own. |
| The example block | `blocks/src/example-notice/`, registered by `src/Blocks.php` from `blocks/build/blocks-manifest.php` and rendered by its `render.php` | Delete `blocks/`, `build:blocks` and `start:blocks` with their calls in `build` and `start`, `register_blocks()` and its `init` hook in `src/Blocks.php`, `BlocksTest`'s block rows, and the editor script's command, which inserts the block, with `tests/EndToEnd/editor.spec.ts`. |
| The editor script | `assets/src/editor.ts`, a command in the editor's command palette that inserts the example block, enqueued in the block editor by `src/Blocks.php` | Delete it, its `editor=` entry in `build:assets` and `start:assets`, `@wordpress/commands` and `@wordpress/data` from `package.json`'s `devDependencies`, `enqueue_editor_script()` and its hook in `src/Blocks.php`, `BlocksTest`'s editor row and `tests/EndToEnd/editor.spec.ts`. Once `assets/src/` holds nothing, delete `build:assets` and `start:assets` with their calls in `build` and `start`. Once the plugin registers no blocks either, delete `src/Blocks.php`, its row in `Plugin::COMPONENTS` and `BlocksTest`. |
| `models/` and `templates/` | Empty extension points, for classes on Composer's classmap and for template partials | Delete the folders the plugin never fills, and `models/` from `composer.json`'s `autoload.classmap` and `phpstan.dist.neon`'s `paths`, since Composer and PHPStan both fail on a missing folder. |
Expand Down
24 changes: 12 additions & 12 deletions composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,14 @@ The request that activates the plugin does not boot it, because WordPress includ

## Where code lives

- `src/` holds the plugin's classes, each final and named for its concept: the composition root; the components, which implement the framework's `ComponentInterface` and add their hooks in `register_hooks()`; and classes that register no hooks, `Options` and `Migrations`. A component takes the plugin's identity and ungated services in its constructor, such as the logger, the scheduler, `AdminNotices` or `Assets`, which `Plugin::get_definitions()` provides; a component that builds a settings page, product data tab or form passes the identity to it.
- `src/` holds the plugin's classes, each final and named for its concept: the composition root; the components, which implement the framework's `ComponentInterface` and add their hooks in `register_hooks()`; and classes that register no hooks, `Options`, `Migrations` and each feature's settings fields, such as `Greeting\GreetingSettings`, which `Settings` spreads into its tab. A component takes the plugin's identity and ungated services in its constructor, such as the logger, the scheduler, `AdminNotices` or `Assets`, which `Plugin::get_definitions()` provides; a component that builds a settings page, product data tab or form passes the identity to it.
- `includes/` extends `functions.php` with more of the plugin's `@api` functions and constants, grouped by topic: the slug, name and version getters a peer plugin reads (`metadata.php`), and whether the greeting is on (`greeting.php`). The global namespace holds nothing else, so the plugin supports only what it tags as API.
- `blocks/src/` and `assets/src/` hold the TypeScript and SCSS sources, which `npm run build` compiles into `blocks/build/` and `assets/build/`, and each block's `render.php`, which it copies beside the block's build and WordPress runs on each request, so the block's text follows the visitor's language; the framework's `Assets` reads the `*.asset.php` file wp-scripts writes beside each build, and `src/Blocks.php` registers the blocks from `blocks/build/blocks-manifest.php`.
- `models/` and `templates/` are empty extension points, for classes on Composer's classmap and for template partials.

## Data

- **Options** are `Options`' constants, read only through its typed readers, which guard each stored value, fall back to the key's default and translate nothing. A translated default belongs to the component that shows it, after `init`: the greeting's field shows its default message as the placeholder and stores an empty value, which the greeting reads as that message in each visitor's language, so no save stores the admin's language.
- **Options** are `Options`' constants, one autoloaded option each, whose defaults `DEFAULTS` holds in the form each option stores. The first migration stores every absent default, a release that adds settings lists that step again, and each field's default reads the same entry. The components read them through `Options`' typed readers, which guard each stored value, fall back to the key's default and translate nothing. A translated default belongs to the component that shows it, after `init`: the greeting's field shows its default message as the placeholder and stores an empty value, which the greeting reads as that message in each visitor's language, so no save stores the admin's language.
- **Migrations** are the append-only steps `Migrations::get_steps()` returns, each a protected static of the class. The framework's runner runs each pending step once per site on `wp_loaded`, behind a lock, and records the revision reached in `dws_tpl_revision`. A step converges from any partial state and throws when a write fails, so a failed step runs again on a later request.
- **The footprint**, `Plugin::FOOTPRINT`, names every kind of data the plugin stores: option and user meta prefixes, network options, scheduled hooks and Action Scheduler groups. Options, transients and user meta under the `dws_tpl_` prefix are covered; meta on posts, terms, comments or orders needs a `meta_prefixes` entry per object type, and data under another name its own entry, which no check enforces beyond the option constants `FootprintTest` covers.
- **Deactivation** unschedules every hook in the footprint's `cron_hooks` and `as_hooks` on the current site.
Expand Down
Loading
Loading