Skip to content

XOOPS 2026 Architecture Vision

Overview

XOOPS 2026 introduces a fundamentally new architecture based on modern PHP standards, particularly the PSR-15 middleware pipeline. This document outlines the architectural vision and design principles.

High-Level Architecture

graph TB
    subgraph Client["Client Layer"]
        Browser["🌐 Web Browser"]
        API["📱 API Client"]
    end

    subgraph Core["XOOPS 2026 Core"]
        direction TB
        Kernel["⚙️ Kernel"]
        DI["📦 DI Container<br/>(PSR-11)"]
        Router["🔀 Router"]
        Middleware["🛡️ Middleware Stack<br/>(PSR-15)"]
        Events["📡 Event Dispatcher<br/>(PSR-14)"]
    end

    subgraph Services["Service Layer"]
        Cache["💾 Cache"]
        DB["🗄️ Database"]
        View["🎨 View Renderer"]
        Logger["📝 Logger<br/>(PSR-3)"]
    end

    subgraph Modules["Module Layer"]
        M1["📦 Publisher"]
        M2["📦 News"]
        M3["📦 Forum"]
        Legacy["📦 Legacy Modules"]
    end

    Browser --> Kernel
    API --> Kernel
    Kernel --> DI
    Kernel --> Middleware
    Middleware --> Router
    Router --> Modules
    DI --> Services
    Events --> Modules
    Modules --> Services

    style Core fill:#e3f2fd,stroke:#1976d2
    style Services fill:#fff3e0,stroke:#f57c00
    style Modules fill:#e8f5e9,stroke:#388e3c

Core Principles

1. Standards Compliance

  • Full PSR compliance for interoperability
  • Composer-based dependency management
  • Type-safe code with PHP 8.2+ features

2. Separation of Concerns

  • Clear boundaries between layers
  • Dependency injection throughout
  • Interface-driven design

3. Backward Compatibility

  • Legacy modules continue to work
  • Gradual migration path
  • Compatibility shims where needed

Request Lifecycle

PSR-15 Middleware Pipeline

The request flows through a stack of middleware, each capable of processing the request or delegating to the next handler:

flowchart TB
    subgraph Request["HTTP Request"]
        REQ[("🌐 Client Request")]
    end

    subgraph Kernel["XOOPS Kernel"]
        direction TB
        subgraph Stack["Middleware Stack"]
            M1[🛡️ Error Handler]
            M2[🔒 Security<br/>CSRF / Rate Limiting]
            M3[📋 Session]
            M4[👤 Authentication]
            M5[🔀 Router]
            M6[📦 Module Loader]
            M7[🎯 Controller Dispatcher]
        end
    end

    subgraph Response["HTTP Response"]
        RES[("📤 Response")]
    end

    REQ --> M1
    M1 --> M2
    M2 --> M3
    M3 --> M4
    M4 --> M5
    M5 --> M6
    M6 --> M7
    M7 --> RES

    style Request fill:#e3f2fd,stroke:#1976d2
    style Kernel fill:#fff3e0,stroke:#f57c00
    style Stack fill:#f5f5f5,stroke:#9e9e9e
    style Response fill:#e8f5e9,stroke:#388e3c

Middleware Onion Model

flowchart LR
    subgraph Onion["Request → Response Flow"]
        direction LR
        A["Request"] --> B
        subgraph B["Error Handler"]
            subgraph C["Security"]
                subgraph D["Session"]
                    subgraph E["Auth"]
                        subgraph F["Router"]
                            G["Controller"]
                        end
                    end
                end
            end
        end
        B --> H["Response"]
    end

    style Onion fill:#fafafa
    style G fill:#4caf50,color:#fff

Middleware Interface

namespace Psr\Http\Server;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;

interface MiddlewareInterface
{
    public function process(
        ServerRequestInterface $request,
        RequestHandlerInterface $handler
    ): ResponseInterface;
}

Example Middleware Implementation

namespace Xoops\Core\Middleware;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;

class CsrfMiddleware implements MiddlewareInterface
{
    public function __construct(
        private readonly CsrfTokenManager $tokenManager
    ) {}

    public function process(
        ServerRequestInterface $request,
        RequestHandlerInterface $handler
    ): ResponseInterface {
        // Skip for safe methods
        if (in_array($request->getMethod(), ['GET', 'HEAD', 'OPTIONS'])) {
            return $handler->handle($request);
        }

        // Validate CSRF token
        $token = $request->getParsedBody()['_csrf_token'] ?? '';

        if (!$this->tokenManager->isValid($token)) {
            throw new CsrfValidationException('Invalid CSRF token');
        }

        return $handler->handle($request);
    }
}

Service Container Architecture

Dependency Injection Flow

flowchart LR
    subgraph Container["PSR-11 Container"]
        direction TB
        Config["📋 Configuration"]
        Factory["🏭 Service Factories"]
        Instances["📦 Service Instances"]
    end

    subgraph Consumers["Service Consumers"]
        Controller["🎯 Controller"]
        Middleware["🛡️ Middleware"]
        Handler["📋 Handler"]
    end

    Config --> Factory
    Factory --> Instances
    Instances --> Controller
    Instances --> Middleware
    Instances --> Handler

    style Container fill:#e8f5e9,stroke:#388e3c
    style Consumers fill:#e3f2fd,stroke:#1976d2

PSR-11 Container

XOOPS 2026 uses a PSR-11 compliant dependency injection container:

use Psr\Container\ContainerInterface;

interface ContainerInterface
{
    public function get(string $id): mixed;
    public function has(string $id): bool;
}

Service Registration

// services.php
return [
    // Core Services
    'logger' => fn(ContainerInterface $c) => new Monolog\Logger('xoops'),

    'database' => fn(ContainerInterface $c) => new Connection(
        $c->get('config')->get('database')
    ),

    // View Services
    ViewRendererInterface::class => fn(ContainerInterface $c) =>
        new SmartyViewRenderer($c->get('smarty')),

    // Module Services
    'publisher.repository' => fn(ContainerInterface $c) =>
        new ArticleRepository($c->get('database')),
];

Container Bridge for Legacy Modules

namespace Xoops\Core;

use Psr\Container\ContainerInterface;

class Xoops
{
    private static ?ContainerInterface $container = null;

    public static function services(): ContainerInterface
    {
        if (self::$container === null) {
            self::$container = require XOOPS_ROOT_PATH . '/core/bootstrap_container.php';
        }
        return self::$container;
    }

    public static function service(string $id): mixed
    {
        return self::services()->get($id);
    }
}

// Usage in legacy code
$logger = \Xoops::service('logger');
$db = \Xoops::service('database');

Router Architecture

Routing Flow

sequenceDiagram
    participant R as Request
    participant RM as Router Middleware
    participant RC as Route Collection
    participant M as Module
    participant C as Controller

    R->>RM: HTTP Request
    RM->>RC: Match Route
    RC->>RC: Parse module.json routes
    RC-->>RM: RouteMatch
    RM->>M: Load Module
    M->>C: Dispatch to Controller
    C-->>R: Response

Route Definition

Routes are defined in module.json:

{
    "routes": {
        "article.list": {
            "path": "/articles",
            "method": ["GET"],
            "action": "Controller\\ArticleController::list"
        },
        "article.view": {
            "path": "/articles/{id:\\d+}",
            "method": ["GET"],
            "action": "Controller\\ArticleController::view"
        },
        "article.create": {
            "path": "/articles",
            "method": ["POST"],
            "action": "Controller\\ArticleController::create",
            "middleware": ["auth", "csrf"]
        }
    }
}

Router Independence Interface

namespace Xoops\Core\Routing;

interface RouteMatchInterface
{
    public function getName(): ?string;
    public function getParams(): array;
    public function getModuleSlug(): ?string;
    public function getHandler(): string;
    public function getMiddleware(): array;
}

URL Generation

namespace Xoops\Core\Routing;

interface UrlGeneratorInterface
{
    public function generate(
        string $name,
        array $params = [],
        bool $absolute = false
    ): string;
}

// Usage
$url = $urlGenerator->generate('article.view', ['id' => 42]);
// Returns: /modules/publisher/articles/42

View Layer Architecture

ViewRendererInterface

namespace Xoops\Core\View;

interface ViewRendererInterface
{
    /**
     * Render a template with data
     *
     * @param string $template Template path (e.g., '@modules/news/index')
     * @param array $data Template variables
     * @return string Rendered HTML
     */
    public function render(string $template, array $data = []): string;
}

Smarty Adapter

namespace Xoops\Core\View;

class SmartyViewRenderer implements ViewRendererInterface
{
    public function __construct(
        private readonly \Smarty $smarty,
        private readonly TemplatePathResolver $resolver
    ) {}

    public function render(string $template, array $data = []): string
    {
        $path = $this->resolver->resolve($template);

        foreach ($data as $key => $value) {
            $this->smarty->assign($key, $value);
        }

        return $this->smarty->fetch($path);
    }
}

Template Path Resolution

@modules/news/index     → modules/news/templates/index.tpl
@admin/news/list        → modules/news/templates/admin/list.tpl
@theme/blocks/sidebar   → themes/current/templates/blocks/sidebar.tpl

Database Architecture

Database Layer Structure

classDiagram
    class ConnectionInterface {
        <<interface>>
        +query(sql, params) ResultInterface
        +execute(sql, params) int
        +createQueryBuilder() QueryBuilderInterface
        +transaction(callback) mixed
    }

    class QueryBuilderInterface {
        <<interface>>
        +select(columns)
        +from(table)
        +where(condition)
        +orderBy(column)
        +limit(count)
        +getSQL() string
    }

    class Connection {
        -PDO pdo
        +query()
        +execute()
        +beginTransaction()
        +commit()
        +rollback()
    }

    class LegacyBridge {
        -Connection connection
        +queryF(sql)
        +prefix(table)
    }

    ConnectionInterface <|.. Connection
    Connection --> QueryBuilderInterface
    Connection <-- LegacyBridge : wraps

Connection Interface

namespace Xoops\Core\Database;

interface ConnectionInterface
{
    public function query(string $sql, array $params = []): ResultInterface;
    public function execute(string $sql, array $params = []): int;
    public function createQueryBuilder(): QueryBuilderInterface;
    public function transaction(callable $callback): mixed;
}

Safe/Unsafe Query Pattern

namespace Xoops\Core\Database;

trait SafeUnsafeTrait
{
    private bool $unsafeMode = false;

    /**
     * Execute potentially unsafe query (legacy support)
     */
    public function unsafe(callable $callback): mixed
    {
        $previous = $this->unsafeMode;
        $this->unsafeMode = true;

        try {
            return $callback($this);
        } finally {
            $this->unsafeMode = $previous;
        }
    }

    /**
     * Legacy queryF - requires unsafe wrapper in strict mode
     */
    public function queryF(string $sql): mixed
    {
        if (!$this->unsafeMode && getenv('XOOPS_SECURITY_LEVEL') === 'strict') {
            throw new SecurityException(
                'queryF() requires $db->unsafe() wrapper in strict mode'
            );
        }

        return $this->executeRaw($sql);
    }
}

// Usage
$db->unsafe(function($db) use ($sql) {
    return $db->queryF($sql);
});

Event System Architecture

Event Flow

flowchart LR
    subgraph Source["Event Source"]
        A["Article Published"]
    end

    subgraph Dispatcher["Event Dispatcher"]
        D["📡 PSR-14 Dispatcher"]
    end

    subgraph Listeners["Event Listeners"]
        L1["📧 Send Notification"]
        L2["🔍 Update Search Index"]
        L3["📊 Log Analytics"]
        L4["💾 Clear Cache"]
    end

    A --> D
    D --> L1
    D --> L2
    D --> L3
    D --> L4

    style Source fill:#e3f2fd,stroke:#1976d2
    style Dispatcher fill:#fff3e0,stroke:#f57c00
    style Listeners fill:#e8f5e9,stroke:#388e3c

PSR-14 Event Dispatcher

namespace Xoops\Core\Event;

use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\EventDispatcher\ListenerProviderInterface;

class EventDispatcher implements EventDispatcherInterface
{
    public function __construct(
        private readonly ListenerProviderInterface $listenerProvider
    ) {}

    public function dispatch(object $event): object
    {
        foreach ($this->listenerProvider->getListenersForEvent($event) as $listener) {
            $listener($event);

            if ($event instanceof StoppableEventInterface && $event->isPropagationStopped()) {
                break;
            }
        }

        return $event;
    }
}

Event Definition

namespace Xoops\Module\Publisher\Event;

class ArticlePublishedEvent
{
    public function __construct(
        public readonly int $articleId,
        public readonly int $authorId,
        public readonly \DateTimeImmutable $publishedAt
    ) {}
}

Module Architecture

Module Structure

graph TB
    subgraph Module["📦 Publisher Module"]
        direction TB
        subgraph Config["Configuration"]
            MJ["module.json"]
            SV["services.php"]
        end

        subgraph Code["Application Code"]
            CTRL["🎯 Controllers"]
            SVC["⚙️ Services"]
            REPO["🗄️ Repositories"]
            ENT["📋 Entities"]
        end

        subgraph Templates["Templates"]
            TPL["🎨 Smarty Templates"]
            BLK["📦 Block Templates"]
        end
    end

    MJ --> CTRL
    SV --> SVC
    CTRL --> SVC
    SVC --> REPO
    REPO --> ENT
    CTRL --> TPL

    style Module fill:#f5f5f5,stroke:#9e9e9e
    style Config fill:#e3f2fd,stroke:#1976d2
    style Code fill:#fff3e0,stroke:#f57c00
    style Templates fill:#e8f5e9,stroke:#388e3c

Module Service Provider

namespace Xoops\Module\Publisher;

use Psr\Container\ContainerInterface;

class ModuleServiceProvider implements ModuleServiceProviderInterface
{
    public function register(ContainerInterface $container): void
    {
        // Register module-specific services
        $container->set('publisher.article_repository', function($c) {
            return new Repository\ArticleRepository($c->get('database'));
        });

        $container->set('publisher.article_service', function($c) {
            return new Service\ArticleService(
                $c->get('publisher.article_repository'),
                $c->get('event_dispatcher')
            );
        });
    }
}

Module Discovery

Modules are discovered via include/services.php:

// modules/publisher/include/services.php
return new \Xoops\Module\Publisher\ModuleServiceProvider();

Caching Architecture

Cache Strategy

flowchart TB
    subgraph Application["Application"]
        REQ["Request"]
        SVC["Service"]
    end

    subgraph Cache["Cache Layer"]
        direction LR
        L1["🚀 L1: Memory<br/>(APCu)"]
        L2["💾 L2: Distributed<br/>(Redis/Memcached)"]
        L3["📁 L3: File<br/>(Filesystem)"]
    end

    subgraph Storage["Data Storage"]
        DB[("🗄️ Database")]
    end

    REQ --> SVC
    SVC --> L1
    L1 -->|Miss| L2
    L2 -->|Miss| L3
    L3 -->|Miss| DB
    DB -->|Populate| L3
    L3 -->|Populate| L2
    L2 -->|Populate| L1

    style Application fill:#e3f2fd,stroke:#1976d2
    style Cache fill:#fff3e0,stroke:#f57c00
    style Storage fill:#e8f5e9,stroke:#388e3c

VersionedCache Interface

namespace Xoops\Core\Cache;

interface VersionedCacheInterface
{
    public function get(string $namespace, string $key, mixed $default = null): mixed;

    public function set(
        string $namespace,
        string $key,
        mixed $value,
        int $ttl = 0
    ): void;

    public function remember(
        string $namespace,
        string $key,
        callable $factory,
        int $ttl = 0
    ): mixed;

    public function invalidate(string $namespace): void;
}

Null-Safe Caching

class VersionedCache implements VersionedCacheInterface
{
    private const NULL_SENTINEL = '__XOOPS_NULL_SENTINEL__';

    public function get(string $namespace, string $key, mixed $default = null): mixed
    {
        $cacheKey = $this->buildKey($namespace, $key);
        $value = $this->backend->get($cacheKey, self::NULL_SENTINEL);

        if ($value === self::NULL_SENTINEL) {
            return $default;
        }

        return $value === '__NULL__' ? null : $value;
    }

    public function set(string $namespace, string $key, mixed $value, int $ttl = 0): void
    {
        $cacheKey = $this->buildKey($namespace, $key);
        $this->backend->set(
            $cacheKey,
            $value === null ? '__NULL__' : $value,
            $ttl
        );
    }
}

Security Architecture

Safe IO Specification

All user input MUST go through the Safe IO layer:

namespace Xoops\Core\SafeIo;

class Request
{
    public static function getInt(string $key, int $default = 0): int;
    public static function getString(string $key, string $default = ''): string;
    public static function getBool(string $key, bool $default = false): bool;
    public static function getArray(string $key, array $default = []): array;
}

class Redirect
{
    public static function to(string $path, string $message = '', int $delay = 0): never;
    public static function toReturnPath(string $candidate, string $fallback): never;
    public static function external(string $url, string $message = '', int $delay = 0): never;
}

class Url
{
    public static function toModule(string $module, string $path, array $params = []): string;
    public static function toRoute(string $routeName, array $params = []): string;
}

See Also


xoops2026 #architecture #psr-15 #middleware #design-patterns