PHPackages                             cmatosbc/clotho - 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. cmatosbc/clotho

ActiveLibrary

cmatosbc/clotho
===============

A PHP event system with attribute-based event handling

1.0.0(1y ago)02GPL-3.0-or-laterPHPPHP ^8.1

Since Dec 2Pushed 1y ago1 watchersCompare

[ Source](https://github.com/cmatosbc/clotho)[ Packagist](https://packagist.org/packages/cmatosbc/clotho)[ RSS](/packages/cmatosbc-clotho/feed)WikiDiscussions main Synced 1mo ago

READMEChangelog (1)Dependencies (2)Versions (2)Used By (0)

Clotho - PHP Event Attribute Library
====================================

[](#clotho---php-event-attribute-library)

[![PHP Version](https://camo.githubusercontent.com/04744bae0a61d2ffe29c26f07a9612eae20445fc6feaeb77b3af1f0e9be6447c/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f7068702d253345253344382e312d3838393242462e737667)](https://php.net/)[![License](https://camo.githubusercontent.com/c8e817d0fab13b6b935489e0692f5301982fdcc96451d589d7444f2055cf9a7c/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4c6963656e73652d47504c2d2d332e302d626c75652e737667)](https://www.gnu.org/licenses/gpl-3.0.html)

Why Clotho?
-----------

[](#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
--------

[](#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
------------

[](#installation)

```
composer require cmatosbc/clotho
```

Basic Usage
-----------

[](#basic-usage)

For more detailed examples and use cases, check out the `examples/` folder in this repository.

### 1. Simple Event Handling

[](#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

[](#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

[](#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

[](#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

[](#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

[](#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
-----------------

[](#advanced-features)

### Event Objects

[](#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

[](#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

[](#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
--------------

[](#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

[](#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

[](#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

[](#exception-hierarchy)

```
EventException
├── EventDispatchException  // For dispatch-related errors
├── EventListenerException  // For listener configuration errors
└── EventPropagationException  // Reserved for future propagation-related errors

```

Event Payload Structure
-----------------------

[](#event-payload-structure)

When using named events, Clotho dispatches an array with the following structure:

#### Before Method Events

[](#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

[](#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

[](#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

[](#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
--------------

[](#best-practices)

1. **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`)
2. **Priority Levels**:

    - 20+ : Critical operations (validation, security)
    - 10-19: Core business logic
    - 0-9: Logging, notifications, non-critical operations
3. **Event Payload**:

    - Keep payloads serializable
    - Include only necessary data
    - Consider security implications

Contributing
------------

[](#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
-------

[](#license)

This project is licensed under the GNU General Public License v3.0 - see the [LICENSE](LICENSE) file for details.

Acknowledgments
---------------

[](#acknowledgments)

- PSR-14 Event Dispatcher Interface
- PHP 8.1+ Attributes
- The PHP Community

---

Built with ❤️ by [Carlos Artur Matos](https://github.com/cmatosbc)

###  Health Score

25

—

LowBetter than 37% of packages

Maintenance38

Infrequent updates — may be unmaintained

Popularity2

Limited adoption so far

Community7

Small or concentrated contributor base

Maturity47

Maturing project, gaining track record

 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.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

531d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/3d97279f52b70d502ac21c0618e2822a0bae0523a7e69bb3fd33a592f21d5f06?d=identicon)[cmatosbc](/maintainers/cmatosbc)

---

Top Contributors

[![cmatosbc](https://avatars.githubusercontent.com/u/118901076?v=4)](https://github.com/cmatosbc "cmatosbc (7 commits)")

---

Tags

event-listenerseventslistenersphp-eventsphp-libraryphp8

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/cmatosbc-clotho/health.svg)

```
[![Health](https://phpackages.com/badges/cmatosbc-clotho/health.svg)](https://phpackages.com/packages/cmatosbc-clotho)
```

###  Alternatives

[symfony/symfony

The Symfony PHP framework

31.3k86.3M2.2k](/packages/symfony-symfony)[league/commonmark

Highly-extensible PHP Markdown parser which fully supports the CommonMark spec and GitHub-Flavored Markdown (GFM)

2.9k404.0M702](/packages/league-commonmark)[symfony/mailer

Helps sending emails

1.6k368.1M955](/packages/symfony-mailer)[irazasyed/telegram-bot-sdk

The Unofficial Telegram Bot API PHP SDK

3.3k4.5M84](/packages/irazasyed-telegram-bot-sdk)[shopware/platform

The Shopware e-commerce core

3.3k1.5M3](/packages/shopware-platform)[phpro/soap-client

A general purpose SoapClient library

8885.6M46](/packages/phpro-soap-client)

PHPackages © 2026

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