Download the PHP package x3p0-dev/x3p0-framework without Composer
On this page you can find all versions of the php package x3p0-dev/x3p0-framework. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.
Download x3p0-dev/x3p0-framework
More information about x3p0-dev/x3p0-framework
Files in x3p0-dev/x3p0-framework
Package x3p0-framework
Short Description A lightweight, modern dependency injection framework for WordPress plugins and themes.
License GPL-2.0-or-later
Homepage https://github.com/x3p0-dev/x3p0-framework
Informations about the package x3p0-framework
X3P0: Framework
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.
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
- Requirements
- Installation
- Quick Start
- Service Providers
- The Container
- Binding services
- Resolving services
- Autowiring
- Attribute-based injection
- Contextual bindings
- Named parameters
- Tagging
- Lifecycle hooks
- Introspection
- The Application
- Contracts
- Exceptions
- License
Requirements
- PHP 8.1 or higher
- WordPress (latest version recommended)
- Composer
Installation
Distributing a plugin or theme? Vendor-prefix your dependencies with a tool like PHP-Scoper so your copy of the framework can't collide with another plugin's.
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
Write plain classes. Constructor dependencies are autowired, so you rarely wire anything by hand.
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.
3. Create your application
List your providers on the PROVIDERS constant. They're registered when the application is constructed.
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:
That's the whole loop. The rest of this document covers what each piece can do.
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.).
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:
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:
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:
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
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:
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:
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
A parameterized make() (one given overrides) is never cached.
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:
Mark a class #[Singleton] to have the container share a single instance whenever it autowires the class, without an explicit binding:
Use #[SingletonWhen] instead when the shared lifetime should only apply conditionally — it takes the same kind of condition as singletonWhen():
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
Parameter attributes configure how a single dependency is resolved, right where it's declared.
| Attribute | Target | Injects |
|---|---|---|
#[Get($id)] |
parameter | the result of get($id) |
#[Make($id, $params)] |
parameter | make($id, $params) — resolves $id, honoring a cached singleton when $params is omitted |
#[Defer($id)] |
parameter | a Closure that resolves $id on each call |
#[Tagged($tag)] |
parameter | an array of the tag's resolved services |
#[TaggedWith($tag, $attr)] |
parameter | resolved services keyed by a chosen tag attribute's value |
#[DeferTagged($tag)] |
parameter | array<class-string, Closure> of deferred resolvers, keyed by abstract |
#[DeferTaggedWith($tag, $attr)] |
parameter | array<mixed, Closure> of deferred resolvers, keyed by a tag attribute's value |
#[TaggedAbstracts($tag)] |
parameter | the tag's unresolved abstracts, for building only what's needed |
#[TaggedAbstractsWith($tag, $attr)] |
parameter | unresolved 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)] |
parameter | a container-backed named parameter value set via setParam() |
#[NoAutowire] |
parameter | nothing — skips autowiring so the declared default (or null) is kept |
#[Singleton] |
class | opts an autowired class into a shared lifetime |
#[SingletonWhen($condition)] |
class | opts an autowired class into a shared lifetime when $condition is truthy |
#[Tag($tag, $attributes = [])] |
class | declares the class's own tag membership, applied via tagFromAttributes() |
You can build your own by implementing ContextualAttribute:
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:
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:
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 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.
A value is only injected when the constructor parameter is explicitly marked with #[Param] — matching the name alone is never enough:
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 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.
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.
| Method | Returns |
|---|---|
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 assigned |
Because 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:
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
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:
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:
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:
Lifecycle hooks
Observe or transform services as they're built.
Introspection
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.
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.
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
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:
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
The X3P0\Framework\Contracts namespace holds small, dependency-free interfaces.
Bootable— aboot(): voidmethod for deferred setup that shouldn't live in a constructor (registering hooks, etc.). Service providers implement it, and any abstract listed in a provider'sBOOTABLEconstant must too.
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
X3P0 Framework is licensed under the GPL-2.0-or-later license.
Credits
Created and maintained by Justin Tadlock under the X3P0 umbrella.