bmd / wp-framework
Generic WordPress framework plugin
Requires
Requires (Dev)
- 10up/wp_mock: *
- phpcsstandards/phpcsutils: ^1.0
- phpstan/phpstan: ^1.10
- phpunit/phpunit: ^9.6
- squizlabs/php_codesniffer: ^3.7
- symfony/var-dumper: *
- szepeviktor/phpstan-wordpress: ^1.3
- wp-coding-standards/wpcs: ^3.0
README
A small Composer library for bootstrapping WordPress plugins and themes with a PHP-DI service container, auto-mounted controllers, context-aware asset loading, and shared path/URL helpers.
The framework is intentionally light: it does not own routing, templating, build tooling, or plugin headers. It gives a package a predictable lifecycle and a few reusable services so feature code can stay in small classes.
Requirements
- PHP 8.1 or newer
- WordPress
- Composer
Installation
Install the package with Composer:
composer require bmd/wp-framework
Then load Composer's autoloader from your plugin or theme entrypoint:
<?php require_once __DIR__ . '/vendor/autoload.php';
Quick Start
Create an application class that extends Bmd\WPFramework\Main, set a package slug, and mount it after WordPress has loaded enough for plugin path and URL helpers to work.
<?php namespace Acme\Plugin; use Bmd\WPFramework\Main; final class App extends Main { public const PACKAGE = 'acme_plugin'; } ( new App( [ 'config.dir' => __DIR__, 'config.url' => plugin_dir_url( __FILE__ ), ] ) )->mount();
config.package defaults to static::PACKAGE, and the package slug is used to namespace WordPress actions and filters.
Lifecycle
Main::mount() performs the framework boot sequence:
- Registers configuration values in the service container.
- Registers the core controllers.
- Builds the PHP-DI container.
- Mounts any registered service that implements
Bmd\WPFramework\Interfaces\Controller.
Controllers extend Bmd\WPFramework\Abstracts\Controller. When a controller is mounted, its mountActions() and mountFilters() methods are called.
<?php namespace Acme\Plugin\Controllers; use Bmd\WPFramework\Abstracts\Controller; final class NoticesController extends Controller { public function mountActions(): void { add_action( 'admin_notices', [ $this, 'renderNotice' ] ); } public function renderNotice(): void { // Render your notice. } }
Service Container
Services are registered as PHP-DI definitions. The framework exposes convenience wrappers through Bmd\WPFramework\Services\ServiceLocator, including:
ServiceLocator::autowire()ServiceLocator::create()ServiceLocator::get()ServiceLocator::factory()ServiceLocator::decorate()ServiceLocator::value()ServiceLocator::string()
Add your own services or controllers by extending Main::getServiceDefinitions():
<?php namespace Acme\Plugin; use Acme\Plugin\Controllers\NoticesController; use Acme\Plugin\Services\ApiClient; use Bmd\WPFramework\Main; use Bmd\WPFramework\Services\ServiceLocator; final class App extends Main { public const PACKAGE = 'acme_plugin'; public static function getServiceDefinitions(): array { return array_merge( parent::getServiceDefinitions(), [ ApiClient::class => ServiceLocator::autowire(), NoticesController::class => ServiceLocator::autowire(), ] ); } }
You can retrieve a service after boot with:
$api_client = App::locateService( ApiClient::class );
Configuration
The default configuration entries are:
config.package: the package slug used for framework hooks.config.dir: the root directory used byFilePathResolver.config.url: the root URL used byUrlResolver.
Configuration is filterable before it is added to the container:
add_filter( 'acme_plugin_config', static function ( array $config ): array { $config['config.dir'] = plugin_dir_path( __FILE__ ); $config['config.url'] = plugin_dir_url( __FILE__ ); return $config; } );
Context Handling
The context provider determines where WordPress currently is and dispatches a chain of context names from most specific to broadest fallback.
Built-in context handlers:
ADMINmaps toBmd\WPFramework\Context\AdminFRONTENDmaps toBmd\WPFramework\Context\FrontendLOGINmaps toBmd\WPFramework\Context\Login
Context chains include:
- Block editor:
EDITOR,ADMIN - Front page:
FRONTPAGE,SINGLE,FRONTEND - Blog home:
BLOG,ARCHIVE,FRONTEND - Search results:
SEARCH,ARCHIVE,FRONTEND - Archive:
ARCHIVE,FRONTEND - Singular content:
SINGLE,FRONTEND - 404:
ERROR404,FRONTEND - Admin:
ADMIN - Login:
LOGIN - Ajax:
AJAX - Cron:
CRON
The first context name that resolves to a registered class implementing Bmd\WPFramework\Interfaces\ContextHandler is mounted through the package context hook.
do_action( 'acme_plugin_mount_context', $handler );
Built-in handlers enqueue conventional bundles:
build/frontend.jsandbuild/frontend.cssbuild/admin.jsandbuild/admin.cssbuild/login.jsandbuild/login.css
Asset Loading
Context handlers extend Bmd\WPFramework\Abstracts\ContextHandler, which provides:
enqueueScript( $handle, $path, $dependencies = [], $version = '', $in_footer = true )enqueueStyle( $handle, $path, $dependencies = [], $version = null, $screens = 'all' )
Local files are resolved relative to config.dir and converted to URLs relative to config.url. Empty local files are skipped. Remote URLs and protocol-relative URLs are allowed.
For scripts, a sibling WordPress asset file is detected automatically:
build/frontend.js
build/frontend.asset.php
When present, the asset file can provide dependencies and a version generated by WordPress build tooling.
Dependency arrays are filterable per handle:
add_filter( 'acme_plugin-frontend_script_dependencies', static fn ( array $dependencies ): array => array_merge( $dependencies, [ 'wp-i18n' ] ) ); add_filter( 'acme_plugin-frontend_style_dependencies', static fn ( array $dependencies ): array => array_merge( $dependencies, [ 'wp-components' ] ) );
Path And URL Services
The framework registers:
Bmd\WPFramework\Services\FilePathResolverBmd\WPFramework\Services\UrlResolverBmd\WPFramework\Services\ScriptLoaderBmd\WPFramework\Services\StyleLoader
Example usage from an autowired service:
<?php namespace Acme\Plugin\Services; use Bmd\WPFramework\Services\FilePathResolver; use Bmd\WPFramework\Services\UrlResolver; final class Manifest { public function __construct( private FilePathResolver $paths, private UrlResolver $urls ) {} public function path(): string { return $this->paths->resolve( 'build/manifest.json' ); } public function url(): string { return $this->urls->resolve( 'build/manifest.json' ); } }
Helpers
Bmd\WPFramework\Helpers includes small utility methods for:
- Class, interface, and trait checks:
classUses(),className(),implements(),uses(),getTraits() - Array handling:
isList(),arrayMerge() - Value normalization:
truthyFalsy() - WordPress plugin checks:
isPluginActive() - String formatting:
slugify(),hyphenate()
Development
Install development dependencies:
composer install
Run the test suite:
composer run phpunit
Run static analysis:
composer run phpstan
Run PHPCS:
composer run phpsniff
Changelog
0.2.7 - 2026-05-05
- Changed context mounting to dispatch the resolved context handler instance through the package-level mount action.
- Expanded README documentation for installation, bootstrapping, services, contexts, assets, helpers, and development commands.
0.2.6 - 2026-04-30
- Fixed PHP 8.2 deprecation warning from passing null to class_exists() in helper class checks.