T ToutadminDokumentation

Die PHP-Edition installieren

Die PHP-Edition wird so, wie sie ist, auf ein Shared Hosting gelegt: kein Composer, keine Paketverwaltung, kein Dienst, der laufen muss. Ein Ordner, eine Konfigurationsdatei, eine geplante Aufgabe. Diese Anleitung führt vom leeren Ordner zur Instanz im Produktivbetrieb, samt allem, was die meisten Anleitungen auslassen: Berechtigungen, TLS, die geplante Aufgabe, Sicherungen und Aktualisierung.

Was nötig ist#

ElementVersionWarum
PHP8.1 oder neuerUnion-Typen, enum, readonly, never: der Code nutzt sie.
pdo_sqliteDie gesamte Datenbank. Ohne sie läuft nichts.
mbstringRichtiger Umgang mit Zeichenketten in sechzehn Sprachen.
opensslempfohlenVerschlüsselung der Geheimnisse in der Datenbank (SMTP-Passwörter, Auslagerungs-Token).
curlempfohlenAusgehende Webhooks und das Ablegen von Sicherungen bei Dritten.
intlempfohlenDatumsangaben wie in der Node-Edition. Ohne sie kommen die Monatsnamen aus den Wörterbüchern.

Der Installationsassistent prüft all das und zeigt es auf dem Bildschirm, bevor er Sie weitergehen lässt: was blockiert, hält die Installation an, was optional ist, wird gemeldet, ohne zu blockieren.

Keine Abhängigkeiten

Es gibt nichts mit composer zu installieren: der IMAP-Leser, das tar.gz-Archiv, das PDF, das CSV, der QR-Code und der HTTP-Client sind im Produkt selbst geschrieben. Genau das erlaubt es, es auf ein Hosting zu legen, auf dem Sie keine Kommandozeile haben.

1. Die Dateien hochladen#

Holen Sie das Archiv der PHP-Edition und laden Sie es hoch. Der Ordner sieht so aus:

toutadmin/
├── app/            der Code (nie vom Webserver ausgeliefert)
│   ├── Core/       Kern: Routing, Datenbank, Sitzungen, Sicherheit
│   ├── Modules/    Fachliches: Personal, Verwaltung, Projekte, Qualität…
│   ├── Controllers/
│   ├── views/      Vorlagen
│   ├── locales/    die sechzehn Wörterbücher
│   └── schema.sql  das Schema, gleich dem der Node-Edition
├── public/         ← die Web-Wurzel zeigt hierhin, und nirgends sonst
│   ├── index.php   der vorgelagerte Controller
│   ├── css/ js/
├── data/           Datenbank, Tresor, Unterschriftenmappe, eingegangene Belege, Sicherungen
├── tools/          cron.php, check-keys.php
├── tests/
├── config.sample.php
└── VERSION
Die Web-Wurzel zeigt auf public/, nicht auf den Ordner

Das ist der wichtigste Punkt dieser Seite. Liefert der Server den ganzen Ordner aus, wird data/app.sqlite für jeden herunterladbar: die gesamte Datenbank, samt Passwort-Prüfsummen und Tresor. Der Ordner data/ liegt mit Absicht oberhalb von public/.

Auf einem Shared Hosting#

Zwei Fälle treten auf. Erlaubt Ihr Anbieter, die Wurzel einer Domain zu wählen (cPanel, Plesk und die meisten Hoster im Modus „Wurzelordner“), legen Sie den Ordner außerhalb von www/ ab und lassen Sie die Domain auf toutadmin/public zeigen.

Ist die Wurzel vorgegeben — oft www/ oder public_html/ —, legen Sie den Inhalt von public/ hinein und den Rest darüber, und passen Sie dann den Pfad am Anfang von public/index.php an:

www/                  ← vorgegebene Wurzel
├── index.php
├── css/  js/
toutadmin/            ← der Rest, außer Reichweite des Webs
├── app/
├── data/
└── config.php
// www/index.php — die anzupassende Zeile
require_once __DIR__ . '/../toutadmin/app/bootstrap.php';

Als letzter Ausweg, wenn Sie oberhalb der Wurzel nichts ablegen können: die mitgelieferte Datei .htaccess in data/ verweigert den Zugriff bereits. Das ist ein Gürtel, keine Lösung: bevorzugen Sie immer den Ordner außerhalb der Wurzel.

2. Die Konfiguration schreiben#

cp config.sample.php config.php

Öffnen Sie dann config.php:

<?php
return [
    // SQLite-Datenbank und Datenordner: außerhalb der Web-Wurzel.
    'db_path'  => __DIR__ . '/data/app.sqlite',
    'data_dir' => __DIR__ . '/data',

    // Ein Schlüssel dieser Instanz. Einmal zu erzeugen:
    //   php -r "echo bin2hex(random_bytes(32));"
    'session_secret' => '…',

    // Installationstoken: siehe nächster Schritt.
    'install_token' => '',

    // Öffentliche Adresse, für die per E-Mail versandten Links.
    'base_url' => 'https://intranet.beispiel.de',

    // Nur hinter einem vertrauenswürdigen Proxy einschalten.
    'trust_proxy' => false,

    // Grenzen: Anmeldeversuche je Viertelstunde und Adresse,
    // Anfragen je Minute und Adresse.
    'login_rate_limit'  => 10,
    'global_rate_limit' => 300,

    // Ablauf: Untätigkeit, dann eine absolute Dauer, die nichts verlängert.
    'session_idle_minutes' => 60,
    'session_max_hours'    => 12,
];
Der Instanzschlüssel

Er versiegelt die Unterschriften der Unterschriftenmappe und verschlüsselt Geheimnisse in der Datenbank. Ihn zu verlieren macht Unterschriften ungültig. Lassen Sie den Beispielwert stehen, erzeugt das Produkt selbst einen in data/.instance-key (Berechtigungen 0600) — denken Sie dann daran, diese Datei mit zu sichern.

Sie können die Konfiguration auch anderswo halten: die Umgebungsvariable TOUTADMIN_CONFIG weist auf eine andere Datei.

3. Ein Installationstoken setzen#

Zwischen dem Hochladen der Dateien und Ihrem Besuch im Assistenten gehört die Instanz dem, der sie findet: wer zuerst kommt, legt das Administrationskonto an. Das Fenster ist kurz, es genügt. Setzen Sie also vor dem Online-Gehen einen zufälligen Wert:

php -r "echo bin2hex(random_bytes(16));"
# dann in config.php:
'install_token' => 'c3f1…',

Der Assistent verlangt ihn, vergleicht ihn in gleichbleibender Zeit — ein gewöhnlicher Vergleich ließe das Token Zeichen für Zeichen erraten — und protokolliert jede Abweisung. Ohne Token wird nichts angelegt. Ist die Instanz installiert, schließt sich der Assistent von selbst: das Token darf bleiben, es dient nichts mehr.

4. Berechtigungen#

PfadRechteWer schreibt
data/0770Der Webserver, und nur er
data/app.sqlite0660Von der Installation angelegt
config.php0640Niemand — für den Server nur lesbar
app/, public/0755Niemand — nur lesbar
# Beispiel auf einem Server, auf dem PHP als www-data läuft
sudo chown -R sie:www-data /var/www/toutadmin
sudo find /var/www/toutadmin -type d -exec chmod 750 {} \;
sudo find /var/www/toutadmin -type f -exec chmod 640 {} \;
sudo chmod 770 /var/www/toutadmin/data
sudo chmod 750 /var/www/toutadmin/public

SQLite schreibt außerdem Begleitdateien (-wal, -shm): beschreibbar sein muss der Ordner, nicht nur die Datenbank.

5. Der Webserver#

Nginx#

server {
    listen 443 ssl http2;
    server_name intranet.beispiel.de;

    root /var/www/toutadmin/public;   # und nichts anderes
    index index.php;

    ssl_certificate     /etc/letsencrypt/live/intranet.beispiel.de/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/intranet.beispiel.de/privkey.pem;

    client_max_body_size 20M;          # Ablagen im Tresor, eingegangene Belege

    location / {
        try_files $uri $uri/ /index.php$is_args$args;
    }

    location ~ \.php$ {
        fastcgi_pass unix:/run/php/php8.3-fpm.sock;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    }

    # Hochgeladene Dateien werden nie unmittelbar ausgeliefert: sie laufen
    # über eine Route, die Sitzung und Prüfsumme kontrolliert.
    location ~ ^/(data|app|tools|tests)/ { deny all; }
}

server {
    listen 80;
    server_name intranet.beispiel.de;
    return 301 https://$host$request_uri;
}

Apache#

<VirtualHost *:443>
    ServerName intranet.beispiel.de
    DocumentRoot /var/www/toutadmin/public

    SSLEngine on
    SSLCertificateFile    /etc/letsencrypt/live/intranet.beispiel.de/fullchain.pem
    SSLCertificateKeyFile /etc/letsencrypt/live/intranet.beispiel.de/privkey.pem

    <Directory /var/www/toutadmin/public>
        AllowOverride All
        Require all granted
    </Directory>
</VirtualHost>

Die mitgelieferte Datei public/.htaccess leitet bereits alle Adressen auf index.php. Auf einem Apache-Shared-Hosting ist das alles, was nötig ist.

TLS#

Das Produkt setzt den Header Strict-Transport-Security, sobald die Verbindung verschlüsselt ist, und nie im Klartext: von einer unverschlüsselten Seite aus angekündigt würde er nicht gelesen, und er würde einen lokalen Versuch sechs Monate lang in https einsperren. Auf einem Server, den Sie verwalten:

sudo certbot --nginx -d intranet.beispiel.de

Hinter einem Proxy oder Lastverteiler, der TLS beendet, setzen Sie 'trust_proxy' => true — und nur dort: sonst könnte jeder in einem Header die IP-Adresse und das Protokoll seiner Wahl ankündigen.

6. Die geplante Aufgabe#

Eine PHP-Seite läuft nur im Moment einer Anfrage. Was die Node-Edition in ihrem stündlichen Durchlauf erledigt — acht Vorgänge —, geschieht hier vom Cron des Hosters aus:

* * * * * /usr/bin/php /var/www/toutadmin/tools/cron.php >> /var/log/toutadmin-cron.log 2>&1
Was die Aufgabe tutIn welchem Takt
Schließt Konten, deren Vertrag ausgelaufen istbei jedem Durchgang
Stellt die Rechnungen fälliger Abonnements ausbei jedem Durchgang, nie zweimal
Macht aus Fristen Benachrichtigungenbei jedem Durchgang, ohne Dopplung
Räumt gelesene Benachrichtigungen und das Prüfprotokoll aufgemäß der gewählten Aufbewahrung
Ruft das Buchhaltungspostfach ab (IMAP)wenn der Abruf eingerichtet ist
Leert die Webhook-Warteschlange samt Wiederholungenbei jedem Durchgang
Erstellt die automatische Sicherung und lagert sie auswenn der Abstand verstrichen ist

Das Skript öfter aufzurufen als der eingestellte Abstand sichert nicht öfter: vor der Fälligkeit löst nichts aus. Auf einem Hosting, das den Cron auf einen Durchgang alle fünfzehn Minuten beschränkt, ersetzen Sie * * * * * durch */15 * * * * — die Webhooks gehen dann einfach mit dieser Verzögerung hinaus.

Auf einem Hosting ohne Cron

Manche Einsteigerangebote bieten keinen. Ein HTTP-Aufruf von einem Fremddienst (ein Erreichbarkeitswächter und dergleichen) genügt nicht: cron.php verweigert die Ausführung außerhalb der Kommandozeile, gerade damit keine öffentliche Adresse eine Sicherung auslösen kann. In diesem Fall stoßen Sie die Sicherung von Hand im Fenster Sicherungen an.

7. Der Assistent#

Öffnen Sie Ihre Domain. Solange kein Konto besteht, leitet jede Adresse auf /installation. Der Assistent passt auf eine Seite:

  1. Die Voraussetzungen, geprüft und angezeigt — was blockiert, ist als solches gekennzeichnet.
  2. Das Installationstoken, sofern eines eingerichtet ist.
  3. Das Unternehmen: der überall angezeigte Name und die Zahl der Jahresurlaubstage, die jede neue festangestellte Person erhält.
  4. Die Standardsprache der Instanz, unter den 16.
  5. Das Administrationskonto: Adresse und ein Passwort von mindestens zwölf Zeichen.

Alles wird in einer einzigen Transaktion geschrieben. Sobald ein Konto besteht, leitet /installation auf die Anmeldeseite: der Assistent hat sich selbst geschlossen, es gibt keine Datei von Hand zu löschen.

8. Prüfen, dass alles läuft#

# die Tests, auf der Maschine (sie schreiben nicht in Ihre Datenbank)
php tests/run.php

# die Übersetzungen: keine Maske nutzt einen Schlüssel, der in den Wörterbüchern fehlt
php tools/check-keys.php

Danach in der Oberfläche: ein Mitglied anlegen, sich damit anmelden, einen Anhang ablegen, eine Sicherung von Hand anstoßen und sie im Fenster Sicherungen prüfen. Diese vier Handgriffe berühren Datenbank, Dateien, Rechte und Archiv — also alles, was das Hosting können muss.

9. Aktualisieren#

# 1. zuerst eine Sicherung, immer
php tools/cron.php                     # oder das Fenster Sicherungen

# 2. den Code ersetzen, nicht die Daten
#    app/ public/ tools/ tests/ VERSION  ← ersetzt
#    data/ config.php                    ← unverändert behalten

# 3. die Datenbank wandert bei der ersten Anfrage von selbst mit

Das Schema entwickelt sich über idempotente Migrationen: die Aktualisierung erneut zu spielen zerstört nichts. Eine Instanz der Node-Edition und eine der PHP-Edition teilen dasselbe Schema — 142 Tabellen, 1311 Spalten — und dieselben Passwörter: eine Datenbank wandert ohne Umwandlung von der einen zur anderen.

10. Wirklich sichern#

Das Archiv tar.gz trägt die Datenbank (über VACUUM INTO kopiert, also auch während eines Schreibvorgangs in sich stimmig) und die fünf Dateiordner: Profilbilder, eingegangene Lebensläufe, Tresor, Unterschriftenmappe, eingegangene Belege. Jede Datei darin trägt ihre SHA-256-Prüfsumme, beim Zurückspielen geprüft.

Ein Archiv, das auf dem Server bleibt, den es schützt, schützt nichts: richten Sie im Fenster Sicherungen ein entferntes Ziel ein (FTPS oder Google Drive). Das Nähere steht auf der Seite Sicherung und Wiederherstellung.

Erst einmal örtlich ausprobieren#

cp config.sample.php config.php
php -S localhost:8000 -t public public/index.php

Der eingebaute Server von PHP genügt, um das Produkt kennenzulernen. Für den Produktivbetrieb taugt er nicht: ein einziger Prozess, kein TLS, keine Lastbegrenzung.

Toutadmin-Dokumentation — erstellt am 2026-09-13. Eigenständige Website, unabhängig von der Software.