For udviklere

Arkitektur-
oversigt.

EZ24 er en distribueret PHP-platform: en central hub styrer en flåde af customer-appliances (Lenovo mini-PCs hos kunden). Hver appliance kører en isoleret node med sin egen kode, sine egne moduler og sin egen database. Denne side er for udviklere der vil forstå modellen og potentielt bygge moduler.

Systemoversigt

Hub + nodes + appliances.

Hub

Operatør-kontrolplan på app.ez24.dk. Registrerer appliances, distribuerer releases, modtager heartbeats, holder reverse-SSH-tunneler. Ingen kunde-data. PHP + MySQL.

Node

Customer-facing applikation der kører på hver appliance. Indeholder platform-admin (/login, /pages, /produkter…) + storefront. Samme kodebase, forskellige hosts (HostKind).

Appliance

Lenovo mini-PC med Proxmox, en LXC-container, og et 5G-modem. Henter releases fra hubben, applicerer migrations, holder en WireGuard-tunnel åben mod operatøren.

┌─────────────────────────────────┐ │ HUB (app.ez24.dk) │ Operator + appliance registry │ ─ operators, nodes table │ Release distribution │ ─ release tarballs │ Heartbeats / audit log │ ─ reverse-SSH gateway │ No customer data └──────────────┬──────────────────┘ │ HTTPS (heartbeat, release pull) │ reverse-SSH tunnel (operator hop) │ ┌─────────┴──────┬───────────────────┬─────────────────┐ │ │ │ │ ▼ ▼ ▼ ▼ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ NODE │ │ NODE │ │ NODE │ │ NODE │ │ whamwam │ │ fonline │ │ acme │ │ … │ │ /var/www │ │ /var/www │ │ /var/www │ │ │ │ + per- │ │ + per- │ │ + per- │ │ │ │ project │ │ project │ │ project │ │ │ │ DBs │ │ DBs │ │ DBs │ │ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ (each appliance physically isolated — own LAN, own DB)
Mental model

Tre kerne-ideer.

1

Per-projekt DB

Hver appliance kører én platform-DB (Ez24: brugere, projekter, modul-aktivering) plus én DB per projekt (ez24_proj_<slug>: produkter, ordrer, sider). Databar isolation = lille blast radius. Modul-tabeller (module_text, module_hero…) lever i projekt-DB'en.

2

To-fase modul-load

Hvert modul har fase 1 (module_X_register_blocks() — registrér block-typer, side-blokke) og fase 2 (module_X_register() — registrér ruter). Ruter dispatcher med get() der afslutter ved match, så blocks SKAL være registreret før ruterne fyrer.

3

Hook-registries

I stedet for klasse-hierarkier eller DI bruger platformen process-local hook-registries: block_register(), search_register_provider(), payment_register_adapter(), map_register_linkable(). Et modul opter ind ved at ringe sin ene linje fra module_X_register().

Konkret eksempel

Et modul fra bunden — notes.

Et minimalt notes-modul der tilføjer en /notes admin-side hvor operatøren kan se en liste af noter. Tre filer + en migration.

Mappe-struktur

modules/notes/
├── module.json          manifest (vises i modul-katalog)
├── module.php           registreringspunkt (rute + block)
├── migrations/
│   └── 0001_init.sql    notes-tabellen
├── data/
│   └── Notes.php        data_notes_list / _get / _create
├── kontroller/
│   └── NotesList.php    /notes route target
└── views/
    └── NotesList.php    thin wrapper (view_start + block + view_end)

1. module.json

modules/notes/module.json
{
  "name": "Noter",
  "description": "Operatør-vendt note-bibliotek per projekt.",
  "version": "1.0.0",
  "category": "content",
  "tier": "free",
  "vendor": "EZ24",
  "icon": "bi-sticky",
  "menu": [{
    "label": "Noter", "url": "/notes",
    "icon": "bi-sticky", "weight": 250, "category": "content"
  }],
  "project_kinds": ["ecommerce", "tools"]
}

2. migrations/0001_init.sql

Per-projekt DB, kører automatisk på modul-aktivering
CREATE TABLE IF NOT EXISTS `notes` (
    `id`         INT UNSIGNED NOT NULL AUTO_INCREMENT,
    `title`      VARCHAR(255) NOT NULL,
    `body`       TEXT         NULL,
    `created_at` DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

3. module.php

Registrér rute + (valgfrit) side-block
<?php
function module_notes_register(array $config): void
{
    require_once EZ24_ROOT . "/modules/notes/data/Notes.php";

    $hostKind = $GLOBALS['HostKind'] ?? 'platform';
    if ($hostKind === 'platform') {
        get('/notes', 'modules/notes/kontroller/NotesList.php');
    }
}

4. data/Notes.php

Per-projekt DB-helpers
<?php
require_once EZ24_ROOT . "/core/data/ProjectConnection.php";

function data_notes_list(int $projectId): array
{
    return project_dbquery($projectId,
        "SELECT * FROM notes ORDER BY created_at DESC");
}

function data_notes_create(int $projectId, string $title, ?string $body): int
{
    project_dbexecute($projectId,
        "INSERT INTO notes (title, body) VALUES (:t, :b)",
        [':t' => $title, ':b' => $body]);
    return (int)project_lastInsertId($projectId);
}

5. kontroller/NotesList.php

<?php
require_once EZ24_ROOT . "/core/bootstrap/kontroller_start.php";
require_once EZ24_ROOT . "/modules/notes/data/Notes.php";

if (empty($GLOBALS['ProjectId'])) {
    http_response_code(400); die("This route requires an active project.");
}

$GLOBALS['title'] = 'Noter';
$notes = data_notes_list((int)$GLOBALS['ProjectId']);
include EZ24_ROOT . "/modules/notes/views/NotesList.php";
include EZ24_ROOT . "/core/bootstrap/kontroller_end.php";

Det er hele løkken. Drop mappen i modules/, aktivér modulet fra /modules-admin, migrationen kører automatisk, og /notes dukker op i menuen.

Idiomer

De fem kanoniske mønstre.

Form / Preform

AjxForm3: én PHP-fil håndterer både GET (render modal-body) og POST (handle + JSON-action-array). Preform er knappen der åbner modalen. Actions: redirectUrl, getBlock, UpdateSnippet, closeModal, toasthtml.

Dual-purpose blocks

En block-fil kan både include()'es server-side OG hentes via AJAX på /blocks/<scope>/<folder>/<file>. Samme fil, to leveringsveje. AJAX-loaded blocks skal require BlockStart.php og selv hente data.

AJAX snippets

Snippets er AJAX-only fragments (/snippets/<scope>/<folder>/<file>). Bruges til ting der ikke skal renderes server-side ved første visning — søgeresultater, autocomplete, modal-bodies. Aldrig include()'es.

Hook-registries

Når flere moduler bidrager til samme feature — søgning, betalinger, kort-markører — bruges en proces-lokal registry: map_register_linkable('product', [...]) i produkt-modulets module.php. Ingen DI-container, ingen events.

Block trees (pageblocks)

Operatører bygger sider via drag-drop blokke (hero, text, cards, map…). Hver block registreres med block_register() — manifest-style spec med data_table, variants, style_knobs. Framework auto-injicerer margin_top/margin_bottom/visibility/custom_css-knapper på alle blocks der har style_knobs. Containere (column_split) håndteres af samme tree-renderer.

Næste skridt

Læs videre.

For udviklere

  • Eksempel-modul hello-world/ i kodebasen — implementerer hvert mønster én gang.
  • core/bootstrap/blocks.php — block-registreringssystemet kommenteret.
  • core/AjxForm3/, core/AjxBlocks/, core/AjxSnippets/ — dispatcher-implementationerne.
  • core/module/MigrationRunner.php — sådan kører migrations.

Kontakt

Vil du have adgang til source, eller diskutere et modul-projekt? Skriv direkte til Frank.

frank@frankolesen.dk

Denne side er v1 — auto-genereret refererence (alle hook-registries, alle block_register-kald, alle helper-funktioner) kommer i v2.