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.
Operatør-kontrolplan på app.ez24.dk.
Registrerer appliances, distribuerer releases,
modtager heartbeats, holder reverse-SSH-tunneler.
Ingen kunde-data. PHP + MySQL.
Customer-facing applikation der kører på hver
appliance. Indeholder platform-admin
(/login, /pages,
/produkter…) + storefront. Samme
kodebase, forskellige hosts (HostKind).
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.
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.
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.
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().
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.
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)
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"]
}
migrations/0001_init.sqlCREATE 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;
module.php<?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');
}
}
data/Notes.php<?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);
}
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.
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.
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.
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.
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.
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.
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.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.