cmatosbc / clotho
A PHP event system with attribute-based event handling
Requires
- php: ^8.1
- psr/event-dispatcher: ^1.0
Requires (Dev)
- phpunit/phpunit: ^9.0
README
Why Clotho?
In Greek mythology, Clotho (meaning "spinner") was one of the three Fates, or Moirai. She was responsible for spinning the thread of human life, determining when pivotal events would occur. Just as Clotho spun the threads that would become significant moments in one's life, this library helps you weave together the events that shape your application's behavior.
Overview
Clotho is a modern, attribute-based event system for PHP 8.1+. It provides a powerful and intuitive way to handle events in your application using PHP attributes. The library is PSR-14 compliant and offers features like:
- Declarative event handling using PHP attributes
- Support for both PSR-14 event objects and named events
- Priority-based event execution
- Wildcard event patterns
- Event groups
- Comprehensive error handling
Installation
composer require cmatosbc/clotho
Basic Usage
For more detailed examples and use cases, check out the examples/
folder in this repository.
1. Simple Event Handling
use Clotho\Attribute\EventBefore; use Clotho\Attribute\EventAfter; class UserService { #[EventBefore('user.create')] #[EventAfter('user.create')] public function createUser(string $name, string $email): array { return [ 'id' => 1, 'name' => $name, 'email' => $email, ]; } } // Set up the event system $dispatcher = new EventDispatcher(); $handler = new EventAttributeHandler($dispatcher); // Add event listeners $dispatcher->addEventListener('user.create', function (array $payload) { echo "User creation started with email: " . $payload['arguments'][1]; }); // Wrap and use the method $service = new UserService(); $createUser = $handler->wrapMethod($service, 'createUser'); $result = $createUser('John Doe', 'john@example.com');
2. Priority-based Execution
use Clotho\Event\BeforeMethodEvent; use Clotho\Event\AfterMethodEvent; class OrderService { #[EventBefore] public function submitOrder(array $items): array { return ['order_id' => 123, 'items' => $items]; } } // Higher priority listeners execute first $dispatcher->addEventListener(BeforeMethodEvent::class, function (BeforeMethodEvent $event) { // Validate stock levels if (!$this->checkStock($event->getArguments()[0])) { $event->stopPropagation(); } }, 20); $dispatcher->addEventListener(BeforeMethodEvent::class, function (BeforeMethodEvent $event) { // Check user credit }, 10); $dispatcher->addEventListener(BeforeMethodEvent::class, function (BeforeMethodEvent $event) { // Log order attempt }, 0); // After events with priority $dispatcher->addEventListener(AfterMethodEvent::class, function (AfterMethodEvent $event) { // High priority post-processing $result = $event->getResult(); // Process order result }, 20);
3. Wildcard Event Patterns
// Listen to all user events $dispatcher->addEventListener('user.*', function ($payload) { echo "User operation detected: " . $payload['event']; }); // Listen to all create operations $dispatcher->addEventListener('*.create', function ($payload) { echo "Create operation detected in module: " . $payload['module']; }); class UserService { #[EventBefore('user.profile.update')] #[EventBefore('user.settings.update')] public function updateUserData(string $section, array $data): array { return ['section' => $section, 'data' => $data]; } }
4. Event Groups
class GroupService { #[EventBefore('admin.group:create')] #[EventBefore('user.group:update')] public function manageGroup(string $operation, array $data): array { return ['operation' => $operation, 'data' => $data]; } } // Listen to all group creation events $dispatcher->addEventListener('*.group:create', function ($payload) { echo "Group creation in progress..."; });
5. Event Propagation Control
use Clotho\Event\BeforeMethodEvent; class UserService { #[EventBefore] public function deleteUser(int $userId): bool { // Delete user logic return true; } } $dispatcher->addEventListener(BeforeMethodEvent::class, function (BeforeMethodEvent $event) { if (!$this->hasPermission('delete_users')) { echo "Permission denied"; $event->stopPropagation(); return; } }, 20); $dispatcher->addEventListener(BeforeMethodEvent::class, function (BeforeMethodEvent $event) { // This won't execute if the previous listener stops propagation echo "Deleting user..."; }, 10);
Event Objects and Payload
Clotho supports both event objects and named events with payloads. When using attributes, both types are dispatched:
use Clotho\Event\BeforeMethodEvent; use Clotho\Event\AfterMethodEvent; use Clotho\Attribute\EventBefore; use Clotho\Attribute\EventAfter; class UserService { #[EventBefore('user.create')] #[EventAfter('user.create')] public function createUser(string $name, string $email): array { return [ 'id' => 1, 'name' => $name, 'email' => $email, ]; } } // Listen for event objects (useful for priority-based handling) $dispatcher->addEventListener(BeforeMethodEvent::class, function (BeforeMethodEvent $event) { $arguments = $event->getArguments(); echo "Creating user: " . $arguments[0]; }, 20); // Listen for named events (useful for domain-specific handling) $dispatcher->addEventListener('user.create', function (array $payload) { $event = $payload['event']; // BeforeMethodEvent or AfterMethodEvent $arguments = $payload['arguments']; echo "User creation event: " . $arguments[0]; });
Advanced Features
Event Objects
Clotho provides dedicated event objects for different scenarios:
use Clotho\Event\BeforeMethodEvent; use Clotho\Event\AfterMethodEvent; use Clotho\Event\BeforeFunctionEvent; use Clotho\Event\AfterFunctionEvent; // Listen for method events $dispatcher->addEventListener(BeforeMethodEvent::class, function (BeforeMethodEvent $event) { $methodName = $event->getMethodName(); $arguments = $event->getArguments(); if (!$this->isValid($arguments)) { $event->stopPropagation(); } }, 10); // Listen for after events with results $dispatcher->addEventListener(AfterMethodEvent::class, function (AfterMethodEvent $event) { if ($event->hasException()) { $this->handleError($event->getException()); return; } $result = $event->getResult(); $this->processResult($result); });
Priority-based Event Handling
Events can be handled with different priorities, where higher priority listeners execute first:
use Clotho\Event\BeforeMethodEvent; // High priority validation $dispatcher->addEventListener(BeforeMethodEvent::class, function (BeforeMethodEvent $event) { // Runs first (priority 20) if (!$this->hasPermission()) { $event->stopPropagation(); } }, 20); // Normal priority logging $dispatcher->addEventListener(BeforeMethodEvent::class, function (BeforeMethodEvent $event) { // Runs second (priority 10) $this->logAccess($event->getMethodName()); }, 10); // Low priority operations $dispatcher->addEventListener(BeforeMethodEvent::class, function (BeforeMethodEvent $event) { // Runs last (priority 0) $this->notify($event->getMethodName()); }, 0);
Event Propagation Control
All event objects implement StoppableEventInterface
, allowing you to control event propagation:
use Clotho\Event\BeforeMethodEvent; class UserService { #[EventBefore] public function deleteUser(int $userId): void { // Delete user logic } } // High priority security check $dispatcher->addEventListener(BeforeMethodEvent::class, function (BeforeMethodEvent $event) { if (!$this->hasPermission('delete_users')) { $event->stopPropagation(); // Prevents further listeners from executing throw new SecurityException('Permission denied'); } }, 20); // This won't execute if permission check fails $dispatcher->addEventListener(BeforeMethodEvent::class, function (BeforeMethodEvent $event) { $this->logDeletion($event->getArguments()[0]); }, 10);
Error Handling
Clotho provides a set of custom exceptions for handling error conditions in the event system. These exceptions are used only for truly exceptional cases, not for normal flow control.
Exception Types
use Clotho\Exception\EventDispatchException; use Clotho\Exception\EventListenerException; use Clotho\Exception\EventPropagationException; // Invalid event name try { $dispatcher->dispatch(''); // Empty event name } catch (EventDispatchException $e) { // Handles dispatch-related errors: // - Empty or invalid event names // - Errors thrown by event listeners } // Invalid listener priority try { // Priority must be between -100 and 100 $dispatcher->addEventListener('user.create', $listener, 150); } catch (EventListenerException $e) { // Handles listener-related errors: // - Invalid priority values // - Invalid listener types }
Normal Flow Control
The following scenarios are handled as normal flow control, not exceptions:
// 1. Events with no listeners - perfectly valid $dispatcher->dispatch('user.created', ['id' => 123]); // 2. Stopping event propagation - normal control flow $dispatcher->addEventListener('user.delete', function (BeforeMethodEvent $event) { if (!$this->hasPermission()) { $event->stopPropagation(); // Stops further listeners, no exception return; } // Process the event }, 20); // 3. Wildcard events with no matches - also valid $dispatcher->dispatch('custom.event');
Exception Hierarchy
EventException
├── EventDispatchException // For dispatch-related errors
├── EventListenerException // For listener configuration errors
└── EventPropagationException // Reserved for future propagation-related errors
Event Payload Structure
When using named events, Clotho dispatches an array with the following structure:
Before Method Events
[ 'event' => BeforeMethodEvent, // The actual event object 'object' => object, // The object instance the method belongs to 'method' => string, // Name of the method being called 'arguments' => array, // Array of arguments passed to the method ]
After Method Events
[ 'event' => AfterMethodEvent, // The actual event object 'object' => object, // The object instance the method belongs to 'method' => string, // Name of the method being called 'arguments' => array, // Array of arguments passed to the method 'result' => mixed, // The return value from the method (if successful) 'exception' => Throwable|null // Exception object if method threw an exception ]
Before Function Events
[ 'event' => BeforeFunctionEvent, // The actual event object 'function' => string, // Name of the function being called 'arguments' => array, // Array of arguments passed to the function ]
After Function Events
[ 'event' => AfterFunctionEvent, // The actual event object 'function' => string, // Name of the function being called 'arguments' => array, // Array of arguments passed to the function 'result' => mixed, // The return value from the function (if successful) 'exception' => Throwable|null // Exception object if function threw an exception ]
Example usage with payload:
use Clotho\Attribute\EventBefore; use Clotho\Attribute\EventAfter; class UserService { #[EventBefore('user.create')] #[EventAfter('user.create')] public function createUser(string $name, string $email): array { return [ 'id' => 1, 'name' => $name, 'email' => $email, ]; } } // Access payload in before event $dispatcher->addEventListener('user.create', function (array $payload) { $event = $payload['event']; // BeforeMethodEvent instance $object = $payload['object']; // UserService instance $method = $payload['method']; // "createUser" $arguments = $payload['arguments']; // [$name, $email] echo "Creating user {$arguments[0]} with email {$arguments[1]}"; }); // Access payload in after event $dispatcher->addEventListener('user.create', function (array $payload) { if (isset($payload['exception'])) { echo "Error creating user: " . $payload['exception']->getMessage(); return; } $result = $payload['result']; // The returned user array echo "Created user with ID: {$result['id']}"; });
Best Practices
-
Event Naming:
- Use lowercase, dot-separated names
- Start with module/context (e.g.,
user.
,order.
) - Use verbs for actions (e.g.,
create
,update
) - Use colons for groups (e.g.,
admin.group:create
)
-
Priority Levels:
- 20+ : Critical operations (validation, security)
- 10-19: Core business logic
- 0-9: Logging, notifications, non-critical operations
-
Event Payload:
- Keep payloads serializable
- Include only necessary data
- Consider security implications
Contributing
Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.
License
This project is licensed under the GNU General Public License v3.0 - see the LICENSE file for details.
Acknowledgments
- PSR-14 Event Dispatcher Interface
- PHP 8.1+ Attributes
- The PHP Community
Built with ❤️ by Carlos Artur Matos