PHPackages                             x3p0-dev/x3p0-framework - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Framework](/categories/framework)
4. /
5. x3p0-dev/x3p0-framework

ActiveLibrary[Framework](/categories/framework)

x3p0-dev/x3p0-framework
=======================

A lightweight, modern dependency injection framework for WordPress plugins and themes.

1.0.0(1w ago)532GPL-2.0-or-laterPHPPHP &gt;=8.1

Since Jun 21Pushed yesterdayCompare

[ Source](https://github.com/x3p0-dev/x3p0-framework)[ Packagist](https://packagist.org/packages/x3p0-dev/x3p0-framework)[ Docs](https://github.com/x3p0-dev/x3p0-framework)[ RSS](/packages/x3p0-dev-x3p0-framework/feed)WikiDiscussions master Synced today

READMEChangelogDependencies (5)Versions (2)Used By (0)

X3P0: Framework
===============

[](#x3p0-framework)

[![Nova, a blue alien, as a construction worker wearing a toolbelt and holding a wrench in a city construction zone.](https://repository-images.githubusercontent.com/1098370533/fc172954-cd4e-4669-be63-ef92774fcbbf)](https://repository-images.githubusercontent.com/1098370533/fc172954-cd4e-4669-be63-ef92774fcbbf)

A lightweight, modern dependency injection framework for WordPress plugins and themes. Built with PHP 8.1+, it provides a robust DI container and abstract application layer to help you write cleaner, more maintainable WordPress code.

[![License](https://camo.githubusercontent.com/26f8b6541ea045cc1dbc2267208158b5a7ebbf5cf437c4b486d80fee9386f77e/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d47504c2d2d322e302d2d6f722d2d6c617465722d626c75652e737667)](LICENSE.md)[![PHP Version](https://camo.githubusercontent.com/04744bae0a61d2ffe29c26f07a9612eae20445fc6feaeb77b3af1f0e9be6447c/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f7068702d253345253344382e312d3838393242462e737667)](https://php.net)

Features
--------

[](#features)

- **Autowiring container** — resolves constructor dependencies by type, including union and intersection types.
- **Declarative service providers** — describe bindings, aliases, tags, and bootables with simple class constants; drop to code only when you need it.
- **Attribute-driven injection** — `#[Get]`, `#[Defer]`, `#[Tagged]`, `#[DeferredTagged]`, and `#[Singleton]` configure resolution right at the point of use.
- **Flexible lifetimes** — singletons, transients, pre-built instances, aliases, and "register only if missing" defaults that extensions can override.
- **Tagging** — group related services under a label and resolve them together, eagerly or lazily.
- **Lifecycle hooks** — observe (`resolving()`) or wrap (`decorate()`) services as they are built.
- **WordPress-friendly lifecycle** — register and boot across multiple load phases (`plugins_loaded`, `after_setup_theme`, …).
- **Type-safe** — full PHP 8.1+ type declarations for first-class IDE and static-analysis support.

Table of Contents
-----------------

[](#table-of-contents)

- [Requirements](#requirements)
- [Installation](#installation)
- [Quick Start](#quick-start)
- [Service Providers](#service-providers)
- [The Container](#the-container)
    - [Binding services](#binding-services)
    - [Resolving services](#resolving-services)
    - [Autowiring](#autowiring)
    - [Attribute-based injection](#attribute-based-injection)
    - [Tagging](#tagging)
    - [Lifecycle hooks](#lifecycle-hooks)
    - [Introspection](#introspection)
- [The Application](#the-application)
- [Contracts](#contracts)
- [Exceptions](#exceptions)
- [License](#license)

Requirements
------------

[](#requirements)

- PHP 8.1 or higher
- WordPress (latest version recommended)
- Composer

Installation
------------

[](#installation)

```
composer require x3p0-dev/x3p0-framework
```

**Distributing a plugin or theme?** Vendor-prefix your dependencies with a tool like [PHP-Scoper](https://github.com/humbug/php-scoper) so your copy of the framework can't collide with another plugin's.

Quick Start
-----------

[](#quick-start)

The framework leans on **declarative configuration**: you describe what your providers contribute using class constants, list those providers on your application, and decide when registration and booting happen.

### 1. Define your services

[](#1-define-your-services)

Write plain classes. Constructor dependencies are autowired, so you rarely wire anything by hand.

```
namespace Your\Project;

interface Cache {}

final class FileCache implements Cache {}

final class ReportBuilder
{
    // The container injects a Cache implementation automatically.
    public function __construct(private readonly Cache $cache) {}
}
```

### 2. Register them with a service provider

[](#2-register-them-with-a-service-provider)

Prefer the declarative constants — `SINGLETONS`, `TRANSIENTS`, `ALIASES`, `TAGS`, `BOOTABLE` — over imperative calls. The base `register()` and `boot()` handle them for you.

```
namespace Your\Project;

use X3P0\Framework\Core\ServiceProvider;

final class CacheServiceProvider extends ServiceProvider
{
    // Bind an interface to a concrete, shared across the request.
    protected const SINGLETONS = [
        Cache::class => FileCache::class
    ];

    // Resolve `'cache'` to the same binding as `Cache::class`.
    protected const ALIASES = [
        'cache' => Cache::class
    ];
}
```

### 3. Create your application

[](#3-create-your-application)

List your providers on the `PROVIDERS` constant. They're registered when the application is constructed.

```
namespace Your\Project;

use X3P0\Framework\Core\Application;

final class Plugin extends Application
{
    protected const PROVIDERS = [
        CacheServiceProvider::class
    ];
}
```

### 4. Bootstrap it

[](#4-bootstrap-it)

The framework fires no hooks of its own — you choose when to register and boot. A typical plugin instantiates the application, fires a registration hook so third parties can add providers, then boots:

```
namespace Your\Project;

use X3P0\Framework\Container\ServiceContainer;

require_once __DIR__ . '/vendor/autoload.php';

function plugin(): Plugin
{
    static $plugin;

    return $plugin ??= new Plugin(new ServiceContainer());
}

add_action('plugins_loaded', static function (): void {
    do_action('your/project/register', plugin());
    plugin()->boot();
}, -999);
```

That's the whole loop. The rest of this document covers what each piece can do.

Service Providers
-----------------

[](#service-providers)

A service provider is the home for a slice of your project's wiring. Extend `ServiceProvider` and describe its contributions with constants. You only override `register()` or `boot()` when a binding needs real logic (a closure factory, a conditional, etc.).

```
use X3P0\Framework\Core\ServiceProvider;

final class BlockServiceProvider extends ServiceProvider
{
    // Shared instances. A bare value is self-bound (the class is its own
    // concrete); a key => value pair binds an abstract to a concrete.
    protected const SINGLETONS = [
        BlockRegistry::class,
        Renderer::class => HtmlRenderer::class
    ];

    // New instance on every resolution. Same key conventions as SINGLETONS.
    protected const TRANSIENTS = [
        RequestContext::class
    ];

    // Overridable defaults: registered only if the abstract isn't already
    // bound, so an extension can replace them regardless of load order.
    protected const SINGLETONS_IF = [
        Logger::class => NullLogger::class
    ];
    protected const TRANSIENTS_IF = [
        View::class => PhpView::class
    ];

    // Alias => abstract. Resolving the alias resolves the abstract.
    protected const ALIASES = [
        'blocks' => BlockRegistry::class
    ];

    // Tag => list of abstracts, resolvable together via `tagged()`.
    protected const TAGS = [
        'theme.blocks' => [AlertBlock::class, CalloutBlock::class]
    ];

    // Services resolved and booted during the provider's boot phase. Each
    // must implement `Bootable`. They boot in the order listed.
    protected const BOOTABLE = [
        BlockRegistrar::class
    ];
}
```

### When you need code

[](#when-you-need-code)

Override `register()` for bindings that need a closure or runtime decisions, and call `parent::register()` to keep the constant-driven bindings:

```
public function register(): void
{
    parent::register();

    $this->container->singleton(Connection::class, function (Container $c): Connection {
        return new Connection($c->get(Config::class)->dsn());
    });
}
```

Override `boot()` the same way (calling `parent::boot()`) when you need to do more than boot the `BOOTABLE` services — for example, hooking into WordPress:

```
public function boot(): void
{
    parent::boot();

    add_action('init', $this->registerBlocks(...));
}
```

### Provider dependencies

[](#provider-dependencies)

Providers given by class name are resolved through the container, so they can type-hint their own dependencies. Accept the `Container`, pass it to the parent, and add whatever else you need:

```
use X3P0\Framework\Container\Container;
use X3P0\Framework\Core\ServiceProvider;

final class ReportServiceProvider extends ServiceProvider
{
    public function __construct(Container $container, private readonly Clock $clock)
    {
        parent::__construct($container);
    }
}
```

The Container
-------------

[](#the-container)

`ServiceContainer` is the framework's implementation of the `Container` contract. Inside a provider it's available as `$this->container`; elsewhere, via `plugin()->container()`.

### Binding services

[](#binding-services)

```
// Shared instance, built once and reused.
$container->singleton(Cache::class, FileCache::class);

// New instance on every resolution.
$container->transient(RequestContext::class);

// A value the container stores and returns as-is (never built or autowired).
$container->instance('config', new Config([...]));

// Alias one identifier to another (followed transitively).
$container->alias('cache', Cache::class);
```

The `*If` variants register a binding **only if the identifier isn't already bound**, which makes them ideal for defaults an extension may override regardless of load order:

```
$container->singletonIf(Logger::class, NullLogger::class);
$container->transientIf(RequestContext::class);
```

Re-binding an identifier with `singleton()`/`transient()` replaces any existing binding and clears its cached instance, so the replacement takes effect on the next resolution.

### Resolving services

[](#resolving-services)

```
// Resolve by identifier.
$cache = $container->get(Cache::class);

// Resolve a class, optionally overriding constructor parameters by name.
$report = $container->make(ReportBuilder::class, ['format' => 'pdf']);

// Invoke a callable with its parameters resolved from the container.
$result = $container->call([$controller, 'handle']);
$result = $container->call(SomeController::class . '::handle');

// Get a closure that resolves the service on each call — without the
// consumer needing the container itself.
$makeReport = $container->defer(ReportBuilder::class);
$report = $makeReport();
```

A parameterized `make()` (one given overrides) is never cached.

### Autowiring

[](#autowiring)

When the container builds a class, it resolves each constructor parameter from its type — including union and intersection types, falling back to a default value or `null` when a parameter allows it. Most classes need no binding at all:

```
final class ReportBuilder
{
    public function __construct(
        private readonly Cache $cache,      // resolved by type
        private readonly int $limit = 50,   // default used if not provided
    ) {}
}

$container->make(ReportBuilder::class);
```

Mark a class `#[Singleton]` to have the container share a single instance whenever it autowires the class, without an explicit binding:

```
use X3P0\Framework\Container\Attributes\Singleton;

#[Singleton]
final class FileCache implements Cache {}
```

> **Note:** values that can't be constructed — enums, interfaces, and abstract classes — can't be autowired. Provide them with an explicit binding, a `make()` override, or an attribute (below).

### Attribute-based injection

[](#attribute-based-injection)

Parameter attributes configure how a single dependency is resolved, right where it's declared.

```
use X3P0\Framework\Container\Attributes\Get;
use X3P0\Framework\Container\Attributes\Defer;
use X3P0\Framework\Container\Attributes\Tagged;
use X3P0\Framework\Container\Attributes\DeferredTagged;

final class Dashboard
{
    public function __construct(
        // Resolve a specific identifier (a keyed binding or a chosen concrete).
        #[Get('config')] private readonly Config $config,

        // Inject a closure that resolves the service lazily, on demand.
        #[Defer(ReportBuilder::class)] private readonly Closure $makeReport,

        // Inject every service assigned to a tag, already resolved.
        #[Tagged('theme.blocks')] private readonly iterable $blocks,

        // Inject the tagged services as deferred resolvers, keyed by class,
        // so you build only the ones you actually use.
        #[DeferredTagged('report.sections')] private readonly array $sections
    ) {}
}
```

AttributeTargetInjects`#[Get($id)]`parameterthe result of `get($id)``#[Defer($id)]`parametera `Closure` that resolves `$id` on each call`#[Tagged($tag)]`parameteran array of the tag's resolved services`#[DeferredTagged($tag)]`parameter`array` of deferred resolvers, keyed by abstract`#[Singleton]`classopts an autowired class into a shared lifetimeYou can build your own by implementing `ContextualAttribute`:

```
use X3P0\Framework\Container\Attributes\ContextualAttribute;
use X3P0\Framework\Container\Container;

#[Attribute(Attribute::TARGET_PARAMETER)]
final class CurrentUser implements ContextualAttribute
{
    public function resolve(Container $container): object
    {
        return $container->get(UserRepository::class)->current();
    }
}
```

### Tagging

[](#tagging)

Tagging groups related abstracts under a label so they can be resolved together — blocks, widgets, REST controllers, CLI commands, and the like — without maintaining a master list by hand.

```
$container->tag([AlertBlock::class, CalloutBlock::class], 'theme.blocks');

foreach ($container->tagged('theme.blocks') as $block) {
    $block->register();
}
```

Tagged abstracts resolve through the container like anything else, so singletons stay shared and unbound classes are autowired. An unknown tag resolves to an empty array.

MethodReturns`tag($abstracts, $tag)`— assigns one or more abstracts to a tag`untag($abstracts, $tag)`— removes abstracts from a tag`tagged($tag)`the tag's services, resolved`taggedAbstracts($tag)`the tag's abstracts, **without** resolving them`hasTag($tag)`whether any abstracts are currently assignedBecause tags accumulate, several providers — or third-party code hooking your registration action — can contribute to the same tag without touching the provider that consumes it:

```
add_action('your/project/register', static function ($app): void {
    $app->container()->singleton(TestimonialBlock::class);
    $app->container()->tag(TestimonialBlock::class, 'theme.blocks');
});
```

For large or expensive collections, pair a tag with `#[DeferredTagged]` so consumers receive per-service resolver closures (keyed by class name) and build only what they need.

### Lifecycle hooks

[](#lifecycle-hooks)

Observe or transform services as they're built.

```
// Run after the service is built, to mutate it in place. Runs once per build,
// so a singleton is observed only the first time it's created.
$container->resolving(ReportBuilder::class, function (object $builder, Container $c): void {
    $builder->setTimezone($c->get(Config::class)->timezone());
});

// Wrap or replace the service with something honoring the same contract.
// Decorators stack in registration order. If the service is already
// resolved, the decorator is applied to the stored instance immediately.
$container->decorate(Cache::class, function (Cache $cache, Container $c): Cache {
    return new LoggingCache($cache, $c->get(Logger::class));
});
```

### Introspection

[](#introspection)

```
$container->has(Cache::class);            // resolvable? (bound or an autowirable class)
$container->registered(Cache::class);     // explicitly bound or instance-registered?
$container->resolved(Cache::class);       // already built and cached?
$container->forgetInstance(Cache::class); // drop the cached instance; rebuild next time
```

The Application
---------------

[](#the-application)

`Application` is the hub that registers and boots your service providers. Subclass it, list providers on `PROVIDERS`, and drive the lifecycle from your plugin or theme.

```
final class Plugin extends Application
{
    protected const PROVIDERS = [
        CoreServiceProvider::class,
        AdminServiceProvider::class,
        FrontendServiceProvider::class
    ];
}
```

### Registering and booting

[](#registering-and-booting)

`register()` is variadic and accepts provider instances or class names. Class names are resolved through the container, so providers can declare their own dependencies.

```
$app->register(AdminServiceProvider::class, RestServiceProvider::class);
```

`boot()` boots every registered-but-unbooted provider and is safe to call repeatedly — each provider boots only once. Once the application has booted, a provider registered afterward boots immediately, so nothing registered late is left dormant. A batch passed to `register()` is registered in full before any of it boots.

### Multiple load phases

[](#multiple-load-phases)

To register across more than one WordPress phase, call `begin()` to open each pass. It clears the booted state (so that pass's providers register as a batch before booting) and returns the application, ready to hand to a registration hook:

```
add_action('plugins_loaded', static function (): void {
    do_action('your/project/register/plugin', plugin()->begin());
    plugin()->boot();
}, -999);

add_action('after_setup_theme', static function (): void {
    do_action('your/project/register/theme', plugin()->begin());
    plugin()->boot();
}, -999);
```

A single register-then-boot pass doesn't need `begin()`; it's required only to open each additional pass (and harmless on the first).

Contracts
---------

[](#contracts)

The `X3P0\Framework\Contracts` namespace holds small, dependency-free interfaces.

- **`Bootable`** — a `boot(): void` method for deferred setup that shouldn't live in a constructor (registering hooks, etc.). Service providers implement it, and any abstract listed in a provider's `BOOTABLE` constant must too.
- **`Renderable`** — a `render(): string` method for classes that produce escaped, safe HTML.
- **`ClassRegistry`** — a registry of class *names* (not instances) indexed by key: `register()`, `unregister()`, `isRegistered()`, and `get()`.

Exceptions
----------

[](#exceptions)

All container failures surface as `X3P0\Framework\Container\ContainerException`; an unknown identifier throws `NotFoundException` (a subtype, so catching the base covers both). On the application side, `InvalidProviderException` is thrown when a registered class isn't a `ServiceProvider`, and `UnbootableServiceException` when a `BOOTABLE` entry doesn't implement `Bootable`. Both extend `ApplicationException`.

License
-------

[](#license)

X3P0 Framework is licensed under the [GPL-2.0-or-later](LICENSE.md) license.

Credits
-------

[](#credits)

Created and maintained by [Justin Tadlock](https://github.com/justintadlock) under the [X3P0](https://github.com/x3p0-dev) umbrella.

Support
-------

[](#support)

- [GitHub Issues](https://github.com/x3p0-dev/x3p0-framework/issues)
- [Packagist](https://packagist.org/packages/x3p0-dev/x3p0-framework)

###  Health Score

42

—

FairBetter than 88% of packages

Maintenance99

Actively maintained with recent releases

Popularity14

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity42

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

13d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/ef868a4b45ef0263f49361744b8bb04e8f3cfcb8f3f79b274fc62823115ee596?d=identicon)[justintadlock](/maintainers/justintadlock)

---

Top Contributors

[![justintadlock](https://avatars.githubusercontent.com/u/1816309?v=4)](https://github.com/justintadlock "justintadlock (77 commits)")

---

Tags

wordpresswordpress-developmentcontainerframeworkwordpressAutowiringdependency-injectiondiservice provider

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Type Coverage Yes

### Embed Badge

![Health badge](/badges/x3p0-dev-x3p0-framework/health.svg)

```
[![Health](https://phpackages.com/badges/x3p0-dev-x3p0-framework/health.svg)](https://phpackages.com/packages/x3p0-dev-x3p0-framework)
```

###  Alternatives

[yiisoft/di

Yii DI container

2391.4M120](/packages/yiisoft-di)[joomla/di

Joomla DI Package

15433.2k12](/packages/joomla-di)[mouf/mouf

The Mouf PHP framework: an open-source PHP framework providing an easy way to download, install, use and reuse components, with a graphical user interface.

55146.7k17](/packages/mouf-mouf)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
