Entwickler-Hilfe
Diese Seite beschreibt Architektur, Datenfluss und Setup von Schatzilo.
Sie ist nicht für Endanwender gedacht (noindex,nofollow) und nur über das
?-Icon im Header erreichbar.
1. Überblick
Schatzilo ist eine standortbasierte Schatzsuch-Plattform. Eltern oder Schulen kaufen einen Spiel-Token, Kinder spielen die zugehörige Tour offline-tolerant am Smartphone und erhalten am Ende eine personalisierte Urkunde.
Drei Kern-Anwendungen
/de/standort/<slug>)./play/<token>/…), Mobile-First, Outdoor-Kontrast./admin/…).Architektur-Prinzipien
- Alle Texte aus der DB. Keine hartkodierten Strings im Code – Buttons, Hero-Texte,
Mails, Rätselfragen, alles über
ui_stringsoder*_i18n-Tabellen. - Pflege ausschließlich über das Admin. Kein direktes SQL nötig.
- Hauptzeile + i18n-Begleiter. Pro Entität gibt es
foo(sprachneutrale Stammdaten) undfoo_i18n(Texte pro Sprache).
2. Tech-Stack
| Schicht | Technologie | Datei |
|---|---|---|
| Sprache | PHP 8.1+ (8.1.2-1ubuntu2.23) | composer.json |
| Routing / HTTP | Slim 4 | app/Http/routes.php |
| DI-Container | PHP-DI 7 | app/bootstrap.php |
| Templates | Twig 3 | templates/ |
| Datenbank | MariaDB 10.6+ via PDO (no-emulate-prepares) | app/Database/Connection.php |
| i18n | Eigenes System (DB-getrieben) | app/I18n/{LanguageResolver,Translator}.php |
| Env | vlucas/phpdotenv | private/.env (Mode 0640, Gruppe web-marco) |
PHPMailer mit mail()-Fallback | app/Support/Mailer.php | |
| Bezahlung | PayPal Orders v2 (REST, ohne SDK) | app/Payments/PayPalClient.php |
| QR-Codes | endroid/qr-code (geplant) | — |
| Frontend | Vanilla CSS + minimales JS (kein Build) | public/assets/css/{app,admin,play}.css |
3. Verzeichnisstruktur
/var/www/marco/schatzilo/
├── app/ PHP-Code (PSR-4: Schatzilo\)
│ ├── bootstrap.php DI-Container, Slim-App-Factory
│ ├── Admin/ Admin-Controller (Auth, Dashboard, CRUD)
│ ├── Database/Connection.php PDO-Factory
│ ├── Http/
│ │ ├── routes.php Alle Slim-Routen
│ │ ├── Controllers/ Web-Controller (Home, Location, Play, Checkout, …)
│ │ └── Middleware/ Session, Sprache, Admin-Auth, Play-Session
│ ├── I18n/ LanguageResolver, Translator
│ ├── Payments/ PayPalClient, PaymentService
│ └── Support/ Csrf, Flash, AuditLogger, Mailer
├── bin/
│ ├── composer.phar lokal (nicht im Repo)
│ └── admin-create.php CLI: php bin/admin-create.php <email> [pass]
├── database/
│ └── schema.sql Vollständiges Schema + Seed-Daten
├── docs/ Konzept-Dokumente (00–08)
├── private/ nicht im DocumentRoot
│ ├── .env Secrets (Mode 0640, group web-marco)
│ ├── .env.example Vorlage
│ ├── backups/ Future: mysqldump-Tarballs
│ └── uploads/ Future: location_assets, certificates
├── public/ Webserver-DocumentRoot
│ ├── index.php Front-Controller
│ ├── .htaccess Apache-Rewrite (zur Vollständigkeit; live: nginx)
│ ├── assets/css/ app.css, admin.css, play.css
│ └── _tinyfm/ Legacy-File-Manager (nginx blockt 403)
├── storage/ Cache + Logs (writable von web-marco)
├── templates/ Twig
│ ├── layouts/ Web-Layout
│ ├── web/ Marketing-Seiten
│ ├── play/ Spiel-App
│ ├── admin/ Backoffice
│ └── mail/ Bestätigungsmails
└── vendor/ Composer-Pakete (über composer install)
4. Datenbank-Architektur
Schema in database/schema.sql. Engine InnoDB, Charset utf8mb4,
BIGINT-IDs, durchgehend created_at/updated_at, Soft-Delete via
deleted_at wo Audit relevant ist.
Sprachen & UI-Strings
| Tabelle | Inhalt |
|---|---|
languages | Sprach-Codes (de, en …), Default- + Aktiv-Flag |
ui_strings | Statische UI-Texte: PK (string_key, language_code), optionaler context-Filter (cta, navigation, play, mail, checkout …) |
Inhalt: Hauptzeile + i18n-Begleiter
| Hauptzeile | i18n-Begleiter | Übersetzte Felder |
|---|---|---|
regions | regions_i18n | name, description |
locations | locations_i18n | name, hero_title, hero_text, description, instructions, meta_description |
stations | stations_i18n | title, intro, hint_intro |
riddles | riddles_i18n | question, accepted_answers (JSON), success_text, hint_1, hint_2, hint_3 |
cms_blocks | cms_blocks_i18n | title, body |
pages | pages_i18n | title, body, meta_description |
certificate_templates | certificate_templates_i18n | display_name, html, css |
Geschäftsdaten
users (optional, Gast-Checkout möglich) → bookings (eine pro PayPal-Order, Status-Enum)
→ payments (Roh-Webhook-Events) → sessions (eine pro Buchung, Token-basiert)
→ session_progress (eine Zeile pro Rätsel-Versuch).
Plus certificates, inquiries, admins, audit_log.
Konventionen
- FK-Auflösung über
ON DELETE CASCADEfür i18n-Begleiter, sonstON DELETE RESTRICT. - Slugs sind sprachneutral. Sprach-Prefix kommt aus der URL (
/de/standort/knirpsenfarm). PDO::ATTR_EMULATE_PREPARES = false→ Named Placeholder dürfen nicht mehrfach im selben Statement stehen (:loc_r,:loc_lstatt einmal:loc).
5. i18n-System
Alles, was der User sieht, kommt aus der DB. Es gibt zwei Datenquellen, gegen die die Übersetzung läuft:
- UI-Strings (Buttons, Labels, Mails):
ui_strings.string_keyper Twig-Filter{{ 'btn.start_hunt'|t }}. - Inhaltliche Felder (Standort-Name, Rätselfrage): direktes JOIN in der Query
gegen
locations_i18n.language_code = :lang.
Sprach-Auflösung
Reihenfolge in app/I18n/LanguageResolver.php:
- URL-Prefix (
{lang}-Routenparameter) - Cookie
schatzilo_session_lang Accept-Language-Header- Default-Sprache aus
languages.is_default = 1(zur Zeitde)
Twig-Filter / Funktionen
| Aufruf | Effekt |
|---|---|
{{ 'btn.start_hunt'|t }} | UI-String aus aktueller Sprache (Fallback: Default → Key selbst) |
{{ 'mail.booking.subject'|t({location: name}) }} | Mit Platzhalter-Substitution {location} |
{{ price(loc.price_cents, loc.currency) }} | Formatiert Preis als 6,49 € |
Spielsprache
Die sessions.language_code friert die Spiel-Sprache zur Buchung ein. Das
PlaySessionMiddleware liest die Session-Sprache und überschreibt damit den
URL-/Cookie-Resolver, damit ein in DE gekauftes Spiel auch DE bleibt, wenn das Kind aus
Versehen en/… öffnet.
6. Routing
Definiert in app/Http/routes.php. Slim 4 mit Middleware-Gruppen.
Public
GET / → 302 zur Default-Sprache
GET /_health Health-Check (JSON)
GET /dev/hilfe Diese Seite
GET /{lang}/ Startseite (Region-Kacheln)
GET /{lang}/standort/{slug} Standort-Detail
GET /{lang}/checkout/{slug} Buchungs-Formular
POST /{lang}/checkout/{slug} PayPal-Order anlegen
GET /checkout/return?token=… PayPal-Return (Capture)
GET /checkout/cancel Abbruch-Seite
GET /{lang}/checkout/danke/{token} Thanks + Spiel-Link
POST /api/paypal/webhook PayPal-Webhook
Spiel
GET /play/{token}/ Start oder Resume
POST /play/{token}/begin Markiert Session als active
GET /play/{token}/station/{n} Aktuelle Station mit Rätsel
POST /play/{token}/station/{n}/check Antwort-Prüfung
GET /play/{token}/finish Erfolgsbildschirm + Namenseingabe
POST /play/{token}/finish Namen speichern
GET /play/{token}/urkunde Personalisierte Urkunde (HTML, druckbar)
Admin
GET/POST /admin/login Login (CSRF, Argon2id)
GET /admin/logout
GET /admin Dashboard mit KPIs
GET/POST /admin/standorte/… CRUD Standorte (i18n-Editor)
GET/POST /admin/standorte/{id}/stationen/… Sub-CRUD Stationen
GET/POST /…/stationen/{sid}/raetsel/… Sub-CRUD Rätsel
GET/POST /admin/ui-texte/… UI-Strings-Editor
POST /admin/standorte/{id}/test-session Test-Session für QA
Middleware-Stack
| Middleware | Zweck |
|---|---|
SessionMiddleware | session_start() mit sicherer Cookie-Konfiguration |
LanguageMiddleware | Sprache resolven, Twig-Globals setzen, in Translator pushen |
AdminAuthMiddleware | 302→/admin/login wenn keine $_SESSION['admin_id'] |
PlaySessionMiddleware | Session per Token laden, Sprache aus DB übernehmen |
7. Admin-Backend
Login
Tabelle admins, Argon2id-Hash, CSRF-Token aus Session,
session_regenerate_id(true) bei Erfolg. Anlage / Reset:
php bin/admin-create.php marco.schauart@gmail.com [neuesPasswort] --role=owner --name="Marco"
Module
- Dashboard – KPI-Kacheln + Übersetzungs-Lücken-Report.
- Standorte – CRUD mit Tab-Editor pro aktiver Sprache. Buttons „Stationen verwalten" und „Test-Session starten".
- Stationen – Sub-CRUD pro Standort, Reihenfolge-Bulk-Update mit Auto-Swap bei Position-Konflikten.
- Rätsel – Sub-CRUD pro Station, akzeptierte Antworten als „eine pro Zeile" → JSON-Array, bis zu 3 Hinweise.
- UI-Texte – Tabellen-Editor mit Kontext-Filter und Suche.
Neuanlage über
/admin/ui-texte/neu.
Audit-Log
Tabelle audit_log bekommt für jede mutierende Operation einen Eintrag mit
action (create, update, delete, reorder,
login.success, login.failed, translate.update, …),
entity (Tabellenname), entity_id und JSON-diff.
8. Spielablauf
Aus bookings entsteht beim Capture genau eine sessions-Zeile mit
40-Hex-Char-Token. Lebenszyklus:
| Status | Bedeutung | Aktion bei GET /play/{token}/ |
|---|---|---|
issued | Token ausgestellt, noch nicht gestartet | Start-Bildschirm |
active | Spiel läuft | Redirect zur aktuellen Station |
finished | Alle Rätsel gelöst | Redirect zu /finish |
expired | Älter als expires_at (Default +14 Tage) | Fehler-Seite |
Antwort-Matching
riddles_i18n.accepted_answersist ein JSON-Array.- Wenn
trim_whitespace = 1→ User-Eingabe und jede Antwort getrimmt. - Wenn
case_sensitive = 0→ Vergleich gegenmb_strtolower. - Erste Übereinstimmung gewinnt →
session_progress.solved_atwird gesetzt.
Hinweis-Staffelung
Im Twig-Template: hint_1 ist ab Beginn sichtbar, hint_2 nach 1 Versuch,
hint_3 nach 2 Versuchen.
9. PayPal-Flow
Implementiert ohne PayPal-SDK (reines curl in app/Payments/PayPalClient.php).
Sync-Flow
- User klickt „Mit PayPal bezahlen" auf
/{lang}/checkout/{slug}. PaymentService::startCheckout()legtbookingsalspendingan, ruft PayPal-Orders-API auf, speichertpaypal_order_id.- 302 zur PayPal-Approval-URL.
- Käufer approved → PayPal redirected zu
/checkout/return?token=<orderId>. CheckoutController::returnFromPaypal()ruftcaptureOrder()auf, protokolliert inpayments, ruftafterCapture().PaymentService::afterCapture()ist idempotent: setzt Statuspaid, legtsessions-Token an, sendet Bestätigungsmail.- 302 zu
/{lang}/checkout/danke/<token>.
Webhook
POST /api/paypal/webhook dient als Sicherheitsnetz, falls der Käufer den Browser
schließt, bevor der Sync-Return läuft. Verarbeitet
CHECKOUT.ORDER.APPROVED|COMPLETED und PAYMENT.CAPTURE.COMPLETED,
ruft denselben afterCapture()-Pfad → durch DB-Idempotenz keine doppelten
Sessions/Mails.
Wenn PAYPAL_WEBHOOK_ID gesetzt ist, wird die Signatur via
/v1/notifications/verify-webhook-signature geprüft.
Konfiguration in private/.env
PAYPAL_MODE=sandbox # oder live
PAYPAL_CLIENT_ID=…
PAYPAL_CLIENT_SECRET=…
PAYPAL_WEBHOOK_ID=… # nach Webhook-Anlage in der PayPal-Console
Status aktuell: Sandbox-Credentials fehlen sandbox
10. Lokal entwickeln
Dev-Server starten
php -S 127.0.0.1:8765 -t public
Greift auf private/.env zu. Der Built-in-Server reicht für alles außer
.htaccess-Tests.
Test-Session ohne PayPal
Im Admin auf einem Standort den Button „Test-Session starten" klicken. Erzeugt eine
TEST-…-Buchung (Status paid) und leitet direkt zur Spiel-App.
Composer
php bin/composer.phar install
php bin/composer.phar require <paket>
Datenbank
# Schema neu einspielen (zerstört Daten!)
mysql -u marco_schatzilo -p marco_schatzilo < database/schema.sql
# Schnell-Check
mysql -u marco_schatzilo -p marco_schatzilo -e "SHOW TABLES"
Twig-Cache
Im Debug-Modus deaktiviert (app.debug = true). In Produktion nach
storage/cache/twig/. Bei Template-Änderungen ohne Debug:
rm -rf storage/cache/twig/*.
11. Deployment
Webserver
nginx mit PHP-FPM-Pool marco (User+Group web-marco).
Config: /etc/nginx/sites-available/schatzilo.schauart.de.
- DocumentRoot:
/var/www/marco/schatzilo/public - Front-Controller-Rewrite:
try_files $uri $uri/ /index.php?$query_string - Geblockt:
/_tinyfm/,/includes/, Dot-Files,*.env|*.md|*.sql|*.ini|*.log|*.bak - PHP-Ausführung nur für
/index.php, andere*.php-Anfragen werden zum Front-Controller umgeleitet.
Datei-Berechtigungen
| Pfad | Modus | Eigentümer | Grund |
|---|---|---|---|
private/ | 2750 | ki-claude:web-marco | web-marco darf rein, andere nicht |
private/.env | 0640 | dito | web-marco darf lesen, schreiben nur Owner |
storage/ | 2775 | dito | web-marco schreibt Twig-Cache + Logs |
private/uploads/ | 2775 | dito | für PDF-Urkunden + Asset-Uploads |
nginx neu laden
sudo nginx -t && sudo systemctl reload nginx
12. Live-Status
| Schalter | Wert |
|---|---|
APP_ENV | development |
APP_DEBUG | an |
| PHP | 8.1.2-1ubuntu2.23 |
| PayPal-Mode | sandbox |
| PayPal-Credentials | fehlen |
Diese Seite wird durch den Code unter app/Http/Controllers/HelpController.php und
templates/web/dev-help.html.twig erzeugt. Anpassungen direkt dort, kein DB-Inhalt.