A starting point for WordPress plugins with a clean object-oriented architecture, written as the companion code to the Modern WordPress Plugin Development book.
Plugins hook into WordPress at many points in time, under many conditions. Written the usual way, every new feature has to account for every existing one, and the nested conditionals grow with each addition. This scaffold keeps that complexity linear: every concern is its own service, and each service declares where and when it acts in the WordPress lifecycle through small interfaces. The plugin infrastructure works out when to trigger what.
final class SampleLoopService implements Service, Registerable, Conditional, Delayed {
public static function is_needed(): bool {
return \is_singular(); // Only on singular views…
}
public static function get_registration_action(): string {
return 'wp'; // …and only once the query has run.
}
public function register(): void {
\add_filter( 'the_content', [ $this, 'prepend_post_header' ] );
}
// …
}The architecture in this scaffold powers two of Google's WordPress plugins. Each keeps its own adapted copy of the infrastructure, and fixes proven there flow back into this scaffold.
| Plugin | Active installs | Infrastructure |
|---|---|---|
| AMP: the official AMP plugin for WordPress | src/Infrastructure |
|
| Web Stories: Google's visual storytelling plugin | includes/Infrastructure |
- Services with a declared lifecycle:
Registerable,Conditional,Delayed,HasDependencies,ActivateableandDeactivateablelet each service state when it is needed and what it hooks into. Services that a request does not need are never even instantiated. - Dependency injection through a single composition root,
src/BasicScaffoldPlugin.php, with a simple built-in injector and service container that you can swap for your own. - Views with escaping by default and child-theme and parent-theme overrides.
- No runtime dependencies. The plugin needs only PHP and WordPress on the
server. Composer is used for development tooling, and a bundled autoloader takes
over when
vendor/is absent. - Strict tooling: PHPUnit, PHPStan at
max, Psalm, the WordPress Coding Standards and PHPCompatibility, all run in CI.
The reasoning behind all of this is written down in the design principles.
- PHP 7.4 or later
- WordPress 6.0 or later
- Create your own repository with Use this template on GitHub, or
gh repo create my-plugin --template mwpd/basic-scaffold. - Make it yours. Rename:
- the plugin header and file name in
basic-scaffold.php, including the text domain; - the
MWPD\BasicScaffoldnamespace insrc/,tests/and theautoloadsections ofcomposer.json, and the packagename; - the namespaces under
parameters.architectureinphpstan.neon.dist, so the architecture rules check your code; BasicScaffoldPlugin,BasicScaffoldPluginFactoryandBasicScaffoldException;- the
HOOK_PREFIXandSERVICE_PREFIXconstants insrc/BasicScaffoldPlugin.php, so your hooks and service identifiers don't clash with other plugins built on this scaffold.
- the plugin header and file name in
- Replace the sample services in
src/SampleSubsystem/and the views inviews/with your own, and list your services inBasicScaffoldPlugin::get_service_classes().
composer install
composer test # unit, integration and architecture tests
composer lint # PHPCS, PHPStan and Psalm, plus the PHPStan rule testsThe architecture rules check that new code follows the design principles, and say what to do instead when it does not. docs/architecture-rules.md lists them.
Psalm 5 supports PHP up to 8.3, so run the lint step on PHP 8.3. CI
(.github/workflows/ci.yml) is the reference environment.
Work is planned and tracked in GitHub issues,
including work done by coding agents. docs/process/github.md
describes the labels and the lifecycle of an issue, and AGENTS.md
has the rules every change follows.