quellabs/signal-hub

Type-safe signal/slot event system

Maintainers

Package info

github.com/quellabs/signal-hub

pkg:composer/quellabs/signal-hub

Statistics

Installs: 75

Dependents: 4

Suggesters: 0

Stars: 0

Open Issues: 0

1.1.0 2026-05-18 07:54 UTC

This package is auto-updated.

Last update: 2026-05-18 07:59:03 UTC


README

Latest Version License Downloads

A Qt-inspired signal-slot implementation for PHP. Loose coupling between components through automatic signal discovery, with PHP's type system handling slot type safety.

Installation

composer require quellabs/signal-hub

Basic Usage

Declare signals as typed properties — your class needs no knowledge of the hub:

use Quellabs\SignalHub\Signal;

class MollieController {

    public Signal $paymentPaid;
    public Signal $paymentFailed;

    public function handleWebhook(array $data): void {
        $payment = $this->fetchPayment($data['id']);
        $payment->isPaid() ? $this->paymentPaid->emit($payment) : $this->paymentFailed->emit($payment);
    }
}

Connect using a Slot — a wrapper around a callable that gives it stable object identity. Signal owns connected Slots strongly, so inline Slots are always safe. Store a Slot as a property only if you need to call disconnect() explicitly later.

use Quellabs\SignalHub\Slot;

$hub->discoverSignals($controller);

// Connect directly if you hold a reference...
$controller->paymentPaid->connect(new Slot(fn(Payment $p) => ...));

// ...or via the hub if you don't
$hub->getSignal(MollieController::class, 'paymentPaid')
    ->connect(new Slot(fn(Payment $p) => ...));

Standalone signals work without any owning object:

$signal = new Signal('app.booted');
$signal->connect(new Slot(fn() => ...));
$signal->emit();

Framework Integration

Call discoverSignals() from whatever instantiates your objects. The emitting class stays hub-unaware:

$hub->discoverSignals($controller);

try {
    $controller->handle($request);
} finally {
    $hub->unregisterSignals($controller);
}

Consumers connect in their constructor — no controller reference needed:

class InventoryService {
    public function __construct(SignalHub $hub) {
        $hub->getSignal(OrderController::class, 'orderPlaced')
            ->connect(new Slot([$this, 'onOrderPlaced']));
    }
}

Hub API

$hub->discoverSignals($controller);                      // discover Signal properties on an object
$hub->unregisterSignals($controller);                    // unregister all signals for an object
$hub->getSignal(MollieController::class, 'paymentPaid'); // look up by class name
$hub->getSignal('app.booted');                           // look up standalone signal
$hub->findSignals('payment.*');                          // wildcard search
$hub->findSignals('payment.*', $controller);             // wildcard + instance

Advanced Features

Priorities — priority belongs to the connection, not the slot, so the same Slot can have different priorities on different signals:

$signal->connect($auditSlot, priority: 100);    // runs first
$signal->connect($cleanupSlot, priority: -10);  // runs last

Shared slots — a single Slot can be connected to multiple signals simultaneously:

$slot = new Slot([$this, 'handleChange']);
$signalA->connect($slot, priority: 5);
$signalB->connect($slot, priority: 10);

$signalA->disconnect($slot); // still connected to $signalB

Explicit disconnect — store the Slot as a property and call disconnect() when needed:

class InventoryListener {
    private Slot $handlePrePersist;

    public function __construct(UnitOfWork $unitOfWork) {
        $this->unitOfWork = $unitOfWork;
        $this->handlePrePersist = new Slot([$this, 'handlePrePersist']);
        $unitOfWork->signalPrePersist->connect($this->handlePrePersist);
    }

    public function detach(): void {
        $this->unitOfWork->signalPrePersist->disconnect($this->handlePrePersist);
    }
}

Meta-signals — react to hub activity:

$hub->signalRegistered()->connect(new Slot(function(Signal $signal) {
    if (str_starts_with($signal->getName(), 'payment.')) {
        $signal->connect(new Slot($this->auditLogger(...)));
    }
}));

Architecture

Four classes, no traits:

  • Signal — holds connections, emits to slots (connect, disconnect, emit, isConnected)
  • Slot — wraps a callable with stable object identity; the unit of connection
  • SignalHub — registry and rendezvous point (discoverSignals, unregisterSignals, getSignal, findSignals)
  • SignalHubLocator — optional static accessor for use outside DI contexts

Signal owns its Slots via a plain array keyed by spl_object_id(), giving connections an unambiguous lifetime: a Slot stays connected until disconnect() is called or the Signal is destroyed. Object-owned signals on the hub are stored in a WeakMap so they are garbage collected automatically when the owning object goes out of scope.

License

MIT