PHPackages                             x3p0-dev/x3p0-event - 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-event

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

x3p0-dev/x3p0-event
===================

A dependency-free event system for WordPress plugins and themes.

510—0%PHP

Since Jul 28Pushed 2w agoCompare

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

READMEChangelogDependenciesVersions (1)Used By (0)

X3P0: Event
===========

[](#x3p0-event)

A small, dependency-free **event system** for WordPress plugins and themes.

It gives you a clean, object-based way to let the different parts of a plugin or theme react to each other — without wiring them together directly. One part announces that something happened; anything interested responds.

---

What an event dispatcher does
-----------------------------

[](#what-an-event-dispatcher-does)

An event dispatcher decouples the code that *announces* something happened from the code that *reacts* to it. One part of your application dispatches an **event** — a plain object describing what occurred — and any number of **listeners** registered for that event run in response. Neither side has to know the other exists; they meet only at the event.

That indirection is what makes a codebase extensible. You can bolt on behavior — logging, notifications, cache invalidation, an audit trail — without touching the code that triggers it, and other modules (or other plugins) can react to your events the same way. Features stay loosely coupled, and each listener is a small, isolated unit you can test on its own.

The whole system is five small pieces:

TermWhat it is**Event**An object describing something that happened**Listener**A callable that reacts to an event**Dispatcher**Sends an event to its listeners**Listener provider**Decides *which* listeners apply to an event**Subscriber**One class that registers many listeners at onceAn **event** is data. A **listener** is code that runs when that data shows up. The **dispatcher** connects the two, asking a **provider** for the right listeners. If you've used WordPress hooks, this is the same idea as `do_action()`and `add_action()` — expressed with typed objects instead of string tags, and it can still [talk to your existing hooks](#talking-to-wordpress-hooks).

---

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

[](#quick-start)

```
use X3P0\Event\PriorityListenerRegistry;
use X3P0\Event\EventDispatcher;

// 1. A registry holds your listeners; a dispatcher fires events at them.
$listeners  = new PriorityListenerRegistry();
$dispatcher = new EventDispatcher($listeners);

// 2. An event is just a class. Give it whatever data it needs.
final class PostViewed
{
	public function __construct(public readonly int $postId) {}
}

// 3. A listener is any callable that accepts the event. Register it on the registry.
$listeners->listen(PostViewed::class, function (PostViewed $event): void {
	error_log("Post {$event->postId} was viewed.");
});

// 4. Dispatch the event, through the dispatcher, wherever it happens.
$dispatcher->dispatch(new PostViewed(42));
```

`dispatch()` returns the same event object it was given, so you can read anything the listeners changed on it (see the next section).

The two objects have distinct jobs: you **register** listeners on the registry (`$listeners`) and **fire** events through the dispatcher (`$dispatcher`). Keep one of each for your whole plugin so every part shares the same listeners.

---

Events can carry data back
--------------------------

[](#events-can-carry-data-back)

Because an event is an object passed by reference, listeners can **change it**, and the code that dispatched it can read the result. This is how you'd replace a WordPress *filter* (`apply_filters()`):

```
final class PriceCalculated
{
	public function __construct(public float $price) {}
}

$listeners->listen(PriceCalculated::class, function (PriceCalculated $event): void {
	$event->price *= 0.9; // apply a 10% discount
});

$event = $dispatcher->dispatch(new PriceCalculated(100.0));
echo $event->price; // 90.0
```

---

Priorities
----------

[](#priorities)

Listeners run in priority order. **A lower number runs first**, and the default is `0`. Listeners with the same priority run in the order they were added. To run *before* a default listener, use a negative number.

```
$listeners->listen(PostViewed::class, $runsSecond);         // priority 0 (default)
$listeners->listen(PostViewed::class, $runsFirst, -10);     // negative runs earlier
$listeners->listen(PostViewed::class, $runsLast, 20);       // higher runs later
```

For the common cases you can use the `ListenerPriority` enum instead of a bare number — the cases name the *order*, not a magnitude, so they read the same way the rule does ("lower runs first"):

```
use X3P0\Event\ListenerPriority;

$listeners->listen(PostViewed::class, $early, ListenerPriority::First);   // before all
$listeners->listen(PostViewed::class, $usual, ListenerPriority::Normal);  // 0 (the default)
$listeners->listen(PostViewed::class, $late,  ListenerPriority::Last);    // after all
```

`First` and `Last` are the integer extremes, so they run before and after every other listener respectively — true bookends. Pass a plain integer for any ordering in between; you can mix the two freely. The same values work as a subscriber's `priority` and with `listenOnce()`.

---

One-time listeners
------------------

[](#one-time-listeners)

A listener registered with `listenOnce()` fires for the first matching event and then removes itself — handy for one-shot work that should react to an event but never again:

```
$listeners->listenOnce(BootCompleted::class, function (BootCompleted $event): void {
	// runs on the first BootCompleted, then unregisters itself
});
```

It takes the same priority argument as `listen()` and is otherwise identical. The listener is removed *before* it runs, so it fires at most once even if it — or something it calls — dispatches the same event again.

---

Inferring the event from the listener
-------------------------------------

[](#inferring-the-event-from-the-listener)

A typed listener already names the event it handles — it's right there in the parameter type. `listenTo()` reads it from there, so you don't repeat the class you just type-hinted:

```
// listen() — the event class is named twice:
$listeners->listen(PostViewed::class, function (PostViewed $event): void { /* … */ });

// listenTo() — named once, on the parameter:
$listeners->listenTo(function (PostViewed $event): void { /* … */ });
```

It's the same registration either way — same storage, same priority ordering, same matching — so a `listenTo()` listener typed against a base class or interface still fires for every subtype, exactly as with `listen()`. The priority argument works the same too, as an integer or a `ListenerPriority`:

```
$listeners->listenTo($handler, ListenerPriority::Last);
```

Any callable works, because the type is read from whichever parameter comes first — a closure, an `[$object, 'method']` pair, or an invokable object:

```
$listeners->listenTo([$analytics, 'onPostViewed']);
```

There's a once-only counterpart, `listenOnceTo()`, that combines this with `listenOnce()`: the event is inferred *and* the listener removes itself after it first runs.

```
$listeners->listenOnceTo(function (BootCompleted $event): void { /* … */ });
```

`listenTo()` needs a type to read, so reach for plain `listen()` when there isn't one: a class name (resolved lazily, so there's no signature to inspect yet), a named event's string key, or a listener whose first parameter is untyped. A first parameter that is untyped, a builtin such as `string`, or a union type throws `InvalidListener` — it names no single event to register against, and guessing would be worse than asking you to say it.

---

Stoppable events
----------------

[](#stoppable-events)

Sometimes one listener should be able to stop the rest from running. Make the event implement `StoppableEvent` and pull in the `Stoppable` trait for a ready-made implementation:

```
use X3P0\Event\Stoppable;
use X3P0\Event\StoppableEvent;

final class CommentSubmitted implements StoppableEvent
{
	use Stoppable;

	public function __construct(public readonly string $text) {}
}

$listeners->listen(CommentSubmitted::class, function (CommentSubmitted $event): void {
	if (str_contains($event->text, 'spam')) {
		$event->stopPropagation(); // later listeners won't run
	}
});
```

The dispatcher checks `isPropagationStopped()` before calling each listener.

---

Named events
------------

[](#named-events)

An event is matched by its class, but it can *also* expose a string name so listeners may register against a friendly identifier as well as (or instead of) the class. Implement `NamedEvent`, and back it with a `NAME` constant using the `Named` trait:

```
use X3P0\Event\Named;
use X3P0\Event\NamedEvent;

final class OrderPlaced implements NamedEvent
{
	use Named;

	public const NAME = 'order.placed';

	public function __construct(public readonly int $orderId) {}
}
```

Now the event matches listeners registered under either key:

```
$listeners->listen(OrderPlaced::class, $byClass);   // by class, as always
$listeners->listen(OrderPlaced::NAME,  $byName);    // by name ('order.placed')

$dispatcher->dispatch(new OrderPlaced(42));          // both listeners run
```

You still dispatch an **object** — the name is an *additional* routing key the event opts into, not a replacement for the typed event, so listeners still get the real object and its typed data. And because the name lives on the class as a constant, registering with `OrderPlaced::NAME` keeps autocomplete, "find usages," and refactoring working — unlike a bare string. The name key composes with everything else: priorities, `listenOnce()`, `forget()`, and subscribers (call `$registry->listen(OrderPlaced::NAME, ...)` from `subscribeListeners()`).

---

Listeners as classes
--------------------

[](#listeners-as-classes)

A listener can be any callable, and an object with an `__invoke()` method is a callable — so a listener can be a class:

```
final class NotifyWarehouse
{
	public function __invoke(OrderPlaced $event): void { /* … */ }
}

$listeners->listen(OrderPlaced::class, new NotifyWarehouse());
```

If you'd rather register it by **class name** and have it built only when the event fires, pass the class name of any invokable class:

```
final class NotifyWarehouse
{
	public function __invoke(OrderPlaced $event): void { /* … */ }
}

$listeners->listen(OrderPlaced::class, NotifyWarehouse::class);   // resolved lazily
```

No marker interface is needed — the class only has to define `__invoke()`, which keeps its real, typed parameter. The class is instantiated the first time its event fires and reused after that. By default it's built with `new $class()`, so a plain listener class needs no constructor arguments; to resolve listeners that have dependencies, give the registry a resolver — for example a container:

```
use X3P0\Event\PriorityListenerRegistry;

$registry   = new PriorityListenerRegistry(
	fn (string $class): object => $container->get($class)
);
$dispatcher = new EventDispatcher($registry);
```

This also works with `listenOnce()`. (A class-name listener is matched by identity like any other, so remove it with `forget(OrderPlaced::class)` rather than by passing the class name back.)

---

Subscribers
-----------

[](#subscribers)

A **subscriber** is a single class that registers several listeners at once — handy for grouping related logic. It registers its own listeners on the registry it is given, the same way you would by hand:

```
use X3P0\Event\ListenerRegistry;
use X3P0\Event\Subscriber;

final class AnalyticsSubscriber implements Subscriber
{
	public function subscribeListeners(ListenerRegistry $registry): void
	{
		$registry->listen(PostViewed::class,       [$this, 'onPostViewed']);       // priority 0 (default)
		$registry->listen(CommentSubmitted::class, [$this, 'onComment'], 5);       // priority 5
	}

	public function onPostViewed(PostViewed $event): void { /* … */ }
	public function onComment(CommentSubmitted $event): void { /* … */ }
}

$listeners->subscribe(new AnalyticsSubscriber());
```

`subscribe()` just calls `subscribeListeners()`, handing it the registry to register on — so a subscriber can use `listen()`, `listenTo()`, or either once-only variant, freely mixing them for different listeners in the same class. Everything it registers is tracked together, so the whole set can be removed in one call: `$listeners->unsubscribe($subscriber)`.

Make just *one* of a subscriber's listeners once-only by calling `listenOnce()`/`listenOnceTo()` for that one, and `listen()`/`listenTo()` for the rest — there's no separate registration mode for it, since the subscriber already has full control:

```
public function subscribeListeners(ListenerRegistry $registry): void
{
	$registry->listen(PostViewed::class, [$this, 'onPostViewed']);            // every time
	$registry->listenOnce(PostViewed::class, [$this, 'onFirstView']);         // once only
}
```

---

Declaring a subscriber's listeners with attributes
--------------------------------------------------

[](#declaring-a-subscribers-listeners-with-attributes)

`subscribeListeners()` can also be generated for you, so the registration lives on the method itself instead of being spelled out by hand. There is one attribute per `ListenerRegistry` method, named to match it, so nothing new has to be learned beyond the method names already covered above:

AttributeRegistered with`Listen``listen()``ListenTo``listenTo()``ListenOnce``listenOnce()``ListenOnceTo``listenOnceTo()`Pull in the `DiscoversListeners` trait and mark each listening method with whichever attribute matches how you'd otherwise have registered it by hand:

```
use X3P0\Event\Attributes\ListenTo;
use X3P0\Event\DiscoversListeners;
use X3P0\Event\Subscriber;

final class AnalyticsSubscriber implements Subscriber
{
	use DiscoversListeners;

	#[ListenTo]
	public function onPostViewed(PostViewed $event): void { /* … */ }

	#[ListenTo(priority: 5)]
	public function onComment(CommentSubmitted $event): void { /* … */ }
}

$listeners->subscribe(new AnalyticsSubscriber());
```

This is the same `Subscriber` contract, just discovered instead of hand-written — `subscribe()` and `unsubscribe()` both work exactly as above. `ListenTo` and `ListenOnceTo` read the event type from the method's own first parameter, the same way `listenTo()`/`listenOnceTo()` read it for a plain callable, so it's declared once rather than repeated as an attribute argument; `priority` defaults to `0` and accepts a `ListenerPriority` case just like everywhere else:

```
#[ListenTo(priority: ListenerPriority::Last)]
public function onComment(CommentSubmitted $event): void { /* … */ }
```

`Listen` and `ListenOnce` take the event type as their first argument instead, for a named event's string key or any type reflection can't read — exactly when you'd reach for `listen()`/`listenOnce()` by hand:

```
use X3P0\Event\Attributes\Listen;

#[Listen(OrderPlaced::class)]
public function onOrderPlaced(OrderPlaced $event): void { /* … */ }
```

Both are repeatable, so the same method can be registered under more than one key, such as both a class and a named event's string:

```
#[Listen(OrderPlaced::class)]
#[Listen(OrderPlaced::NAME)]
public function onOrderPlaced(OrderPlaced $event): void { /* … */ }
```

Use `ListenOnce`/`ListenOnceTo` for a listener that should remove itself after it first runs — a method can even carry a mix of once and not-once attributes, since each is independent:

```
use X3P0\Event\Attributes\ListenOnceTo;

#[ListenOnceTo]
public function onFirstView(PostViewed $event): void { /* … */ }
```

Reach for a hand-written `subscribeListeners()` instead of attributes when registration has to branch conditionally at runtime; the discovered declarations are fixed once per class and cached.

Each attribute is a small `ListenerAttribute` — it knows only how to call its own `ListenerRegistry` method, given the registry and the method's own callable. `DiscoversListeners` never branches on which one it found; that same interface is what a fifth, custom attribute would need to implement to be discovered the same way.

---

Removing listeners
------------------

[](#removing-listeners)

To drop individual listeners, use `forget()`. Pass the event type and the exact listener to remove just that one, or the event type alone to remove every listener for it:

```
$listener = function (PostViewed $event): void { /* … */ };

$listeners->listen(PostViewed::class, $listener);

$listeners->forget(PostViewed::class, $listener); // remove that one listener
$listeners->forget(PostViewed::class);            // remove all PostViewed listeners
```

Listeners are matched by identity, so with `forget()` an inline closure can only be removed by passing back the same closure instance. (Subscribers are removed as a group with `unsubscribe()`, above.)

### By handle

[](#by-handle)

Every `listen()` call — and `listenTo()`, `listenOnce()`, `listenOnceTo()` — returns a `ListenerId`, an opaque handle to that one registration. Pass it to `forgetId()` to remove exactly that listener, no event type or closure reference required:

```
$id = $listeners->listen(PostViewed::class, function (PostViewed $event): void { /* … */ });

$listeners->forgetId($id); // removes that listener, and only that one
```

This is the clean way to drop an inline closure — you keep the tiny handle instead of the closure itself. It works the same for a once-only listener, so you can cancel one before it ever fires:

```
$id = $listeners->listenOnceTo(function (BootCompleted $event): void { /* … */ });

$listeners->forgetId($id); // it will now never run
```

Treat the handle as opaque: hold it and hand it back, nothing more. Removing an unknown or already-removed handle does nothing.

---

Checking for listeners
----------------------

[](#checking-for-listeners)

`hasListeners()` reports whether anything is registered for an event type — useful for skipping work, like building an expensive event, when nothing is listening:

```
if ($listeners->hasListeners(ReportGenerated::class)) {
	$dispatcher->dispatch(new ReportGenerated($this->buildExpensiveReport()));
}
```

It respects the same matching as dispatch, so a listener registered against a base class or interface counts for its subtypes. Pass a named event's name to check listeners registered under that name. (It reflects listeners on the registry, not any WordPress `add_action()` callbacks — for those, use `has_action()`.)

---

Where listeners come from: providers
------------------------------------

[](#where-listeners-come-from-providers)

The provider is the part that answers "which listeners apply to this event?" There are two, and you can **combine** them. Both implement the `ListenerProvider` interface, and all of it — the interface and the concrete classes — lives under `X3P0\Event`.

The name tells you which way each one goes: a **`…Registry`** is writable — you register listeners on it — while a read-only **`…Provider`** only supplies listeners it sources elsewhere. So of the two, only `PriorityListenerRegistry`accepts registrations.

### `PriorityListenerRegistry` (the default)

[](#prioritylistenerregistry-the-default)

An in-memory, writable registry. This is what you register listeners and subscribers on, with priority ordering. A listener registered against a base class or interface also fires for any event that extends or implements it.

### `AggregateListenerProvider` (combine providers)

[](#aggregatelistenerprovider-combine-providers)

Wraps several providers and draws listeners from all of them, in the order you list them. It is **read-only** — it combines what its children *provide* but accepts no registrations itself (mirroring PSR-14's own aggregation model). You register on the concrete registry; the aggregate reads through to it.

```
use X3P0\Event\AggregateListenerProvider;

// Combine registries from different modules into one view.
$provider = new AggregateListenerProvider(
	$coreListeners,    // a PriorityListenerRegistry
	$moduleListeners   // another PriorityListenerRegistry
);

$dispatcher = new EventDispatcher($provider);

// Register on the concrete registry, not the aggregate or the dispatcher.
$coreListeners->listen(PostViewed::class, $listener);
```

---

Talking to WordPress hooks
--------------------------

[](#talking-to-wordpress-hooks)

The dispatcher never touches WordPress hooks on its own — it calls listeners and nothing else, so no event of yours surfaces as an action unless you say so. But an event is just an object and `dispatch()` returns it, so bridging to `do_action()` is a one-liner you write wherever you dispatch. That keeps the hook policy — which events become hooks, and under what tag — entirely in your hands.

### Fire a hook for an event

[](#fire-a-hook-for-an-event)

Dispatch, then hand the same event to `do_action()`. Using the event's class name as the tag gives you a unique, namespaced hook for free:

```
$event = $dispatcher->dispatch(new PostViewed(42));

do_action($event::class, $event);
```

Anywhere else — another plugin, a theme's `functions.php` — a developer reacts with the class name (use `::class` so the namespaced string is exact):

```
add_action(PostViewed::class, function (PostViewed $event): void {
	// Read or modify $event here.
});
```

### Respect stopped propagation

[](#respect-stopped-propagation)

If the event is a `StoppableEvent`, check it before firing, so a listener that stopped propagation also suppresses the hook:

```
use X3P0\Event\StoppableEvent;

$event = $dispatcher->dispatch(new PostViewed(42));

if (! $event instanceof StoppableEvent || ! $event->isPropagationStopped()) {
	do_action($event::class, $event);
}
```

### Custom hook names

[](#custom-hook-names)

Nothing forces the class name — pass whatever stable, readable tag you want to publish. That decouples the public hook from the class, so you can rename or move the event later without breaking anyone listening:

```
do_action('acme/post_viewed', $event);
```

### Firing from many places

[](#firing-from-many-places)

If you fire the same event-and-hook from several spots, wrap the pair once rather than repeat it — a small helper (or your own dispatcher decorator) keeps it DRY and puts the stop-propagation check in a single place:

```
use X3P0\Event\Dispatcher;
use X3P0\Event\StoppableEvent;

function dispatch_with_hook(Dispatcher $dispatcher, object $event, string $hook = ''): object
{
	$event = $dispatcher->dispatch($event);

	if (! $event instanceof StoppableEvent || ! $event->isPropagationStopped()) {
		do_action($hook ?: $event::class, $event);
	}

	return $event;
}
```

---

Putting it all together
-----------------------

[](#putting-it-all-together)

No service container or framework is required — you wire it up by hand:

```
use X3P0\Event\PriorityListenerRegistry;
use X3P0\Event\EventDispatcher;

// The in-memory registry holds the listeners; the dispatcher reads it.
$inMemory   = new PriorityListenerRegistry();
$dispatcher = new EventDispatcher($inMemory);

// Register listeners on the in-memory provider…
$inMemory->listen(PostViewed::class, fn (PostViewed $e) => /* … */ null);

// …and, if you want, bridge to WordPress yourself after dispatch:
// do_action(PostViewed::class, $dispatcher->dispatch(new PostViewed(42)));

// Then dispatch events from your code.
$dispatcher->dispatch(new PostViewed(42));
```

Keep one `$dispatcher` (and one provider set-up) for your whole plugin so every part shares the same listeners.

---

Class reference
---------------

[](#class-reference)

Class / interfaceRole`Dispatcher`PSR-14-style contract: just `dispatch()``EventDispatcher`Dispatches events to their listeners, in the current request`ListenerProvider`Contract for "which listeners apply to this event?"`ListenerPriority`Enum of named priorities (`First` / `Normal` / `Last`) for `listen()``ListenerRegistry`Contract for the write side: `listen()` / `listenTo()` / `listenOnce()` / `listenOnceTo()` / `subscribe()` / `unsubscribe()` / `forget()` / `forgetId()` / `hasListeners()``ListenerId`Opaque handle to one registration, returned by `listen()` and removed with `forgetId()``PriorityListenerRegistry`In-memory registry; priority-ordered; `listen()` / `subscribe()``AggregateListenerProvider`Combines several providers into one`StoppableEvent`Contract for an event whose propagation can be stopped`Stoppable`Trait with a ready-made `StoppableEvent` implementation`NamedEvent`Contract for an event that also matches by a string name`Named`Trait implementing `NamedEvent` from a `NAME` class constant`Subscriber`Contract for a class that registers its own listeners via `subscribeListeners()``DiscoversListeners`Trait implementing `Subscriber::subscribeListeners()` from listener attributes`Attributes\ListenerAttribute`Contract for an attribute that registers the method it marks on a given registry`Attributes\Listen`Attribute counterpart to `listen()``Attributes\ListenTo`Attribute counterpart to `listenTo()``Attributes\ListenOnce`Attribute counterpart to `listenOnce()``Attributes\ListenOnceTo`Attribute counterpart to `listenOnceTo()``EventException`Marker interface implemented by every exception the library throws`InvalidListener`Thrown when a listener is neither a callable nor an invokable class name (extends `InvalidArgumentException`)`NotInvokable`Thrown when a class-name listener resolves to a non-invokable object (extends `LogicException`)

###  Health Score

24

—

LowBetter than 30% of packages

Maintenance63

Regular maintenance activity

Popularity12

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

### Embed Badge

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

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

###  Alternatives

[opengento/composer-registration-plugin

This plugin allows to compile the Magento2 components registrations on composer install/update.

101.8k](/packages/opengento-composer-registration-plugin)

PHPackages © 2026

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