PHPackages                             x3p0-dev/x3p0-class-registry - 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. [Utility &amp; Helpers](/categories/utility)
4. /
5. x3p0-dev/x3p0-class-registry

ActiveLibrary[Utility &amp; Helpers](/categories/utility)

x3p0-dev/x3p0-class-registry
============================

A tiny, type-safe registry of class names for building extensible subsystems in WordPress plugins and themes.

02PHP

Since Jul 8Pushed 1mo agoCompare

[ Source](https://github.com/x3p0-dev/x3p0-class-registry)[ Packagist](https://packagist.org/packages/x3p0-dev/x3p0-class-registry)[ RSS](/packages/x3p0-dev-x3p0-class-registry/feed)WikiDiscussions master Synced 1w ago

READMEChangelogDependenciesVersions (1)Used By (0)

X3P0: Class Registry
====================

[](#x3p0-class-registry)

A tiny, type-safe **registry of class names** for building extensible subsystems in WordPress plugins and themes.

It gives you one dependable place to map a string key to a class — and it refuses to store anything a factory couldn't later build. One part of your plugin registers the classes it knows about; other code (yours or a third party's) adds, replaces, or removes entries by key, without any of them referencing each other directly.

---

What a class registry does
--------------------------

[](#what-a-class-registry-does)

A class registry stores `string key => class-string` mappings. It does **not**create objects — it holds the *names* of classes so that something else (a factory) can instantiate them lazily, only when they're actually needed.

That indirection is what makes a subsystem extensible. Instead of a `switch`statement or a hard-coded list of `new` calls, you keep an open map of keys to classes. Want to add a new behavior? Register a class against a new key. Want to swap the built-in one? Register your class against the existing key. Nothing that *uses* the registry has to change.

It's one third of a small, familiar pattern:

PieceResponsibility**Registry**Stores `key => class-string` and guards what goes in**Factory**Looks a key up and instantiates the class (lazily)**Registrar**Seeds the registry with the built-in keys on startup**This package is the registry** — the storage-and-validation half. The factory and registrar are yours to write, because instantiation is where your container (or plain `new`) and your startup lifecycle live. On its own, a registry is a validated array of class names; paired with a factory, it's an extension point.

---

Why not just an array?
----------------------

[](#why-not-just-an-array)

Because an array will happily store a typo. This registry guards every registration and fails loudly — at wire-up time, not deep inside a request — when a class can't fulfill its purpose. A stored entry is guaranteed to be:

- **The right type** — a subclass of the base type the registry is built around.
- **Instantiable** — not abstract, not an interface, and constructable (no private/protected constructor).

So by the time a factory pulls a class-string out, building it can't fail for either of those reasons. The guarantee lives at the boundary instead of in every call site.

---

Quick start
-----------

[](#quick-start)

Extend the abstract `Registry` once per subsystem and name the base type every entry must satisfy with a `CONTRACT` constant:

```
use X3P0\ClassRegistry\Registry;

// The contract: every registered channel must be a subclass of this.
abstract class Channel
{
	abstract public function send(string $message): bool;
}

/**
 * @extends Registry
 */
final class ChannelRegistry extends Registry
{
	protected const CONTRACT = Channel::class;
}
```

If the base type isn't known until runtime, override `contract()` instead of declaring the constant:

```
final class ChannelRegistry extends Registry
{
	protected function contract(): string
	{
		return Channel::class;
	}
}
```

A registry that declares neither throws a `RegistrationException` the first time it needs the base type (on registration).

Now register classes by key, and look them up when you need to build one:

```
final class EmailChannel extends Channel
{
	public function send(string $message): bool
	{
		return wp_mail(get_option('admin_email'), 'Notification', $message);
	}
}

$channels = new ChannelRegistry();
$channels->register('email', EmailChannel::class);

// A factory resolves the key and builds the class — lazily, only now.
$className = $channels->get('email');   // 'EmailChannel' — or null if nothing is registered
$channel   = new $className();          // your factory/container does this step
```

You can also seed the registry at construction time:

```
$channels = new ChannelRegistry([
	'email' => EmailChannel::class,
	'sms'   => SmsChannel::class,
]);
```

---

Pairing it with a factory
-------------------------

[](#pairing-it-with-a-factory)

The registry stores and validates; a **factory** turns a key into an instance. This package intentionally ships no factory, because construction is where *your*world lives — a DI container, constructor arguments, a startup lifecycle — and a generic base would either couple this package to your container or bury that work in ceremony. It's a handful of lines you're better off owning:

```
final class ChannelFactory
{
	public function __construct(private readonly ChannelRegistry $registry) {}

	public function make(string $key): ?Channel
	{
		$className = $this->registry->get($key);

		return $className ? new $className() : null;
	}
}
```

Swap the `new $className()` for a container call (e.g. `$container->make($className)`) when your classes have their own dependencies. Because the registry already guaranteed every entry is the right type and instantiable, the factory never has to re-check either — it just builds. Keep the factory typed to your base class (`?Channel` here) so call sites stay type-safe.

---

Extending and overriding
------------------------

[](#extending-and-overriding)

Because everything is keyed, a third party can reshape a subsystem without touching its source:

```
// Add a brand-new type.
$channels->register('slack', SlackChannel::class);

// Replace a built-in with your own (same key, different class).
$channels->register('email', QueuedEmailChannel::class);

// Remove one entirely.
$channels->unregister('sms');
```

A common convention is for a subsystem to seed its built-in keys only if they aren't already registered, so extensions that ran earlier win:

```
if (! $channels->isRegistered('email')) {
	$channels->register('email', EmailChannel::class);
}
```

---

When registration fails
-----------------------

[](#when-registration-fails)

`register()` throws a `RegistrationException` (a `LogicException`) the moment you hand it something unusable — a programmer error meant to surface in development, not to be caught at runtime:

```
use X3P0\ClassRegistry\RegistrationException;

abstract class AsyncChannel extends Channel {}

$channels->register('bad', \stdClass::class);      // not a subclass of Channel
$channels->register('nope', AsyncChannel::class);  // a subclass, but abstract
```

Both throw with a message naming the offending class and why it was rejected.

---

API
---

[](#api)

The registry is iterable (`key => class-string`) and countable, so it plays well with `foreach`, `count()`, and iterator functions.

MethodReturnsDescription`register(string, string)``void`Map a key to a class; throws if invalid`unregister(string)``void`Remove a key's mapping, if any`isRegistered(string)``bool`Whether a class is registered under a key`get(string)``?class-string`The class for a key, or `null``all()``array`Every `key => class-string` mapping`count()``int`How many classes are registered`getIterator()``ArrayIterator`Iterate `key => class-string`The single extension point is the abstract `contract()` method, which returns the base `class-string` every registered class must be a subclass of.

---

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

[](#requirements)

- PHP 8.1+
- WordPress (exception messages use `esc_html()` and `__()`)

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

---

License
-------

[](#license)

[GPL-2.0-or-later](LICENSE.md). Copyright © Justin Tadlock.

###  Health Score

20

—

LowBetter than 12% of packages

Maintenance60

Regular maintenance activity

Popularity2

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity11

Early-stage or recently created project

 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.

### 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 (6 commits)")

### Embed Badge

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

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

###  Alternatives

[mageplaza/magento-2-product-slider

Magento 2 Product Slider

67232.3k2](/packages/mageplaza-magento-2-product-slider)[nosto/module-nostotagging

Increase your conversion rate and average order value by delivering your customers personalized product recommendations throughout their shopping journey.

27714.5k4](/packages/nosto-module-nostotagging)

PHPackages © 2026

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