feat: implement custom event dispatcher and listener system (#13)
All checks were successful
🧪✨ Tests Workflow / 🛡️ 🔒 Library Audit (push) Successful in 1m38s
🧪✨ Tests Workflow / 📝 ✨ Code Lint (push) Successful in 1m38s
🧪✨ Tests Workflow / 🛡️ 🔒 License Check (push) Successful in 1m40s
🧪✨ Tests Workflow / 🧪 ✨ Database Migrations (push) Successful in 1m52s
🧪✨ Tests Workflow / 🐙 🔍 Code Sniffer (push) Successful in 1m51s
🧪✨ Tests Workflow / 🧪 ✅ Unit Tests (push) Successful in 1m21s

Reviewed-on: #13
Co-authored-by: Ron Rise <ron@siteworxpro.com>
Co-committed-by: Ron Rise <ron@siteworxpro.com>
This commit was merged in pull request #13.
This commit is contained in:
2025-11-11 16:12:19 +00:00
committed by Siteworx Pro Gitea
parent 7d0b00fb89
commit eeb46bc982
8 changed files with 325 additions and 1 deletions

View File

@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace Siteworxpro\App\Annotations\Events;
use Attribute;
#[Attribute(Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)]
readonly class ListensFor
{
public function __construct(public string $eventClass)
{
}
}

205
src/Events/Dispatcher.php Normal file
View File

@@ -0,0 +1,205 @@
<?php
declare(strict_types=1);
namespace Siteworxpro\App\Events;
use Illuminate\Contracts\Events\Dispatcher as DispatcherContract;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Support\Collection;
use Siteworxpro\App\Annotations\Events\ListensFor;
/**
* Class Dispatcher
*
* A custom event dispatcher that automatically registers event listeners
* based on the ListensFor attribute.
*
* @package Siteworxpro\App\Events
*/
class Dispatcher implements DispatcherContract, Arrayable
{
/**
* @var array $listeners Registered event listeners
*/
private array $listeners = [];
/**
* @var Collection $pushed Pushed events collection
*/
private Collection $pushed;
/**
* @var string LISTENERS_NAMESPACE The namespace where listeners are located
*/
private const string LISTENERS_NAMESPACE = 'Siteworxpro\\App\\Events\\Listeners\\';
public function __construct()
{
$this->pushed = new Collection();
$this->registerListeners();
}
/**
* Register event listeners based on the ListensFor attribute.
*
* @return void
*/
private function registerListeners(): void
{
// traverse the Listeners directory and register all listeners
$listenersPath = __DIR__ . '/Listeners';
$iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($listenersPath));
foreach ($iterator as $file) {
if ($file->isFile() && $file->getExtension() === 'php') {
$relativePath = str_replace($listenersPath . '/', '', $file->getPathname());
$className = self::LISTENERS_NAMESPACE . str_replace(['/', '.php'], ['\\', ''], $relativePath);
if (class_exists($className)) {
$reflectionClass = new \ReflectionClass($className);
$attributes = $reflectionClass->getAttributes(ListensFor::class);
foreach ($attributes as $attribute) {
$instance = $attribute->newInstance();
$eventClass = $instance->eventClass;
$this->listen($eventClass, new $className());
}
}
}
}
}
/**
* Register a listener for the given events.
*
* @param $events
* @param $listener
* @return void
*/
public function listen($events, $listener = null): void
{
$this->listeners[$events][] = $listener;
}
/**
* Check if there are listeners for the given event.
*
* @param $eventName
* @return bool
*/
public function hasListeners($eventName): bool
{
return isset($this->listeners[$eventName]) && !empty($this->listeners[$eventName]);
}
/**
* Subscribe a subscriber to the dispatcher.
*
* @param Arrayable $subscriber
* @return void
*/
public function subscribe($subscriber): void
{
$this->listeners = array_merge($this->listeners, (array) $subscriber);
}
/**
* Dispatch an event and halt on the first non-null response.
*
* @param $event
* @param array $payload
* @return array|null
*/
public function until($event, $payload = []): array|null
{
return $this->dispatch($event, $payload, true);
}
/**
* Dispatch an event to its listeners.
*
* @param $event
* @param array $payload
* @param bool $halt
* @return array|null
*/
public function dispatch($event, $payload = [], $halt = false): array|null
{
if (is_object($event)) {
$eventClass = get_class($event);
} else {
$eventClass = $event;
}
$listeners = $this->listeners[$eventClass] ?? null;
if ($listeners === null) {
return null;
}
$responses = [];
foreach ($listeners as $listener) {
$response = $listener($event, $payload);
$responses[] = $response;
if ($halt && $response !== null) {
return $response;
}
}
return $responses;
}
/**
* Push an event to be dispatched later.
*
* @param $event
* @param array $payload
* @return void
*/
public function push($event, $payload = []): void
{
$this->pushed->put($event, $payload);
}
/**
* Flush a pushed event, dispatching it if it exists.
*
* @param $event
* @return void
*/
public function flush($event): void
{
if ($this->pushed->has($event)) {
$payload = $this->pushed->get($event);
$this->dispatch($event, $payload);
$this->pushed->forget([$event]);
}
}
/**
* Forget a pushed event without dispatching it.
*
* @param $event
* @return void
*/
public function forget($event): void
{
$this->pushed->forget([$event]);
}
/**
* Forget all pushed events.
*
* @return void
*/
public function forgetPushed(): void
{
$this->pushed = new Collection();
}
public function toArray(): array
{
return $this->listeners;
}
}

View File

@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace Siteworxpro\App\Events\Listeners\Database;
use Illuminate\Database\Events\ConnectionEstablished;
use Illuminate\Database\Events\ConnectionEvent;
use Siteworxpro\App\Annotations\Events\ListensFor;
use Siteworxpro\App\Events\Listeners\Listener;
use Siteworxpro\App\Services\Facades\Logger;
#[ListensFor(ConnectionEstablished::class)]
class Connected extends Listener
{
/**
* @param ConnectionEvent $event
* @param array $payload
* @return null
*/
public function __invoke($event, array $payload = []): null
{
Logger::info("Database connection event", [get_class($event), $event->connectionName]);
return null;
}
}

View File

@@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Siteworxpro\App\Events\Listeners;
abstract class Listener implements ListenerInterface
{
}

View File

@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace Siteworxpro\App\Events\Listeners;
interface ListenerInterface
{
public function __invoke(mixed $event, array $payload = []): mixed;
}

View File

@@ -8,6 +8,8 @@ use Illuminate\Support\ServiceProvider;
use Siteworx\Config\Config as SWConfig;
use Siteworxpro\App\Services\Facade;
use Siteworxpro\App\Services\Facades\Config;
use Siteworxpro\App\Services\Facades\Dispatcher;
use Siteworxpro\App\Services\ServiceProviders\DispatcherServiceProvider;
use Siteworxpro\App\Services\ServiceProviders\LoggerServiceProvider;
use Siteworxpro\App\Services\ServiceProviders\RedisServiceProvider;
@@ -15,7 +17,8 @@ class Kernel
{
private static array $serviceProviders = [
LoggerServiceProvider::class,
RedisServiceProvider::class
RedisServiceProvider::class,
DispatcherServiceProvider::class,
];
/**
@@ -69,6 +72,7 @@ class Kernel
private static function bootModelCapsule(): void
{
$capsule = new Manager();
$capsule->setEventDispatcher(Dispatcher::getFacadeRoot());
$capsule->addConnection(Config::get('db'));
$capsule->setAsGlobal();
$capsule->bootEloquent();

View File

@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace Siteworxpro\App\Services\Facades;
use Siteworxpro\App\Events\Dispatcher as DispatcherConcrete;
use Siteworxpro\App\Services\Facade;
/**
* Class Dispatcher
*
* A facade for the event dispatcher.
*
* @package Siteworxpro\App\Services\Facades
*
* @method static void listen(string $event, callable|string $listener)
* @method static void dispatch(object|string $event, array $payload = [], bool $halt = false)
* @method static void push(object|string $event, array $payload = [])
* @method static array|null until(object|string $event, array $payload = [])
* @method static bool hasListeners(string $eventName)
* @method static void subscribe(mixed $subscriber)
*/
class Dispatcher extends Facade
{
/**
* Get the registered name of the component.
*
* @return string The name of the component.
*/
protected static function getFacadeAccessor(): string
{
return DispatcherConcrete::class;
}
}

View File

@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Siteworxpro\App\Services\ServiceProviders;
use Illuminate\Support\ServiceProvider;
use Siteworxpro\App\Events\Dispatcher;
class DispatcherServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->singleton(Dispatcher::class, function () {
return new Dispatcher();
});
}
}