Search by

offsetwp / framework

w-jerome

The WordPress framework

Package info

github.com/offsetwp/framework

Homepage

pkg:composer/offsetwp/framework

Fund package maintenance!

Buy Me A Coffee

Ko Fi

Statistics

Installs: 28

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

1.1.0 2026-09-15 11:20 UTC

This package is not auto-updated.

Last update: 2026-09-15 11:25:10 UTC


README

OffsetWP Framework OffsetWP Framework

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 mode is a Symfony-like dependency injection mode that loads a full config/ directory (recommended for structured projects).
  • Standalone mode is 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.autoload tag β€” services carrying it are instantiated eagerly by the kernel's compiler pass while the container compiles, i.e. before any Bundle::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.php or services.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 absent
  • packages/* β€” 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 project
  • MyPlugin: to better organize your plugin
  • MyTheme: 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_path as properties bypasses setConfigPath() / setServicesPath(), so the path is not validated. A wrong path surfaces later as a FileLocatorFileNotFoundException during boot instead of the explicit RuntimeException: The services file does not exist. Prefer Kernel::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}.php or config/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.

  1. Create the GithubBundle bundle (namespace JohnDoe\\Bundle\\GithubBundle). The bundle exposes three configurable options:
  • enabled (bool): Enable/disable the bundle
  • api_base_url (string): base URL for requests to the GitHub API
  • token (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' );
	}
}
  1. Example of a main service GithubClient (uses guzzlehttp/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();
	}
}
  1. The services.php file 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();
};
  1. Save the bundle in config/bundles.php:
// config/bundles.php

return array(
	\JohnDoe\Bundle\GithubBundle\GithubBundle::class => array( 'all' => true ),
);
  1. Configure the bundle in config/packages/github.php (or packages/, 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
	) );
};
  1. 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' ) );