offsetwp / framework
The WordPress framework
Requires
- php: >=8.5
- ext-ctype: *
- symfony/config: ^8.1
- symfony/dependency-injection: ^8.1
Requires (Dev)
- phpunit/phpunit: ^13.0
- wp-coding-standards/wpcs: ^3.3
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is not auto-updated.
Last update: 2026-09-15 11:25:10 UTC
README
OffsetWP Framework
A lightweight, modular, and typed framework for building modern WordPress applications.
- π Dependency injection β Autowiring, autoconfiguration, and service container
- π Flexible architecture β Choose between configuration-driven or standalone mode
- π§© Extensible by design β Build and compose reusable, isolated features with bundles
- πͺΆ Lightweight kernel β Minimal core with a small, focused footprint
- β‘οΈ Type-safe & modern PHP β Strict typing and modern PHP practices
- π Works everywhere β Compatible with themes, plugins, and mu-plugins
- π οΈ Developer-friendly API β Simple helpers for accessing services, parameters, and the application container
Installation
requirements:
- PHP: 8.5+
command:
composer require offsetwp/framework
Add symfony/yaml too if you want to write services.yaml / packages/*.yaml instead of PHP:
composer require symfony/yaml
Otherwise loading a YAML file fails with Unable to load YAML config files as the Symfony Yaml Component is not installed.
Usage
The framework can work in two modes "Configuration" and "Standalone" :
Configuration modeis a Symfony-like dependency injection mode that loads a fullconfig/directory (recommended for structured projects).Standalone modeis a minimal mode where you register one or a few services directly (recommended for small themes, mu-plugins, prototypes).
Configuration mode (directory)
// functions.php or my-mu-plugin.php or my-plugin.php require_once __DIR__ . '/vendor/autoload.php'; use OffsetWP\Framework\Kernel; use OffsetWP\Support\Env; $kernel = Kernel::configure( __DIR__ ) ->environment( Env::type() ) ->debug( Env::isDebug() ) ->config( __DIR__ . '/config' ) ->boot(); // Register it so the app() helper can find it later app( 'app', $kernel );
// config/services.php use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; return static function ( ContainerConfigurator $container ): void { $services = $container->services(); // Configure services $services ->defaults() ->autowire() ->autoconfigure() ->public(); // Set container global variables $container->parameters() ->set( 'app.name', get_bloginfo( 'name' ) ) ->set( 'app.description', get_bloginfo( 'description' ) ) ->set( 'app.url', get_site_url() ); /** * Register in container services the "App\**\*" classes from the "app/" folder * located next to "config/". Relative resources are resolved from the * directory of the current config file, hence "../app/" * and auto call the "__construct()" method */ $services ->load( 'App\\', '../app/' ) ->exclude( '../app/Application.php' ) // never register the kernel itself as a service ->tag( 'kernel.autoload' ); };
About the
kernel.autoloadtag β services carrying it are instantiated eagerly by the kernel's compiler pass while the container compiles, i.e. before anyBundle::boot()runs. Use it for classes whose constructor registers WordPress hooks; do not use it for services that expect a bundle to have booted first. The tag order is not configurable.
// config/bundles.php return array( \OffsetWP\Bundle\DemoBundle\DemoBundle::class => array( 'all' => true ), // all environment );
Configuration mode β use this when your project is organized and you want the full power of a DI container (autowiring, bundles, environment-specific config). Pass the path to a config/ folder that contains:
services.phporservices.yamlβ service definitions (required)bundles.phpβ optional list of bundles to register (each bundle can add its own DI extension); skipped when the file is absentpackages/*β optional per-package configuration files (PHP or YAML) that are loaded per environment
The kernel will scan and import files from the config/ directory similarly to Symfony: global services.*, packages/*, and environment-specific overrides.
With theme
my-theme/ # root
ββ app/ # your "App\*" classes, loaded with '../app/'
ββ config/
β ββ packages/
β β ββ demo.php # demo bundle configuration (.php|.yaml)
β ββ bundles.php # bundle register
β ββ services.php # services register (.php|.yaml)
ββ functions.php
With MU Plugin
mu-plugins/
ββ my-mu-plugin/ # root
β ββ app/ # your "App\*" classes, loaded with '../app/'
β ββ config/
β β ββ packages/
β β β ββ demo.php # demo bundle configuration (.php|.yaml)
β β ββ bundles.php # bundle register
β β ββ services.php # services register (.php|.yaml)
ββ my-mu-plugin.php
With plugin
my-plugin/ # root
ββ app/ # your "App\*" classes, loaded with '../app/'
ββ config/
β ββ packages/
β β ββ demo.php # demo bundle configuration (.php|.yaml)
β ββ bundles.php # bundle register
β ββ services.php # services register (.php|.yaml)
ββ my-plugin.php
Standalone (PHP/YAML) mode
// functions.php or my-mu-plugin.php or my-plugin.php require_once __DIR__ . '/vendor/autoload.php'; use OffsetWP\Framework\Kernel; use OffsetWP\Support\Env; $kernel = Kernel::configure( __DIR__ ) ->environment( Env::type() ) ->debug( Env::isDebug() ) ->services( __DIR__ . '/services.php' ) ->boot(); // Get and use "MyService" instance $my_service = $kernel->service( App\Service\MyService::class );
Standalone mode β use ->services( $file ) when you only need a few services and do not want the kernel to scan bundles or packages/. The single services.php file can use the Symfony dependency injection PHP configurator API (ContainerConfigurator) and behaves like a regular services.php but without bundle discovery.
my-theme/
ββ services.php # services register (.php|.yaml)
ββ functions.php
Extend
You can extend the OffsetWP\Framework\Kernel class to better organize your projects, for example:
Application: for the root of your projectMyPlugin: to better organize your pluginMyTheme: to create a modern theme
// my-theme/app/Application.php namespace App; use OffsetWP\Framework\Kernel; use OffsetWP\Support\Env; final class Application extends Kernel { protected string $environment = Env::DEVELOPMENT; protected bool $is_debug = true; protected string $services_path = __DIR__ . '/../services.php'; // __DIR__ is "app/", so go up one level to reach the project root. }
// my-theme/functions.php β instantiate from the project root, not from app/ require_once __DIR__ . '/vendor/autoload.php'; $kernel = new \App\Application( __DIR__ )->boot(); app( 'app', $kernel );
Overriding
$config_path/$services_pathas properties bypassessetConfigPath()/setServicesPath(), so the path is not validated. A wrong path surfaces later as aFileLocatorFileNotFoundExceptionduring boot instead of the explicitRuntimeException: The services file does not exist.PreferKernel::configure( β¦ )->services( β¦ )unless you really need a subclass.
Methods
Kernel
use OffsetWP\Framework\Kernel; $kernel = Kernel::configure( __DIR__ ) ->services( __DIR__ . '/services.php' ) ->boot(); // Set and get instances app( 'app', $kernel ); // Register the application instance echo app()->environment(); // Get the application instance and display the environment type instance( MyTheme::class, new MyTheme() ); // Register a instance instance( MyTheme::class )->doSomething(); // Get a instance // Get service app()->service( 'myservice' )->doSomething(); // with alias app()->service( \App\Service\MyService::class )->doSomething(); // with classname app()->hasService( 'myservice' ); // Get parameter app()->parameter( 'kernel.root_path' ); app()->hasParameter( 'kernel.is_debug' );
A name can be registered once. Registering it twice throws LogicException: A instance is already registered under "app"., and reading an unknown name throws LogicException: No instance registered under "app". β so call app( 'app', $kernel ) exactly once, at boot, before any app() call.
Parameters exposed by the kernel:
| Parameter | Value |
|---|---|
kernel.root_path |
resolved root path passed to Kernel::configure() |
kernel.environment |
value given to ->environment() |
kernel.is_debug |
value given to ->debug() |
kernel.charset |
UTF-8 unless overridden |
kernel.bundles |
name => class of registered bundles |
kernel.bundles_metadata |
name => [ path, namespace ] |
kernel.build_dir |
APP_BUILD_PATH, else APP_CACHE_PATH, else <root>/var/cache/<env> |
Env
use OffsetWP\Support\Env; // Basic Env::has( 'DB_HOST' ); // Check if a environment variable exist Env::raw( 'DB_HOST' ); // Get raw environment variable, '' if not set Env::raw( 'DB_HOST', 'localhost' ); // Get raw environment variable with a default Env::get( 'MY_VARIABLE' ); // Get casted environment variable (null, boolean, integer, float, json, array, string) Env::get( 'MY_VARIABLE', 'localhost' ); // Get variable, if not exist, set a default value // Casted Env::string( 'DB_HOST' ); Env::integer( 'WP_POST_REVISIONS' ); Env::float( 'MY_RATIO' ); Env::boolean( 'APP_MAINTENANCE' ); Env::array( 'MY_ARRAY' ); Env::array( 'MY_ARRAY', '|' ); // With custom separator Env::json( 'MY_JSON' ); // Environment Env::type(); // 'local', 'development', 'staging' or 'production' Env::isLocal(); Env::isDevelopment(); Env::isStaging(); Env::isProduction(); Env::isDebug();
Bundles
Bundles are small, reusable packages that can extend the kernel by registering services, compiler passes and configuration. The kernel discovers bundles listed in config/bundles.php and, when using configuration mode, will register each bundle's container extension so it can load bundle-specific configuration from config/packages/*.
There are two common bundle flavors:
- Simple bundle β no configuration required, just a class that can register services or hooks in
boot(). - Configurable bundle β exposes configuration and a DI extension so the host application can configure the bundle via
config/packages/{bundle}.phporconfig/packages/{env}/{bundle}.php.
Creating a simple bundle
demo-bundle/
ββ src/
β ββ DemoBundle.php
ββ composer.json
// src/DemoBundle.php namespace JohnDoe\Bundle\DemoBundle; use OffsetWP\Framework\Bundle\Bundle; final class DemoBundle extends Bundle { public function boot(): void { // register hooks or perform runtime initialization } }
composer.json:
{
"name": "johndoe/demo-bundle",
"type": "library",
"autoload": {
"psr-4": {
"JohnDoe\\Bundle\\DemoBundle\\": "src/"
}
},
"minimum-stability": "stable",
"prefer-stable": true,
"require": {
"offsetwp/framework": "^1.0"
}
}
Register the bundle in the application config/bundles.php:
return array( \JohnDoe\Bundle\DemoBundle\DemoBundle::class => array( 'all' => true ), );
Creating a configurable bundle
Here is a minimal example showing how to create a configurable bundle that queries the GitHub API using Guzzle.
- Create the
GithubBundlebundle (namespaceJohnDoe\\Bundle\\GithubBundle). The bundle exposes three configurable options:
enabled(bool): Enable/disable the bundleapi_base_url(string): base URL for requests to the GitHub APItoken(string|null): personal GitHub token (optional)
composer.json :
{
"name": "johndoe/github-bundle",
"type": "library",
"autoload": {
"psr-4": {
"JohnDoe\\Bundle\\GithubBundle\\": "src/"
}
},
"require": {
"offsetwp/framework": "^1.0",
"guzzlehttp/guzzle": "^7.0 || ^8.0"
},
"minimum-stability": "stable",
"prefer-stable": true
}
// src/GithubBundle.php namespace JohnDoe\Bundle\GithubBundle; use OffsetWP\Framework\Bundle\Bundle; use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; use Symfony\Component\Config\Definition\Configurator\DefinitionConfigurator; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; use Symfony\Component\DependencyInjection\ContainerBuilder; final class GithubBundle extends Bundle { public function configure( DefinitionConfigurator $definition ): void { /** * The bundle config definition * * @var ArrayNodeDefinition $root */ $root = $definition->rootNode(); $root ->children() ->booleanNode( 'enabled' )->defaultTrue()->end() ->scalarNode( 'api_base_url' )->defaultValue( 'https://api.github.com' )->end() ->scalarNode( 'token' )->defaultNull()->end() ->end(); } public function loadExtension( array $config, ContainerConfigurator $container, ContainerBuilder $builder ): void { if ( ! $config['enabled'] ) { return; } $builder->setParameter( 'github.enabled', $config['enabled'] ); $builder->setParameter( 'github.api_base_url', $config['api_base_url'] ); $builder->setParameter( 'github.token', $config['token'] ); $container->import( __DIR__ . '/Resources/config/services.php' ); } }
- Example of a main service
GithubClient(usesguzzlehttp/guzzle):
// src/Service/GithubClient.php namespace JohnDoe\Bundle\GithubBundle\Service; use GuzzleHttp\Client; use GuzzleHttp\ClientInterface; final class GithubClient { private ClientInterface $http; public function __construct( private string $base_url, private ?string $token = null ) { $headers = array(); if ( $this->token ) { $headers['Authorization'] = 'token ' . $this->token; $headers['Accept'] = 'application/vnd.github.v3+json'; } $this->http = new Client( array( 'base_uri' => $this->base_url, 'headers' => $headers, ) ); } public function repoInfo( string $owner, string $repo ): array { $response = $this->http->request( 'GET', sprintf( '/repos/%s/%s', $owner, $repo ) ); $content = $response->getBody()->getContents(); return json_decode( $content, true ) ?: array(); } }
- The
services.phpfile in the bundle imports the settings provided by the configuration:
// src/Resources/config/services.php use JohnDoe\Bundle\GithubBundle\Service\GithubClient; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; return function ( ContainerConfigurator $configurator ) { $services = $configurator->services(); $services->set( GithubClient::class ) ->arg( '$base_url', '%github.api_base_url%' ) ->arg( '$token', '%github.token%' ) ->public(); // Create a service alias. $services->alias( 'github', GithubClient::class ) ->public(); };
- Save the bundle in
config/bundles.php:
// config/bundles.php return array( \JohnDoe\Bundle\GithubBundle\GithubBundle::class => array( 'all' => true ), );
- Configure the bundle in
config/packages/github.php(orpackages/, depending on your organization):
// config/packages/github.php use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; return function ( ContainerConfigurator $configurator ) { // Feed the bundle's configuration tree β this is what reaches loadExtension(). $configurator->extension( 'github', array( 'enabled' => true, 'api_base_url' => 'https://api.github.com', 'token' => '%env(GITHUB_TOKEN)%', // your generated Github API token ) ); };
- Using it in the application β both the class id and the alias resolve, since step 3 made each of them public:
$github = app()->service( 'github' ); // find service from alias $github = app()->service( \JohnDoe\Bundle\GithubBundle\Service\GithubClient::class ); // or by classname var_dump( $github->repoInfo( 'offsetwp', 'framework' ) );

