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

Marketing-Site Standort-Landingpages, Buchungsformular, mehrsprachig (/de/standort/<slug>).
Spiel-Web-App Token-basiert (/play/<token>/…), Mobile-First, Outdoor-Kontrast.
Admin-Backend CRUD für alle Inhalte + UI-Strings (/admin/…).

Architektur-Prinzipien

  • Alle Texte aus der DB. Keine hartkodierten Strings im Code – Buttons, Hero-Texte, Mails, Rätselfragen, alles über ui_strings oder *_i18n-Tabellen.
  • Pflege ausschließlich über das Admin. Kein direktes SQL nötig.
  • Hauptzeile + i18n-Begleiter. Pro Entität gibt es foo (sprachneutrale Stammdaten) und foo_i18n (Texte pro Sprache).

2. Tech-Stack

SchichtTechnologieDatei
SprachePHP 8.1+ (8.1.2-1ubuntu2.23)composer.json
Routing / HTTPSlim 4app/Http/routes.php
DI-ContainerPHP-DI 7app/bootstrap.php
TemplatesTwig 3templates/
DatenbankMariaDB 10.6+ via PDO (no-emulate-prepares)app/Database/Connection.php
i18nEigenes System (DB-getrieben)app/I18n/{LanguageResolver,Translator}.php
Envvlucas/phpdotenvprivate/.env (Mode 0640, Gruppe web-marco)
MailPHPMailer mit mail()-Fallbackapp/Support/Mailer.php
BezahlungPayPal Orders v2 (REST, ohne SDK)app/Payments/PayPalClient.php
QR-Codesendroid/qr-code (geplant)
FrontendVanilla 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

TabelleInhalt
languagesSprach-Codes (de, en …), Default- + Aktiv-Flag
ui_stringsStatische UI-Texte: PK (string_key, language_code), optionaler context-Filter (cta, navigation, play, mail, checkout …)

Inhalt: Hauptzeile + i18n-Begleiter

Hauptzeilei18n-BegleiterÜbersetzte Felder
regionsregions_i18nname, description
locationslocations_i18nname, hero_title, hero_text, description, instructions, meta_description
stationsstations_i18ntitle, intro, hint_intro
riddlesriddles_i18nquestion, accepted_answers (JSON), success_text, hint_1, hint_2, hint_3
cms_blockscms_blocks_i18ntitle, body
pagespages_i18ntitle, body, meta_description
certificate_templatescertificate_templates_i18ndisplay_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 CASCADE für i18n-Begleiter, sonst ON 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_l statt 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_key per 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:

  1. URL-Prefix ({lang}-Routenparameter)
  2. Cookie schatzilo_session_lang
  3. Accept-Language-Header
  4. Default-Sprache aus languages.is_default = 1 (zur Zeit de)

Twig-Filter / Funktionen

AufrufEffekt
{{ '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

MiddlewareZweck
SessionMiddlewaresession_start() mit sicherer Cookie-Konfiguration
LanguageMiddlewareSprache resolven, Twig-Globals setzen, in Translator pushen
AdminAuthMiddleware302→/admin/login wenn keine $_SESSION['admin_id']
PlaySessionMiddlewareSession 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:

StatusBedeutungAktion bei GET /play/{token}/
issuedToken ausgestellt, noch nicht gestartetStart-Bildschirm
activeSpiel läuftRedirect zur aktuellen Station
finishedAlle Rätsel gelöstRedirect zu /finish
expiredÄlter als expires_at (Default +14 Tage)Fehler-Seite

Antwort-Matching

  1. riddles_i18n.accepted_answers ist ein JSON-Array.
  2. Wenn trim_whitespace = 1 → User-Eingabe und jede Antwort getrimmt.
  3. Wenn case_sensitive = 0 → Vergleich gegen mb_strtolower.
  4. Erste Übereinstimmung gewinnt → session_progress.solved_at wird 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

  1. User klickt „Mit PayPal bezahlen" auf /{lang}/checkout/{slug}.
  2. PaymentService::startCheckout() legt bookings als pending an, ruft PayPal-Orders-API auf, speichert paypal_order_id.
  3. 302 zur PayPal-Approval-URL.
  4. Käufer approved → PayPal redirected zu /checkout/return?token=<orderId>.
  5. CheckoutController::returnFromPaypal() ruft captureOrder() auf, protokolliert in payments, ruft afterCapture().
  6. PaymentService::afterCapture() ist idempotent: setzt Status paid, legt sessions-Token an, sendet Bestätigungsmail.
  7. 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

PfadModusEigentümerGrund
private/2750ki-claude:web-marcoweb-marco darf rein, andere nicht
private/.env0640ditoweb-marco darf lesen, schreiben nur Owner
storage/2775ditoweb-marco schreibt Twig-Cache + Logs
private/uploads/2775ditofür PDF-Urkunden + Asset-Uploads

nginx neu laden

sudo nginx -t && sudo systemctl reload nginx

12. Live-Status

SchalterWert
APP_ENVdevelopment
APP_DEBUGan
PHP8.1.2-1ubuntu2.23
PayPal-Modesandbox
PayPal-Credentialsfehlen

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.