XOOPS 2026 Core Specification¶
Overview¶
This document defines the technical specification for XOOPS 2026. Keywords MUST, SHOULD, and MAY are normative per RFC 2119.
Developer Tutorial¶
The "Hello World" Workflow¶
Step 1: Scaffold¶
Step 2: module.json¶
The manifest file with IDE autocompletion support via JSON Schema:
{
"$schema": "https://xoops.org/schemas/module/v1.json",
"schemaVersion": 1,
"identity": {
"slug": "hello",
"namespace": "Xoops\\Module\\Hello",
"name": "@modinfo.name",
"version": "1.0.0"
},
"requirements": {
"xoops": "^2026.0",
"php": ">=8.2"
},
"routes": {
"index": {
"path": "/",
"method": ["GET"],
"action": "Controller\\IndexController::index"
}
}
}
Step 3: Controller¶
Controllers MUST return a ResponseInterface:
namespace Xoops\Module\Hello\Controller;
use Xoops\Core\View\ViewRendererInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\ResponseInterface;
use Xoops\Core\Http\ApiResponse;
class IndexController
{
public function __construct(
private readonly ViewRendererInterface $view,
private readonly ApiResponse $response
) {}
public function index(ServerRequestInterface $request): ResponseInterface
{
$html = $this->view->render('@modules/hello/index', [
'name' => $request->getAttribute('name', 'World')
]);
return $this->response->html($html);
}
}
Core Architecture¶
Request Lifecycle¶
The Core processes requests via a PSR-15 Middleware Pipeline:
flowchart TB
Request["📥 HTTP Request (PSR-7)"]
Kernel["⚙️ Kernel"]
Middleware["🔒 Middleware Stack<br/>• Security<br/>• CSRF<br/>• RateLimit<br/>• Session"]
Router["🔀 Router"]
Controller["🎮 Controller"]
Response["📤 Response (PSR-7)"]
Request --> Kernel
Kernel --> Middleware
Middleware --> Router
Router --> Controller
Controller --> Response
style Request fill:#e3f2fd,stroke:#1976d2
style Kernel fill:#fff3e0,stroke:#f57c00
style Middleware fill:#fce4ec,stroke:#c2185b
style Router fill:#f3e5f5,stroke:#7b1fa2
style Controller fill:#e8f5e9,stroke:#388e3c
style Response fill:#e3f2fd,stroke:#1976d2 Router Independence¶
Modules MUST NOT depend on specific routing library internals:
interface RouteMatchInterface {
public function getName(): ?string;
public function getParams(): array;
public function getModuleSlug(): ?string;
}
Data Layer and Migrations¶
SafeUnsafe Trait¶
Limits queryF() usage and toggles via XOOPS_SECURITY_LEVEL:
- Contract:
queryF()calls MUST be wrapped in a$db->unsafe()closure in strict mode - Re-entrancy: The
unsafe()wrapper MUST be re-entrant and restore previous state even if callback throws
Stable Entity References¶
| Concept | Description |
|---|---|
| Canonical Type | blog.post (Immutable identifier) |
| Alias Registry | Maps old types to new handlers |
| Registration | Modules register types/aliases |
| Finalization | Core validates (no cycles, all canonicals exist) |
Module System and Manifests¶
Manifest Compilation¶
JSON manifests are compiled to PHP arrays:
- Hash Strategy: Compiler MUST use fast non-cryptographic hash (
xxh3/xxh128) if available, falling back tosha256 - Schema Stability:
$schemaURLs MUST be versioned and immutable
Manifest Merger¶
Path Definitions¶
| Path Type | Description | Example |
|---|---|---|
| List Path | Array where order matters | admin.menu, assets.css |
| Object Path | Object with named keys | routes, config |
Empty Node Semantics¶
| Path Type | Input | Behavior | Validation (Strict) |
|---|---|---|---|
| List Path | [] | Clears List | Valid |
| List Path | {} | Error | Fatal |
| Object Path | {} | No-op | Valid |
| Object Path | [] | Error | Fatal |
Merge Modes¶
| Mode | Behavior |
|---|---|
Merge | Deep merge objects, replace indexed arrays |
Append | Add to end |
Unique | Base + Override, dedupe (first occurrence wins) |
Replace | Overwrite entire node |
Security and Permissions¶
Permission Hints¶
Search providers MUST return a PermissionHint for efficient Core evaluation.
Policy Cache Key Contract¶
Policies MUST return cache key components composed ONLY of:
nullboolintstringarrayof these types
Rules: - Core MUST sort associative arrays recursively - Indexed arrays MUST preserve order - Floats are forbidden (use strings for precision)
Search and Discovery¶
Federated Search Engine¶
Solves the "Pagination Trap" using Bounded Overfetch strategy:
Query
│
├──► Provider A ──► Top N Results ──┐
│ │
├──► Provider B ──► Top N Results ──┼──► Sort Global ──► Slice Page ──► Results
│ │
└──► Provider C ──► Top N Results ──┘
Accuracy Contracts¶
Provider Contract¶
Providers MUST return: - isTotalExact (bool): True if count is not estimated - wasCapped (bool): True if internal budget limit was hit
Page Accuracy¶
isPageAccurate is TRUE if and only if:
- No provider asserted an error
- For every provider:
fetchedCount >= requestedCountORproviderExhausted = true requestedCountis defined asmin(providerCap, limit + offset)
Total Accuracy¶
isTotalAccurate is TRUE if and only if all(providers.isTotalExact) AND no_errors
Internationalization (I18n)¶
Scoping and Precedence¶
Precedence Order (highest to lowest):
- Theme
- Site
- Module Locale
- Module Fallback (en)
Collision Rule: Higher precedence layer wins
Frontend and Assets¶
View Renderer¶
interface ViewRendererInterface {
public function render(string $template, array $data = []): string;
}
Implementations: - Smarty (Default) - Twig (Optional)
Widget Provider¶
Contract for Admin Dashboard and Blocks:
interface WidgetProviderInterface {
public function render(WidgetContext $ctx): string;
public function getCacheTtl(): int;
/** MUST return scalar/array-of-scalar components */
public function getCacheKeyComponents(WidgetContext $ctx): array;
/** MAY return events that trigger namespace invalidation */
public function getInvalidationEvents(): array;
}
Operational Strategy¶
Versioned Cache¶
Normalization Rules: - Namespace MUST match [a-z0-9._:-]{1,64} - Key MUST be hashed (preferred xxh3, fallback sha256) and encoded as lowercase hex
Safety Mechanisms: - Null Safety: Uses internal NULL_SENTINEL - Miss Detection: Single-round-trip get($key, $sentinel)
Key Contracts¶
| Concept | Interface | Key Method |
|---|---|---|
| Routing | RouteMatchInterface | getParams(): array |
| Views | ViewRendererInterface | render(string $tpl, array $data) |
| Search | SearchProviderInterface | search(Query $q, Context $c) |
| Cache | VersionedCache | remember(string $ns, string $k, callable $f) |
| Widgets | WidgetProviderInterface | getCacheKeyComponents(Context $c) |
Architectural Decision Record¶
| Decision | Context | Rationale |
|---|---|---|
| Sentinel Cache | Need to cache null | Unique object allows single-round-trip reads |
| Hex Hash Keys | Backend limits | Binary strings cause issues in Redis drivers |
| Bounded Search | Federated pagination cost | Prioritizes top results/performance |
| Immutable Entities | Module renames | Rewriting DB references is risky |
| Path-Based Merge | Ambiguity of [] | assets.css: [] intuitively means "clear" |