Events and Hooks
Overview
The Gold Standard Module implements a comprehensive event system that enables loose coupling between components and allows other modules to extend functionality without modifying core code. This follows the Observer pattern and integrates with XOOPS's notification system.
Event Architecture
flowchart TB
subgraph "Event Flow"
A[Action Occurs] --> B[Event Created]
B --> C[Event Dispatcher]
C --> D{Listeners}
D --> E[Listener 1]
D --> F[Listener 2]
D --> G[Listener N]
E --> H[Side Effects]
F --> H
G --> H
end
subgraph "Event Types"
I[Domain Events]
J[Application Events]
K[Integration Events]
end
Event Types
Domain Events
Domain events represent something significant that happened in the business domain:
namespace Xoops\Modules\GoldStandard\Domain\Events;
use Xoops\Modules\Xmf\Domain\Event\DomainEvent;
final class ArticlePublished implements DomainEvent
{
public function __construct(
private readonly string $articleId,
private readonly string $authorId,
private readonly \DateTimeImmutable $publishedAt,
private readonly string $title,
private readonly array $categoryIds
) {}
public function getArticleId(): string
{
return $this->articleId;
}
public function getAuthorId(): string
{
return $this->authorId;
}
public function getPublishedAt(): \DateTimeImmutable
{
return $this->publishedAt;
}
public function getTitle(): string
{
return $this->title;
}
public function getCategoryIds(): array
{
return $this->categoryIds;
}
public function occurredAt(): \DateTimeImmutable
{
return $this->publishedAt;
}
}
Available Domain Events
| Event | Description | Payload |
ArticleCreated | New article created (draft) | articleId, authorId, title |
ArticlePublished | Article made public | articleId, authorId, publishedAt |
ArticleUpdated | Article content changed | articleId, editorId, changes |
ArticleDeleted | Article removed | articleId, deletedBy |
ArticleArchived | Article moved to archive | articleId, archivedAt |
CategoryCreated | New category added | categoryId, name, parentId |
CategoryMoved | Category hierarchy changed | categoryId, oldParent, newParent |
CommentAdded | Comment posted | commentId, articleId, authorId |
CommentModerated | Comment approved/rejected | commentId, status, moderatorId |
Event Dispatcher
Dispatching Events
namespace Xoops\Modules\GoldStandard\Application\Services;
use Xoops\Modules\Xmf\Application\EventDispatcher;
class ArticleService
{
public function __construct(
private readonly ArticleRepository $repository,
private readonly EventDispatcher $dispatcher
) {}
public function publish(string $articleId): void
{
$article = $this->repository->findById($articleId);
if (!$article) {
throw new ArticleNotFoundException($articleId);
}
$article->publish();
$this->repository->save($article);
// Dispatch the event
$this->dispatcher->dispatch(new ArticlePublished(
articleId: $article->getId(),
authorId: $article->getAuthorId(),
publishedAt: new \DateTimeImmutable(),
title: $article->getTitle(),
categoryIds: $article->getCategoryIds()
));
}
}
Event Dispatcher Implementation
namespace Xoops\Modules\GoldStandard\Infrastructure\Events;
use Xoops\Modules\Xmf\Application\EventDispatcher;
use Xoops\Modules\Xmf\Domain\Event\DomainEvent;
class XoopsEventDispatcher implements EventDispatcher
{
private array $listeners = [];
public function addListener(string $eventClass, callable $listener, int $priority = 0): void
{
$this->listeners[$eventClass][$priority][] = $listener;
}
public function dispatch(DomainEvent $event): void
{
$eventClass = get_class($event);
if (!isset($this->listeners[$eventClass])) {
return;
}
// Sort by priority (higher first)
krsort($this->listeners[$eventClass]);
foreach ($this->listeners[$eventClass] as $listeners) {
foreach ($listeners as $listener) {
$listener($event);
}
}
// Also trigger XOOPS native events for backward compatibility
$this->triggerXoopsEvent($event);
}
private function triggerXoopsEvent(DomainEvent $event): void
{
$eventName = $this->getXoopsEventName($event);
// Trigger through XOOPS preload system
$preload = \XoopsPreload::getInstance();
$preload->triggerEvent($eventName, [$event]);
}
private function getXoopsEventName(DomainEvent $event): string
{
$shortName = (new \ReflectionClass($event))->getShortName();
return 'goldstandard.' . $this->camelToSnake($shortName);
}
private function camelToSnake(string $input): string
{
return strtolower(preg_replace('/(?<!^)[A-Z]/', '_$0', $input));
}
}
Event Listeners
Creating a Listener
namespace Xoops\Modules\GoldStandard\Application\Listeners;
use Xoops\Modules\GoldStandard\Domain\Events\ArticlePublished;
class SendNotificationOnPublish
{
public function __construct(
private readonly NotificationService $notifications
) {}
public function __invoke(ArticlePublished $event): void
{
// Notify subscribers
$this->notifications->notifySubscribers(
'article_published',
[
'article_id' => $event->getArticleId(),
'title' => $event->getTitle()
]
);
// Notify category followers
foreach ($event->getCategoryIds() as $categoryId) {
$this->notifications->notifyCategoryFollowers(
$categoryId,
'new_article',
['article_id' => $event->getArticleId()]
);
}
}
}
Registering Listeners
// In module's bootstrap or service provider
$dispatcher = $container->get(EventDispatcher::class);
// Register with priority (higher = earlier)
$dispatcher->addListener(
ArticlePublished::class,
new SendNotificationOnPublish($notifications),
priority: 100
);
$dispatcher->addListener(
ArticlePublished::class,
new UpdateSearchIndex($searchService),
priority: 50
);
$dispatcher->addListener(
ArticlePublished::class,
new LogArticlePublished($logger),
priority: 10
);
XOOPS Preload Integration
Preload Class
// preload/preload.php
class GoldStandardPreload extends \XoopsPreloadItem
{
// Hook into core XOOPS events
public static function eventCoreIncludeCommonEnd(array $args): void
{
// Module initialization after XOOPS core loads
}
public static function eventCoreHeaderStart(array $args): void
{
// Add custom CSS/JS before header output
}
public static function eventCoreFooterStart(array $args): void
{
// Actions before footer renders
}
// Module-specific events
public static function eventGoldstandardArticlePublished(array $args): void
{
$event = $args[0];
// Handle article published event from other modules
}
}
Available XOOPS Core Events
| Event | Trigger Point |
core.include.common.start | Beginning of common.php |
core.include.common.end | End of common.php |
core.header.start | Before header template |
core.header.end | After header template |
core.footer.start | Before footer template |
core.footer.end | After footer template |
core.class.module.textsanitizer | Text sanitizer loaded |
core.class.theme.blocks | Before blocks render |
Notification Integration
Notification Events
namespace Xoops\Modules\GoldStandard\Application\Listeners;
use Xoops\Modules\GoldStandard\Domain\Events\CommentAdded;
class NotifyOnNewComment
{
public function __invoke(CommentAdded $event): void
{
$notificationHandler = xoops_getHandler('notification');
// Notify article author
$notificationHandler->triggerEvent(
'goldstandard',
'article',
$event->getArticleId(),
'new_comment',
[
'ARTICLE_TITLE' => $event->getArticleTitle(),
'COMMENT_AUTHOR' => $event->getCommentAuthorName()
]
);
// Notify other commenters (comment thread)
$notificationHandler->triggerEvent(
'goldstandard',
'article',
$event->getArticleId(),
'comment_thread',
[
'ARTICLE_TITLE' => $event->getArticleTitle()
]
);
}
}
Notification Configuration
// xoops_version.php notification section
$modversion['notification'] = [
'category' => [
[
'name' => 'global',
'title' => _MI_GOLDSTANDARD_GLOBAL_NOTIFY,
'description' => _MI_GOLDSTANDARD_GLOBAL_NOTIFY_DESC,
'subscribe_from' => ['index.php', 'article.php']
],
[
'name' => 'category',
'title' => _MI_GOLDSTANDARD_CATEGORY_NOTIFY,
'description' => _MI_GOLDSTANDARD_CATEGORY_NOTIFY_DESC,
'subscribe_from' => ['category.php'],
'item_name' => 'category_id',
'allow_bookmark' => true
],
[
'name' => 'article',
'title' => _MI_GOLDSTANDARD_ARTICLE_NOTIFY,
'description' => _MI_GOLDSTANDARD_ARTICLE_NOTIFY_DESC,
'subscribe_from' => ['article.php'],
'item_name' => 'article_id',
'allow_bookmark' => true
]
],
'event' => [
[
'name' => 'new_article',
'category' => 'global',
'title' => _MI_GOLDSTANDARD_NOTIFY_NEW_ARTICLE,
'caption' => _MI_GOLDSTANDARD_NOTIFY_NEW_ARTICLE_CAP,
'mail_template' => 'notify_new_article',
'mail_subject' => _MI_GOLDSTANDARD_NOTIFY_NEW_ARTICLE_SBJ
],
[
'name' => 'new_comment',
'category' => 'article',
'title' => _MI_GOLDSTANDARD_NOTIFY_NEW_COMMENT,
'caption' => _MI_GOLDSTANDARD_NOTIFY_NEW_COMMENT_CAP,
'mail_template' => 'notify_new_comment',
'mail_subject' => _MI_GOLDSTANDARD_NOTIFY_NEW_COMMENT_SBJ
]
]
];
Custom Hooks
Hook System
flowchart LR
A[Hook Point] --> B{Hook Registry}
B --> C[Handler 1]
B --> D[Handler 2]
B --> E[Handler N]
C --> F[Modified Data]
D --> F
E --> F
F --> G[Continue Processing]
Defining Hook Points
namespace Xoops\Modules\GoldStandard\Infrastructure\Hooks;
class HookManager
{
private array $hooks = [];
public function register(string $hookName, callable $handler, int $priority = 10): void
{
$this->hooks[$hookName][$priority][] = $handler;
}
public function apply(string $hookName, mixed $value, array $context = []): mixed
{
if (!isset($this->hooks[$hookName])) {
return $value;
}
ksort($this->hooks[$hookName]);
foreach ($this->hooks[$hookName] as $handlers) {
foreach ($handlers as $handler) {
$value = $handler($value, $context);
}
}
return $value;
}
public function do(string $hookName, array $context = []): void
{
if (!isset($this->hooks[$hookName])) {
return;
}
ksort($this->hooks[$hookName]);
foreach ($this->hooks[$hookName] as $handlers) {
foreach ($handlers as $handler) {
$handler($context);
}
}
}
}
Using Hooks in Templates
// In article display service
public function prepareArticleForDisplay(Article $article): array
{
$data = [
'id' => $article->getId(),
'title' => $article->getTitle(),
'content' => $article->getContent(),
'author' => $article->getAuthorName()
];
// Allow other modules to modify display data
$data = $this->hooks->apply('goldstandard_article_display', $data, [
'article' => $article
]);
// Hook for adding custom fields
$data['custom_fields'] = [];
$this->hooks->do('goldstandard_article_custom_fields', [
'article' => $article,
'fields' => &$data['custom_fields']
]);
return $data;
}
Registering Hook Handlers
// From another module
$hooks = $container->get(HookManager::class);
// Modify article display data
$hooks->register('goldstandard_article_display', function(array $data, array $context) {
// Add reading time estimate
$wordCount = str_word_count(strip_tags($data['content']));
$data['reading_time'] = ceil($wordCount / 200);
return $data;
}, priority: 20);
// Add custom fields
$hooks->register('goldstandard_article_custom_fields', function(array $context) {
$article = $context['article'];
$fields = &$context['fields'];
// Add custom metadata from another module
$fields['popularity_score'] = $this->getPopularityScore($article->getId());
});
Available Hook Points
Content Hooks
| Hook Name | Type | Description |
goldstandard_article_display | Filter | Modify article data before display |
goldstandard_article_save | Filter | Modify article before saving |
goldstandard_content_render | Filter | Modify rendered content HTML |
goldstandard_excerpt_generate | Filter | Customize excerpt generation |
Action Hooks
| Hook Name | Type | Description |
goldstandard_before_publish | Action | Before article publication |
goldstandard_after_publish | Action | After article publication |
goldstandard_before_delete | Action | Before article deletion |
goldstandard_after_delete | Action | After article deletion |
Admin Hooks
| Hook Name | Type | Description |
goldstandard_admin_menu | Filter | Add admin menu items |
goldstandard_admin_dashboard | Action | Add dashboard widgets |
goldstandard_article_form | Filter | Modify article edit form |
Event Sourcing Integration
For modules requiring full audit trails, events can be stored:
namespace Xoops\Modules\GoldStandard\Infrastructure\Events;
class EventStore
{
public function __construct(
private readonly \XoopsDatabase $db
) {}
public function append(DomainEvent $event): void
{
$this->db->queryF(
"INSERT INTO {$this->db->prefix('goldstandard_events')}
(event_type, aggregate_id, payload, occurred_at)
VALUES (?, ?, ?, ?)",
[
get_class($event),
$this->extractAggregateId($event),
json_encode($this->serialize($event)),
$event->occurredAt()->format('Y-m-d H:i:s')
]
);
}
public function getEventsFor(string $aggregateId): array
{
$result = $this->db->query(
"SELECT * FROM {$this->db->prefix('goldstandard_events')}
WHERE aggregate_id = ? ORDER BY occurred_at",
[$aggregateId]
);
$events = [];
while ($row = $this->db->fetchArray($result)) {
$events[] = $this->deserialize($row);
}
return $events;
}
}
Best Practices
- Event Naming: Use past tense for domain events (
ArticlePublished, not PublishArticle) - Event Immutability: Events should be immutable value objects
- Single Responsibility: Each listener should do one thing
- Idempotency: Listeners should handle duplicate events gracefully
- Async Processing: Consider queuing heavy listeners for background processing
- Error Handling: Failed listeners shouldn't break the main flow