Skip to content

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

  1. Event Naming: Use past tense for domain events (ArticlePublished, not PublishArticle)
  2. Event Immutability: Events should be immutable value objects
  3. Single Responsibility: Each listener should do one thing
  4. Idempotency: Listeners should handle duplicate events gracefully
  5. Async Processing: Consider queuing heavy listeners for background processing
  6. Error Handling: Failed listeners shouldn't break the main flow