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(1mo ago)657GPL-2.0-or-laterPHPPHP &gt;=8.1

Since Jun 21Pushed 2w agoCompare

[ 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 1w ago

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]`, `#[Make]`, `#[Defer]`, `#[Tagged]`, `#[TaggedWith]`, `#[DeferTagged]`, `#[DeferTaggedWith]`, `#[TaggedAbstracts]`, `#[TaggedAbstractsWith]`, `#[Build]`, `#[Param]`, `#[NoAutowire]`, `#[Singleton]`, `#[SingletonWhen]`, and `#[Tag]` configure resolution right at the point of use.
- **Flexible lifetimes** — singletons, transients, pre-built instances, aliases, "register only if missing" defaults that extensions can override, and conditional bindings that register only when a runtime check passes.
- **Contextual bindings** — give one consumer a different value or implementation than the rest of the app, by parameter name or by type.
- **Named parameters** — set container-backed scalar or array values by name and inject them explicitly with `#[Param]`.
- **Tagging** — group related services under a label and resolve them together, eagerly or lazily, keyed by a per-member attribute, and optionally constrained to a common contract.
- **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)
    - [Contextual bindings](#contextual-bindings)
    - [Named parameters](#named-parameters)
    - [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. The constant-driven bindings aren't processed by `register()` itself — a separate, final `registerDeclarations()` method handles them, and the application always calls it first — so nothing declared via the constants is lost, whether or not you override `register()` or call its parent:

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

Override `boot()` the same way when you need to do more than boot the `BOOTABLE` services — for example, hooking into WordPress. `bootDeclarations()` boots them first, regardless:

```
public function boot(): void
{
    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);
```

The `*When` variants register a binding **only if the given condition evaluates truthy**, and register nothing at all otherwise — unlike `*If`, they don't guard against overwriting an existing binding:

```
$container->singletonWhen(
    Gateway::class,
    fn (Container $c): bool => $c->get(Config::class)->get('features.stripe'),
    StripeGateway::class
);

$container->transientWhen(RequestContext::class, ! wp_doing_cron());
```

A condition is a `bool`, a `Closure` invoked with the container, or any other callable — run through `call()` so it's autowired like any other container callback.

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']);

// Build a fresh, unshared instance even if the abstract is bound as a
// singleton. The shared instance is left untouched; only this abstract is
// built anew — its dependencies still resolve normally (shared stays shared).
$scratch = $container->build(Cache::class);

// 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 {}
```

Use `#[SingletonWhen]` instead when the shared lifetime should only apply conditionally — it takes the same kind of condition as `singletonWhen()`:

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

#[SingletonWhen([FeatureFlags::class, 'cacheIsShared'])]
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\Make;
use X3P0\Framework\Container\Attributes\Defer;
use X3P0\Framework\Container\Attributes\Tagged;
use X3P0\Framework\Container\Attributes\DeferTagged;
use X3P0\Framework\Container\Attributes\Build;
use X3P0\Framework\Container\Attributes\Param;
use X3P0\Framework\Container\Attributes\NoAutowire;

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

        // Resolve an identifier with inline constructor overrides, still
        // honoring a matching cached singleton when none are given.
        #[Make(ReportBuilder::class, ['format' => 'pdf'])] private readonly ReportBuilder $reportBuilder,

        // 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.
        #[DeferTagged('report.sections')] private readonly array $sections,

        // Build a fresh, unshared instance with inline constructor
        // overrides — a private copy configured right here.
        #[Build(TransientCache::class, ['ttl' => 3600])] private readonly Cache $cache,

        // Resolve a container-backed named parameter set via `setParam()`.
        #[Param('timezone')] private readonly string $timezone,

        // Skip autowiring so the parameter keeps its default instead of the
        // container building the type — here, leaving `$user` null rather
        // than constructing a WP_User.
        #[NoAutowire] private readonly ?WP_User $user = null
    ) {}
}
```

AttributeTargetInjects`#[Get($id)]`parameterthe result of `get($id)``#[Make($id, $params)]`parameter`make($id, $params)` — resolves `$id`, honoring a cached singleton when `$params` is omitted`#[Defer($id)]`parametera `Closure` that resolves `$id` on each call`#[Tagged($tag)]`parameteran array of the tag's resolved services`#[TaggedWith($tag, $attr)]`parameterresolved services keyed by a chosen tag attribute's value`#[DeferTagged($tag)]`parameter`array` of deferred resolvers, keyed by abstract`#[DeferTaggedWith($tag, $attr)]`parameter`array` of deferred resolvers, keyed by a tag attribute's value`#[TaggedAbstracts($tag)]`parameterthe tag's unresolved abstracts, for building only what's needed`#[TaggedAbstractsWith($tag, $attr)]`parameterunresolved abstracts keyed by a chosen tag attribute's value`#[Build($id, $params)]`parameter`build($id, $params)` — a fresh, unshared instance with literal overrides`#[Param($name)]`parametera container-backed named parameter value set via `setParam()``#[NoAutowire]`parameternothing — skips autowiring so the declared default (or `null`) is kept`#[Singleton]`classopts an autowired class into a shared lifetime`#[SingletonWhen($condition)]`classopts an autowired class into a shared lifetime when `$condition` is truthy`#[Tag($tag, $attributes = [])]`classdeclares the class's own tag membership, applied via `tagFromAttributes()`You 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();
    }
}
```

### Contextual bindings

[](#contextual-bindings)

Sometimes a single consumer needs a value that differs from the rest of the app — a scalar the container can't autowire, or a different implementation of an interface. Contextual bindings say "when the container builds *this* class, supply *this* for that parameter." The consumer is the concrete class being built.

Bind **by parameter name** to supply a value the container can't resolve by type (a scalar, an array). The name is given without a leading `$`, and the value is passed as-is — or, if it's a closure, its return value is used:

```
$container->whenNeedsParam(Mailer::class, 'apiKey', 'secret');
$container->whenNeedsParam(Mailer::class, 'timeout',
    fn ($container) => $container->get('config')->get('mail.timeout'));
```

Bind **by type** to give one consumer a different implementation than everyone else. The concrete is a class-string resolved through the container (honoring its own binding, lifetime, and hooks), or a closure:

```
// Cache resolves to FileCache application-wide, but ExportJob gets RedisCache.
$container->singleton(Cache::class, FileCache::class);
$container->whenNeedsType(ExportJob::class, Cache::class, RedisCache::class);
```

A contextual binding sits below an explicit `make()` override and any parameter attribute, but above ordinary type autowiring — so `make(Mailer::class, ['apiKey' => '…'])` still wins, and a binding registered for one consumer never leaks to another.

### Named parameters

[](#named-parameters)

Named parameters give the container a place to hold scalar or array configuration values that any consumer can opt into by name — a lighter alternative to `whenNeedsParam()` for a value that isn't specific to one consumer.

```
$container->setParam('timezone', 'America/Chicago');
$container->setParam('retryLimit', 3);

$container->hasParam('timezone'); // true
$container->getParam('timezone'); // 'America/Chicago'
```

A value is only injected when the constructor parameter is explicitly marked with `#[Param]` — matching the name alone is never enough:

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

final class ReportBuilder
{
    public function __construct(
        #[Param('timezone')] private readonly string $timezone
    ) {}
}
```

If the named parameter was never set, resolution falls back to the constructor parameter's own default value or `null` (when nullable), the same way an unsatisfiable autowired type does.

### 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, $attrs = [])`— assigns one or more abstracts to a tag, optionally recording per-member attributes`tagFromAttributes($class)`— assigns `$class` to every tag declared on it with `#[Tag]``untag($abstracts, $tag)`— removes abstracts from a tag`tagged($tag)`the tag's services, resolved`taggedWith($tag, $attribute)`the tag's resolved services, keyed by an attribute's value`taggedAbstracts($tag)`the tag's abstracts, **without** resolving them`taggedAbstractsWith($tag, $attribute)`the tag's abstracts, keyed by an attribute's value, **without** resolving them`setTagContract($tag, $contract)`— declares that every member of `$tag` must be a concrete class of `$contract``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 `#[DeferTagged]` so consumers receive per-service resolver closures (keyed by class name) and build only what they need.

#### Tagging with attributes

[](#tagging-with-attributes)

`tag()` accepts an optional attribute map recorded alongside each member. Pair it with `taggedWith()` or `taggedAbstractsWith()` to look a service up by something other than its class name — a slug, say — instead of resolving the whole tag:

```
$container->tag(EmailChannel::class, 'channels', ['slug' => 'email']);
$container->tag(SmsChannel::class,   'channels', ['slug' => 'sms']);

$bySlug   = $container->taggedAbstractsWith('channels', 'slug');
$instance = $container->make($bySlug['email']);
```

A class can declare its own tag membership with the `#[Tag]` attribute instead of a provider hand-wiring it. `tagFromAttributes()` reads the attribute and calls `tag()` on the class's behalf:

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

#[Tag('channels', ['slug' => 'email'])]
final class EmailChannel implements Channel {}

$container->tagFromAttributes(EmailChannel::class);
```

For a tag whose members should all satisfy one contract, `setTagContract()` validates every member — past or future — as a concrete class of it, catching a mistagged member as soon as it's added rather than at resolution:

```
$container->setTagContract('channels', Channel::class);
```

### 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.

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

Maintenance93

Actively maintained with recent releases

Popularity16

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

58d 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 (115 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

2361.4M151](/packages/yiisoft-di)[joomla/di

Joomla DI Package

16445.0k13](/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.8k17](/packages/mouf-mouf)

PHPackages © 2026

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