x3p0-dev/x3p0-class-registry
Composer 安装命令:
composer require x3p0-dev/x3p0-class-registry
包简介
A tiny, type-safe registry of class names for building extensible subsystems in WordPress plugins and themes.
README 文档
README
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
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:
| Piece | Responsibility |
|---|---|
| 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?
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
Extend the abstract Registry once per subsystem and name the base type
every entry must satisfy by returning it from contract():
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<Channel> */ final class ChannelRegistry extends Registry { protected function contract(): string { return Channel::class; } }
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
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
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
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
The registry is iterable (key => class-string) and countable, so it plays well
with foreach, count(), and iterator functions.
| Method | Returns | Description |
|---|---|---|
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
- PHP 8.1+
- WordPress (exception messages use
esc_html()and__())
License
GPL-2.0-or-later. Copyright © Justin Tadlock.
统计信息
- 总下载量: 1
- 月度下载量: 0
- 日度下载量: 0
- 收藏数: 0
- 点击次数: 2
- 依赖项目数: 0
- 推荐数: 0
其他信息
- 授权协议: GPL-2.0-or-later
- 更新时间: 2026-07-08