Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
4f3486a
Update packages
ahegyes Nov 11, 2026
81eaeb5
Drop the stylesheet registrar's media parameter
ahegyes Nov 11, 2026
1b5da3b
Write the option keys as literals
ahegyes Nov 11, 2026
a63369e
Stage the first release's entry as a fragment the fill-in keeps
ahegyes Nov 11, 2026
e4f322b
Let the safeguards call WooCommerce where the plugin requires it
ahegyes Nov 11, 2026
50c0329
Tell a new plugin to switch on vulnerability reporting
ahegyes Nov 11, 2026
51ae864
Share the tests' request and option harnesses
ahegyes Nov 11, 2026
1df6a5d
Stop the Integration suite on what an aborted run left
ahegyes Nov 11, 2026
6522b01
Run the safeguard recipe's own test
ahegyes Nov 11, 2026
83f926f
Uninstall in a request that never loaded the plugin
ahegyes Nov 11, 2026
eb2aaad
Count the greeting's orders on every order storage
ahegyes Nov 11, 2026
d592be6
Make the template's rows observe what they claim
ahegyes Nov 11, 2026
ce7a64a
Publish the example block's style in the end-to-end suite
ahegyes Nov 11, 2026
70cfa39
Give the entry and the scoper config their file docblock tags
ahegyes Nov 11, 2026
ff180a1
Name each docblock's role and give its reason a paragraph
ahegyes Nov 11, 2026
6896922
Tag the plugin's API and state its contract
ahegyes Nov 11, 2026
69a9510
Keep each return beside the statement before it
ahegyes Nov 11, 2026
422fde2
Call methods on new objects without parentheses
ahegyes Nov 11, 2026
adf52da
Say self-updates reach a site only from a public repository
ahegyes Nov 11, 2026
cf99d0c
Warn that wp-env publishes the development site on the network
ahegyes Nov 11, 2026
4a3234f
Correct what the docs say the template does
ahegyes Nov 11, 2026
31654e2
Spell out the checks before a pull request and the release steps
ahegyes Nov 11, 2026
1cdfe19
Say which release receives security fixes
ahegyes Nov 11, 2026
5177aba
Make the issue forms and pull request template plugin-neutral
ahegyes Nov 11, 2026
5e6daf1
Correct the recipes that break or mislead when followed
ahegyes Nov 11, 2026
d914992
Run the first release's two changelog writes apart
ahegyes Nov 11, 2026
1c3abaf
Hook the safeguards that call WooCommerce once it has loaded
ahegyes Nov 11, 2026
0fff1db
Release the first version from the changelog's unreleased block
ahegyes Nov 11, 2026
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
41 changes: 24 additions & 17 deletions .github/ISSUE_TEMPLATE/bug_report.yml
Original file line number Diff line number Diff line change
@@ -1,52 +1,59 @@
name: Bug report
description: Report a defect in the plugin template (the unmodified template, not a forked plugin).
description: Report a defect in the plugin.
labels: [bug, triage]
body:
- type: dropdown
id: area
attributes:
label: Affected area
options:
- Plugin runtime (composition root, components, framework wiring)
- Composer scoping pipeline (scoper.inc.php, post-autoload-dump)
- Changelog tooling (composer changelog:* scripts)
- CI workflows (quality.yml, tests.yml)
- wp-env / integration test infrastructure
- Documentation / readme
- Other / unsure
- Runtime (boot, components, framework wiring)
- Settings
- Blocks and scripts
- CI workflows (.github/workflows/) and release
- Documentation
- Other or unsure
validations:
required: true

- type: input
id: commit
id: version
attributes:
label: Plugin template commit / SHA
placeholder: 'trunk@<sha>'
label: Plugin version or commit
placeholder: 'x.y.z or trunk@<sha>'
validations:
required: true

- type: input
id: php-version
attributes:
label: PHP version
placeholder: '8.5.0'
placeholder: 'x.y.z'
validations:
required: true

- type: input
id: wp-version
attributes:
label: WordPress version
placeholder: '7.0'
placeholder: 'x.y.z'
validations:
required: true

- type: input
id: wc-version
attributes:
label: WooCommerce version, if active
placeholder: 'x.y.z'

- type: textarea
id: reproduction
attributes:
label: Steps to reproduce
placeholder: |
1. Use this template via "Use this template" or git clone
2. Run `composer packages-install` / `npm install` / `npm run wp-env:start`
3. Observe Z
1. Activate the plugin.
2. Go to …
3. See …
validations:
required: true

Expand All @@ -67,5 +74,5 @@ body:
- type: textarea
id: logs
attributes:
label: Relevant logs / stack traces / CI run URL
label: Relevant logs, stack traces or a CI run URL
render: text
32 changes: 10 additions & 22 deletions .github/ISSUE_TEMPLATE/feature_request.yml
Original file line number Diff line number Diff line change
@@ -1,51 +1,39 @@
name: Feature request
description: Propose a new capability for the plugin template scaffold.
description: Propose a new capability for the plugin.
labels: [enhancement, triage]
body:
- type: dropdown
id: area
attributes:
label: Target area
options:
- Plugin runtime scaffold (composition root, components, framework wiring)
- Composer scoping pipeline
- Changelog tooling
- CI workflows
- wp-env / integration test infrastructure
- Documentation / placeholder convention
- New area / unsure
- Runtime (boot, components, framework wiring)
- Settings
- Blocks and scripts
- CI workflows (.github/workflows/) and release
- Documentation
- New area or unsure
validations:
required: true

- type: textarea
id: problem
attributes:
label: Problem
description: What is the template-user-facing problem this would solve?
description: What problem would this solve, and for whom?
validations:
required: true

- type: textarea
id: proposal
attributes:
label: Proposed solution
description: How would you imagine this looking? Code or YAML samples welcome.
description: How would it work? Code samples are welcome.
validations:
required: true

- type: textarea
id: alternatives
attributes:
label: Alternatives considered
description: What other approaches did you weigh? Why this one?

- type: dropdown
id: breaking
attributes:
label: Breaking change for existing forks?
options:
- 'No (additive — existing forks can ignore the new feature)'
- 'Yes (existing forks need to merge / adapt)'
- 'Unsure'
validations:
required: true
description: What other approaches did you weigh, and why this one?
11 changes: 5 additions & 6 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -1,24 +1,23 @@
# Summary

<!-- What changes and why, in 1-2 sentences. -->
<!-- What changes and why, in one or two sentences. -->

## Type of change

- [ ] Bug fix
- [ ] New feature
- [ ] Refactor
- [ ] Build / CI / tooling
- [ ] Build, CI or tooling
- [ ] Documentation
- [ ] Dependency update

## Checklist

- [ ] Changelog fragment added (`composer changelog:add`)
- [ ] Tests added/updated
- [ ] Tests added or updated
- [ ] `composer quality-check` passes locally
- [ ] Plugin header updated if runtime requirements changed

## Breaking changes for forks

<!-- Fill in if this PR changes the scoping pipeline, composer scripts, CI workflow contracts, or anything forks would have customized. Otherwise delete. -->
## Breaking changes

<!-- Required if this pull request breaks the plugin's contract: an @api element, a hook it fires, an option, meta or capability name, or a REST route. Otherwise delete this section. -->
2 changes: 1 addition & 1 deletion .github/workflows/fill-in-scaffold.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ on:
required: true
type: string
wp-env-port:
description: 'Development wp-env port, such as "8821"; the tests and multisite environments take the next two'
description: 'Development wp-env port, such as "8831"; the tests and multisite environments take the next two'
required: true
type: string

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/quality.yml
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ jobs:
NS: AcmeOrders
# The rehearsal fills in the longest prefix the script accepts, so its PHPCS run fails once the cap stops passing.
PREFIX: dws_acmeextraordinarysales
PORT: '8821'
PORT: '8831'
REPO: acme/acme-orders
run: .github/scripts/fill-in-scaffold.sh
- name: Install and scope the filled-in plugin
Expand Down
10 changes: 7 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,16 +52,20 @@ Each runs PHP 8.5 with WooCommerce from `vendor/` and its order tables on; CI ru

## Conventions

1. **Placement.** `src/` holds only the composition root and its components. Code that registers no hooks, such as keys, readers, the footprint, migration steps and asset registrars, is `dws_tpl_` functions and `DWS_TPL_` constants in `includes/`, which parses on PHP 7.4: no `mixed` or union types, typed constants, `match` or first-class callables there. Container definitions are composition and stay in `Plugin::get_definitions()`.
1. **Placement.** `src/` holds only the composition root and its components. Code that registers no hooks, such as keys, readers, the footprint, migration steps and asset registrars, is `dws_tpl_` functions and `DWS_TPL_` constants in `includes/`, which parses on PHP 7.4: no `mixed` or union types, typed constants, `match`, nullsafe calls or first-class callables there. Container definitions are composition and stay in `Plugin::get_definitions()`.
2. **Variants.** A component that exists only in one build variant is a child in a branch of its parent's `get_child_component_classes()`, never a second entry in `COMPONENTS`.
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, 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 `includes/options.php`'s typed readers, safeguards excepted. Everything stored lives under `dws_tpl_`, or `_dws_tpl_` for hidden meta. The footprint covers the prefix's options, transients and user meta; object meta needs a `meta_prefixes` entry per type, and anything else its own entry.
7. **Safeguards** live in `includes/safeguards.php`, call only WordPress and that file, and relax only through their own filter, through `dws_tpl_is_relaxed()` (`docs/recipes.md`). The entry loads them before the floor check, so they parse on PHP 7.4 and call only WordPress APIs present at the oldest WordPress the bootstrap package runs on, or guard them, which `phpstan.safeguards.neon` checks.
7. **Safeguards** live in `includes/safeguards.php`, call WordPress, that file, and WooCommerce where the plugin requires it, and relax only through their own filter, through `dws_tpl_is_relaxed()` (`docs/recipes.md`). The entry loads them before the floor check, so they parse 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 function, which the file 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 whole file, 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.
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

The plugin's contract is the elements tagged `@api`, the hooks it fires, and its option, meta and capability names and REST routes; breaking it ships as the next major. Every other function, constant and class, `src/` included, is internal and changes in any release.

## Style

- Every PHP file opens with `<?php declare( strict_types=1 );`, uses `array()`, `final` classes, `protected` over `private` and `#[\Override]` on overrides, and groups class members in `// region NAME` blocks outside `tests/`.
Expand All @@ -74,5 +78,5 @@ Each runs PHP 8.5 with WooCommerce from `vendor/` and its order tables on; CI ru
## Limitations

- The fill-in pushes with the workflow's own token, which GitHub refuses for workflow changes, so it never edits `.github/workflows/`; the workflows read the slug from `composer.json`'s `extra.text-domain`.
- Integration and end to end share the tests site, so a row that changes shared data in a separate process flushes the parent's object cache.
- Integration and end to end share the tests site, so a row that changes shared data in a separate process restores it there, or flushes the parent's object cache in `#[AfterClass]`.
- The release zip carries the TypeScript and SCSS sources beside the built files, since a plugin's repository may be private and the zip is then the only place its users find them.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

All notable changes to this plugin are documented in this file. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

Pending entries live in [`changelog/`](./changelog) — add via `composer changelog:add`. Aggregate into a release with `composer changelog:write`.
Pending entries live in [`changelog/`](./changelog) — add via `composer changelog:add`. Aggregate them into a release as [`CONTRIBUTING.md`](./CONTRIBUTING.md) describes.

## 1.0.0 - unreleased

Expand Down
32 changes: 29 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,23 @@
# Contributing

## Before opening a pull request

Install the dependencies on the PHP, Node and npm versions the README lists, then run the local equivalents of the CI checks:

```sh
npm run setup
npm run quality-check:all
composer test:integration:multisite
npm run audit
actionlint
zizmor .github/
```

## Changelog entries

Each user-visible change adds one fragment, so pull requests never conflict on `CHANGELOG.md`:

```bash
```sh
composer changelog:add
```

Expand All @@ -14,8 +27,21 @@ The command asks for the change's significance, its type and its entry, and writ

A release writes the fragments into a new version block of `CHANGELOG.md`, whose version the fragments' significance sets:

```bash
```sh
composer changelog:write
```

The release commit also sets the entry file's `Version:` header to that version, and `readme.txt`'s `Stable tag:` where the plugin has one, because the release refuses a tag either differs from. It sets each `blocks/src/*/block.json`'s `version` too, which WordPress puts in the block's stylesheet URL and the Unit suite holds to the header. Pushing the tag `v<version>` on that commit starts the release.
The first release dates the `1.0.0 - unreleased` block the plugin starts with, adding the fragments to it, since a plain write would put a new version above it:

```sh
composer changelog:write -- --amend
```

The release commit also sets the entry file's `Version:` header to that version, and `readme.txt`'s `Stable tag:` where the plugin has one, because the release refuses a tag that either differs from. It sets each `blocks/src/*/block.json`'s `version` too, which WordPress puts in the block's stylesheet URL and the Unit suite holds to the header.

Land the release commit on trunk and wait for its Quality and Tests runs to pass, since the release publishes only a commit whose runs passed. To rehearse, run the Release workflow on trunk from the Actions tab, which checks those runs, builds and tests the zip and publishes nothing. Then sign a tag on the commit and push it, which starts the release:

```sh
git tag -s v<version> -m v<version>
git push origin v<version>
```
14 changes: 9 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,20 @@ Create the plugin's repository from this template with GitHub's **Use this templ
| `plugin-slug` | `acme-orders-for-woocommerce` | the text domain and the names of the entry file and the release zip |
| `php-namespace` | `AcmeOrders` | the namespace segment after `DeepWebSolutions\` |
| `prefix` | `dws_aowc` | the prefix of every global function, constant, option and hook |
| `wp-env-port` | `8821` | the development site's port; the tests site takes the next one |
| `wp-env-port` | `8831` | the development site's port; the tests and multisite sites take the next two |

The workflow:
- renames the entry file and replaces the template's names, description, prefix and ports in every tracked file outside `.github/workflows/`, which read the slug from `composer.json` instead, since GitHub refuses the workflow's own token for workflow changes;
- makes `README.scaffold.md` the plugin's README, which lists the template's demo and what removing each part takes;
- deletes the disabled example include and any changelog fragments, and refreshes the Composer lock's content hash;
- deletes the disabled example include, the safeguard recipe's test and any changelog fragments, and refreshes the Composer lock's content hash;
- fails when any name, description or port of the template is left, and otherwise pushes one commit to the branch it ran on.

Then turn on private vulnerability reporting and Dependabot alerts in the new repository's settings: its `SECURITY.md` sends reports through the first and development-tool advisories to the second, and a repository made from a template copies none of the template's settings.

## Naming

- A WooCommerce plugin's display name reads `X for WooCommerce`, as WooCommerce's trademark guidelines ask, and its slug ends in `-for-woocommerce`.
- The prefix is `dws_` and one segment of up to 22 lowercase letters and digits, such as `dws_lpmwc`, so no plugin's prefix starts with another's and the template's longest constants still pass PHPCS; the framework's admin notices refuse `dws_network` and `dws_hidden`; a WooCommerce plugin's segment ends in `wc`.
- The prefix is `dws_` and one segment of up to 22 lowercase letters and digits, such as `dws_lpmwc`, so no plugin's `{prefix}_` starts with another's and the template's longest constants still pass PHPCS; the framework's admin notices refuse `dws_network` and `dws_hidden`; a WooCommerce plugin's segment ends in `wc`.
- The namespace is flat: `DeepWebSolutions\<Name>\`, with no segment in between.
- The repository is `wc-<name>` for a WooCommerce plugin and `wp-<name>` otherwise, as the plugin mirrors are named, and the fill-in names the Composer package `<owner>/<repository>` and the npm package `@<owner>/<repository>` after it.
- Each plugin takes its own three consecutive wp-env ports, for its development, tests and multisite sites, apart from 8888 and 8889, which wp-env uses by default, and the template's own 8811, 8812 and 8813.
Expand All @@ -41,13 +43,15 @@ Install the dependencies, prefix the framework into `vendor-prefixed/`, build th
npm run setup
```

wp-env publishes the development site on every network interface, with fixed development credentials, so other devices on the local network can reach it.

Run the manifest and changelog checks, PHPCS, PHPStan, and the Unit and Integration suites on the tests site at <http://localhost:8812>:

```sh
composer quality-check
```

Install the browser the end-to-end suite runs in, once per machine, then run the suite:
Install the browser the end-to-end suite runs in, initially and after a Playwright update that needs a new browser revision, then run the suite:

```sh
npx playwright install chromium
Expand All @@ -66,7 +70,7 @@ Update the dependencies within their constraints, and `@wordpress/*` to npm's la
npm run packages-update:all
```

Every pull request also rehearses the fill-in with sample inputs and gates the filled-in plugin, and rehearses the release on the built zip. The template itself publishes no release, even on a tag, and takes no changelog fragments, which the fill-in deletes. `tests/README.md` describes the suites and CI, `AGENTS.md` the conventions, `CONTRIBUTING.md` the changelog, and `SECURITY.md` how to report a vulnerability.
Every pull request also rehearses the fill-in with sample inputs and gates the filled-in plugin, and rehearses the release on the built zip. The template itself publishes no release, even on a tag, and takes no changelog fragments, which the fill-in deletes; `CHANGELOG.md`'s `1.0.0 - unreleased` block is the plugin's first entry. `tests/README.md` describes the suites and CI, `AGENTS.md` the conventions, `CONTRIBUTING.md` the checks before a pull request, the changelog and releases, and `SECURITY.md` how to report a vulnerability.

## License

Expand Down
Loading
Loading