Fragen zum Betrieb, und weiter unten der Aufbau eigener Module.
Die Fragen, die beim Aufsetzen und danach am häufigsten kommen.
config.php sowie alles unter
uploads/ bleiben unberührt..htaccess wirkt nicht.htaccess nicht — ohne
Fehlermeldung. Die Regeln gehören in den Server-Block, siehe unten.config.php stehen
nur Zugangsdaten und ein paar Schalter, die vor der Datenbank greifen
müssen.mail() zurück — das landet
oft im Spam.config.php:
define('TRUSTED_PROXIES', ['127.0.0.1']); — nur dann werden
Weiterleitungs-Kopfzeilen überhaupt gelesen.define('FORUM_DEBUG', true);define('FORUM_CSP', false); sie ab — als Notausgang,
nicht als Dauerzustand.assets/logo.svg ablegen — lokale SVGs werden eingebettet, ein
currentColor darin geht dann in hell und dunkel mit..htaccess-Dateien als Direktiven im Server-Block — vor allem: kein PHP unter
uploads/, und lib/, views/, views_new/,
cache/, lang/, backup/ dichtmachen. Frag im Forum
nach dem fertigen Block, statt ihn nachzubauen.
Forumix hat eine Modul-Schnittstelle. Ein Modul ist ein Ordner unter
modules/ — mehr braucht es nicht, damit der Adminbereich es findet.
Es kann eigene Seiten und Ansichten mitbringen, eigene Tabellen anlegen, sich an
definierten Stellen ins Layout hängen und eigene Einstellungen speichern. Ein Modul
ist kein Fremdkörper: es benutzt dieselben Funktionen wie der Kern.
render('@code/name') holt
views/name.php aus dem Modul und setzt es in das Layout des
Forums — mit Kopf, Fuß und Theme.hooks/ wird an der passenden Stelle
eingebunden, solange das Modul aktiv ist. Ohne Eingriff im Kern.install.sql läuft beim ersten Aktivieren. Die Daten bleiben,
wenn das Modul später wieder ausgeschaltet wird.modules/meinmodul/ mit einer module.php darin. Der
Adminbereich listet ab sofort alles auf, was eine module.php hat.
Beim ersten Einschalten läuft install.sql — genau einmal. Danach
wird das Modul in der Tabelle modules vermerkt.
Nur der Schalter geht auf 0. Tabellen und Inhalte bleiben unangetastet, ein erneutes Einschalten holt alles zurück.
modules/meinmodul/
module.php Pflicht — gibt ein Array zurück (das Manifest)
install.sql optional — läuft EINMAL beim ersten Aktivieren
meinmodul.php optional — eigene Seite, direkt aufrufbar
views/liste.php optional — render('@meinmodul/liste')
hooks/home_top.php optional — Einhängepunkt im Layout
code im Manifest passen. Erlaubt sind nur a–z 0–9 _ - —
der Name landet in einem Dateipfad, deshalb die enge Regel.
<?php
// modules/meinmodul/module.php
if (!defined('FORUM_RUNNING')) return [];
return [
'code' => 'meinmodul', // Pflicht, = Ordnername
'name' => 'Mein Modul',
'description' => 'Was es tut, in einem Satz.',
'icon' => '🧩',
'version' => '1.0',
'author' => 'Dein Name',
// Punkt in Navigation und Fußbereich. Weglassen = kein Eintrag.
// 'admin' => true blendet ihn für alle außer der Verwaltung aus — das ist
// Bequemlichkeit, die Seite dahinter muss ihre Rechte selbst prüfen.
'menu' => ['label' => 'Mein Modul', 'url' => 'modules/meinmodul/meinmodul.php'],
'on_enable' => function () { /* nach dem Einschalten */ },
'on_disable' => function () { /* vor dem Ausschalten */ },
];
„Zitat des Tages": zeigt auf der Startseite ein zufälliges Zitat und hat eine eigene Seite, auf der Admins Zitate pflegen. Fünf Dateien, alles dabei — Tabelle, Seite, Ansicht, Einhängepunkt.
1. modules/zitate/module.php
<?php
if (!defined('FORUM_RUNNING')) return [];
return [
'code' => 'zitate',
'name' => 'Zitat des Tages',
'description' => 'Zeigt oben auf der Startseite ein zufälliges Zitat.',
'icon' => '💭',
'version' => '1.0',
'author' => 'Dein Name',
'menu' => ['label' => 'Zitate', 'url' => 'modules/zitate/zitate.php'],
];
2. modules/zitate/install.sql
CREATE TABLE IF NOT EXISTS `zitate` (
`zitat_id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`text` VARCHAR(500) NOT NULL,
`quelle` VARCHAR(120) NOT NULL DEFAULT '',
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- IGNORE, damit ein zweiter Durchlauf nicht an einem doppelten Schlüssel stirbt
INSERT IGNORE INTO `zitate` (`zitat_id`, `text`, `quelle`) VALUES
(1, 'Erst wenn es kaputt ist, weiß man, wie es funktioniert.', 'Werkstattweisheit');
3. modules/zitate/hooks/home_top.php
<?php
// Wird auf der Startseite eingebunden, solange das Modul aktiv ist.
// $currentUser und $csrfToken stehen hier bereit.
$z = db_fetch('SELECT text, quelle FROM zitate ORDER BY RAND() LIMIT 1');
if ($z):
?>
<div class="card" style="margin-bottom:16px">
💭 <em><?= e($z['text']) ?></em>
<?php if ($z['quelle'] !== ''): ?>
<span style="color:var(--text-muted)">— <?= e($z['quelle']) ?></span>
<?php endif; ?>
</div>
<?php endif; ?>
4. modules/zitate/zitate.php — die eigene Seite
<?php
if (!defined('FORUM_RUNNING')) define('FORUM_RUNNING', true);
require_once __DIR__ . '/../../bootstrap.php';
// Aus heißt aus — sonst bliebe die Seite auch im abgeschalteten Zustand erreichbar.
if (!module_enabled('zitate')) {
error_page(404, 'Nicht gefunden', 'Dieses Modul ist nicht aktiv.');
}
$cu = current_user();
$canAdmin = $cu && ($cu['is_admin'] || !empty($cu['is_owner']));
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// Beides prüfen: Token gegen fremde Formulare, Rechte gegen neugierige Besucher.
if (!verify_csrf($_POST['csrf'] ?? '')) {
error_page(400, 'Sicherheits-Token ungültig', 'Bitte lade die Seite neu.');
}
if (!$canAdmin) {
error_page(403, 'Kein Zugriff', 'Zitate darf nur die Verwaltung pflegen.');
}
if (($_POST['tun'] ?? '') === 'anlegen') {
$text = trim($_POST['text'] ?? '');
$quelle = trim($_POST['quelle'] ?? '');
if ($text !== '') {
// Werte gehören in die Platzhalter, nie in den SQL-Text.
db_execute('INSERT INTO zitate (text, quelle) VALUES (?, ?)',
[mb_substr($text, 0, 500), mb_substr($quelle, 0, 120)]);
flash('Zitat gespeichert.', 'success');
} else {
flash('Ohne Text geht es nicht.', 'error');
}
}
if (($_POST['tun'] ?? '') === 'loeschen') {
db_execute('DELETE FROM zitate WHERE zitat_id = ?', [(int) ($_POST['id'] ?? 0)]);
flash('Zitat gelöscht.', 'success');
}
// Nach dem Schreiben umleiten, sonst legt ein Neuladen alles noch einmal an.
redirect(FORUM_URL . '/modules/zitate/zitate.php');
}
render('@zitate/liste', [
'zitate' => db_fetchAll('SELECT * FROM zitate ORDER BY zitat_id DESC'),
'canAdmin' => $canAdmin,
]);
5. modules/zitate/views/liste.php
<?php if (!defined('FORUM_RUNNING')) { http_response_code(403); die('Forbidden'); } ?>
<h1>Zitate</h1>
<?php if ($canAdmin): ?>
<form method="post" class="form-card">
<!-- csrfToken kommt von render() und ist in jeder Ansicht da -->
<input type="hidden" name="csrf" value="<?= e($csrfToken) ?>">
<input type="hidden" name="tun" value="anlegen">
<div class="form-row">
<label>Zitat</label>
<input type="text" name="text" maxlength="500" required>
</div>
<div class="form-row">
<label>Quelle</label>
<input type="text" name="quelle" maxlength="120">
</div>
<button class="btn btn-primary">Speichern</button>
</form>
<?php endif; ?>
<?php foreach ($zitate as $z): ?>
<div class="card" style="margin-top:10px">
<!-- Alles, was aus der Datenbank kommt, durch e() -->
<em><?= e($z['text']) ?></em>
<?php if ($z['quelle'] !== ''): ?> — <?= e($z['quelle']) ?><?php endif; ?>
<?php if ($canAdmin): ?>
<form method="post" style="display:inline">
<input type="hidden" name="csrf" value="<?= e($csrfToken) ?>">
<input type="hidden" name="tun" value="loeschen">
<input type="hidden" name="id" value="<?= (int) $z['zitat_id'] ?>">
<button class="btn btn-ghost">Löschen</button>
</form>
<?php endif; ?>
</div>
<?php endforeach; ?>
modules/ hochladen, im Adminbereich unter
Module einschalten — fertig. Das Zitat steht auf der Startseite, die
Pflegeseite liegt unter /modules/zitate/zitate.php.
Eine Datei hooks/<name>.php wird an der passenden Stelle eingebunden,
sofern das Modul aktiv ist. Im Geltungsbereich stehen $currentUser und
$csrfToken bereit.
| Name | Wo | Zusätzlich im Geltungsbereich |
|---|---|---|
layout_top | Über allem, auf jeder Seite | – |
home_top | Startseite, über der Forenübersicht | – |
thread_top | Thema, über dem ersten Beitrag | $thread |
post_footer | Unter jedem Beitrag im Thema | $post, $thread |
render_module_hook('name') an der gewünschten Stelle im Layout genügt.
Das Einbinden aller aktiven Module übernimmt der Kern von da an allein.
$cfg = module_config('zitate'); // Array, leer wenn nichts gesetzt
$cfg['anzahl'] = 3;
set_module_config('zitate', $cfg); // als JSON in modules.config
Nach dem require der bootstrap.php stehen diese Funktionen
zur Verfügung. Es gibt keine eigene Modul-API daneben — was der Kern benutzt,
benutzt dein Modul auch.
| Aufruf | Tut |
|---|---|
db_fetchAll($sql, $params) | Liste von Zeilen |
db_fetch($sql, $params) | Eine Zeile oder null |
db_count($sql, $params) | Eine Zahl |
db_execute($sql, $params) | Schreiben, liefert betroffene Zeilen |
current_user() | Angemeldetes Konto oder null |
module_enabled($code) | Ist das Modul an? |
module_config($code) · set_module_config($code, $cfg) | Einstellungen des Moduls |
render($view, $daten) | Ansicht ins Layout setzen |
e($text) | Ausgabe maskieren |
verify_csrf($token) | Token prüfen |
flash($text, $art) · redirect($url) | Meldung merken, umleiten |
error_page($code, $titel, $text) | Fehlerseite im Layout |
if (!defined('FORUM_RUNNING')) { http_response_code(403); die('Forbidden'); }db_fetchAll('… WHERE id = ?', [$id]) — nie Werte in den
SQL-Text kleben, auch nicht „nur diese eine Zahl".e(). Für Beitragstext gibt es die BBCode-Ausgabe des Kerns.current_user() prüfen, nicht über das, was das
Formular mitschickt. Für Admin-Aktionen zusätzlich
is_admin oder is_owner.CREATE TABLE IF NOT EXISTS und INSERT IGNORE. Bricht
die Aktivierung mittendrin ab, läuft die Datei beim nächsten Versuch erneut.NOT NULL ohne Vorgabewert gehören in jedes
INSERT. Mit STRICT_TRANS_TABLES bricht es sonst mit
Fehler 1364 ab.