diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 00000000..20fa5735
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,22 @@
+# Snapshots werden byte-genau verglichen: immer LF, auch unter Windows (core.autocrlf)
+tests/GoldenMaster/__snapshots__/** text eol=lf
+# Tests enthalten Ligadateien als Textblock und vergleichen sie zeilenweise: immer LF
+tests/**/*.php text eol=lf
+# Shell-Skripte laufen im Linux-Container: mit CRLF startet das Docker-Image nicht
+*.sh text eol=lf
+# wird unter Linux direkt ausgefuehrt (Shebang)
+bin/* text eol=lf
+# Nutzer bekommen per Composer nur den LMO, nicht die Entwicklungsumgebung
+/.github export-ignore
+/docker export-ignore
+/docker-compose.yml export-ignore
+/phpunit.xml.dist export-ignore
+/phpstan.neon.dist export-ignore
+/phpstan-strict.neon.dist export-ignore
+/phpstan-classlib.neon.dist export-ignore
+/phpstan-tests.neon.dist export-ignore
+/phpstan-liga.neon.dist export-ignore
+/phpstan-liga-basis.neon.dist export-ignore
+/phpstan-bootstrap.php export-ignore
+/tests export-ignore
+/TESTING.md export-ignore
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
new file mode 100644
index 00000000..b0624d7a
--- /dev/null
+++ b/.github/workflows/tests.yml
@@ -0,0 +1,70 @@
+# Golden-Master-Tests direkt auf dem Runner (ohne Docker), siehe TESTING.md
+name: Tests
+
+on:
+ push:
+ pull_request:
+ workflow_dispatch:
+
+jobs:
+ phpstan:
+ runs-on: ubuntu-24.04
+ name: PHPStan
+
+ steps:
+ - uses: actions/checkout@v5
+
+ # die Pruefung selbst gilt fuer PHP 8.0 bis 8.5 (phpVersion in phpstan.neon.dist)
+ - name: PHP einrichten
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: '8.3'
+ coverage: none
+ tools: composer:v2
+
+ - name: Abhaengigkeiten installieren
+ run: composer install --no-interaction --no-progress --prefer-dist
+
+ - name: PHPStan
+ run: composer phpstan
+
+ golden-master:
+ runs-on: ubuntu-24.04
+ strategy:
+ fail-fast: false
+ matrix:
+ # Snapshots stammen von 8.3; 8.0 ist die aelteste unterstuetzte Version (lmo/version.json), 8.5 die neueste (README)
+ php: ['8.0', '8.3', '8.4', '8.5']
+ name: PHP ${{ matrix.php }}
+
+ steps:
+ - uses: actions/checkout@v5
+
+ # faketime fixiert die Zeit (Kalender, "heute"-Markierungen) wie im Docker-Image
+ - name: faketime installieren
+ run: sudo apt-get update && sudo apt-get install -y --no-install-recommends faketime
+
+ - name: PHP einrichten
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: ${{ matrix.php }}
+ extensions: gd, zip
+ # wie das offizielle Docker-Image: keine php.ini, nur Standardwerte
+ ini-file: none
+ coverage: none
+ tools: composer:v2
+
+ - name: Composer-Cache
+ uses: actions/cache@v5
+ with:
+ path: ~/.composer/cache
+ key: composer-${{ matrix.php }}-${{ hashFiles('composer.lock') }}
+ restore-keys: composer-${{ matrix.php }}-
+
+ - name: Abhaengigkeiten installieren
+ run: composer install --no-interaction --no-progress --prefer-dist
+
+ - name: Tests
+ env:
+ GOLDEN_MODE: compare
+ run: vendor/bin/phpunit
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 00000000..68acd93c
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,14 @@
+/vendor/
+.original/
+.phpunit.result.cache
+coverage/
+# Laufzeitdateien einer Installation (legt lmo-setup.php an)
+lmo/config/init-parameters.php
+lmo/config/lmo-auth.php
+lmo/config/.defaults-stamp
+lmo/config/.update-check.json
+lmo/config/cfg.txt
+lmo/config/*/cfg.txt
+lmo/addon/tipp/lmo-tippauth.txt
+# Rohdaten von tests/RealData/bin/fetch_openligadb.php
+tests/RealData/.cache/
diff --git a/README.md b/README.md
index 1d9b0073..bf31ad5b 100644
--- a/README.md
+++ b/README.md
@@ -17,87 +17,36 @@ Currently supported languages (_sorted alphabetically_)
- [](https://www.liga-manager-online.org/test/lmo_git/lmo.php?file=1l_2023-24.l98&lmouserlang=Hrvatski) Croatian
- [](https://www.liga-manager-online.org/test/lmo_git/lmo.php?file=1l_2023-24.l98&lmouserlang=Cestina) Czech
- [](https://www.liga-manager-online.org/test/lmo_git/lmo.php?file=1l_2023-24.l98&lmouserlang=Nederlands) Dutch
-- [](https://www.liga-manager-online.org/test/lmo_git/lmo.php?file=1l_2023-24.l98&lmouserlang=English) English (also installation language)
-- [](https://www.liga-manager-online.org/test/lmo_git/lmo.php?file=1l_2023-24.l98&lmouserlang=Francais) French (also installation language)
-- [](https://www.liga-manager-online.org/test/lmo_git/lmo.php?file=1l_2023-24.l98&lmouserlang=Deutsch) German (also installation language and [help files](https://www.liga-manager-online.org/test/lmo_git/help/Deutsch/index.html))
+- [](https://www.liga-manager-online.org/test/lmo_git/lmo.php?file=1l_2023-24.l98&lmouserlang=English) English
+- [](https://www.liga-manager-online.org/test/lmo_git/lmo.php?file=1l_2023-24.l98&lmouserlang=Francais) French
+- [](https://www.liga-manager-online.org/test/lmo_git/lmo.php?file=1l_2023-24.l98&lmouserlang=Deutsch) German ([help files](https://www.liga-manager-online.org/test/lmo_git/help/Deutsch/index.html))
- [](https://www.liga-manager-online.org/test/lmo_git/lmo.php?file=1l_2023-24.l98&lmouserlang=Magyar) Hungarian
- [](https://www.liga-manager-online.org/test/lmo_git/lmo.php?file=1l_2023-24.l98&lmouserlang=Italiano) Italian
- [](https://www.liga-manager-online.org/test/lmo_git/lmo.php?file=1l_2023-24.l98&lmouserlang=Norsk) Norwegian
- [](https://www.liga-manager-online.org/test/lmo_git/lmo.php?file=1l_2023-24.l98&lmouserlang=Portugues) Portuguese
- [](https://www.liga-manager-online.org/test/lmo_git/lmo.php?file=1l_2023-24.l98&lmouserlang=Romanian) Romanian
- [](https://www.liga-manager-online.org/test/lmo_git/lmo.php?file=1l_2023-24.l98&lmouserlang=Slovenskega) Slovenian
-- [](https://www.liga-manager-online.org/test/lmo_git/lmo.php?file=1l_2023-24.l98&lmouserlang=Espanol) Spanish (also installation language)
+- [](https://www.liga-manager-online.org/test/lmo_git/lmo.php?file=1l_2023-24.l98&lmouserlang=Espanol) Spanish
### System requirement
-- Web server with at least PHP 8.0 - maximum PHP 8.4
+- Web server with at least PHP 8.0 - maximum PHP 8.5
-> [!NOTE]
-This LMO version is still a beta version. Please note this. Before copying over an existing LMO, make a backup first!
-
-After joker has removed the forum on the LMO's project page (because of the General Data Protection Regulation GDPR), I offer a support forum for this LMO on my website (https://www.liga-manager-online.org/forum/).
+### Installation
+LMO is installed with [Composer](https://getcomposer.org/).
+1. Run this in your project folder (the folder that contains or will contain `composer.json`):
+ ```
+ composer require lmo/lmo_php8
+ vendor/bin/lmo-install
+ ```
+ LMO is set up in the folder `lmo` of your project. For another folder, name it, e.g. `vendor/bin/lmo-install public/lmo`.
+2. Open `lmo/lmo.php` in your browser. LMO sets itself up.
+3. Open `lmo/lmoadmin.php` **right away** and choose the admin user name and password.
---------------------------------------------------------------------------------------------------------
-# (DEUTSCH) LMO auf PHP8
-Ich möchte hier mit anderen die Lauffähigkeit des Liga Managers Online unter PHP 8 gewährleisten.
-Die Projektseite ist aber weiterhin https://www.liga-manager-online.de/homepage/
-## Beschreibung des LMO's
-Mit diesem Skript können Sie Ihren Besuchern einen tollen Ergebnisdienst über Ihre Lieblingsligen anbieten. Das Beste daran ist, dass die gesamte Verwaltung der Ligen direkt auf dem Server abläuft. Sie müssen sich also nur noch um die Ergebnisse kümmern und Ihre Besucher erhalten sofort die aktuellen Tabellen, Statistiken und mehr. Das ist wirklich super praktisch, denn so müssen Sie sich nicht mehr mit Fremdsoftware befassen und sparen sich auch noch viel Zeit, die Sie sonst für Exporte und Uploads aufwenden müssten.
-
-Momentan unterstützte Sprachen (_alphabetisch sortiert_)
-- [](https://www.liga-manager-online.org/test/lmo_git/lmo.php?file=1l_2023-24.l98&lmouserlang=Bosanski) Bosnisch
-- [](https://www.liga-manager-online.org/test/lmo_git/lmo.php?file=1l_2023-24.l98&lmouserlang=Deutsch) Deutsch (auch Installationssprache und [Hilfedateien](https://www.liga-manager-online.org/test/lmo_git/help/Deutsch/index.html))
-- [](https://www.liga-manager-online.org/test/lmo_git/lmo.php?file=1l_2023-24.l98&lmouserlang=English) Englisch (auch Installationssprache)
-- [](https://www.liga-manager-online.org/test/lmo_git/lmo.php?file=1l_2023-24.l98&lmouserlang=Francais) Französisch (auch Installationssprache)
-- [](https://www.liga-manager-online.org/test/lmo_git/lmo.php?file=1l_2023-24.l98&lmouserlang=Italiano) Italienisch
-- [](https://www.liga-manager-online.org/test/lmo_git/lmo.php?file=1l_2023-24.l98&lmouserlang=Hrvatski) Kroatisch
-- [](https://www.liga-manager-online.org/test/lmo_git/lmo.php?file=1l_2023-24.l98&lmouserlang=Nederlands) Niederländisch
-- [](https://www.liga-manager-online.org/test/lmo_git/lmo.php?file=1l_2023-24.l98&lmouserlang=Norsk) Norwegisch
-- [](https://www.liga-manager-online.org/test/lmo_git/lmo.php?file=1l_2023-24.l98&lmouserlang=Portugues) Portugiesisch
-- [](https://www.liga-manager-online.org/test/lmo_git/lmo.php?file=1l_2023-24.l98&lmouserlang=Romanian) Rumänisch
-- [](https://www.liga-manager-online.org/test/lmo_git/lmo.php?file=1l_2023-24.l98&lmouserlang=Slovenskega) Slowenisch
-- [](https://www.liga-manager-online.org/test/lmo_git/lmo.php?file=1l_2023-24.l98&lmouserlang=Espanol) Spanisch (auch Installationssprache)
-- [](https://www.liga-manager-online.org/test/lmo_git/lmo.php?file=1l_2023-24.l98&lmouserlang=Cestina) Tschechisch
-- [](https://www.liga-manager-online.org/test/lmo_git/lmo.php?file=1l_2023-24.l98&lmouserlang=Magyar) Ungarisch
-
-### Systemvorraussetzung
-- Webserver mit mindestens PHP 8.0 - maximal PHP 8.4
-
-> [!NOTE]
-Bei dieser LMO Version handelt es sich immer noch um eine Beta Version. Beachtet das bitte. Vor dem drüberkopieren über einen vorhandenen LMO macht zuerst eine Sicherung!
-
-Nachdem joker auf der Projektseite des LMO's das Forum entfernt hat (wegen der Datenschutz-Grundverordnung DSGVO), biete ich auf meiner Webseite ein Supportforum für diesen LMO an (https://www.liga-manager-online.org/forum/).
-[](https://www.liga-manager-online.org/forum)
-[](https://github.com/henshingly/lmo_php8/releases/)
-[](https://github.com/henshingly/lmo_php8/commits/main/)
-[](https://github.com/henshingly/lmo_php8/releases/latest "Download status")
-[](https://github.com/henshingly/lmo_php8/discussions)
-[](https://github.com/henshingly/lmo_php8/issues)
-[](https://github.com/henshingly/LMO_PHP8/blob/master/changelog.md)
-[](https://paypal.me/LMOforum)
-# (ENGLISH) LMO on PHP8
-Together with others, I want to ensure that Liga Manager Online runs on PHP 8.
-However, the project page is still https://www.liga-manager-online.de/homepage/
-## Description of the LMO
-With this script you can offer your visitors a great results service for your favorite leagues. The best part is that all league management takes place directly on the server. So all you have to worry about is the results and your visitors will immediately receive the latest tables, statistics and more. This is really super convenient because it means you no longer have to deal with third-party software and also saves a lot of time that you would otherwise have to spend on exports and uploads.
+Update: run `composer update`. The LMO in the folder `lmo` is brought up to date automatically. Your settings, leagues, tips and a changed `.htaccess` are kept.
-Currently supported languages (_sorted alphabetically_)
-- [](https://www.liga-manager-online.org/test/lmo_git/lmo.php?file=1l_2023-24.l98&lmouserlang=Bosanski) Bosnian
-- [](https://www.liga-manager-online.org/test/lmo_git/lmo.php?file=1l_2023-24.l98&lmouserlang=Hrvatski) Croatian
-- [](https://www.liga-manager-online.org/test/lmo_git/lmo.php?file=1l_2023-24.l98&lmouserlang=Cestina) Czech
-- [](https://www.liga-manager-online.org/test/lmo_git/lmo.php?file=1l_2023-24.l98&lmouserlang=Nederlands) Dutch
-- [](https://www.liga-manager-online.org/test/lmo_git/lmo.php?file=1l_2023-24.l98&lmouserlang=English) English (also installation language)
-- [](https://www.liga-manager-online.org/test/lmo_git/lmo.php?file=1l_2023-24.l98&lmouserlang=Francais) French (also installation language)
-- [](https://www.liga-manager-online.org/test/lmo_git/lmo.php?file=1l_2023-24.l98&lmouserlang=Deutsch) German (also installation language and [help files](https://www.liga-manager-online.org/test/lmo_git/help/Deutsch/index.html))
-- [](https://www.liga-manager-online.org/test/lmo_git/lmo.php?file=1l_2023-24.l98&lmouserlang=Magyar) Hungarian
-- [](https://www.liga-manager-online.org/test/lmo_git/lmo.php?file=1l_2023-24.l98&lmouserlang=Italiano) Italian
-- [](https://www.liga-manager-online.org/test/lmo_git/lmo.php?file=1l_2023-24.l98&lmouserlang=Norsk) Norwegian
-- [](https://www.liga-manager-online.org/test/lmo_git/lmo.php?file=1l_2023-24.l98&lmouserlang=Portugues) Portuguese
-- [](https://www.liga-manager-online.org/test/lmo_git/lmo.php?file=1l_2023-24.l98&lmouserlang=Romanian) Romanian
-- [](https://www.liga-manager-online.org/test/lmo_git/lmo.php?file=1l_2023-24.l98&lmouserlang=Slovenskega) Slovenian
-- [](https://www.liga-manager-online.org/test/lmo_git/lmo.php?file=1l_2023-24.l98&lmouserlang=Espanol) Spanish (also installation language)
+Existing installation (uploaded by FTP): name its folder as the target of `lmo-install`. The program files are replaced, your data is kept. Make a backup first.
-### System requirement
-- Web server with at least PHP 8.0 - maximum PHP 8.4
+If the web server delivers your whole project folder, the folder `vendor` next to `lmo` must not be reachable from a browser; `lmo-install` puts an `.htaccess` there. On nginx or other servers without `.htaccess`, block `vendor` and the folders `config`, `includes`, `ligen` and `vendor` inside `lmo` yourself.
> [!NOTE]
This LMO version is still a beta version. Please note this. Before copying over an existing LMO, make a backup first!
@@ -113,9 +62,9 @@ Mit diesem Skript können Sie Ihren Besuchern einen tollen Ergebnisdienst über
Momentan unterstützte Sprachen (_alphabetisch sortiert_)
- [](https://www.liga-manager-online.org/test/lmo_git/lmo.php?file=1l_2023-24.l98&lmouserlang=Bosanski) Bosnisch
-- [](https://www.liga-manager-online.org/test/lmo_git/lmo.php?file=1l_2023-24.l98&lmouserlang=Deutsch) Deutsch (auch Installationssprache und [Hilfedateien](https://www.liga-manager-online.org/test/lmo_git/help/Deutsch/index.html))
-- [](https://www.liga-manager-online.org/test/lmo_git/lmo.php?file=1l_2023-24.l98&lmouserlang=English) Englisch (auch Installationssprache)
-- [](https://www.liga-manager-online.org/test/lmo_git/lmo.php?file=1l_2023-24.l98&lmouserlang=Francais) Französisch (auch Installationssprache)
+- [](https://www.liga-manager-online.org/test/lmo_git/lmo.php?file=1l_2023-24.l98&lmouserlang=Deutsch) Deutsch ([Hilfedateien](https://www.liga-manager-online.org/test/lmo_git/help/Deutsch/index.html))
+- [](https://www.liga-manager-online.org/test/lmo_git/lmo.php?file=1l_2023-24.l98&lmouserlang=English) Englisch
+- [](https://www.liga-manager-online.org/test/lmo_git/lmo.php?file=1l_2023-24.l98&lmouserlang=Francais) Französisch
- [](https://www.liga-manager-online.org/test/lmo_git/lmo.php?file=1l_2023-24.l98&lmouserlang=Italiano) Italienisch
- [](https://www.liga-manager-online.org/test/lmo_git/lmo.php?file=1l_2023-24.l98&lmouserlang=Hrvatski) Kroatisch
- [](https://www.liga-manager-online.org/test/lmo_git/lmo.php?file=1l_2023-24.l98&lmouserlang=Nederlands) Niederländisch
@@ -123,12 +72,29 @@ Momentan unterstützte Sprachen (_alphabetisch sortiert_)
- [](https://www.liga-manager-online.org/test/lmo_git/lmo.php?file=1l_2023-24.l98&lmouserlang=Portugues) Portugiesisch
- [](https://www.liga-manager-online.org/test/lmo_git/lmo.php?file=1l_2023-24.l98&lmouserlang=Romanian) Rumänisch
- [](https://www.liga-manager-online.org/test/lmo_git/lmo.php?file=1l_2023-24.l98&lmouserlang=Slovenskega) Slowenisch
-- [](https://www.liga-manager-online.org/test/lmo_git/lmo.php?file=1l_2023-24.l98&lmouserlang=Espanol) Spanisch (auch Installationssprache)
+- [](https://www.liga-manager-online.org/test/lmo_git/lmo.php?file=1l_2023-24.l98&lmouserlang=Espanol) Spanisch
- [](https://www.liga-manager-online.org/test/lmo_git/lmo.php?file=1l_2023-24.l98&lmouserlang=Cestina) Tschechisch
- [](https://www.liga-manager-online.org/test/lmo_git/lmo.php?file=1l_2023-24.l98&lmouserlang=Magyar) Ungarisch
### Systemvorraussetzung
-- Webserver mit mindestens PHP 8.0 - maximal PHP 8.4
+- Webserver mit mindestens PHP 8.0 - maximal PHP 8.5
+
+### Installation
+Der LMO wird mit [Composer](https://getcomposer.org/) installiert.
+1. Im Projektordner ausführen (dort, wo die `composer.json` liegt oder liegen soll):
+ ```
+ composer require lmo/lmo_php8
+ vendor/bin/lmo-install
+ ```
+ Der LMO wird im Ordner `lmo` des Projekts eingerichtet. Für einen anderen Ordner diesen angeben, z. B. `vendor/bin/lmo-install public/lmo`.
+2. `lmo/lmo.php` im Browser aufrufen. Der LMO richtet sich selbst ein.
+3. **Sofort** `lmo/lmoadmin.php` aufrufen und Benutzername und Passwort des Admins festlegen.
+
+Update: `composer update` ausführen. Der LMO im Ordner `lmo` wird dabei automatisch aktualisiert. Einstellungen, Ligen, Tipps und eine angepasste `.htaccess` bleiben erhalten.
+
+Bestehende Installation (per FTP hochgeladen): deren Ordner als Ziel von `lmo-install` angeben. Die Programmdateien werden ersetzt, die Daten bleiben erhalten. Vorher eine Sicherung machen.
+
+Liefert der Webserver den ganzen Projektordner aus, darf der Ordner `vendor` neben `lmo` nicht per Browser erreichbar sein; `lmo-install` legt dort eine `.htaccess` ab. Bei nginx oder anderen Servern ohne `.htaccess` diesen Ordner und in `lmo` die Ordner `config`, `includes`, `ligen` und `vendor` selbst sperren.
> [!NOTE]
Bei dieser LMO Version handelt es sich immer noch um eine Beta Version. Beachtet das bitte. Vor dem drüberkopieren über einen vorhandenen LMO macht zuerst eine Sicherung!
diff --git a/TESTING.md b/TESTING.md
new file mode 100644
index 00000000..8c64a6a5
--- /dev/null
+++ b/TESTING.md
@@ -0,0 +1,325 @@
+# Golden-Master-Tests (PHPUnit + Docker)
+
+Ziel: Das Verhalten des **unveränderten** Codes festhalten, bevor refaktoriert wird, und nach jedem
+Schritt prüfen, dass sich nichts geändert hat. Die Tests brauchen keinen Webserver und keinen
+Installer: Jede Anfrage läuft in einem eigenen PHP-Prozess gegen eine frische Kopie der App.
+
+**Stand:** Das Gerüst läuft in Docker. Die Snapshots sind am unveränderten Original aufgezeichnet,
+die Erweiterungen (v1 bis v16) sind unten in der Reihenfolge ihrer Entstehung beschrieben.
+
+## Was geprüft wird
+| Test | Prüft |
+|---|---|
+| `PagesTest` | HTML der öffentlichen Seiten: Ligenübersicht, Tabelle (alle Typen, Teil-Spieltage, Heim/Auswärts, Punktespalten), Ergebnisse, Spielplan, Kreuztabelle, Statistik, Fieberkurve, Kalender, Info, Sprachen |
+| `CalculationTest` | `Lmo\Liga\Tabelle` über `tests/GoldenMaster/bin/calctable.php` (inkl. direktem Vergleich): Sortierschlüssel und alle Bilanzspalten je Liga, 5 Tabellentypen, mehrere Spieltage, Hin-/Rückrunden-Kombinationen, Admin-Ansicht |
+| `FirstRunTest` | **Neues Verhalten, kein Golden Master:** Ersteinrichtung ohne Installer (`lmo-setup.php`): Konfiguration wird aus `config-default/` angelegt, vorhandene bleibt, neue Optionen nach einem Update werden ergänzt (nur wenn sich `config-default/` geändert hat), `init-parameters.php` hat Vorrang, Fehlermeldungen, Admin-Konto per Formular (Hash, Validierung, kein zweites Setup, Sitzung endet ohne Konto), Installer und FTP-Code sind entfernt |
+| `UrlDetectionTest` | **Neues Verhalten, kein Golden Master:** `lmo_detect_url()` – Unterordner, Addon-Skripte, Alias, Einbindung von außen, HTTPS/Proxy, Port, Kommandozeile |
+
+Die 9 mitgelieferten Ligen haben **keine einzige gespielte Partie**. Deshalb erzeugt
+`LeagueFactory` 8 Testligen mit festem Seed: normale Liga, Minuspunkte + direkter Vergleich,
+Kegelwertung, Strafpunkte/-tore ab Spieltag, Sonderwertung (n.V./i.E.), Handicap-Reihenfolge,
+Wertung am grünen Tisch, Platzmarkierungen (Meister, Champions League, Abstieg).
+
+## Ablauf
+
+```bash
+# 1. Unveränderten Originalstand bereitstellen (ZIP von GitHub, NICHT den refaktorierten Stand)
+unzip lmo_php8-master.zip -d .original # ergibt .original/lmo_php8-master/lmo/...
+
+# 2. Snapshots am Original aufzeichnen (einmalig, danach ins Git einchecken)
+# Spaeter hinzugekommene Faelle: GOLDEN_MODE=record-missing legt nur FEHLENDE Snapshots an und
+# vergleicht vorhandene (sicherer als "record", das alles ueberschreibt).
+ORIGINAL_DIR=./.original/lmo_php8-master LMO_SOURCE=/original GOLDEN_MODE=record \
+ docker compose run --rm tests
+
+# 3. Gegen den aktuellen Arbeitsstand prüfen (nach jedem Refactoring-Schritt)
+docker compose run --rm tests
+
+# Nur eine Gruppe oder ein Fall:
+docker compose run --rm tests --filter CalculationTest
+docker compose run --rm tests --filter 'PagesTest.*action=table'
+
+# Andere PHP-Version (README nennt 8.0 bis 8.5)
+PHP_VERSION=8.0 docker compose run --rm tests
+```
+
+Ohne Docker: `composer install && php vendor/bin/phpunit` (PHP ≥ 8.0, optional `faketime`).
+
+**Wichtig:** Snapshots nur am unveränderten Original aufzeichnen und nur dann neu aufzeichnen, wenn
+eine Abweichung geprüft und gewollt ist. Eine Abweichung ist zunächst ein Befund, kein Grund
+zum Aktualisieren.
+
+## Statische Analyse mit PHPStan
+
+```bash
+docker compose run --rm phpstan # im Docker
+composer phpstan # ohne Docker
+```
+
+Es laufen sechs Pruefungen nacheinander:
+
+| Konfiguration | Stufe | Umfang |
+|---|---|---|
+| `phpstan.neon.dist` | 0 | gesamter Code (`lmo/`, `src/`, `bin/`, `tests/`): unbekannte Klassen, Funktionen und Methoden |
+| `phpstan-classlib.neon.dist` | 5 | Klassen und Funktionen der Classlib (`lmo/addon/classlib`): zusaetzlich nicht gesetzte Variablen, Typangaben in den Kommentaren, Rueckgabetypen und Parametertypen bei Aufrufen |
+| `phpstan-liga.neon.dist` | 6 | 33 alte Dateien, die am Dateianfang nennen, welche Variablen sie erwarten |
+| `phpstan-liga-basis.neon.dist` | 1 | 31 weitere solche Dateien, die auf hoeheren Stufen noch Meldungen haben |
+| `phpstan-tests.neon.dist` | 5 | Tests ohne die Hilfsskripte in `tests/GoldenMaster/bin`: falsche Parametertypen, unmoegliche Vergleiche |
+| `phpstan-strict.neon.dist` | 8 | neuer, typisierter Code (`src/`, `bin/lmo-install`, `lmo/lmo-version.php`) |
+
+Ab Stufe 1 meldet PHPStan die globalen Variablen des alten Codes, die eine Datei setzt und eine
+andere per `require` benutzt (rund 9500 Stellen). Deshalb bleibt der alte Code auf Stufe 0. Die
+Classlib besteht aus Klassen und steht deshalb auf Stufe 5; ab Stufe 6 verlangt PHPStan Typen an
+jedem Parameter und Rueckgabewert. Wird
+eine Datei typisiert und von globalen Variablen befreit, kommt sie in `phpstan-strict.neon.dist`.
+
+Auf Stufe 0 ist der gesamte Code ohne Meldung; eine Baseline gibt es deshalb nicht. Die einzige
+Ausnahme steht mit Begruendung in `phpstan.neon.dist` (`ignoreErrors`).
+
+Geprueft wird fuer PHP 8.0 bis 8.5 (`phpVersion` in der Konfiguration), unabhaengig davon, mit
+welcher PHP-Version PHPStan selbst laeuft.
+
+### Kopfkommentar einer alten Datei
+
+Alte Dateien werden eingebunden und arbeiten mit Variablen, die der einbindende Code gesetzt hat.
+Damit PHPStan sie pruefen kann, nennt die Datei diese Variablen am Anfang:
+
+```php
+/**
+ * Diese Datei wird eingebunden und erwartet:
+ *
+ * @var \Lmo\Liga\Rohdaten\Liga $lmo_liga Rohdaten der geladenen Liga (lmo-openfile.php)
+ * @var array $text Sprachtexte
+ * @var string $file Ligadatei relativ zum Ligenordner, leer wenn keine geladen ist
+ */
+```
+
+Die Kommentare der 62 Dateien sind erzeugt: Welche Variablen eine Datei braucht, hat PHPStan
+gemeldet; der Typ ist `array`, wenn die Datei mit `[...]` zugreift, sonst `mixed`, und `|null`,
+wenn sie selbst mit `isset()` prueft. Die Gruppe "nicht auf jedem Weg gesetzt" nennt Variablen,
+die die Datei nur unter einer Bedingung setzt und spaeter liest: Stellen zum Hinsehen.
+`phpstan-bootstrap.php` liefert PHPStan die Konstanten, die `init.php` erst zur Laufzeit anlegt.
+
+## So werden Abweichungen angezeigt
+- HTML wird normalisiert (Session-IDs, Temp-Pfade, Zufallsfarben der Fieberkurve entfernt,
+ Whitespace zusammengefasst, ein Tag pro Zeile). Dadurch stören Einrückungsänderungen durch
+ Twig nicht, Strukturänderungen aber schon.
+- Numerische Strings und Zahlen gelten in `CalculationTest` als gleich (`"0"` = `0`), weil das
+ Original `array_pad(..., "0")` benutzt.
+- PHP-Warnungen und -Notices erscheinen als `` im Snapshot. Neue Warnungen
+ nach einem Refactoring führen also ebenfalls zu einer Abweichung.
+
+## Bekannte Grenzen
+- **Nicht abgedeckt:** Admin-Bereich (Speichern in `.l98`), KO-Ligen (`Type` ungleich 0), Tippspiel,
+ Spielerstatistik, `savehtml`, Mailversand, erzeugte Grafiken (`lmo-gif`). Der Schreibpfad ist der
+ nächste Baustein: Admin-Aktionen mit Sitzung nachspielen und die geschriebene Datei vergleichen.
+- **Zeit:** `faketime` setzt die Uhr auf 2025-03-15 12:00 (läuft danach weiter). Ohne `faketime`
+ hängen die Kalenderseiten vom Tagesdatum ab. Die Berechnungszeit ist in der Testinstanz abgeschaltet.
+- **GET überschreibt Konfiguration:** `init.php` extrahiert `$_GET` nach dem Laden von `cfg.txt`
+ in den globalen Scope. Die Tests nutzen das für `tabonres=2` und `tabpkt=0`. Es ist zugleich
+ ein Sicherheitsproblem des Originals (jede Konfigurationsvariable lässt sich per URL setzen).
+- Die Kalenderseite (`action=cal`) hängt von Optionen der Liga ab und kann leer sein; das ist dann
+ Teil des Golden Masters.
+
+## Coverage-Messung: welcher Code wird vom Golden Master nie ausgeführt?
+
+Misst mit Xdebug (nur in den Kindprozessen aktiv), welche Zeilen von `lmo/` während aller Tests laufen (jeder Kindprozess schreibt
+seine Abdeckung, am Ende wird zusammengeführt). Damit wird "deckt der Golden Master alles ab" zu
+einer Zahl je Datei.
+
+```powershell
+docker compose build tests # einmalig, installiert Xdebug im Image
+$env:GOLDEN_COVERAGE = "1"
+docker compose run --rm tests
+Remove-Item Env:GOLDEN_COVERAGE
+```
+
+Ergebnis auf der Konsole (Zusammenfassung je Bereich + Kerndateien, schlechteste zuerst) und in
+`coverage/`: `report.txt` (alle Dateien), `report.csv` (für Excel, Trennzeichen `;`) und
+`uncovered.txt` (nicht ausgeführte Zeilenbereiche je Datei).
+
+Bedeutung: "ausgeführt" heißt, die Zeile lief mindestens einmal. Es heißt nicht, dass eine
+Änderung dort auffallen würde. Ein Fehler in einer Zeile, deren Ergebnis nirgends in der Ausgabe
+landet, bleibt unbemerkt. Die Zahl ist eine Untergrenze für Lücken, kein Beweis für Sicherheit.
+Bei nie geladenen Dateien ist die Zeilenzahl geschätzt (`~`).
+
+### Bekanntes Problem: pcov stuerzt ab
+Mit pcov endeten die Kindprozesse mit Exit-Code 139 (Segmentation fault), Coverage-Daten wurden nie
+geschrieben. Deshalb nutzt die Messung Xdebug. Die Coverage-Messung laeuft ohne `faketime`;
+zeitabhaengige Seiten (Kalender) koennen im Coverage-Lauf vom Snapshot abweichen. Das ist hier
+unerheblich, der Report wird trotzdem erzeugt. Mit `GOLDEN_NO_FAKETIME=1` laesst sich `faketime`
+auch ausserhalb der Messung abschalten.
+
+Achtung PowerShell: `$env:GOLDEN_COVERAGE` bleibt in der Sitzung gesetzt, bis es mit
+`Remove-Item Env:GOLDEN_COVERAGE` entfernt wird. Die erste Ausgabezeile des Laufs zeigt
+`Coverage: 1` oder `Coverage: 0`.
+
+Tipp: `docker compose run --rm tests --stop-on-failure` bricht nach dem ersten Fehler ab und
+vermeidet seitenlange Ausgaben.
+
+### Hermetische Tests: kein Netzwerkzugriff
+Nur der Admin-Bereich fragt nach einer neueren Version (`lmo-adminmain.php`, `lmo_update_info()` in
+`lmo-version.php`): bei Packagist, hoechstens einmal am Tag, und nur wenn eine getaggte Version
+installiert ist. Im Checkout ist die Version `dev`, es wird also nie gefragt. Die Testprozesse
+laufen zusaetzlich mit `allow_url_fopen=0`.
+
+Die Versionsnummer selbst kommt aus dem Git-Tag (Composer) und steht in keiner Datei. Auf der
+Info-Seite ersetzt der Normalizer sie durch `{LMO_VERSION}`; ein neuer Tag aendert keine Snapshots.
+
+### Snapshot-Modi
+| `GOLDEN_MODE` | Verhalten |
+|---|---|
+| (leer) | Vergleichen. Fehlender Snapshot = Fehler |
+| `record-missing` | Fehlende Snapshots anlegen, vorhandene vergleichen. Standard fuer neue Testfaelle |
+| `record` | **Alle** Snapshots neu schreiben. Nur am unveraenderten Original und nur bewusst |
+
+Neue Szenarien (`direct_kegel`, `direct_special`, `direct_wertung`, `direct_handicap`) pruefen den
+direkten Vergleich mit Kegelwertung, Sonderpunkten, Wertung am gruenen Tisch und Handicap; sie
+testen nur Tabellenseiten und die Berechnung (`pages => table`).
+
+### Szenarien nach Coverage-Auswertung (v7)
+Lücken des ersten Coverage-Laufs und was sie schließt:
+| Lücke (Original) | Ursache | Szenario |
+|---|---|---|
+| `Lmo\Liga\Tabelle` (direkter Vergleich)| Sonderpunkte im direkten Vergleich | `direct_special` |
+| `Lmo\Liga\Tabelle` (direkter Vergleich)| Wertung am grünen Tisch im direkten Vergleich | `direct_wertung` |
+| `Lmo\Liga\Tabelle` (direkter Vergleich)| Kegelwertung im direkten Vergleich | `direct_kegel` |
+| `lmo-showtable.php` 144-145, 188, 229-230 | Favorit, Vereins-URL, Teamnotiz | `viewopts` |
+| `lmo-showstats.php` 59-296 | Statistik-Vergleich braucht `stat1`/`stat2` | Seiten `action=stats&stat1=...` |
+| `lmo-calctable.php` 100-102, 117-119, 185-187, 202-204 | nach Analyse nicht erreichbar (siehe unten) | keines |
+| `lmo-calctable*.php` 23, 26 | Standardwerte, wenn `$tabtype`/`$newtabtype` fehlen | keines |
+
+Die vier Blöcke in `lmo-calctable.php` aktualisieren den "höchsten Sieg" bzw. die "höchste
+Niederlage" aus einem Ergebnis, das per Wertung entschieden wurde. Beim Einlesen setzt `lmo-openfile.php`
+dafür ein Tor auf `0`. Dann kann die Tordifferenz den Startwert 0 nie übertreffen, die Bedingung
+ist nie wahr. Das ist eine Herleitung aus dem Code, kein Beweis durch Test.
+
+### v8: letzte kleine Lücken
+| Lücke | Szenario/Seite |
+|---|---|
+| `Lmo\Liga\Tabelle` (direkter Vergleich)(Gastteam verliert durch Heim-Wertung im direkten Vergleich) | `direct_wertung_many` (viele Wertungsspiele) |
+| `lmo-showstats.php` 26-27 (`stat2` ohne `stat1`) | Seite `action=stats&stat1=0&stat2=2` |
+
+Nicht abgedeckt bleiben bewusst: die vier Wertungsblöcke in `lmo-calctable.php` (nicht erreichbar, s. o.)
+und die Standardwerte in Zeile 23/26 (`$tabtype` ist im Webablauf immer gesetzt).
+
+## Admin-Bereich (v9)
+
+Neu: `bin/http.php` fuehrt beliebige GET/POST-Anfragen gegen jeden Einstiegspunkt aus, auf Wunsch
+innerhalb einer Sitzung (`AdminClient` merkt sich die Sitzungs-ID zwischen den Schritten).
+`StateTracker` meldet nach jedem Schritt, welche Dateien neu, geaendert oder geloescht sind (als
+Unified Diff). Kennwort-Hashes (Zufalls-Salt) und die Laufzeit in der Fusszeile werden normalisiert.
+
+| Test | Prueft |
+|---|---|
+| `AdminViewsTest` | ca. 60 lesende Admin-Ansichten als Hauptadmin (Startseite, Neu, Oeffnen, Loeschen, Upload, Download, Optionen, Addons, Design, Benutzer, Update, Tippspiel-Seiten, Viewer; je Liga Bearbeiten/Grunddaten/Mannschaften/Anzahl/Spieltage/Tabellenkorrektur). Der Snapshot enthaelt auch Dateien, die der Seitenaufruf veraendert hat (normalerweise keine) |
+| `AdminLoginTest` | Anmeldung (falsch/richtig), Abmelden, Kennwort-Upgrade auf Hash, Hilfsadmin und erweiterter Hilfsadmin (erlaubte und gesperrte Seiten) |
+| `StyleTest` | `lmo-style.php`, `lmo-style-nc.php` |
+
+Pruefen, ob das Admin-Geruest funktioniert (nach dem ersten `record-missing`-Lauf):
+```powershell
+# darf keinen Treffer liefern: sonst wurde die Sitzung nicht uebernommen und alle Seiten zeigen das Anmeldeformular
+Select-String -Path tests\GoldenMaster\__snapshots__\admin-views\*.txt -Pattern 'name="xusername"' -List | Measure-Object
+# Anmeldeablauf ansehen
+Get-Content tests\GoldenMaster\__snapshots__\admin-flows\login_logout-*.txt | Select-String "^===|GEAENDERT|NEU |HASH"
+```
+
+Noch **nicht** enthalten: schreibende Admin-Aktionen (Ergebnisse speichern, Mannschaften, Grunddaten,
+Optionen, Benutzer, neue Liga, Loeschen, Upload). Die kommen als naechster Baustein.
+
+## Schreibende Admin-Aktionen (v10)
+
+`FormSubmitter` liest ein Formular wie ein Browser aus (Standardwerte, Auswahl, Haken) und `AdminClient`
+schickt es mit gezielten Abweichungen ab. `Fixture::loggedInClient()` liefert je Test eine frische, angemeldete
+Instanz; jedes Szenario ist von den anderen unabhaengig.
+
+| Test | Prueft |
+|---|---|
+| `AdminWriteTest` | Ergebnisse/Termine/Notizen speichern, Grunddaten, Mannschaften inkl. Straf- und Torkorrektur, Anzahl Spieltage, Tabellenkorrektur, Optionen, Design, Benutzer anlegen/bearbeiten/loeschen, Liga loeschen, Liga neu anlegen (4 Schritte) |
+| `AdminRoundtripTest` | jedes Formular unveraendert abschicken (Optionen, Addons, Design, Benutzer, Viewer, Tippspiel, je Liga Ergebnisse/Grunddaten/Mannschaften/Anzahl/Spieltage/Tabellenkorrektur) |
+| `AdminAuthProbeTest` | Sicherheitsprobe: Admin-Bereich und direkt aufrufbare Include-Dateien ohne Anmeldung |
+
+Protokollformat eines Schritts: `=== Titel ===`, gesendete Felder, Antwort, `--- Dateiaenderungen ---` (Diff).
+Steht dort `KEIN FORMULAR GEFUNDEN` oder `LEERES FORMULAR`, hat der Test das Formular nicht gefunden
+(falsche Annahme, nicht Verhalten der Anwendung).
+
+Fixture-Aenderung gegenueber v9: `Instance` legt jetzt auch an, was der Installer anlegen laesst
+(`addon/tipp/lmo-tippauth.txt`, Tipp- und Spieler-Verzeichnisse). Ohne diese Datei bricht
+`todo=tippuser` mit einem Fatal Error ab. Dadurch aendern sich einige Snapshots unter
+`admin-views` (`*todo=tipp*`); sie muessen neu aufgezeichnet werden.
+
+Nicht abgedeckt: Datei-Upload (`todo=upload`), Download als Datei (`lmo-admindownload.php` mit gueltiger Sitzung),
+Zufallsspielplan (`xprogram=random`, nicht reproduzierbar), JavaScript im Browser.
+
+### Normalisierung von Stack-Traces (v11)
+PHP-Fehlermeldungen im Snapshot enthalten einen Stack-Trace mit den Hilfsskripten der Tests
+(`.../bin/http.php(57)`). Wird ein Hilfsskript geaendert, verschiebt sich die Zeilennummer und der
+Snapshot wird rot, obwohl sich die Anwendung nicht geaendert hat. Der Normalizer ersetzt diese
+Angaben durch `{HARNESS}/http.php(N)`. Snapshots, die einen Stack-Trace enthalten, einmal neu aufzeichnen
+(betrifft bisher `todo=pdfoptions` und `todo=tippuser`).
+
+### Uhrzeit nach dem Speichern (v12)
+`faketime` setzt nur die Uhr, nicht die Dateizeit: Eine vom Admin gespeicherte Ligadatei hat die echte Zeit.
+Die oeffentlichen Seiten zeigen sie als "Letztes Update der Liga". In Admin-Protokollen ersetzt der Normalizer
+diese Angabe durch `{STAND}`. Betroffene Snapshots (`admin-write`: results, basic_data, teams,
+table_correction, create_league) muessen einmal neu aufgezeichnet werden.
+Tipp: Nach dem Aufzeichnen zwei Vergleichslaeufe mit mindestens einer Minute Abstand starten,
+sonst bleiben Zeitabhaengigkeiten unbemerkt.
+
+### Coverage-Report: Admin getrennt ausgewiesen (v13)
+Der Bereichsbericht zeigt jetzt "Kern: oeffentlich" und "Kern: Admin" getrennt (vorher ein
+gemeinsames "Kern (lmo/*.php)"), damit sichtbar ist, wie weit der Admin-Bereich abgedeckt ist,
+ohne ihn mit den oeffentlichen Seiten zu vermischen. report.csv/uncovered.txt unveraendert.
+
+## Verbleibende Kern-Luecken geschlossen (v14)
+
+Nach der getrennten Admin-/oeffentlich-Auswertung (v13) wurden die uebrig gebliebenen 0%-Dateien
+im Kern einzeln geprueft:
+
+| Datei | Befund | Massnahme |
+|---|---|---|
+| `index.php` | reiner Redirect auf `lmo.php` | `CoreGapsTest::testIndexRedirectsToLmoPhp` |
+| `lmo-adminopenprogram.php` | Spielplan "aus Datei uebernehmen"; `$xprogram` wird ungeprueft an `fopen()` uebergeben (nur Suffix `.l98` verlangt) - eigener, kleiner Befund zum Pfad-Handling, siehe Klassenkommentar in `CoreGapsTest.php` | `CoreGapsTest::testCreateLeagueFromTemplate` |
+| `lmo-adminrndprogram.php` | erzeugt einen echten Zufallsspielplan (nicht deterministisch) | `RandomScheduleSmokeTest`: kein Golden-Master-Vergleich, sondern Plausibilitaetspruefung (jedes Team pro Spieltag genau einmal) + Pruefung auf PHP-Fehler |
+| `lmo-admindir.php` | **toter Code** - keine einzige Referenz im gesamten Repository (auch nicht in Addons) | bewusst nicht getestet, bleibt bei 0% |
+| `lmo-paintgraph.php` | war toter Code ohne Referenz und ist in 4.2.1 entfernt (die Fieberkurve zeichnet der Browser mit Chart.js; das Tippspiel-Addon hat eine eigene Datei `lmo-tipppaintgraph.php`) | entfaellt |
+| `lmo-adminuserpass.php`, `lmo-openfiledat.php` | nur vom Tippspiel-Addon genutzt (Zufallspasswort bei neuem Tipper bzw. E-Mail-Versand mit Spieltagsbezug) | zurueckgestellt, gehoert zum separaten Block "Tippspiel-Addon" (aktuell 4% Abdeckung, 64 Dateien) |
+
+KO-Ligen (`Type=1`: `lmo-showkoprogram.php`, `lmo-showkoresults.php`, Teile von `lmo-adminnew.php`)
+sind bewusst **nicht** Teil dieser Runde: Das ist ein eigener, groesserer Baustein (eigene
+Fixture-Struktur mit Turniermodus/Spieltagsmodi, eigene Admin-Formularfelder), vergleichbar im
+Umfang mit dem Tippspiel-Addon. Die einzige reale Liga im Repo (`1l_2024-25.l98`) ist `Type=0`.
+
+## Dritter Befund: Spielplan-Bug in lmo-adminrndprogram.php (v15)
+
+`RandomScheduleSmokeTest` deckte beim ersten echten Lauf einen eigenstaendigen Fehler im
+unveraenderten Original auf (unabhaengig von extract() und dem xprogram-Pfadproblem):
+
+**Fisher-Yates-Shuffle mit Off-by-One** (~Zeile 160): `$j = @mt_rand(0, $i+1);` statt
+`mt_rand(0, $i)`. Beim ersten Schleifendurchlauf (`$i` = Teamzahl-1) liegt die Obergrenze um 1 zu
+hoch; mit Wahrscheinlichkeit 1/(Teamzahl+1) wird ein Index ausserhalb des Arrays gezogen. Das
+erzeugt nicht nur eine "Undefined array key"-Warnung, sondern laesst eine Teamposition auf `NULL`
+stehen - der erzeugte Zufallsspielplan verliert ein Team. Bei 6 Teams: ca. 14% der Aufrufe.
+
+**Behoben in 4.2.1:** `mt_rand(0, $i+1)` ist durch `mt_rand(0, $i)` ersetzt. Der Test toleriert die
+Warnung nicht mehr. Weil der Fehler nur manchmal auftrat, erzeugt er 30 Zufallsspielplaene und prueft
+bei jedem, dass an jedem Spieltag alle Mannschaften genau einmal spielen und keine PHP-Meldung
+erscheint.
+
+## KO-Ligen / Pokalmodus (v16)
+
+Eigenes Datenformat (`Type=1`): Ergebnisfelder sind "Spiel+Durchgang"-kodiert (z. B. `GA00` =
+Spiel 1, 1. Durchgang), Team-Zuordnung je Runde erfolgt manuell durch den Admin (kein
+automatisches Nachruecken der Sieger). Modus je Runde: 1=Einzelspiel, 2=Hin-/Rueckspiel,
+3/5/7=Best-of-3/5/7 (steuert, wie viele "Durchgaenge" pro Spiel moeglich sind).
+
+`KoLeagueTest` erzeugt die Liga ueber den echten Admin-Assistenten (`todo=new&xtype=1`) statt als
+Fixture-Datei nachzubauen - das Dateiformat ist komplex genug, dass der "richtige" Weg ueber die
+Anwendung selbst zuverlaessiger ist. Abgedeckt: 4er-Turnierbaum (Halbfinale + Finale),
+Einzelspiel-Modus (Standard), oeffentliche Seiten (`action=program&selteam=N`,
+`action=results&st=N`, `action=cal`), Admin-Bearbeitung und ein Roundtrip (unveraendert speichern).
+
+NICHT abgedeckt (eigener, groesserer Block): Hin-/Rueckspiel und Best-of-3/5/7 (Modus 2/3/5/7),
+Playoffmode-Varianten (2-2-1 usw., siehe `$playoffmode` in `lmo-adminedit.php`/
+`lmo-showkoresults.php`), Spielerstatistik-Addon fuer KO, Tippspiel-Addon fuer KO
+(`lmo-tippeditko.php` und Verwandte), groessere Turnierbaeume (8/16/24/32+ Teams).
diff --git a/bin/lmo-install b/bin/lmo-install
new file mode 100755
index 00000000..7ef85f1c
--- /dev/null
+++ b/bin/lmo-install
@@ -0,0 +1,26 @@
+#!/usr/bin/env php
+einstellungen()->mannschaften()`, `->mitVerlaengerung()`. Dazu an den Rohdaten `Mannschaft::anzeigename()`, `Liga::mannschaftsname()` sowie `Spieltag::ersterTag()` und `letzterTag()`. Damit können Seiten direkt auf den Rohdaten arbeiten; die beiden Tippspiel-Lader tun das bereits.
+* `Lmo\Liga\Spiel`: ein Spiel zum Anzeigen, abgeleitet aus den Rohdaten (Tore mit »nicht gespielt«, Wertung am grünen Tisch, Zusatz, Notiz, Anstoß) – `Spiel::ausRohdaten($liga, $spieltag, $partie)`. An der Mannschaft dazu `kurzname()` und `mittelname()`, die einen fehlenden Namen aus dem Namen bilden. Der Hauptlader stellt die Rohdaten als `$lmo_liga` bereit.
+* Zum Durchlaufen mit `foreach`: `Rohdaten\Liga::alleMannschaften()`, `spiele($spieltag)` und `alleSpiele()` liefern vollständig und in der Reihenfolge der Nummern, auch wenn in der Datei etwas fehlt oder anders sortiert ist; im Pokal je Paarung die Spiele des Modus. `Einstellungen::partienImSpieltag()` nennt die Zahl der Partien einer Runde.
+* `Lmo\Liga\Paarung`: zwei Mannschaften und ihre Spiele in einer Runde, mit `sieger()` (ein Spiel, Hin- und Rückspiel, Best of 3/5/7). `Rohdaten\Liga::paarungen($runde)` liefert sie zum Durchlaufen.
+* `Lmo\Liga\Tabelle`: rechnet die Tabelle einer Liga aus den Spielen der Rohdaten – gesamt, heim, auswärts, Hin- und Rückrunde, bis zu einem Spieltag, mit Strafen ab Stichtag, Wertungen am grünen Tisch, Kegel-Sortierung und direktem Vergleich. Die Zeilen (`Tabellenzeile`) haben feste Typen und tragen auch Serien sowie höchsten Sieg und höchste Niederlage (`Rekord`). Aufruf: `Tabelle::berechne($liga, Tabelle::HEIM, 1, 17)`.
+
+### Changed
+* Die Classlib liest und schreibt Ligadateien über dieselbe Klasse wie der Kern (`Lmo\Liga\Rohdaten\Liga`). Von der Classlib geschriebene Dateien haben dadurch die Abschnittsfolge des Kerns und enthalten immer `[News]`, `[Teamm]` und `[Teamk]`; die Werte sind dieselben. Ob eine Datei Liga oder Pokal ist, entscheidet jetzt auch in der Classlib der Typ der Datei, nicht mehr der Modus der einzelnen Runde.
+* Die Ligalisten (Ligaübersicht, Admin: Öffnen, Löschen, Download, neue Liga, Viewer-Optionen; Tippspiel) lesen Name, Typ und Größe einer Liga über `Lmo\Liga\LigaDatei::liesAbschnitt()` statt über neun eigene Leseschleifen. Leerzeichen um Schlüssel und Wert werden dabei wie beim Laden der Liga entfernt; eine nicht lesbare Datei bricht die Liste nicht mehr ab.
+* Die Spielplanvorlage beim Anlegen einer Liga (»aus Datei übernehmen«) sowie die Tippübersicht und die Erinnerungsmail des Tippspiels lesen Ligadateien über `Lmo\Liga\Rohdaten\Liga` bzw. `Lmo\Liga\LigaDatei` statt über eigene Leseschleifen. Damit liest außer der Classlib und dem Kern niemand mehr eine Ligadatei selbst.
+* Eine Ligadatei wird immer komplett gelesen. Die beiden kleinen Lader des Tippspiels (`lmo-openfilename.php`, `lmo-openfiledat.php`) und der Schalter `$lmo_only_st` lasen bisher nur einzelne Schlüssel bzw. einen Spieltag und hinterließen eine halb gefüllte Liga; jetzt liefern alle Lader dasselbe. Nur die Ligalisten lesen weiterhin allein den Abschnitt `[Options]`. Sichtbar: Eine Liga ohne Namen heißt im Tippspiel nicht mehr »No Name«, sondern hat wie überall sonst keinen Namen. In der Erinnerungsmail für einen Spieltag wird dessen Datum wie im Hauptlader geprüft: `1.9.2024` wird zu `01.09.2024` (bisher leer), ein Datum, das es nicht gibt (`31.02.2025`), wird geleert (bisher auf den 03.03. umgerechnet).
+* Tippübersicht und Erinnerungsmail des Tippspiels führen die anstehenden Spiele als Liste von `Lmo\Tipp\AnstehendesSpiel`. Bisher sammelten sie die Werte mehrerer Ligen als Listen in den Feldern der geladenen Liga (`$ligaDaten->titel`, `->teama` …), die dafür nicht gedacht sind.
+* Die Anzeige-Seiten der Liga arbeiten direkt auf den Rohdaten statt auf `$ligaDaten`: Spielplan einer Mannschaft (`lmo-showprogram.php`), Ergebnisse eines Spieltags (`lmo-showresults.php`), Kalender (`lmo-cal.php`) und Kreuztabelle (`lmo-showcross.php`). Die Ausgabe ist unverändert. `Lmo\Liga\Spiel` hat dafür feste Typen (Tore `?int`, `null` = nicht gespielt) und liefert die Anzeigetexte selbst (`toreHeimText()`, `zusatzText()`).
+* Im Pokal arbeiten der Spielplan einer Mannschaft (`lmo-showkoprogram.php`), die Ergebnisse einer Runde (`lmo-showkoresults.php`) und der Kalender (`lmo-kocal.php`) ebenfalls auf den Rohdaten. Der Kalender richtet seinen Monat jetzt nach dem frühesten Anstoß aller Spiele der gewählten Runde; bisher zählten durch einen Indexfehler nur die Spiele einer einzigen Paarung.
+* Die Tabellenberechnung (`lmo-calctable.php`) rechnet nicht mehr selbst, sondern über `Lmo\Liga\Tabelle` und füllt daraus die bisherigen Variablen. Alle Seiten, die die Tabelle nutzen (Tabelle, Statistik, HTML-Ausgabe, Admin), bekommen dieselben Werte wie zuvor.
+* Adminbereich: Die Seiten tragen ihre Änderungen nach und nach direkt in die Rohdaten der Liga ein und speichern über `lmo-saveliga.php`. `Lmo\Liga\Normalform` ergänzt die Rohdaten dabei zu der vollständigen Datei, die der LMO schreibt (jede Einstellung, jede Mannschaft, jeder Spieltag mit allen Partien); die geschriebene Datei ist dieselbe wie bisher. Schlüssel, die der LMO nicht kennt, bleiben auf diesem Weg auch bei Mannschaften, Spieltagen und Spielen erhalten. Bisher arbeiten so die Tabellenkorrektur (`lmo-admintab.php`, mit ihrer Tabelle über `Lmo\Liga\Tabelle`) die Anzahl der Spieltage und Partien (`lmo-adminanz.php`), die Einstellungen der Liga (`lmo-adminbasic.php`), die Mannschaften (`lmo-adminteams.php`), die Ergebnisse eines Spieltags (`lmo-adminedit.php`) und das Anlegen einer Liga (`lmo-adminnew.php`); eine neue Liga hat damit von Anfang an Mittelnamen für ihre Mannschaften und die (leeren) Einträge für eigene Bezeichnungen von Punkten und Toren, die bisher erst das erste Speichern ergänzte. Die Unternavigation des Adminbereichs liest aus den Rohdaten. Eine Mannschaft anlegen oder aus der Liga nehmen können die Rohdaten selbst (`Rohdaten\Liga::neueMannschaft()`, `entferneMannschaft()`).
+* Die Statistikdatei einer Liga (`lmo-zustat.php`) wird aus den Rohdaten erzeugt. `lmo-savefile.php` rechnet den Stand, der gespeichert wird, einmal in Rohdaten um, gibt ihn an die Statistik weiter und schreibt ihn dann; danach steht er auch in `$lmo_liga`.
+* Das Spieltagsmenü (`lmo-spieltagsmenu.php`), die Spielerstatistik im Adminbereich (`addon/spieler/lmo-statadmin.php`) und die Spielanzeige des Viewer-Addons lesen Spieltage, Art der Liga und Mannschaften aus den Rohdaten bzw. benutzen `$ligaDaten` nicht mehr als Zwischenspeicher.
+* Tippspiel, Erinnerungsmail für eine einzelne Liga (`lmo-tippemail.php`) und der Lader der Tipps eines Spieltags (`lmo-tippopenfile.php`): Spiele, Mannschaften und Einstellungen kommen aus den Rohdaten. Der Lader zählt die Paarungen über `Lmo\Tipp\Tipprunde`, statt sie in `$ligaDaten->anzsp` zu schreiben, und ändert bei einem Tipp mit dem Wert -2 nicht mehr die Wertung des Spiels in der geladenen Liga. Damit greift das Tippspiel nur noch an einer Stelle auf `$ligaDaten` zu: bei der Auswertung direkt nach dem Speichern im Adminbereich.
+* Tippspiel, Speichern der Tipps, Einsicht-Berechnung und Joker-Prüfung (`lmo-tippsavefile.php`, `lmo-tippsaveeinsicht1.php`, `lmo-tippcalceinsicht.php`, `lmo-tippjokeranticheat.php`) nehmen Paarungen, Spiele je Paarung und Anstoßzeiten aus `Lmo\Tipp\Tipprunde`, statt die Paarungen einer Pokalrunde jeweils selbst zu zählen und in `$ligaDaten->anzsp` zu schreiben. Menü, Fußzeile und Überschrift des Tippspiels sowie `lmo-tippopenfileall.php` lesen Art und Name der Liga aus den Rohdaten. Die Ligenliste (`lmo-tippnewdir.php`) bestimmt die Spieltage jeder Liga aus deren eigenen Einstellungen; bisher überschrieb sie damit die Spieltage der geladenen Liga, und eine Liga ohne die Angabe `Rounds` bekam die Zahl der Liga davor.
+* Tippspiel, Tabelle nach den Tipps (`lmo-tipptabelle.php`, `lmo-tippcalctable.php`, `lmo-tippcalctable1.php`, `lmo-tippcalcwertverein.php`): Spiele, Mannschaften, Strafen und Einstellungen kommen aus den Rohdaten. Die Wertung nach Vereinen zählt ihre Vereine selbst, statt die Anzahl der Mannschaften der geladenen Liga zu überschreiben.
+* Tippspiel, Auswertung (`lmo-tippsavewert.php`): Ergebnisse, Wertungen und Einstellungen kommen aus den Rohdaten, die Spiele eines Spieltags über `Lmo\Tipp\Tipprunde`. Nach dem Speichern im Adminbereich wird weiter der gerade eingegebene Stand ausgewertet. Ein Tipp mit dem Wert -2 ändert dabei nicht mehr die Wertung des Spiels (Sieg am grünen Tisch) für diesen und alle folgenden Tipper.
+* Tippspiel, Rangliste und Tipp-Fieberkurve (`lmo-tippwert.php`, `lmo-tippcalcwert.php`, `lmo-tippcalcwertteam.php`, `lmo-tippfieber.php`, `lmo-tippcalcgraph.php`): Die Anzahl der Spieltage kommt aus den Rohdaten. Die Rangliste setzte für die Gesamtwertung bisher die Anzahl der Spieltage der geladenen Liga auf 0, die Fieberkurve schrieb die Nummer des eigenen Tippers in die für die Ligastatistik gewählte Mannschaft; beides entfällt. Die Lader `lmo-openfilename.php` und `lmo-openfiledat.php` stellen dafür wie der Hauptlader `$lmo_liga` bereit.
+* Tippspiel, Tippabgabe für einen Spieltag (`lmo-tippedit.php`, `lmo-tippeditliga.php`, `lmo-tippeditko.php`): Spiele, Mannschaften und Einstellungen kommen aus den Rohdaten. Die Paarungen der Runde liefert `Lmo\Tipp\Tipprunde`, statt dass die Dateien sie über `$ligaDaten->anzsp` teilen. Auch die Einsicht (`lmo-tippeinsicht.php`) arbeitet darauf.
+* Die HTML-Ausgabe zum Drucken (`lmo-savehtml.php`, `lmo-savehtml1.php`) und die Spielerstatistik (`addon/spieler/lmo-statshow.php`) lesen Spiele, Mannschaften und Einstellungen aus den Rohdaten. Die HTML-Ausgabe veränderte dabei bisher die Tore der geladenen Liga im Speicher; das entfällt. Der Inhalt der geschriebenen Dateien ist unverändert und jetzt durch einen eigenen Test (`HtmlExportTest`) festgehalten.
+* Das Ticker-Addon liest seine Ligen direkt als Rohdaten. Bisher lud es jede Liga über den Hauptlader in `$ligaDaten` und überschrieb damit die Liga der Seite, den gewählten Spieltag und bei eigenen Bezeichnungen für Punkte und Tore auch die Sprachtexte.
+* Der Hauptrahmen der öffentlichen Seiten (`lmo-showmain2.php`) liest die Einstellungen der Liga (Menüpunkte, Startseite, Name) aus den Rohdaten. Nur die gewählten Mannschaften im Link zur Statistik kommen weiter aus dem Seitenaufruf.
+* Tabellenseite (`lmo-showtable.php`), der Rahmen für Ergebnisse und Tabelle (`lmo-showrestab.php`) und die Statistik (`lmo-showstats.php`) holen Einstellungen, Mannschaftsnamen, Strafen und Notizen aus den Rohdaten statt aus `$ligaDaten`. Die Tabellenseite arbeitet dabei direkt mit `Lmo\Liga\Tabelle` und ihren Zeilen; der Rahmen rechnet die Tabellen selbst (gewählte Tabelle, Heim- und Auswärtsbilanz, Vorspieltag für die Pfeile) statt viermal `lmo-calctable.php` einzubinden. Auch die Statistik rechnet ihre Tabelle über die Klasse; den Text für höchsten Sieg und höchste Niederlage liefert `Rekord::alsText()`.
+* Die beiden abgespeckten Lader `lmo-openfilename.php` und `lmo-openfiledat.php` benutzen denselben Leser und dieselbe Zuordnung der Schlüssel wie der Hauptlader.
+* Die ganze Ligadatei wird über `Lmo\Liga\LigaDaten` geschrieben; `lmo-savefile.php` enthält nur noch das Öffnen, Sperren und Schreiben der Zeilen. Die geschriebene Datei ist unverändert.
+* News, Spieltage und Partien liest `Lmo\Liga\LigaDaten`; der Lader `lmo-openfile.php` enthält nur noch den Ablauf. Die gelesenen Werte sind unverändert.
+* Auch die Abschnitte der Mannschaften (`[Teams]`, `[Teamk]`, `[Teamm]`, `[Team1]` …) werden über `Lmo\Liga\LigaDaten` gelesen und gespeichert, einschließlich der abgeleiteten Kurz- und Mittelnamen. Gelesene Werte und geschriebene Datei sind unverändert.
+* Welcher Schlüssel des Abschnitts `[Options]` zu welchem Wert der Liga gehört, für Liga oder Pokal gilt und welchen Vorgabewert hat, steht jetzt in einer Tabelle in `Lmo\Liga\LigaDaten`. Der Lader `lmo-openfile.php` und das Speichern in `lmo-savefile.php` benutzen beide diese Tabelle; die geschriebene Datei ist unverändert.
+* Das Format der Ligadatei (Abschnitte, Schlüssel, Werte) liest jetzt eine gemeinsame Klasse (`Lmo\Liga\LigaDatei`), die der Lader `lmo-openfile.php` und die Classlib benutzen.
+* Die Vorgabewerte einer Liga (Punkte je Sieg, Anzahl der Spieltage, Datumsformat …) stehen jetzt in `Lmo\Liga\LigaDaten` statt als rund 60 Einzelzeilen im Lader `lmo-openfile.php`. Am Verhalten ändert sich nichts.
+* Die Werte einer Ligadatei liegen zusätzlich gebündelt in einem Objekt (`Lmo\Liga\LigaDaten`, Variable `$ligaDaten`). Die bisherigen einzelnen Variablen (`$titel`, `$teams`, `$goala` …) bleiben gültig und zeigen auf dieselben Werte. Der gesamte LMO liest und schreibt die Liga jetzt über das Objekt (62 Dateien); an der Ausgabe ändert sich nichts. Eigene Erweiterungen, die die alten Variablen benutzen, funktionieren unverändert.
+* 62 alte Dateien nennen am Dateianfang, welche Variablen sie erwarten. PHPStan prüft damit 33 von ihnen auf Stufe 6 und 31 auf Stufe 1 statt auf Stufe 0.
+
+### Removed
+* `Lmo\Liga\LigaDaten` (die Variable `$ligaDaten`), `lmo-ligadaten.php` und `lmo-savefile.php` gibt es nicht mehr. Alle Seiten lesen die Liga aus den Rohdaten (`$lmo_liga`, `Lmo\Liga\Rohdaten\Liga`) und speichern über `lmo-saveliga.php`. Damit entfallen auch die alten einzelnen Variablen einer geladenen Liga (`$titel`, `$anzst`, `$anzteams`, `$teams`, `$teama`, `$goala`, `$msieg`, `$stx` …): Der LMO legt sie nicht mehr an. Eigene Erweiterungen, die sie benutzen, müssen auf `$lmo_liga` umgestellt werden, z. B. `$lmo_liga->einstellungen()->spieltage()` statt `$anzst` oder `Lmo\Liga\Spiel::ausRohdaten()` für ein Spiel.
+
+### Fixed
+* Ergebnisse im Adminbereich, Pokal: Die Seite einer Runde brach mit einem Fatal Error ab („Unsupported operand types: string + string“ in `gewinn()`), wenn in einer Runde davor mit Hin- und Rückspiel bei einer Paarung nur das Rückspiel eingetragen war. Wer ausgeschieden ist, bestimmt die Seite jetzt über `Lmo\Liga\Paarung::sieger()`.
+* Ergebnisse im Adminbereich: Ohne Newszeilen und bei ausgeschaltetem Newsticker bleibt der Abschnitt `[News]` leer. Bisher schrieb diese Seite `NC=0` hinein und jede andere Seite des Adminbereichs nahm es beim nächsten Speichern wieder heraus.
+* Mannschaft löschen im Adminbereich: Torkorrektur, der Spieltag, ab dem eine Strafe gilt, und der Vermerk „nicht tippen“ eines Spiels wandern jetzt mit der Mannschaft bzw. dem Spiel mit. Bisher blieben sie an der alten Nummer stehen und gehörten danach zur nachgerückten Mannschaft bzw. zum nachgerückten Spiel. Fiel die erste Mannschaft der Fieberkurve weg, behielt die nachrückende zweite ihre alte Nummer. In einem Pokal lief das Löschen mit Warnungen und schrieb leere Werte für die Tore. Eine neu angelegte Mannschaft bekommt Kurz- und Mittelnamen sofort, nicht erst beim nächsten Laden.
+* Einstellungen einer Liga im Adminbereich: Eigene Bezeichnungen für Punkte und Tore bleiben leer, wenn keine vergeben sind. Bisher schrieb jeder Reiter außer „Grunddaten“ den Text der gerade eingestellten Sprache (z. B. „Pkt.“) als eigene Bezeichnung in die Datei; nach einem Wechsel der Sprache blieb er stehen. Zweimal dieselbe Mannschaft für die Fieberkurve wird gleich beim Speichern als eine geführt, nicht erst beim nächsten Laden.
+* Statistikdatei: Bei der Anzahl der Spiele mit den meisten Toren zählen nur noch gespielte Spiele. War noch kein Tor gefallen, wurden bisher nach dem Laden einer Liga alle nicht gespielten Spiele mitgezählt, direkt nach dem Anlegen dagegen keines.
+* Tippspiel, Auswertung: Die Tipper und in der Gesamtauswertung die Ligen werden in fester (alphabetischer) Reihenfolge ausgewertet. Bisher hingen die Reihenfolge in den Auswertungsdateien, die Nummern der Ligen in der Gesamtauswertung und der Zwischenstand in den Dateien der Vereinswertung davon ab, in welcher Reihenfolge das Dateisystem die Dateien lieferte.
+* Tippspiel, Erinnerungsmail für eine einzelne Liga: Die noch nicht getippten Spiele stehen jetzt je in einer eigenen Zeile statt ohne Trennung hintereinander. Für einen einzelnen Spieltag wird nur noch dieser durchsucht; bisher kam auch der folgende dazu und wurde mit den Tipps des gewählten verglichen, sodass Spiele in der Mail landeten, die schon getippt waren, und andere fehlten.
+* Tippspiel, Tabelle nach den Tipps: In Ligen mit direktem Vergleich brach die Seite unter PHP 8 mit einem Fatal Error ab (`lmo-tippcalctable1.php`, „Unsupported operand types: string - string“), sobald Mannschaften gleichauf lagen.
+* Classlib, Pokal: Stand in der Datei ein Wert eines Spiels (z. B. die Notiz des Hinspiels) hinter den Toren des nächsten Spiels, wurde er dem falschen Spiel zugeordnet. Sieht der Modus einer Runde mehr Spiele vor, als die Datei nennt, bricht das Laden nicht mehr ab.
+* Einstellungen in `[Options]`, die der LMO nicht kennt (auch `stats`), und ganze unbekannte Abschnitte einer Ligadatei gingen beim Speichern im Admin-Bereich verloren. Sie bleiben jetzt erhalten.
+* Werden in einem Aufruf mehrere Ligen nacheinander geladen, blieben Werte der vorigen Liga stehen, die die nächste Datei nicht nennt (z. B. Strafpunkte einer Liga beim anschließenden Laden eines Pokals). Vor jedem Laden werden jetzt alle Werte geleert.
+* Ein senkrechter Strich (`|`) in einer Notiz, einem Mannschaftsnamen, einer Adresse oder einer Newszeile schnitt beim Laden alles dahinter ab.
+* Tippspiel: Derselbe Fehler in der Tippübersicht und der Erinnerungsmail – ein `|` im Mannschaftsnamen oder in der Notiz schnitt den Rest ab.
+* Tippspiel, Tippübersicht und Erinnerungsmail: Die Werte eines Spiels hingen von der Reihenfolge der Zeilen in der Ligadatei ab. Fehlte einem Spiel eine Angabe (Notiz, Tore, Mannschaft), galt die des vorigen Spiels weiter; im Pokal konnten die Tore des Rückspiels beim Hinspiel landen. Jetzt hat jedes Spiel seine eigenen Werte, und was eine Liga nicht nennt, bekommt den Vorgabewert statt des Werts der zuvor gelesenen Liga.
+* Tippspiel, Tippabgabe für einen Liga-Spieltag: Der Link zum Spielbericht und der Hinweis »Spiel wird für Tippspiel nicht gewertet« in der Notiz fehlten, und die Seite warf PHP-Warnungen (`Undefined variable $n`), sobald Spielberichte eingeschaltet waren.
+* Tippspiel, Tippübersicht, Hinweis-Symbol am Spiel: Der Zusatz zum Ergebnis (n.V., i.E.) fehlte dort immer, der Torfaktor der Liga wurde nicht angewendet und der Link zum Spielbericht zeigte ins Leere.
+* Spielplan einer Mannschaft, Hinweis-Symbol bei einer Wertung am grünen Tisch gegen beide: Das umgekehrte Ergebnis fehlte dort oder stammte von einer anderen Partie des gerade gewählten Spieltags.
+* Ein Spiel ohne Anstoßzeit wurde bei eingeschalteter Anzeige der Anstoßzeit mit »01.01.1970 01:00« angezeigt: in den Ergebnissen eines Spieltags, im Spielplan einer Mannschaft, im Pokal und in der Spieltagsübersicht des Admin-Bereichs. Jetzt bleibt die Zelle leer. Dasselbe gilt für `datumString()` und `zeitString()` der Classlib, die für `0` nun den Text für »keine Zeit« liefern.
+* Fieberkurve: Sie rechnete den Platz je Spieltag mit einer eigenen, älteren Kopie der Tabellenrechnung und zeigte deshalb in Ligen mit direktem Vergleich oder Strafen an einzelnen Spieltagen einen anderen Platz als die Tabelle. Jetzt nutzt sie dieselbe Rechnung (`Lmo\Liga\Tabelle`); die Kurve stimmt mit der Tabelle des jeweiligen Spieltags überein. `lmo-calctable1.php` entfällt.
+* Ein unlesbares Datum im letzten Spieltag einer Ligadatei (z. B. `D1=ab.cd.efgh`) ließ das Laden der Liga abbrechen; es wird jetzt geleert.
+* Classlib (Addons mini, viewer, ticker): Eine eckige Klammer in einem Liganamen, Mannschaftsnamen oder einer Notiz schnitt den Rest des Abschnitts ab (Name gekürzt, Mannschaft oder Zusatz „n. V.“ fehlten). Kommentarzeilen mit `;` am Anfang wurden als Schlüssel gelesen.
+* Tippspiel und Benutzerverwaltung: Fehlte einer Ligadatei die Anzahl der Spieltage (`Rounds`), brach der abgespeckte Lader `lmo-openfilename.php` mit „Division by zero“ ab. Beide abgespeckten Lader ergänzen fehlende Angaben jetzt wie der Hauptlader.
+
+
+***
+## [4.3.2] - 2026-10-11
+
+_Hier hätten wir die Changelogs für 4.3.2._
+
+### Changed
+* PHPStan prüft die Klassen und Funktionen der Classlib und die Tests jetzt auf Stufe 5.
+* Classlib: Die Typangaben in den Kommentaren stimmen mit dem Verhalten überein (z. B. `team|null` statt `object` bei den Suchfunktionen, `$` vor den Parameternamen). Am Code selbst ändert sich dadurch nichts.
+
+### Fixed
+* Classlib: Ein unlesbares Spieltagsdatum in der Ligadatei (z. B. `D1=ab.cd.efgh` oder `D1=23.08.`) ließ das Laden der Liga mit einem Fehler abbrechen; es gilt jetzt als fehlend.
+* Classlib: Das Sortieren einer Tabelle ohne Zeilen (`sortTable()`, `sortDirectTable()`) brach mit einem Fehler ab; `getVerlaufTeam()` und `getVerlaufTeams()` liefern für ein Team außerhalb der Liga ein leeres Array statt einer PHP-Warnung.
+
+
+***
+## [4.3.1] - 2026-10-11
+
+_Hier hätten wir die Changelogs für 4.3.1._
+
+### Added
+* Statische Analyse mit PHPStan (`composer phpstan` bzw. `docker compose run --rm phpstan`, auch in der GitHub-Action): gesamter Code auf Stufe 0, neuer Code auf Stufe 8.
+
+### Fixed
+* Update-Prüfung: Erlaubt der Server keine Abfragen nach außen (`allow_url_fopen` aus), wird nichts mehr versucht und keine Merkdatei geschrieben; eine beschädigte Merkdatei wird ignoriert.
+* `lmo-install` findet den Autoloader auch beim direkten Aufruf im Git-Checkout (`vendor/` neben `lmo/`).
+* Kalender im Admin-Bereich: Im Mai erschien im Kopf die Abkürzung statt des ausgeschriebenen Monatsnamens.
+
+
+***
+## [4.3.0] - 2026-10-11
+
+_Hier hätten wir die Changelogs für 4.3.0._
+
+### Added
+* Installation per Composer: `composer require lmo/lmo_php8`, danach richtet `vendor/bin/lmo-install` den LMO im Ordner `lmo` ein. `composer update` aktualisiert ihn dort automatisch; Einstellungen, Ligen und Tipps bleiben erhalten (siehe README).
+
+### Changed
+* Nur noch eine `composer.json` (im Hauptordner). Lizenz und PHP-Mindestversion liest der LMO jetzt aus `lmo/version.json`; die alte `lmo/composer.json` kann nach einem Update gelöscht werden.
+* Die Versionsnummer kommt aus dem Git-Tag des Composer-Pakets und steht in keiner Datei mehr. Ohne getaggte Version (Git-Checkout) zeigt der LMO `dev`.
+* Die Suche nach einer neuen Version fragt Packagist statt der Prüfdatei auf vest-sport.de: nur noch im Admin-Bereich für den Hauptadmin und höchstens einmal am Tag, nicht mehr bei jedem Seitenaufruf.
+* `pear/html_template_it` in der stabilen Version 1.3.3 statt `dev-master`.
+* Die Update-Seite im Admin-Bereich nennt bei einer neuen Version `composer update` statt eines Download-Links; der Link zur Ankündigung im Forum entfällt.
+
+### Removed
+* Installation per FTP-Upload des Ordners `lmo`: Der LMO wird nur noch per Composer installiert. Bestehende Installationen lassen sich mit `lmo-install` übernehmen (siehe README).
+### Security
+* Die Composer-Abhängigkeiten liegen nicht mehr im LMO-Ordner (bisher `lmo/vendor`); `lmo-install` sperrt den Ordner `vendor` per `.htaccess`.
+
+
+***
+## [4.2.1] - 2026-10-10
+
+_Hier hätten wir die Changelogs für 4.2.1._
+
+### Added
+* Automatische Tests für „Rückrundenspielplan erstellen“ und das Verschieben von Partien im Admin-Bereich.
+* Automatische Tests für das Addon „mini“: Die Minitabelle muss dieselbe Tabelle zeigen wie die Haupttabelle.
+* Automatische Tests für die Addons „viewer“ (Ansicht speichern, Spiele nach Datum und Spieltag, Cache) und „ticker“ (Lauftext mit Wertungen, News).
+* Automatische Tests für das Addon „spieler“ (Spielerstatistik): Spieler und Spalten verwalten, Formelspalten, öffentliche Seite und Druckansicht.
+* Automatische Tests für den Mailversand (Liga per E-Mail, Passwort vergessen im Tippspiel).
+* Automatische Tests für Ticker, Viewer und Mini-Spielplan über mehrere Ligen.
+
+### Changed
+* PHPMailer wird jetzt über Composer installiert (`phpmailer/phpmailer` 7.1) statt als veränderte Kopie in `lmo/includes`.
+* Entwicklungsumgebung (Docker): lokales Postfach für alle Mails von LMO, Übersichtsseite der Addons mit zwei Beispielligen, Doku in `docker/README.md`.
+* GitHub Action: Die Tests laufen mit PHP 8.0, 8.3, 8.4 und 8.5.
+* PHP 8.5 wird unterstützt (README: PHP 8.0 bis 8.5).
+
+### Deprecated
+
+### Removed
+* Reste des PDF-Addons (PDF-Erzeuger der Anleitung unter `help/`, auskommentierte Einbindung) und die unbenutzte Datei `lmo-paintgraph.php`. Das separat erhältliche PDF-Addon lässt sich weiterhin einbinden.
### Fixed
+* „Rückrundenspielplan erstellen“ bei einer Liga mit ungerader Anzahl Spieltage: Die Fehlermeldung erschien nicht, und die Liga wurde trotzdem neu gespeichert und als erfolgreich gemeldet. Jetzt erscheint die Meldung, gespeichert wird nichts.
+* Minitabelle: Der Aufruf mit den mitgelieferten Standardeinstellungen brach unter PHP 8 mit einem Fehler ab (leere Werte für „Plätze darüber/darunter“). Ohne Liga erscheint jetzt die Meldung „Liga nicht gefunden“ ohne PHP-Warnung.
+* Minitabelle: Am ersten Spieltag wurde eine Tendenz angezeigt, obwohl es keinen Vorspieltag gibt. Jetzt ist sie dort für alle Mannschaften 0.
+* Ticker: Der Hinweis „… bekam den Sieg zugesprochen“ stand nach einem gewerteten Spiel auch bei allen folgenden Spielen des Spieltags.
+* Ticker: Breite und Geschwindigkeit ließen sich beim Einbinden nicht übergeben; die Einstellung „Notizen anzeigen“ wirkte bei Pokal-Ligen nicht.
+* Ticker mit mehreren Ligen: Die Spiele der ersten Liga standen bei jeder weiteren Liga noch einmal. Eine fehlende Liga in der Liste zeigte die Spiele der vorigen statt der Meldung „keine passenden Ligen gefunden“.
+* Mini-Spielplan: PHP-Warnung, wenn die beiden Mannschaften in einer Liga nie aufeinandertreffen. Bei den früheren Begegnungen aus dem Archiv konnte die falsche Mannschaft erscheinen, wenn sich zwei Namen nur in Ziffern unterscheiden (z.B. „TSV Muster“ und „TSV Muster 2“).
+* Viewer: PHP-Warnung beim ersten Aufruf einer Ansicht (Cache-Datei noch nicht vorhanden); im Admin-Formular eine PHP-Warnung, wenn das Ligenverzeichnis nicht `ligen/` heißt.
+* classlib: Werte einer Partie (z.B. Verlängerung, beidseitiges Ergebnis) werden im Ligamodus auch gelesen, wenn sie in der Ligadatei vor den Mannschaften der Partie stehen. Minitabelle und Statistik zeigten für solche Dateien andere Punkte als die Haupttabelle.
+* classlib: Bei völligem Gleichstand (gleiche Punkte und Tore) war die Reihenfolge zufällig. Jetzt steht wie in der Haupttabelle die Mannschaft mit der höheren Nummer vorn.
+* classlib: Die Handicap-Reihenfolge (Option „HandS“) wird jetzt wie in der Haupttabelle auf die Gesamttabelle angewendet. Minitabelle und Statistik zeigten bei solchen Ligen bisher andere Plätze.
+* Spielerstatistik: Eine ungültige Formel erzeugte PHP-Warnungen statt des Fehlertexts, eine unvollständige Formel (z.B. `Tore/`) brach die Admin-Seite ab; außerdem erschienen Reste der Formel als Debug-Ausgabe in der Seite.
+* Spielerstatistik: Die öffentliche Seite und die Druckansicht brachen bei einem nicht-numerischen Seitenanfang ab und warnten bei einer unbekannten Sortierspalte; die Druckansicht warnte, wenn es zur Liga keine Statistik gibt.
+* Mailversand: Ein fehlgeschlagener Versand (Liga per E-Mail; Tippspiel: Rundmail, Freischaltung, Benachrichtigung) endete mit einem Fatal Error statt einer Meldung; bei der Rundmail brach schon eine ungültige Adresse den ganzen Lauf ab.
+* Tippspiel: Links in Mails waren fehlerhaft, wenn der Webserver `REQUEST_SCHEME` nicht liefert, und konnten die Parameter der aktuellen Anfrage enthalten.
+* „Liga per E-Mail“ ohne die PHP-Erweiterung `zip` brach mit einem Fehler ab; jetzt erscheint eine Meldung.
+* „Alle Ligen herunterladen“ brach ohne die PHP-Erweiterung `zip` ebenfalls ab; jetzt erscheint eine Meldung. Das Archiv entsteht im Ausgabeordner statt im Programmverzeichnis.
+* Spielerstatistik: Das Löschen einer Spalte, die es nicht gibt, erzeugte eine PHP-Warnung.
+* Zufallsspielplan: Beim Mischen der Mannschaften wurde gelegentlich neben die Liste gegriffen (bei 6 Mannschaften in etwa jedem siebten Aufruf); im erzeugten Spielplan fehlte dann eine Mannschaft.
+* Admin-Bereich ohne Anmeldung: Die Sprachauswahl erzeugte je Sprache eine PHP-Warnung.
+* Admin-Bereich: Der Aufruf der PDF-Optionen ohne installiertes PDF-Addon endete mit einem Fatal Error.
+* Liga aus einer Vorlage mit mehr Mannschaften anlegen: PHP-Warnungen beim Erzeugen der Statistikdatei; Partien mit Mannschaften, die es in der Liga nicht gibt, wurden mitgezählt.
+* Admin-Bereich: Rief ein Hilfsadmin ohne erweiterte Rechte die Mannschaftsverwaltung auf, erschien der Ergebnis-Editor für einen Spieltag, den es nicht gibt (PHP-Warnungen, unbrauchbares Formular). Jetzt öffnet sich der aktuelle Spieltag.
+
+### Security
+* Spielerstatistik: Die Parameter für Sortierung, Seite und Mannschaft wurden unmaskiert in die Seite geschrieben (Cross-Site-Scripting).
+* Dateien, die LMO nur einbindet (131 Dateien in `lmo/` und den Addons), beenden sich bei direktem Aufruf im Browser sofort. Bisher liefen sie ohne Konfiguration und Rechteprüfung los und gaben Warnungen, Seitenbruchstücke und teils Fehlermeldungen mit Dateipfaden aus.
+
+
+***
+## [4.2.0] - 2026-10-10
+
+_Hier hätten wir die Changelogs für 4.2.0._
+
+### Added
+* Ersteinrichtung ohne Installer: Beim ersten Aufruf wird die Konfiguration aus `config-default/` angelegt, das erste Admin-Konto wird über ein Formular erstellt (Texte dafür in allen Sprachen).
+* Sprachauswahl im Fuß des Admin-Bereichs.
+* Automatische Tests (PHPUnit, Docker, GitHub Action), siehe TESTING.md: Golden-Master-Tests für öffentliche Seiten und Admin-Bereich, Unit-Tests für die classlib, Tabellenvergleich mit 75 echten Ligen (OpenLigaDB, Abschlusstabellen aus Wikipedia) und 10 konstruierten Ligen.
+* classlib: Spieltage merken sich weitere Werte der Ligadatei (`spieltag::setParameter()` / `getParameter()`), z.B. die Handicap-Reihenfolge.
+
+### Changed
+* Die Mindestanforderung des Webservers beträgt jetzt PHP 8.0 (bisher 7.4). Der Admin-Bereich weist bei älteren PHP-Versionen darauf hin.
+* Seitenquelltextausgabe der beiden Datein lmo-savehtml.php und lmo-savehtml1.php verbessert. Außerdem den Tabellen der beiden Seiten runde Ecken verpasst.
+* Template-System über Composer (`pear/html_template_it`, neue Klasse `LMO_Template`) statt der mitgelieferten Dateien IT.php und ITX.php.
+* Addon-Reiter, Viewer-Vorlagen und Ligalisten im Admin-Bereich erscheinen in fester (alphabetischer) Reihenfolge statt in der des Dateisystems.
+* Wertung „beidseitiges Ergebnis“: Das Ergebnis zählt für beide Mannschaften vollständig aus Sicht der Heimmannschaft (Sieg/Unentschieden/Niederlage, Punkte, Minuspunkte). Bisher wurden nur Spiel und Tore gezählt.
+* Straf-/Bonuspunkte und -tore „ab Spieltag X“ zählen erst, wenn Spieltag X Ergebnisse hat. Bisher wurden sie in der Gesamttabelle sofort eingerechnet.
+* Die classlib rechnet Tabellen jetzt wie die Haupttabelle (direkter Vergleich, Strafen, beidseitiges Ergebnis). Minitabelle und Statistik zeigen damit dieselbe Tabelle.
+
+### Deprecated
+
+
+### Removed
+* Installer (`install/`) entfernt, ebenso die nicht mehr benötigten PEAR-Dateien für FTP, Socket und Cache.
+### Fixed
+* Direkter Vergleich: Gleichstand wurde nur an der letzten Ziffer der Punkte erkannt (59 und 49 Punkte galten als gleich).
+* Direkter Vergleich mit zehn und mehr punktgleichen Teams und Minuspunkten: Das beste Team stand am Ende der Gruppe.
+* classlib: Spielende (n.V./i.E.) wurde aus der Ligadatei nicht gelesen und ging beim Speichern verloren.
+* classlib, direkter Vergleich: Gleichstand am Tabellenende wurde nicht ausgewertet, ein zugesprochener Auswärtssieg falsch gebucht, und es wurden immer alle Spiele der Saison gewertet statt nur die der angezeigten Tabelle (Spieltag, Heim/Auswärts).
+* classlib, Strafen: Strafe entfiel, wenn die Mannschaft am Spieltag des Strafbeginns spielfrei war; Bonus-Gegentore wurden mit falschem Vorzeichen verrechnet; Strafen zählten auch in Heim- und Auswärtstabelle.
+* classlib, Speichern (`writeFile()`, genutzt von „Rückrunde erzeugen“ und „Spieltage verschieben“): Handicap-Reihenfolge, Titel und aktueller Spieltag gingen verloren; ein zweites Speichern im selben Aufruf brach ab; Ligen in Unterordnern wurden danach nicht gefunden (HTML-Export und Statistik ohne Daten).
+* classlib, Statistik: Es wurden auch nicht gespielte Partien gezählt; die Serienanzeige überschrieb ihren eigenen Text.
+* classlib: `ligaFussball::sortTable()` lief unter PHP 8 nur mit Warnungen, `liga::factory()` war nicht aufrufbar, `aktuellerSpieltag()` lieferte den folgenden Spieltag, `strAfterChar()`, `readLigaDir()` (Endung `.L98`) und `HTML_icon()` (Suche nach .jpg/.png mit Alternativtext) korrigiert.
### Security
* Cross Site Scripting/XSS Lücke geschlossen
+* `lmo-rueckrunde.php` ließ sich ohne Anmeldung direkt aufrufen und schrieb dann die angegebene Ligadatei neu. Jetzt nur noch für angemeldete Admins.
+* Tippspiel: Neue Passwörter werden mit Argon2id bzw. bcrypt gespeichert.
***
@@ -101,21 +323,244 @@ __All significant changes to the LMO PHP script are documented in this file.__
_Here we write changelog notes for the next version._
### Added
+- The raw data of a league can be changed through named methods that know the keys and special values of the league file: on the pairing `setzeMannschaften()`, `setzeErgebnis()` (with awarded results), `setzeZusatz()`, `setzeAnstoss()`, `setzeNotiz()`, `setzeBericht()`, `setzeOhneTippwertung()`; on the round `partie()`, `setzeErstenTag()`, `setzeLetztenTag()`, `setzeTabellenkorrektur()`, `setzeSpieleJePaarung()`; on the team `setzeUrl()`, `setzeNotiz()`, `setzeStrafpunkte()`, `setzeStrafminuspunkte()`, `setzeTorkorrektur()`, `setzeStrafeAbSpieltag()`; on the league `setzeEinstellung()` and `setzeNews()`.
### Changed
+- The admin pages enter their changes through these methods instead of the keys of the file (`GA`, `ET`, `HS` …). The written files are unchanged.
+- The HTML output for printing (`lmo-savehtml.php`, `lmo-savehtml1.php`) computes its table through `Lmo\Liga\Tabelle`. The generated pages are unchanged.
### Deprecated
### Removed
+- `lmo-calctable.php` is gone. Lately the file only translated the table from `Lmo\Liga\Tabelle` into the old individual variables (`$punkte`, `$tab0` …); no page needs that any more. Custom extensions compute the table with `Lmo\Liga\Tabelle::fuerAnsicht($lmo_liga, …)`.
+
+
+### Fixed
+
+
+### Security
+
+
+***
+## [4.4.0] - 2026-10-12
+
+_Here we had the changelogs for 4.4.0._
+
+### Added
+* `Lmo\Liga\Rohdaten\Liga` (with `Mannschaft`, `Spieltag`, `Partie`): holds the content of a league file exactly as it is in the file and loads and saves it as a pair. Nothing is converted for display, added or padded; unknown keys and sections are kept. The core of LMO loads and saves every league through it.
+* `Lmo\Liga\Einstellungen`: the settings of a league (`[Options]`) with their defaults and the computed sizes (rounds, matches), without changing the raw data. Every setting has a method with a fixed type (switches `bool`, numbers `int`, names `string`) – `Rohdaten\Liga::lies($pfad)->einstellungen()->mannschaften()`, `->mitVerlaengerung()`. In addition `Mannschaft::anzeigename()`, `Liga::mannschaftsname()` as well as `Spieltag::ersterTag()` and `letzterTag()` on the raw data. Pages can thereby work directly on the raw data; the two loaders of the prediction game already do.
+* `Lmo\Liga\Spiel`: a game for display, derived from the raw data (goals with "not played", decision at the green table, addition, note, kick-off) – `Spiel::ausRohdaten($liga, $spieltag, $partie)`. In addition `kurzname()` and `mittelname()` on the team, which build a missing name from the name. The main loader provides the raw data as `$lmo_liga`.
+* For iterating with `foreach`: `Rohdaten\Liga::alleMannschaften()`, `spiele($spieltag)` and `alleSpiele()` deliver completely and in the order of the numbers, even if something is missing or sorted differently in the file; in a cup the games of the mode for each pairing. `Einstellungen::partienImSpieltag()` gives the number of matches of a round.
+* `Lmo\Liga\Paarung`: two teams and their games in a round, with `sieger()` (one game, two legs, best of 3/5/7). `Rohdaten\Liga::paarungen($runde)` delivers them for iterating.
+* `Lmo\Liga\Tabelle`: computes the table of a league from the games of the raw data – overall, home, away, first and second half, up to a round, with penalties from their effective round, decisions at the green table, skittles sorting and direct comparison. The rows (`Tabellenzeile`) have fixed types and also carry streaks as well as highest win and highest defeat (`Rekord`). Call: `Tabelle::berechne($liga, Tabelle::HEIM, 1, 17)`.
+
+### Changed
+* The classlib reads and writes league files through the same class as the core (`Lmo\Liga\Rohdaten\Liga`). Files written by the classlib thereby have the section order of the core and always contain `[News]`, `[Teamm]` and `[Teamk]`; the values are the same. Whether a file is a league or a cup is now decided by the file type in the classlib too, no longer by the mode of the single round.
+* The league lists (league overview, admin: open, delete, download, new league, viewer options; prediction game) read name, type and size of a league through `Lmo\Liga\LigaDatei::liesAbschnitt()` instead of nine reading loops of their own. Blanks around key and value are removed as when loading the league; an unreadable file no longer aborts the list.
+* The schedule template when creating a league ("take over from file") as well as the prediction overview and the reminder mail of the prediction game read league files through `Lmo\Liga\Rohdaten\Liga` or `Lmo\Liga\LigaDatei` instead of reading loops of their own. Apart from the classlib and the core, nothing reads a league file by itself any more.
+* A league file is always read completely. The two small loaders of the prediction game (`lmo-openfilename.php`, `lmo-openfiledat.php`) and the switch `$lmo_only_st` used to read only single keys or one round and left a half-filled league; now all loaders deliver the same. Only the league lists still read just the `[Options]` section. Visible: a league without a name is no longer called "No Name" in the prediction game but has no name, as everywhere else. In the reminder mail for one round its date is checked as in the main loader: `1.9.2024` becomes `01.09.2024` (empty before), a date that does not exist (`31.02.2025`) is cleared (converted to 03.03. before).
+* Prediction overview and reminder mail of the prediction game keep the upcoming games as a list of `Lmo\Tipp\AnstehendesSpiel`. So far they collected the values of several leagues as lists in the fields of the loaded league (`$ligaDaten->titel`, `->teama` …), which are not meant for that.
+* The display pages of a league work directly on the raw data instead of `$ligaDaten`: schedule of a team (`lmo-showprogram.php`), results of a round (`lmo-showresults.php`), calendar (`lmo-cal.php`) and cross table (`lmo-showcross.php`). The output is unchanged. `Lmo\Liga\Spiel` has fixed types for that (goals `?int`, `null` = not played) and delivers the display texts itself (`toreHeimText()`, `zusatzText()`).
+* In a cup the schedule of a team (`lmo-showkoprogram.php`), the results of a round (`lmo-showkoresults.php`) and the calendar (`lmo-kocal.php`) work on the raw data as well. The calendar now picks its month by the earliest kick-off of all games of the selected round; so far, because of an index error, only the games of a single pairing counted.
+* The table calculation (`lmo-calctable.php`) no longer computes by itself but through `Lmo\Liga\Tabelle` and fills the previous variables from it. All pages using the table (table, statistics, HTML output, admin) get the same values as before.
+* Admin area: the pages are moved one by one to entering their changes directly into the raw data of the league and saving through `lmo-saveliga.php`. `Lmo\Liga\Normalform` completes the raw data to the full file LMO writes (every setting, every team, every round with all games); the written file is the same as before. Keys LMO does not know are now kept for teams, rounds and games as well on this path. So far the table correction (`lmo-admintab.php`, computing its table through `Lmo\Liga\Tabelle`) the number of rounds and games (`lmo-adminanz.php`), the league settings (`lmo-adminbasic.php`), the teams (`lmo-adminteams.php`), the results of a round (`lmo-adminedit.php`) and creating a league (`lmo-adminnew.php`) work this way; a new league therefore has medium names for its teams and the (empty) entries for custom point and goal labels from the start, which used to be added by the first save; the sub navigation of the admin area reads from the raw data. Adding a team or taking one out of the league is done by the raw data itself (`Rohdaten\Liga::neueMannschaft()`, `entferneMannschaft()`).
+* The statistics file of a league (`lmo-zustat.php`) is generated from the raw data. `lmo-savefile.php` converts the state being saved to raw data once, hands it to the statistics and then writes it; afterwards it is also available in `$lmo_liga`.
+* The round menu (`lmo-spieltagsmenu.php`), the player statistics in the admin area (`addon/spieler/lmo-statadmin.php`) and the game display of the viewer addon read rounds, league type and teams from the raw data and no longer use `$ligaDaten` as scratch storage.
+* Prediction game, reminder mail for a single league (`lmo-tippemail.php`) and the loader for the predictions of one round (`lmo-tippopenfile.php`): games, teams and settings come from the raw data. The loader counts the pairings through `Lmo\Tipp\Tipprunde` instead of writing them to `$ligaDaten->anzsp`, and a prediction with the value -2 no longer changes the verdict of the game in the loaded league. The prediction game now touches `$ligaDaten` in one place only: the evaluation right after saving in the admin area.
+* Prediction game, saving predictions, the insight calculation and the joker check (`lmo-tippsavefile.php`, `lmo-tippsaveeinsicht1.php`, `lmo-tippcalceinsicht.php`, `lmo-tippjokeranticheat.php`) take pairings, games per pairing and kick-off times from `Lmo\Tipp\Tipprunde` instead of each counting the pairings of a cup round and writing them to `$ligaDaten->anzsp`. Menu, footer and heading of the prediction game and `lmo-tippopenfileall.php` read type and name of the league from the raw data. The league list (`lmo-tippnewdir.php`) determines the rounds of each league from its own settings; it used to overwrite the rounds of the loaded league, and a league without `Rounds` got the number of the league before it.
+* Prediction game, table by predictions (`lmo-tipptabelle.php`, `lmo-tippcalctable.php`, `lmo-tippcalctable1.php`, `lmo-tippcalcwertverein.php`): games, teams, penalties and settings come from the raw data. The ranking by clubs counts its clubs itself instead of overwriting the number of teams of the loaded league.
+* Prediction game, evaluation (`lmo-tippsavewert.php`): results, verdicts and settings come from the raw data, the games of a round through `Lmo\Tipp\Tipprunde`. After saving in the admin area the state just entered is still the one that gets evaluated. A prediction with the value -2 no longer changes the verdict of the game (awarded win) for this and all following tippers.
+* Prediction game, ranking and prediction fever chart (`lmo-tippwert.php`, `lmo-tippcalcwert.php`, `lmo-tippcalcwertteam.php`, `lmo-tippfieber.php`, `lmo-tippcalcgraph.php`): the number of rounds comes from the raw data. For the overall ranking the ranking page used to set the number of rounds of the loaded league to 0, and the fever chart wrote the number of the own tipper into the team chosen for the league statistics; both are gone. The loaders `lmo-openfilename.php` and `lmo-openfiledat.php` now provide `$lmo_liga` like the main loader.
+* Prediction game, entering predictions for one round (`lmo-tippedit.php`, `lmo-tippeditliga.php`, `lmo-tippeditko.php`): games, teams and settings come from the raw data. The pairings of the round come from `Lmo\Tipp\Tipprunde` instead of being shared between the files through `$ligaDaten->anzsp`. The insight page (`lmo-tippeinsicht.php`) works on it as well.
+* The HTML output for printing (`lmo-savehtml.php`, `lmo-savehtml1.php`) and the player statistics (`addon/spieler/lmo-statshow.php`) read games, teams and settings from the raw data. The HTML output used to modify the goals of the loaded league in memory; that is gone. The content of the written files is unchanged and now pinned by a test of its own (`HtmlExportTest`).
+* The ticker addon reads its leagues directly as raw data. So far it loaded every league through the main loader into `$ligaDaten` and thereby overwrote the league of the page, the selected round and, with custom names for points and goals, the language texts as well.
+* The main frame of the public pages (`lmo-showmain2.php`) reads the settings of the league (menu items, start page, name) from the raw data. Only the selected teams in the link to the statistics still come from the request.
+* Table page (`lmo-showtable.php`), the frame for results and table (`lmo-showrestab.php`) and the statistics (`lmo-showstats.php`) take settings, team names, penalties and notes from the raw data instead of `$ligaDaten`. The table page works directly with `Lmo\Liga\Tabelle` and its rows; the frame computes the tables itself (selected table, home and away record, previous round for the arrows) instead of including `lmo-calctable.php` four times. The statistics compute their table through the class as well; the text for highest win and highest defeat comes from `Rekord::alsText()`.
+* The two reduced loaders `lmo-openfilename.php` and `lmo-openfiledat.php` use the same reader and the same key mapping as the main loader.
+* The whole league file is written through `Lmo\Liga\LigaDaten`; `lmo-savefile.php` only contains opening, locking and writing the lines. The written file is unchanged.
+* News, rounds and matches are read by `Lmo\Liga\LigaDaten`; the loader `lmo-openfile.php` only contains the sequence. The values read are unchanged.
+* The team sections (`[Teams]`, `[Teamk]`, `[Teamm]`, `[Team1]` …) are now read and saved through `Lmo\Liga\LigaDaten` as well, including the derived short and medium names. Values read and the written file are unchanged.
+* Which key of the `[Options]` section belongs to which league value, applies to league or cup and has which default now is kept in one table in `Lmo\Liga\LigaDaten`. The loader `lmo-openfile.php` and saving in `lmo-savefile.php` both use this table; the written file is unchanged.
+* The format of the league file (sections, keys, values) is now read by one shared class (`Lmo\Liga\LigaDatei`) that the loader `lmo-openfile.php` and the classlib use.
+* The default values of a league (points per win, number of rounds, date format …) now live in `Lmo\Liga\LigaDaten` instead of about 60 single lines in the loader `lmo-openfile.php`. The behaviour does not change.
+* The values of a league file are additionally bundled in an object (`Lmo\Liga\LigaDaten`, variable `$ligaDaten`). The existing single variables (`$titel`, `$teams`, `$goala` …) stay valid and point to the same values. All of LMO now reads and writes the league through the object (62 files); the output does not change. Own extensions that use the old variables keep working unchanged.
+* 62 old files state at the top which variables they expect. PHPStan thereby checks 33 of them on level 6 and 31 on level 1 instead of level 0.
+
+### Removed
+* `Lmo\Liga\LigaDaten` (the variable `$ligaDaten`), `lmo-ligadaten.php` and `lmo-savefile.php` are gone. All pages read the league from the raw data (`$lmo_liga`, `Lmo\Liga\Rohdaten\Liga`) and save through `lmo-saveliga.php`. This also removes the old individual variables of a loaded league (`$titel`, `$anzst`, `$anzteams`, `$teams`, `$teama`, `$goala`, `$msieg`, `$stx` …): LMO no longer creates them. Custom extensions using them have to switch to `$lmo_liga`, e.g. `$lmo_liga->einstellungen()->spieltage()` instead of `$anzst`, or `Lmo\Liga\Spiel::ausRohdaten()` for a game.
+
+### Fixed
+* Results in the admin area, cup: the page of a round aborted with a fatal error ("Unsupported operand types: string + string" in `gewinn()`) when, in an earlier round with two legs, only the second leg of a pairing had been entered. The page now determines who is eliminated through `Lmo\Liga\Paarung::sieger()`.
+* Results in the admin area: without news lines and with the news ticker switched off the `[News]` section stays empty. Until now this page wrote `NC=0` into it and every other admin page removed it again on the next save.
+* Deleting a team in the admin area: goal correction, the round from which a penalty applies and the "do not predict" flag of a game now move with the team or the game. Until now they stayed at the old number and then belonged to the team or game that moved up. If the first team of the fever chart was removed, the second one moving up kept its old number. In a cup the deletion ran with warnings and wrote empty goal values. A newly added team gets its short and medium name right away, not only on the next load.
+* League settings in the admin area: custom labels for points and goals stay empty when none are set. Until now every tab except the basic one wrote the text of the current language (e.g. "Pkt.") into the file as a custom label; it stayed after switching the language. Choosing the same team twice for the fever chart is stored as one right when saving, not only on the next load.
+* Statistics file: only played games count towards the number of games with the most goals. With no goal scored yet, all unplayed games used to be counted after loading a league, but none right after creating it.
+* Prediction game, evaluation: tippers, and in the overall evaluation the leagues, are evaluated in a fixed (alphabetical) order. Until now the order in the evaluation files, the numbers of the leagues in the overall evaluation and the intermediate state in the club ranking files depended on the order in which the file system returned the files.
+* Prediction game, reminder mail for a single league: the games not yet predicted are now listed one per line instead of run together. For a single round only that round is searched; until now the following round was added as well and compared with the predictions of the chosen one, so the mail listed games that had already been predicted and missed others.
+* Prediction game, table by predictions: in leagues with head-to-head comparison the page aborted under PHP 8 with a fatal error (`lmo-tippcalctable1.php`, "Unsupported operand types: string - string") as soon as teams were level.
+* Classlib, cup: if a value of a game (e.g. the note of the first leg) stood in the file after the goals of the next game, it was assigned to the wrong game. If the mode of a round expects more games than the file contains, loading no longer aborts.
+* Settings in `[Options]` that LMO does not know (including `stats`) and whole unknown sections of a league file were lost when saving in the admin area. They are now kept.
+* When several leagues are loaded in one request, values of the previous league remained that the next file does not mention (e.g. penalty points of a league when a cup is loaded afterwards). All values are now cleared before each load.
+* A vertical bar (`|`) in a note, a team name, an address or a news line cut off everything after it when loading.
+* Prediction game: the same bug in the prediction overview and the reminder mail – a `|` in a team name or a note cut off the rest.
+* Prediction game, prediction overview and reminder mail: the values of a game depended on the order of the lines in the league file. If a game lacked a value (note, goals, team), the one of the previous game stayed in effect; in a cup the goals of the second leg could end up at the first leg. Now every game has its own values, and what a league does not state gets the default instead of the value of the league read before.
+* Prediction game, entering predictions for a league round: the link to the match report and the hint that a game does not count for the prediction game were missing, and the page raised PHP warnings (`Undefined variable $n`) as soon as match reports were switched on.
+* Prediction game, prediction overview, hint icon at a game: the addition to the result (a.e.t., on penalties) was always missing there, the goal factor of the league was not applied and the link to the match report led nowhere.
+* Schedule of a team, hint icon for a decision at the green table against both: the reversed result was missing there or came from another match of the currently selected round.
+* A game without kick-off time was shown as "01.01.1970 01:00" when the display of kick-off times was switched on: in the results of a round, in the schedule of a team, in a cup and in the round overview of the admin area. Now the cell stays empty. The same applies to `datumString()` and `zeitString()` of the classlib, which now return the text for "no time" for `0`.
+* Fever chart: it computed the position per round with its own, older copy of the table calculation and therefore showed a different position than the table on single rounds in leagues with direct comparison or penalties. Now it uses the same calculation (`Lmo\Liga\Tabelle`); the chart matches the table of the respective round. `lmo-calctable1.php` is removed.
+* An unreadable date in the last round of a league file (e.g. `D1=ab.cd.efgh`) aborted loading the league; it is now cleared.
+* Classlib (addons mini, viewer, ticker): a square bracket in a league name, team name or note cut off the rest of the section (name shortened, team or the "a.e.t." marker missing). Comment lines starting with `;` were read as keys.
+* Tip game and user administration: if a league file lacked the number of rounds (`Rounds`), the reduced loader `lmo-openfilename.php` aborted with "Division by zero". Both reduced loaders now fill in missing values like the main loader.
+
+
+***
+## [4.3.2] - 2026-10-11
+
+_Here we had the changelogs for 4.3.2._
+
+### Changed
+* PHPStan now checks the classes and functions of the classlib and the tests on level 5.
+* Classlib: the types in the comments match the behaviour (e.g. `team|null` instead of `object` for the lookup functions, `$` in front of parameter names). The code itself does not change.
+
+### Fixed
+* Classlib: an unreadable round date in the league file (e.g. `D1=ab.cd.efgh` or `D1=23.08.`) aborted loading the league with an error; it now counts as missing.
+* Classlib: sorting a table without rows (`sortTable()`, `sortDirectTable()`) aborted with an error; `getVerlaufTeam()` and `getVerlaufTeams()` return an empty array for a team outside the league instead of a PHP warning.
+
+
+***
+## [4.3.1] - 2026-10-11
+
+_Here we had the changelogs for 4.3.1._
+
+### Added
+* Static analysis with PHPStan (`composer phpstan` or `docker compose run --rm phpstan`, also in the GitHub action): all code on level 0, new code on level 8.
+
+### Fixed
+* Update check: if the server does not allow outgoing requests (`allow_url_fopen` off), nothing is tried and no cache file is written; a damaged cache file is ignored.
+* `lmo-install` also finds the autoloader when called directly in a Git checkout (`vendor/` next to `lmo/`).
+* Calendar in the admin area: in May the header showed the abbreviation instead of the full month name.
+
+
+***
+## [4.3.0] - 2026-10-11
+
+_Here we had the changelogs for 4.3.0._
+
+### Added
+* Installation with Composer: `composer require lmo/lmo_php8`, then `vendor/bin/lmo-install` sets LMO up in the folder `lmo`. `composer update` keeps it up to date automatically; settings, leagues and tips are kept (see README).
+
+### Changed
+* Only one `composer.json` is left (in the main folder). LMO now reads license and minimum PHP version from `lmo/version.json`; the old `lmo/composer.json` can be deleted after an update.
+* The version number comes from the Git tag of the Composer package and is no longer kept in a file. Without a tagged version (Git checkout) LMO shows `dev`.
+* The search for a new version asks Packagist instead of the check file on vest-sport.de: only in the admin area for the main admin and at most once a day, no longer on every page request.
+* `pear/html_template_it` in the stable version 1.3.3 instead of `dev-master`.
+* The update page in the admin area names `composer update` for a new version instead of a download link; the link to the forum announcement is gone.
+
+### Removed
+* Installation by uploading the `lmo` folder via FTP: LMO is now installed with Composer only. Existing installations can be taken over with `lmo-install` (see README).
+
+### Security
+* The Composer dependencies are no longer inside the LMO folder (previously `lmo/vendor`); `lmo-install` blocks the `vendor` folder with an `.htaccess`.
+***
+## [4.2.1] - 2026-10-10
+
+_Here we had the changelogs for 4.2.1._
+
+### Added
+* Automated tests for "create second half of season" and for moving matches in the admin area.
+* Automated tests for the "mini" addon: the mini table must show the same table as the main table.
+* Automated tests for the addons "viewer" (saving a view, matches by date and by match day, cache) and "ticker" (ticker text with ratings, news).
+* Automated tests for the "spieler" addon (player statistics): managing players and columns, formula columns, public page and print view.
+* Automated tests for sending mail (league by e-mail, forgotten password in the betting game).
+* Automated tests for ticker, viewer and mini schedule across several leagues.
+
+### Changed
+* PHPMailer is now installed via Composer (`phpmailer/phpmailer` 7.1) instead of the modified copy in `lmo/includes`.
+* Development environment (Docker): local mailbox for all mails sent by LMO, overview page of the addons with two sample leagues, documentation in `docker/README.md`.
+* GitHub Action: the tests run with PHP 8.0, 8.3, 8.4 and 8.5.
+* PHP 8.5 is supported (README: PHP 8.0 to 8.5).
+
+### Deprecated
+
+### Removed
+* Remains of the PDF addon (PDF generator of the manual under `help/`, commented-out includes) and the unused file `lmo-paintgraph.php`. The separately available PDF addon can still be plugged in.
+
### Fixed
+* "Create second half of season" for a league with an odd number of match days: the error message was not shown, and the league was saved again anyway and reported as successful. Now the message is shown and nothing is saved.
+* Mini table: the call with the shipped default settings aborted with an error under PHP 8 (empty values for "places above/below"). Without a league the message "league not found" is now shown without a PHP warning.
+* Mini table: on the first match day a trend was shown although there is no previous match day. Now it is 0 for all teams there.
+* Ticker: after a match decided by ruling, the note "… was awarded the win" also appeared on all following matches of the match day.
+* Ticker: width and speed could not be passed when including the ticker; the setting "show notes" had no effect for cup leagues.
+* Ticker with several leagues: the matches of the first league were repeated for every further league. A missing league in the list showed the matches of the previous one instead of the message "no matching leagues found".
+* Mini schedule: PHP warning when the two teams never meet in a league. For previous meetings from the archive the wrong team could appear when two names differ only in digits (e.g. "TSV Muster" and "TSV Muster 2").
+* Viewer: PHP warning on the first call of a view (cache file not yet present); a PHP warning in the admin form when the league directory is not named `ligen/`.
+* classlib: values of a match (e.g. extra time, result for both sides) are now also read in league mode when they precede the teams of the match in the league file. For such files the mini table and the statistics showed different points than the main table.
+* classlib: with a complete tie (same points and goals) the order was arbitrary. Now, as in the main table, the team with the higher number comes first.
+* classlib: the handicap order (option "HandS") is now applied to the overall table as in the main table. Until now the mini table and the statistics showed different places for such leagues.
+* Player statistics: an invalid formula produced PHP warnings instead of the error text, an incomplete formula (e.g. `Tore/`) aborted the admin page; in addition, remains of the formula appeared in the page as debug output.
+* Player statistics: the public page and the print view aborted on a non-numeric page start and warned on an unknown sort column; the print view warned when there are no statistics for the league.
+* Sending mail: a failed dispatch (league by e-mail; betting game: circular mail, activation, notification) ended with a fatal error instead of a message; with the circular mail a single invalid address aborted the whole run.
+* Betting game: links in mails were broken when the web server does not provide `REQUEST_SCHEME`, and could contain the parameters of the current request.
+* "League by e-mail" without the PHP extension `zip` aborted with an error; now a message is shown.
+* "Download all leagues" also aborted without the PHP extension `zip`; now a message is shown. The archive is created in the output folder instead of the program directory.
+* Player statistics: deleting a column that does not exist produced a PHP warning.
+* Random schedule: shuffling the teams occasionally accessed an entry outside the list (with 6 teams in about every seventh call); the generated schedule then lacked a team.
+* Admin area without login: the language selection produced a PHP warning per language.
+* Admin area: calling the PDF options without the PDF addon installed ended with a fatal error.
+* Creating a league from a template with more teams: PHP warnings while generating the statistics file; matches with teams that do not exist in the league were counted.
+* Admin area: when a helper admin without extended rights opened the team administration, the result editor appeared for a match day that does not exist (PHP warnings, unusable form). Now the current match day opens.
+
+### Security
+* Player statistics: the parameters for sorting, page and team were written into the page unescaped (cross-site scripting).
+* Files that LMO only includes (131 files in `lmo/` and the addons) now exit immediately when called directly in the browser. Until now they started without configuration and permission check and printed warnings, page fragments and in some cases error messages with file paths.
+
+
+***
+## [4.2.0] - 2026-10-10
+
+_Here we had the changelogs for 4.2.0._
+
+### Added
+* First-time setup without the installer: on the first request the configuration is created from `config-default/`, and the first admin account is created through a form (texts available in all languages).
+* Language selection in the footer of the admin area.
+* Automated tests (PHPUnit, Docker, GitHub Action), see TESTING.md: golden master tests for public pages and the admin area, unit tests for the classlib, table comparison against 75 real leagues (OpenLigaDB, final tables from Wikipedia) and 10 constructed leagues.
+* classlib: match days keep additional values of the league file (`spieltag::setParameter()` / `getParameter()`), e.g. the handicap order.
+
+### Changed
+* The minimum requirement for the web server is now PHP 8.0 (previously 7.4). The admin area shows a notice on older PHP versions.
+* Improved the page source output of lmo-savehtml.php and lmo-savehtml1.php; the tables on both pages now have rounded corners.
+* Template system via Composer (`pear/html_template_it`, new class `LMO_Template`) instead of the bundled files IT.php and ITX.php.
+* Addon tabs, viewer templates and league lists in the admin area appear in a fixed (alphabetical) order instead of the file system order.
+* Rating "result for both sides": the result counts completely for both teams from the home team's point of view (win/draw/loss, points, minus points). Previously only the match and the goals were counted.
+* Penalty/bonus points and goals "from match day X" only count once match day X has results. Previously they were included in the overall table immediately.
+* The classlib now calculates tables like the main table (head-to-head comparison, penalties, result for both sides). The mini table and the statistics therefore show the same table.
+
+### Deprecated
+### Removed
+* Removed the installer (`install/`) and the PEAR files for FTP, socket and cache that are no longer needed.
+
+### Fixed
+* Head-to-head comparison: a tie was detected by the last digit of the points only (59 and 49 points were treated as equal).
+* Head-to-head comparison with ten or more tied teams and minus points: the best team was placed last in its group.
+* classlib: the end of a match (after extra time / penalties) was not read from the league file and was lost when saving.
+* classlib, head-to-head comparison: ties at the bottom of the table were not evaluated, an awarded away win was booked incorrectly, and all matches of the season were counted instead of only those of the displayed table (match day, home/away).
+* classlib, penalties: a penalty was dropped if the team had no match on the match day the penalty starts; bonus goals against were applied with the wrong sign; penalties also counted in the home and away tables.
+* classlib, saving (`writeFile()`, used by "create second half of season" and "move match days"): handicap order, title and current match day were lost; a second save in the same request aborted; leagues in subfolders were not found afterwards (HTML export and statistics without data).
+* classlib, statistics: unplayed matches were counted as well; the streak output overwrote its own text.
+* classlib: `ligaFussball::sortTable()` only ran with warnings under PHP 8, `liga::factory()` could not be called, `aktuellerSpieltag()` returned the following match day; fixed `strAfterChar()`, `readLigaDir()` (extension `.L98`) and `HTML_icon()` (search for .jpg/.png with alternative text).
+
### Security
+* Closed a cross site scripting (XSS) vulnerability.
+* `lmo-rueckrunde.php` could be called directly without login and then rewrote the given league file. Now restricted to logged-in admins.
+* Prediction game: new passwords are stored with Argon2id or bcrypt.
***
diff --git a/composer.json b/composer.json
new file mode 100644
index 00000000..ae9aef72
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,78 @@
+{
+ "name": "lmo/lmo_php8",
+ "description": "The Liga Manager Online (LMO for short) is free software for managing sports leagues with tables on the Internet.",
+ "type": "library",
+ "homepage": "https://github.com/henshingly/lmo_php8",
+ "keywords": [
+ "LMO",
+ "Liga Manager Online",
+ "Sportligen verwalten"
+ ],
+ "license": "GPL-2.0-only",
+ "authors": [
+ {
+ "name": "Rene Marth (joker)",
+ "homepage": "https://www.liga-manager-online.de",
+ "role": "Extension developer"
+ },
+ {
+ "name": "Henshingly",
+ "homepage": "https://www.vest-sport.de",
+ "role": "Developer"
+ }
+ ],
+ "support": {
+ "forum": "https://www.liga-manager-online.org/forum/"
+ },
+ "require": {
+ "php": ">=8.0",
+ "pear/html_template_it": "^1.3.3",
+ "phpmailer/phpmailer": "^7.1"
+ },
+ "require-dev": {
+ "phpstan/phpstan": "^2.3",
+ "phpunit/phpunit": "^9.6"
+ },
+ "suggest": {
+ "ext-gd": "Fuer die Grafiken (Fieberkurve, Tippspiel-Grafiken)",
+ "ext-zip": "Fuer Download und E-Mail-Versand der Ligadateien im Admin-Bereich"
+ },
+ "autoload": {
+ "classmap": [
+ "lmo/includes/LmoTemplate.php"
+ ],
+ "psr-4": {
+ "Lmo\\Install\\": "src/Install/",
+ "Lmo\\Liga\\": "src/Liga/",
+ "Lmo\\Tipp\\": "src/Tipp/"
+ }
+ },
+ "bin": [
+ "bin/lmo-install"
+ ],
+ "autoload-dev": {
+ "psr-4": {
+ "Lmo\\Tests\\GoldenMaster\\": "tests/GoldenMaster/",
+ "Lmo\\Tests\\Unit\\": "tests/Unit/",
+ "Lmo\\Tests\\RealData\\": "tests/RealData/"
+ }
+ },
+ "scripts": {
+ "phpstan": [
+ "phpstan analyse --no-progress --memory-limit=1G",
+ "phpstan analyse -c phpstan-classlib.neon.dist --no-progress --memory-limit=1G",
+ "phpstan analyse -c phpstan-liga.neon.dist --no-progress --memory-limit=1G",
+ "phpstan analyse -c phpstan-liga-basis.neon.dist --no-progress --memory-limit=1G",
+ "phpstan analyse -c phpstan-tests.neon.dist --no-progress --memory-limit=1G",
+ "phpstan analyse -c phpstan-strict.neon.dist --no-progress --memory-limit=1G"
+ ],
+ "test": "phpunit",
+ "test:record": "GOLDEN_MODE=record phpunit"
+ },
+ "config": {
+ "platform": {
+ "php": "8.0.30"
+ },
+ "sort-packages": true
+ }
+}
diff --git a/composer.lock b/composer.lock
new file mode 100644
index 00000000..23cce040
--- /dev/null
+++ b/composer.lock
@@ -0,0 +1,2179 @@
+{
+ "_readme": [
+ "This file locks the dependencies of your project to a known state",
+ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
+ "This file is @generated automatically"
+ ],
+ "content-hash": "04131496de5f122775ac2a111bf2a90c",
+ "packages": [
+ {
+ "name": "pear/console_getopt",
+ "version": "v1.4.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/pear/Console_Getopt.git",
+ "reference": "a41f8d3e668987609178c7c4a9fe48fecac53fa0"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/pear/Console_Getopt/zipball/a41f8d3e668987609178c7c4a9fe48fecac53fa0",
+ "reference": "a41f8d3e668987609178c7c4a9fe48fecac53fa0",
+ "shasum": ""
+ },
+ "type": "library",
+ "autoload": {
+ "psr-0": {
+ "Console": "./"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "include-path": [
+ "./"
+ ],
+ "license": [
+ "BSD-2-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Andrei Zmievski",
+ "email": "andrei@php.net",
+ "role": "Lead"
+ },
+ {
+ "name": "Stig Bakken",
+ "email": "stig@php.net",
+ "role": "Developer"
+ },
+ {
+ "name": "Greg Beaver",
+ "email": "cellog@php.net",
+ "role": "Helper"
+ }
+ ],
+ "description": "More info available on: http://pear.php.net/package/Console_Getopt",
+ "support": {
+ "issues": "http://pear.php.net/bugs/search.php?cmd=display&package_name[]=Console_Getopt",
+ "source": "https://github.com/pear/Console_Getopt"
+ },
+ "time": "2019-11-20T18:27:48+00:00"
+ },
+ {
+ "name": "pear/html_template_it",
+ "version": "v1.3.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/pear/HTML_Template_IT.git",
+ "reference": "47bff60a57ddefd3e177383339e717e598221544"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/pear/HTML_Template_IT/zipball/47bff60a57ddefd3e177383339e717e598221544",
+ "reference": "47bff60a57ddefd3e177383339e717e598221544",
+ "shasum": ""
+ },
+ "require": {
+ "pear/pear-core-minimal": "^1.10.1",
+ "php": ">=5.6"
+ },
+ "require-dev": {
+ "yoast/phpunit-polyfills": "^2.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-0": {
+ "HTML": "./"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "include-path": [
+ "./"
+ ],
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Gregory Currie",
+ "email": "gregorycu@php.net",
+ "role": "Lead"
+ },
+ {
+ "name": "Pierre-Alain Joye",
+ "email": "pajoye@php.net",
+ "role": "Lead"
+ },
+ {
+ "name": "David Soria Parra",
+ "email": "dsp@php.net",
+ "role": "Lead"
+ },
+ {
+ "name": "Thorsten Rinne",
+ "email": "thorstenr@php.net",
+ "role": "Developer"
+ },
+ {
+ "name": "Ulf Wendel",
+ "email": "ulf.wendel@phpdoc.de",
+ "role": "Developer"
+ }
+ ],
+ "description": "More info available on: http://pear.php.net/package/HTML_Template_IT",
+ "support": {
+ "issues": "http://pear.php.net/bugs/search.php?cmd=display&package_name[]=HTML_Template_IT",
+ "source": "https://github.com/pear/HTML_Template_IT"
+ },
+ "abandoned": true,
+ "time": "2025-12-14T16:27:16+00:00"
+ },
+ {
+ "name": "pear/pear-core-minimal",
+ "version": "v1.10.18",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/pear/pear-core-minimal.git",
+ "reference": "c7b55789d01de0ce090d289b73f1bbd6a2f113b1"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/pear/pear-core-minimal/zipball/c7b55789d01de0ce090d289b73f1bbd6a2f113b1",
+ "reference": "c7b55789d01de0ce090d289b73f1bbd6a2f113b1",
+ "shasum": ""
+ },
+ "require": {
+ "pear/console_getopt": "~1.4",
+ "pear/pear_exception": "~1.0",
+ "php": ">=5.4"
+ },
+ "replace": {
+ "rsky/pear-core-min": "self.version"
+ },
+ "type": "library",
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "include-path": [
+ "src/"
+ ],
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Christian Weiske",
+ "email": "cweiske@php.net",
+ "role": "Lead"
+ }
+ ],
+ "description": "Minimal set of PEAR core files to be used as composer dependency",
+ "support": {
+ "issues": "http://pear.php.net/bugs/search.php?cmd=display&package_name[]=PEAR",
+ "source": "https://github.com/pear/pear-core-minimal"
+ },
+ "time": "2025-12-14T20:37:07+00:00"
+ },
+ {
+ "name": "pear/pear_exception",
+ "version": "v1.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/pear/PEAR_Exception.git",
+ "reference": "b14fbe2ddb0b9f94f5b24cf08783d599f776fff0"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/pear/PEAR_Exception/zipball/b14fbe2ddb0b9f94f5b24cf08783d599f776fff0",
+ "reference": "b14fbe2ddb0b9f94f5b24cf08783d599f776fff0",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.2.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "<9"
+ },
+ "type": "class",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "PEAR/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "include-path": [
+ "."
+ ],
+ "license": [
+ "BSD-2-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Helgi Thormar",
+ "email": "dufuz@php.net"
+ },
+ {
+ "name": "Greg Beaver",
+ "email": "cellog@php.net"
+ }
+ ],
+ "description": "The PEAR Exception base class.",
+ "homepage": "https://github.com/pear/PEAR_Exception",
+ "keywords": [
+ "exception"
+ ],
+ "support": {
+ "issues": "http://pear.php.net/bugs/search.php?cmd=display&package_name[]=PEAR_Exception",
+ "source": "https://github.com/pear/PEAR_Exception"
+ },
+ "time": "2021-03-21T15:43:46+00:00"
+ },
+ {
+ "name": "phpmailer/phpmailer",
+ "version": "v7.1.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/PHPMailer/PHPMailer.git",
+ "reference": "1bc1716a507a65e039d4ac9d9adebbbd0d346e15"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/1bc1716a507a65e039d4ac9d9adebbbd0d346e15",
+ "reference": "1bc1716a507a65e039d4ac9d9adebbbd0d346e15",
+ "shasum": ""
+ },
+ "require": {
+ "ext-ctype": "*",
+ "ext-filter": "*",
+ "ext-hash": "*",
+ "php": ">=5.5.0"
+ },
+ "require-dev": {
+ "dealerdirect/phpcodesniffer-composer-installer": "^1.0",
+ "doctrine/annotations": "^1.2.6 || ^1.13.3",
+ "php-parallel-lint/php-console-highlighter": "^1.0.0",
+ "php-parallel-lint/php-parallel-lint": "^1.3.2",
+ "phpcompatibility/php-compatibility": "^10.0.0@dev",
+ "squizlabs/php_codesniffer": "^3.13.5",
+ "yoast/phpunit-polyfills": "^1.0.4"
+ },
+ "suggest": {
+ "decomplexity/SendOauth2": "Adapter for using XOAUTH2 authentication",
+ "directorytree/imapengine": "For uploading sent messages via IMAP, see gmail example",
+ "ext-imap": "Needed to support advanced email address parsing according to RFC822",
+ "ext-mbstring": "Needed to send email in multibyte encoding charset or decode encoded addresses",
+ "ext-openssl": "Needed for secure SMTP sending and DKIM signing",
+ "greew/oauth2-azure-provider": "Needed for Microsoft Azure XOAUTH2 authentication",
+ "hayageek/oauth2-yahoo": "Needed for Yahoo XOAUTH2 authentication",
+ "league/oauth2-google": "Needed for Google XOAUTH2 authentication",
+ "psr/log": "For optional PSR-3 debug logging",
+ "symfony/polyfill-mbstring": "To support UTF-8 if the Mbstring PHP extension is not enabled (^1.2)",
+ "thenetworg/oauth2-azure": "Needed for Microsoft XOAUTH2 authentication"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "PHPMailer\\PHPMailer\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "LGPL-2.1-only"
+ ],
+ "authors": [
+ {
+ "name": "Marcus Bointon",
+ "email": "phpmailer@synchromedia.co.uk"
+ },
+ {
+ "name": "Jim Jagielski",
+ "email": "jimjag@gmail.com"
+ },
+ {
+ "name": "Andy Prevost",
+ "email": "codeworxtech@users.sourceforge.net"
+ },
+ {
+ "name": "Brent R. Matzelle"
+ }
+ ],
+ "description": "PHPMailer is a full-featured email creation and transfer class for PHP",
+ "support": {
+ "issues": "https://github.com/PHPMailer/PHPMailer/issues",
+ "source": "https://github.com/PHPMailer/PHPMailer/tree/v7.1.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/Synchro",
+ "type": "github"
+ }
+ ],
+ "time": "2026-05-18T08:06:14+00:00"
+ }
+ ],
+ "packages-dev": [
+ {
+ "name": "doctrine/instantiator",
+ "version": "1.5.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/doctrine/instantiator.git",
+ "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/doctrine/instantiator/zipball/0a0fa9780f5d4e507415a065172d26a98d02047b",
+ "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.1 || ^8.0"
+ },
+ "require-dev": {
+ "doctrine/coding-standard": "^9 || ^11",
+ "ext-pdo": "*",
+ "ext-phar": "*",
+ "phpbench/phpbench": "^0.16 || ^1",
+ "phpstan/phpstan": "^1.4",
+ "phpstan/phpstan-phpunit": "^1",
+ "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5",
+ "vimeo/psalm": "^4.30 || ^5.4"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Marco Pivetta",
+ "email": "ocramius@gmail.com",
+ "homepage": "https://ocramius.github.io/"
+ }
+ ],
+ "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors",
+ "homepage": "https://www.doctrine-project.org/projects/instantiator.html",
+ "keywords": [
+ "constructor",
+ "instantiate"
+ ],
+ "support": {
+ "issues": "https://github.com/doctrine/instantiator/issues",
+ "source": "https://github.com/doctrine/instantiator/tree/1.5.0"
+ },
+ "funding": [
+ {
+ "url": "https://www.doctrine-project.org/sponsorship.html",
+ "type": "custom"
+ },
+ {
+ "url": "https://www.patreon.com/phpdoctrine",
+ "type": "patreon"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2022-12-30T00:15:36+00:00"
+ },
+ {
+ "name": "myclabs/deep-copy",
+ "version": "1.14.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/myclabs/DeepCopy.git",
+ "reference": "8680aa248f8e07bc8fb43f56f0f5fc77a0c96aae"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/8680aa248f8e07bc8fb43f56f0f5fc77a0c96aae",
+ "reference": "8680aa248f8e07bc8fb43f56f0f5fc77a0c96aae",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^8.0"
+ },
+ "conflict": {
+ "doctrine/collections": "<1.6.8",
+ "doctrine/common": "<2.13.3 || >=3 <3.2.2"
+ },
+ "require-dev": {
+ "doctrine/collections": "^1.6.8",
+ "doctrine/common": "^2.13.3 || ^3.2.2",
+ "phpspec/prophecy": "^1.10",
+ "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "src/DeepCopy/deep_copy.php"
+ ],
+ "psr-4": {
+ "DeepCopy\\": "src/DeepCopy/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "Create deep copies (clones) of your objects",
+ "keywords": [
+ "clone",
+ "copy",
+ "duplicate",
+ "object",
+ "object graph"
+ ],
+ "support": {
+ "issues": "https://github.com/myclabs/DeepCopy/issues",
+ "source": "https://github.com/myclabs/DeepCopy/tree/1.14.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/mnapoli",
+ "type": "github"
+ }
+ ],
+ "time": "2026-08-11T10:17:44+00:00"
+ },
+ {
+ "name": "nikic/php-parser",
+ "version": "v5.9.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/nikic/PHP-Parser.git",
+ "reference": "9e33da9553fe7786f0962b35f4e4ecf01be89def"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/9e33da9553fe7786f0962b35f4e4ecf01be89def",
+ "reference": "9e33da9553fe7786f0962b35f4e4ecf01be89def",
+ "shasum": ""
+ },
+ "require": {
+ "ext-json": "*",
+ "ext-tokenizer": "*",
+ "php": ">=7.4"
+ },
+ "require-dev": {
+ "ircmaxell/php-yacc": "^0.0.7",
+ "phpunit/phpunit": "^9.0"
+ },
+ "bin": [
+ "bin/php-parse"
+ ],
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "5.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "PhpParser\\": "lib/PhpParser"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Nikita Popov"
+ }
+ ],
+ "description": "A PHP parser written in PHP",
+ "keywords": [
+ "parser",
+ "php"
+ ],
+ "support": {
+ "issues": "https://github.com/nikic/PHP-Parser/issues",
+ "source": "https://github.com/nikic/PHP-Parser/tree/v5.9.0"
+ },
+ "time": "2026-09-13T18:51:52+00:00"
+ },
+ {
+ "name": "phar-io/manifest",
+ "version": "2.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phar-io/manifest.git",
+ "reference": "54750ef60c58e43759730615a392c31c80e23176"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176",
+ "reference": "54750ef60c58e43759730615a392c31c80e23176",
+ "shasum": ""
+ },
+ "require": {
+ "ext-dom": "*",
+ "ext-libxml": "*",
+ "ext-phar": "*",
+ "ext-xmlwriter": "*",
+ "phar-io/version": "^3.0.1",
+ "php": "^7.2 || ^8.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0.x-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Arne Blankerts",
+ "email": "arne@blankerts.de",
+ "role": "Developer"
+ },
+ {
+ "name": "Sebastian Heuer",
+ "email": "sebastian@phpeople.de",
+ "role": "Developer"
+ },
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "Developer"
+ }
+ ],
+ "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)",
+ "support": {
+ "issues": "https://github.com/phar-io/manifest/issues",
+ "source": "https://github.com/phar-io/manifest/tree/2.0.4"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/theseer",
+ "type": "github"
+ }
+ ],
+ "time": "2024-03-03T12:33:53+00:00"
+ },
+ {
+ "name": "phar-io/version",
+ "version": "3.2.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phar-io/version.git",
+ "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74",
+ "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2 || ^8.0"
+ },
+ "type": "library",
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Arne Blankerts",
+ "email": "arne@blankerts.de",
+ "role": "Developer"
+ },
+ {
+ "name": "Sebastian Heuer",
+ "email": "sebastian@phpeople.de",
+ "role": "Developer"
+ },
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "Developer"
+ }
+ ],
+ "description": "Library for handling version information and constraints",
+ "support": {
+ "issues": "https://github.com/phar-io/version/issues",
+ "source": "https://github.com/phar-io/version/tree/3.2.1"
+ },
+ "time": "2022-02-21T01:04:05+00:00"
+ },
+ {
+ "name": "phpstan/phpstan",
+ "version": "2.3.1",
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phpstan/phpstan/zipball/6155239b488e95d26d1ebd95c6b4bb294d7f09db",
+ "reference": "6155239b488e95d26d1ebd95c6b4bb294d7f09db",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.4|^8.0"
+ },
+ "conflict": {
+ "phpstan/phpstan-shim": "*"
+ },
+ "bin": [
+ "phpstan",
+ "phpstan.phar"
+ ],
+ "type": "library",
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Ondřej Mirtes"
+ },
+ {
+ "name": "Markus Staab"
+ },
+ {
+ "name": "Vincent Langlet"
+ }
+ ],
+ "description": "PHPStan - PHP Static Analysis Tool",
+ "keywords": [
+ "dev",
+ "static analysis"
+ ],
+ "support": {
+ "docs": "https://phpstan.org/user-guide/getting-started",
+ "forum": "https://github.com/phpstan/phpstan/discussions",
+ "issues": "https://github.com/phpstan/phpstan/issues",
+ "security": "https://github.com/phpstan/phpstan/security/policy",
+ "source": "https://github.com/phpstan/phpstan-src"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/ondrejmirtes",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/phpstan",
+ "type": "github"
+ }
+ ],
+ "time": "2026-10-08T21:09:11+00:00"
+ },
+ {
+ "name": "phpunit/php-code-coverage",
+ "version": "9.2.32",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-code-coverage.git",
+ "reference": "85402a822d1ecf1db1096959413d35e1c37cf1a5"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/85402a822d1ecf1db1096959413d35e1c37cf1a5",
+ "reference": "85402a822d1ecf1db1096959413d35e1c37cf1a5",
+ "shasum": ""
+ },
+ "require": {
+ "ext-dom": "*",
+ "ext-libxml": "*",
+ "ext-xmlwriter": "*",
+ "nikic/php-parser": "^4.19.1 || ^5.1.0",
+ "php": ">=7.3",
+ "phpunit/php-file-iterator": "^3.0.6",
+ "phpunit/php-text-template": "^2.0.4",
+ "sebastian/code-unit-reverse-lookup": "^2.0.3",
+ "sebastian/complexity": "^2.0.3",
+ "sebastian/environment": "^5.1.5",
+ "sebastian/lines-of-code": "^1.0.4",
+ "sebastian/version": "^3.0.2",
+ "theseer/tokenizer": "^1.2.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.6"
+ },
+ "suggest": {
+ "ext-pcov": "PHP extension that provides line coverage",
+ "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "9.2.x-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.",
+ "homepage": "https://github.com/sebastianbergmann/php-code-coverage",
+ "keywords": [
+ "coverage",
+ "testing",
+ "xunit"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
+ "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy",
+ "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.32"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2024-08-22T04:23:01+00:00"
+ },
+ {
+ "name": "phpunit/php-file-iterator",
+ "version": "3.0.6",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-file-iterator.git",
+ "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf",
+ "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "FilterIterator implementation that filters files based on a list of suffixes.",
+ "homepage": "https://github.com/sebastianbergmann/php-file-iterator/",
+ "keywords": [
+ "filesystem",
+ "iterator"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues",
+ "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/3.0.6"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2021-12-02T12:48:52+00:00"
+ },
+ {
+ "name": "phpunit/php-invoker",
+ "version": "3.1.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-invoker.git",
+ "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/5a10147d0aaf65b58940a0b72f71c9ac0423cc67",
+ "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "ext-pcntl": "*",
+ "phpunit/phpunit": "^9.3"
+ },
+ "suggest": {
+ "ext-pcntl": "*"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.1-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Invoke callables with a timeout",
+ "homepage": "https://github.com/sebastianbergmann/php-invoker/",
+ "keywords": [
+ "process"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-invoker/issues",
+ "source": "https://github.com/sebastianbergmann/php-invoker/tree/3.1.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-09-28T05:58:55+00:00"
+ },
+ {
+ "name": "phpunit/php-text-template",
+ "version": "2.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-text-template.git",
+ "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28",
+ "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Simple template engine.",
+ "homepage": "https://github.com/sebastianbergmann/php-text-template/",
+ "keywords": [
+ "template"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-text-template/issues",
+ "source": "https://github.com/sebastianbergmann/php-text-template/tree/2.0.4"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-10-26T05:33:50+00:00"
+ },
+ {
+ "name": "phpunit/php-timer",
+ "version": "5.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-timer.git",
+ "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2",
+ "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "5.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Utility class for timing",
+ "homepage": "https://github.com/sebastianbergmann/php-timer/",
+ "keywords": [
+ "timer"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-timer/issues",
+ "source": "https://github.com/sebastianbergmann/php-timer/tree/5.0.3"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-10-26T13:16:10+00:00"
+ },
+ {
+ "name": "phpunit/phpunit",
+ "version": "9.6.38",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/phpunit.git",
+ "reference": "8b5f757e8e92071bfdbfa93943f94f84c4e31007"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/8b5f757e8e92071bfdbfa93943f94f84c4e31007",
+ "reference": "8b5f757e8e92071bfdbfa93943f94f84c4e31007",
+ "shasum": ""
+ },
+ "require": {
+ "doctrine/instantiator": "^1.5.0 || ^2",
+ "ext-dom": "*",
+ "ext-filter": "*",
+ "ext-json": "*",
+ "ext-libxml": "*",
+ "ext-mbstring": "*",
+ "ext-xmlwriter": "*",
+ "myclabs/deep-copy": "^1.13.4",
+ "phar-io/manifest": "^2.0.4",
+ "phar-io/version": "^3.2.1",
+ "php": ">=7.3",
+ "phpunit/php-code-coverage": "^9.2.32",
+ "phpunit/php-file-iterator": "^3.0.6",
+ "phpunit/php-invoker": "^3.1.1",
+ "phpunit/php-text-template": "^2.0.4",
+ "phpunit/php-timer": "^5.0.3",
+ "sebastian/cli-parser": "^1.0.2",
+ "sebastian/code-unit": "^1.0.8",
+ "sebastian/comparator": "^4.0.10",
+ "sebastian/diff": "^4.0.6",
+ "sebastian/environment": "^5.1.5",
+ "sebastian/exporter": "^4.0.9",
+ "sebastian/global-state": "^5.0.8",
+ "sebastian/object-enumerator": "^4.0.4",
+ "sebastian/resource-operations": "^3.0.4",
+ "sebastian/type": "^3.2.1",
+ "sebastian/version": "^3.0.2"
+ },
+ "suggest": {
+ "ext-soap": "To be able to generate mocks based on WSDL files",
+ "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage"
+ },
+ "bin": [
+ "phpunit"
+ ],
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "9.6-dev"
+ }
+ },
+ "autoload": {
+ "files": [
+ "src/Framework/Assert/Functions.php"
+ ],
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "The PHP Unit Testing framework.",
+ "homepage": "https://phpunit.de/",
+ "keywords": [
+ "phpunit",
+ "testing",
+ "xunit"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/phpunit/issues",
+ "security": "https://github.com/sebastianbergmann/phpunit/security/policy",
+ "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.38"
+ },
+ "funding": [
+ {
+ "url": "https://phpunit.de/sponsoring.html",
+ "type": "other"
+ }
+ ],
+ "time": "2026-10-05T05:44:26+00:00"
+ },
+ {
+ "name": "sebastian/cli-parser",
+ "version": "1.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/cli-parser.git",
+ "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/2b56bea83a09de3ac06bb18b92f068e60cc6f50b",
+ "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Library for parsing CLI options",
+ "homepage": "https://github.com/sebastianbergmann/cli-parser",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/cli-parser/issues",
+ "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.2"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2024-03-02T06:27:43+00:00"
+ },
+ {
+ "name": "sebastian/code-unit",
+ "version": "1.0.8",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/code-unit.git",
+ "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/1fc9f64c0927627ef78ba436c9b17d967e68e120",
+ "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Collection of value objects that represent the PHP code units",
+ "homepage": "https://github.com/sebastianbergmann/code-unit",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/code-unit/issues",
+ "source": "https://github.com/sebastianbergmann/code-unit/tree/1.0.8"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-10-26T13:08:54+00:00"
+ },
+ {
+ "name": "sebastian/code-unit-reverse-lookup",
+ "version": "2.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git",
+ "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5",
+ "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Looks up which function or method a line of code belongs to",
+ "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues",
+ "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/2.0.3"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-09-28T05:30:19+00:00"
+ },
+ {
+ "name": "sebastian/comparator",
+ "version": "4.0.10",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/comparator.git",
+ "reference": "e4df00b9b3571187db2831ae9aada2c6efbd715d"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/e4df00b9b3571187db2831ae9aada2c6efbd715d",
+ "reference": "e4df00b9b3571187db2831ae9aada2c6efbd715d",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3",
+ "sebastian/diff": "^4.0",
+ "sebastian/exporter": "^4.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ },
+ {
+ "name": "Jeff Welch",
+ "email": "whatthejeff@gmail.com"
+ },
+ {
+ "name": "Volker Dusch",
+ "email": "github@wallbash.com"
+ },
+ {
+ "name": "Bernhard Schussek",
+ "email": "bschussek@2bepublished.at"
+ }
+ ],
+ "description": "Provides the functionality to compare PHP values for equality",
+ "homepage": "https://github.com/sebastianbergmann/comparator",
+ "keywords": [
+ "comparator",
+ "compare",
+ "equality"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/comparator/issues",
+ "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.10"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ },
+ {
+ "url": "https://liberapay.com/sebastianbergmann",
+ "type": "liberapay"
+ },
+ {
+ "url": "https://thanks.dev/u/gh/sebastianbergmann",
+ "type": "thanks_dev"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-01-24T09:22:56+00:00"
+ },
+ {
+ "name": "sebastian/complexity",
+ "version": "2.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/complexity.git",
+ "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/25f207c40d62b8b7aa32f5ab026c53561964053a",
+ "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a",
+ "shasum": ""
+ },
+ "require": {
+ "nikic/php-parser": "^4.18 || ^5.0",
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Library for calculating the complexity of PHP code units",
+ "homepage": "https://github.com/sebastianbergmann/complexity",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/complexity/issues",
+ "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.3"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2023-12-22T06:19:30+00:00"
+ },
+ {
+ "name": "sebastian/diff",
+ "version": "4.0.6",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/diff.git",
+ "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/ba01945089c3a293b01ba9badc29ad55b106b0bc",
+ "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3",
+ "symfony/process": "^4.2 || ^5"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ },
+ {
+ "name": "Kore Nordmann",
+ "email": "mail@kore-nordmann.de"
+ }
+ ],
+ "description": "Diff implementation",
+ "homepage": "https://github.com/sebastianbergmann/diff",
+ "keywords": [
+ "diff",
+ "udiff",
+ "unidiff",
+ "unified diff"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/diff/issues",
+ "source": "https://github.com/sebastianbergmann/diff/tree/4.0.6"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2024-03-02T06:30:58+00:00"
+ },
+ {
+ "name": "sebastian/environment",
+ "version": "5.1.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/environment.git",
+ "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/830c43a844f1f8d5b7a1f6d6076b784454d8b7ed",
+ "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "suggest": {
+ "ext-posix": "*"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "5.1-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Provides functionality to handle HHVM/PHP environments",
+ "homepage": "http://www.github.com/sebastianbergmann/environment",
+ "keywords": [
+ "Xdebug",
+ "environment",
+ "hhvm"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/environment/issues",
+ "source": "https://github.com/sebastianbergmann/environment/tree/5.1.5"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2023-02-03T06:03:51+00:00"
+ },
+ {
+ "name": "sebastian/exporter",
+ "version": "4.0.9",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/exporter.git",
+ "reference": "4352c1a3df741a7ba9e61af6fed51d1fee41cbf7"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/4352c1a3df741a7ba9e61af6fed51d1fee41cbf7",
+ "reference": "4352c1a3df741a7ba9e61af6fed51d1fee41cbf7",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3",
+ "sebastian/recursion-context": "^4.0"
+ },
+ "require-dev": {
+ "ext-mbstring": "*",
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ },
+ {
+ "name": "Jeff Welch",
+ "email": "whatthejeff@gmail.com"
+ },
+ {
+ "name": "Volker Dusch",
+ "email": "github@wallbash.com"
+ },
+ {
+ "name": "Adam Harvey",
+ "email": "aharvey@php.net"
+ },
+ {
+ "name": "Bernhard Schussek",
+ "email": "bschussek@gmail.com"
+ }
+ ],
+ "description": "Provides the functionality to export PHP variables for visualization",
+ "homepage": "https://www.github.com/sebastianbergmann/exporter",
+ "keywords": [
+ "export",
+ "exporter"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/exporter/issues",
+ "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.9"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ },
+ {
+ "url": "https://liberapay.com/sebastianbergmann",
+ "type": "liberapay"
+ },
+ {
+ "url": "https://thanks.dev/u/gh/sebastianbergmann",
+ "type": "thanks_dev"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-08-11T04:55:59+00:00"
+ },
+ {
+ "name": "sebastian/global-state",
+ "version": "5.0.8",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/global-state.git",
+ "reference": "b6781316bdcd28260904e7cc18ec983d0d2ef4f6"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/b6781316bdcd28260904e7cc18ec983d0d2ef4f6",
+ "reference": "b6781316bdcd28260904e7cc18ec983d0d2ef4f6",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3",
+ "sebastian/object-reflector": "^2.0",
+ "sebastian/recursion-context": "^4.0"
+ },
+ "require-dev": {
+ "ext-dom": "*",
+ "phpunit/phpunit": "^9.3"
+ },
+ "suggest": {
+ "ext-uopz": "*"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "5.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Snapshotting of global state",
+ "homepage": "http://www.github.com/sebastianbergmann/global-state",
+ "keywords": [
+ "global state"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/global-state/issues",
+ "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.8"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ },
+ {
+ "url": "https://liberapay.com/sebastianbergmann",
+ "type": "liberapay"
+ },
+ {
+ "url": "https://thanks.dev/u/gh/sebastianbergmann",
+ "type": "thanks_dev"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/sebastian/global-state",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-08-10T07:10:35+00:00"
+ },
+ {
+ "name": "sebastian/lines-of-code",
+ "version": "1.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/lines-of-code.git",
+ "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/e1e4a170560925c26d424b6a03aed157e7dcc5c5",
+ "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5",
+ "shasum": ""
+ },
+ "require": {
+ "nikic/php-parser": "^4.18 || ^5.0",
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Library for counting the lines of code in PHP source code",
+ "homepage": "https://github.com/sebastianbergmann/lines-of-code",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/lines-of-code/issues",
+ "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.4"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2023-12-22T06:20:34+00:00"
+ },
+ {
+ "name": "sebastian/object-enumerator",
+ "version": "4.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/object-enumerator.git",
+ "reference": "5c9eeac41b290a3712d88851518825ad78f45c71"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/5c9eeac41b290a3712d88851518825ad78f45c71",
+ "reference": "5c9eeac41b290a3712d88851518825ad78f45c71",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3",
+ "sebastian/object-reflector": "^2.0",
+ "sebastian/recursion-context": "^4.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Traverses array structures and object graphs to enumerate all referenced objects",
+ "homepage": "https://github.com/sebastianbergmann/object-enumerator/",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/object-enumerator/issues",
+ "source": "https://github.com/sebastianbergmann/object-enumerator/tree/4.0.4"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-10-26T13:12:34+00:00"
+ },
+ {
+ "name": "sebastian/object-reflector",
+ "version": "2.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/object-reflector.git",
+ "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/b4f479ebdbf63ac605d183ece17d8d7fe49c15c7",
+ "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Allows reflection of object attributes, including inherited and non-public ones",
+ "homepage": "https://github.com/sebastianbergmann/object-reflector/",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/object-reflector/issues",
+ "source": "https://github.com/sebastianbergmann/object-reflector/tree/2.0.4"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-10-26T13:14:26+00:00"
+ },
+ {
+ "name": "sebastian/recursion-context",
+ "version": "4.0.7",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/recursion-context.git",
+ "reference": "c85be6922b7fd365942b986b9a50397d65407611"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/c85be6922b7fd365942b986b9a50397d65407611",
+ "reference": "c85be6922b7fd365942b986b9a50397d65407611",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ },
+ {
+ "name": "Jeff Welch",
+ "email": "whatthejeff@gmail.com"
+ },
+ {
+ "name": "Adam Harvey",
+ "email": "aharvey@php.net"
+ }
+ ],
+ "description": "Provides functionality to recursively process PHP variables",
+ "homepage": "https://github.com/sebastianbergmann/recursion-context",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/recursion-context/issues",
+ "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.7"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ },
+ {
+ "url": "https://liberapay.com/sebastianbergmann",
+ "type": "liberapay"
+ },
+ {
+ "url": "https://thanks.dev/u/gh/sebastianbergmann",
+ "type": "thanks_dev"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-08-11T05:25:24+00:00"
+ },
+ {
+ "name": "sebastian/resource-operations",
+ "version": "3.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/resource-operations.git",
+ "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/05d5692a7993ecccd56a03e40cd7e5b09b1d404e",
+ "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "3.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Provides a list of PHP built-in functions that operate on resources",
+ "homepage": "https://www.github.com/sebastianbergmann/resource-operations",
+ "support": {
+ "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.4"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2024-03-14T16:00:52+00:00"
+ },
+ {
+ "name": "sebastian/type",
+ "version": "3.2.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/type.git",
+ "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7",
+ "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.5"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.2-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Collection of value objects that represent the types of the PHP type system",
+ "homepage": "https://github.com/sebastianbergmann/type",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/type/issues",
+ "source": "https://github.com/sebastianbergmann/type/tree/3.2.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2023-02-03T06:13:03+00:00"
+ },
+ {
+ "name": "sebastian/version",
+ "version": "3.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/version.git",
+ "reference": "c6c1022351a901512170118436c764e473f6de8c"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c6c1022351a901512170118436c764e473f6de8c",
+ "reference": "c6c1022351a901512170118436c764e473f6de8c",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Library that helps with managing the version number of Git-hosted PHP projects",
+ "homepage": "https://github.com/sebastianbergmann/version",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/version/issues",
+ "source": "https://github.com/sebastianbergmann/version/tree/3.0.2"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-09-28T06:39:44+00:00"
+ },
+ {
+ "name": "theseer/tokenizer",
+ "version": "1.3.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/theseer/tokenizer.git",
+ "reference": "b7489ce515e168639d17feec34b8847c326b0b3c"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b7489ce515e168639d17feec34b8847c326b0b3c",
+ "reference": "b7489ce515e168639d17feec34b8847c326b0b3c",
+ "shasum": ""
+ },
+ "require": {
+ "ext-dom": "*",
+ "ext-tokenizer": "*",
+ "ext-xmlwriter": "*",
+ "php": "^7.2 || ^8.0"
+ },
+ "type": "library",
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Arne Blankerts",
+ "email": "arne@blankerts.de",
+ "role": "Developer"
+ }
+ ],
+ "description": "A small library for converting tokenized PHP source code into XML and potentially other formats",
+ "support": {
+ "issues": "https://github.com/theseer/tokenizer/issues",
+ "source": "https://github.com/theseer/tokenizer/tree/1.3.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/theseer",
+ "type": "github"
+ }
+ ],
+ "time": "2025-11-17T20:03:58+00:00"
+ }
+ ],
+ "aliases": [],
+ "minimum-stability": "stable",
+ "stability-flags": {},
+ "prefer-stable": false,
+ "prefer-lowest": false,
+ "platform": {
+ "php": ">=8.0"
+ },
+ "platform-dev": {},
+ "platform-overrides": {
+ "php": "8.0.30"
+ },
+ "plugin-api-version": "2.9.0"
+}
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 00000000..bc09c15d
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,71 @@
+# Aufruf siehe TESTING.md
+services:
+ tests:
+ build:
+ context: .
+ dockerfile: docker/Dockerfile
+ args:
+ PHP_VERSION: ${PHP_VERSION:-8.3}
+ image: lmo-golden-tests:php${PHP_VERSION:-8.3}
+ volumes:
+ - .:/app
+ # unveraenderter Originalstand fuer das Aufzeichnen der Snapshots (optional)
+ - ${ORIGINAL_DIR:-./.original}:/original:ro
+ environment:
+ LMO_SOURCE: ${LMO_SOURCE:-/app}
+ GOLDEN_MODE: ${GOLDEN_MODE:-compare}
+ GOLDEN_COVERAGE: ${GOLDEN_COVERAGE:-0}
+ GOLDEN_NO_FAKETIME: ${GOLDEN_NO_FAKETIME:-0}
+
+ # Statische Analyse mit PHPStan (siehe TESTING.md): docker compose run --rm phpstan
+ phpstan:
+ extends:
+ service: tests
+ entrypoint: ["sh", "-c", "[ -x vendor/bin/phpstan ] || composer install --no-interaction --no-progress --prefer-dist; composer phpstan"]
+
+ # Webserver zum Ausprobieren, arbeitet live auf ./lmo: docker compose up -d web
+ # Aufruf: http://localhost:${WEB_PORT:-8082}/lmo/
+ # Addons mit Beispielligen ausprobieren: http://localhost:${WEB_PORT:-8082}/demo/
+ web:
+ build:
+ context: .
+ dockerfile: docker/web.Dockerfile
+ args:
+ PHP_VERSION: ${PHP_VERSION:-8.3}
+ image: lmo-web:php${PHP_VERSION:-8.3}
+ ports:
+ - "${WEB_PORT:-8082}:80"
+ volumes:
+ - ./lmo:/var/www/html/lmo
+ # Composer-Abhaengigkeiten liegen wie im Repo neben lmo/ (vorher: composer install bzw. docker compose run --rm tests)
+ - ./vendor:/var/www/html/vendor:ro
+ # die Klassen des LMO (Lmo\Liga\...), die der Composer-Autoloader neben vendor/ erwartet
+ - ./src:/var/www/html/src:ro
+ # Laufzeitdaten getrennt vom Code (Neustart von vorn: docker compose down -v)
+ - ./lmo:/srv/lmo-src:ro
+ - web-config:/var/www/html/lmo/config
+ - web-ligen:/var/www/html/lmo/ligen
+ - web-output:/var/www/html/lmo/output
+ - web-tipps:/var/www/html/lmo/addon/tipp/tipps
+ - web-stats:/var/www/html/lmo/addon/spieler/stats
+ # Uebersichtsseite der Addons und Beispieldaten (werden beim Start in die Volumes kopiert)
+ - ./docker/demo/index.php:/var/www/html/demo/index.php:ro
+ - ./docker/demo:/srv/lmo-demo:ro
+ environment:
+ MAIL_PORT: ${MAIL_PORT:-8025}
+ depends_on:
+ - mail
+
+ # Faengt alle Mails ab, die LMO im Web-Container verschickt (nichts geht nach draussen).
+ # Postfach im Browser: http://localhost:${MAIL_PORT:-8025}/
+ mail:
+ image: axllent/mailpit
+ ports:
+ - "${MAIL_PORT:-8025}:8025"
+
+volumes:
+ web-config:
+ web-ligen:
+ web-output:
+ web-tipps:
+ web-stats:
diff --git a/docker/Dockerfile b/docker/Dockerfile
new file mode 100644
index 00000000..93e469f2
--- /dev/null
+++ b/docker/Dockerfile
@@ -0,0 +1,25 @@
+# Testumgebung fuer die Golden-Master-Tests.
+# PHP-Version waehlbar: PHP_VERSION=8.1 docker compose build tests
+ARG PHP_VERSION=8.3
+FROM php:${PHP_VERSION}-cli
+
+RUN apt-get update \
+ && apt-get install -y --no-install-recommends \
+ git unzip faketime \
+ libpng-dev libjpeg-dev libfreetype6-dev \
+ && docker-php-ext-configure gd --with-freetype --with-jpeg \
+ && docker-php-ext-install -j"$(nproc)" gd \
+ && rm -rf /var/lib/apt/lists/*
+
+# Xdebug als Coverage-Treiber (pcov stuerzt hier mit Segmentation fault ab).
+# Standardmaessig aus: kein Overhead und keine zusaetzliche Ausgabe. Nur die Kindprozesse der
+# Coverage-Messung starten mit -d xdebug.mode=coverage.
+RUN pecl install xdebug && docker-php-ext-enable xdebug \
+ && echo "xdebug.mode=off" > /usr/local/etc/php/conf.d/zz-xdebug-mode.ini
+
+COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
+
+WORKDIR /app
+COPY docker/run-tests.sh /usr/local/bin/run-tests
+RUN chmod +x /usr/local/bin/run-tests
+ENTRYPOINT ["run-tests"]
diff --git a/docker/README.md b/docker/README.md
new file mode 100644
index 00000000..399d9b48
--- /dev/null
+++ b/docker/README.md
@@ -0,0 +1,124 @@
+# Docker für Entwickler
+
+`docker-compose.yml` im Projektordner stellt drei Dienste bereit. Alle Befehle im Projektordner ausführen.
+
+| Dienst | Zweck | Zugriff |
+|---|---|---|
+| `web` | LMO im Browser ausprobieren | http://localhost:8082/lmo/ |
+| `web` | Addons mit Beispielligen ausprobieren | http://localhost:8082/demo/ |
+| `mail` | Postfach für alle Mails, die LMO verschickt | http://localhost:8025/ |
+| `tests` | automatische Tests (PHPUnit) | nur Kommandozeile, siehe [TESTING.md](../TESTING.md) |
+
+## Starten und stoppen
+
+```bash
+docker compose up -d web # startet web und mail
+docker compose stop # anhalten, Daten bleiben
+docker compose down # Container entfernen, Daten bleiben
+docker compose down -v # alles entfernen, auch Konfiguration, Ligen und Tipps
+docker compose up -d --build web # nach Änderungen an docker/web.Dockerfile
+```
+
+## LMO im Browser (`web`)
+
+- Ligaseite: http://localhost:8082/lmo/
+- Admin-Bereich: http://localhost:8082/lmo/lmoadmin.php
+ Beim ersten Aufruf legt LMO die Konfiguration an und fragt nach einem Admin-Konto.
+- Der Ordner `lmo/` ist live eingebunden: Änderungen am Code wirken sofort, ohne Neustart.
+- PHP-Fehler und -Warnungen werden im Browser angezeigt.
+
+### Wo die Daten liegen
+
+Konfiguration, Ligen und alles, was LMO zur Laufzeit schreibt, liegen in Docker-Volumes und nicht im
+Projektordner. So landet nichts davon versehentlich im Git.
+
+| Ordner im Container | Volume | Inhalt |
+|---|---|---|
+| `lmo/config` | `web-config` | Konfiguration, Admin-Konto |
+| `lmo/ligen` | `web-ligen` | Ligadateien (`.l98`) |
+| `lmo/output` | `web-output` | erzeugte Dateien, Caches |
+| `lmo/addon/tipp/tipps` | `web-tipps` | Tippspiel |
+| `lmo/addon/spieler/stats` | `web-stats` | Spielerstatistik |
+
+Beim ersten Start werden die Volumes mit dem Stand aus dem Projektordner gefüllt (Beispielligen).
+Später im Projektordner hinzugefügte Ligen erscheinen nicht von selbst im Container.
+
+```bash
+# Datei in den Container kopieren, z.B. eine Liga
+docker compose cp meine-liga.l98 web:/var/www/html/lmo/ligen/
+docker compose exec web chown www-data:www-data /var/www/html/lmo/ligen/meine-liga.l98
+
+# Datei aus dem Container holen
+docker compose cp web:/var/www/html/lmo/ligen/meine-liga.l98 .
+
+# Shell im Container
+docker compose exec web bash
+
+# Ausgaben des Webservers
+docker compose logs -f web
+```
+
+## Addons ausprobieren (`/demo/`)
+
+http://localhost:8082/demo/ zeigt alle Addons auf einer Seite, jeweils eingebettet und mit dem
+direkten Link:
+
+- mit einer Liga: Minitabelle und Spielerstatistik; die Liga lässt sich oben umschalten
+- einmal mit einer und einmal mit drei Ligen: Ticker, Viewer und Mini-Spielplan
+- Viewer nach Datum über drei Ligen
+
+Die Seite und ihre Daten liegen in `docker/demo/` und gehören nicht zu LMO; sie werden nicht mit
+ausgeliefert. Beim Start des Containers werden fehlende Dateien in die Volumes kopiert, vorhandene
+bleiben unangetastet:
+
+| Datei in `docker/demo/` | Ziel im Container | Inhalt |
+|---|---|---|
+| `ligen/demo-laufend.l98` | `lmo/ligen/` | 1. Bundesliga 2026/27, Stand 5. Spieltag |
+| `ligen/demo-beendet.l98` | `lmo/ligen/` und `lmo/ligen/demo-archiv/` | 1. Bundesliga 2025/26, komplett |
+| `ligen/demo-aelter.l98` | `lmo/ligen/` und `lmo/ligen/demo-archiv/` | 1. Bundesliga 2024/25, komplett |
+| `viewer/demo-*.view` | `lmo/config/viewer/` | Viewer-Ansichten über eine und über drei Ligen |
+| `stats/demo-laufend.stat` | `lmo/addon/spieler/stats/` | erfundene Spielerstatistik |
+
+So kommen mehrere Ligen in ein Addon:
+
+- Viewer: Die Ansicht nennt die Ligen als `liga1`, `liga2`, ...
+- Ticker: kommagetrennte Liste, direkt aufgerufen über `tickerligen=a.l98,b.l98` oder in der
+ Konfiguration als `standard_ligen`
+- Mini-Spielplan: Die bisherigen Begegnungen sucht er zusätzlich in allen Ligen eines Archivordners
+ (`folder=demo-archiv`); die Mannschaften werden über ihren Namen gefunden. Mit `mini_withArchiv=0`
+ bleibt es bei der eigenen Liga.
+
+Die Ergebnisse der Beispielligen stammen von OpenLigaDB. Die Termine sind erfunden: ein Spieltag pro
+Woche, alle Spiele samstags 15:30 Uhr. Wer eine Beispieldatei im Container verändert hat und den
+Ausgangsstand zurückhaben will, löscht sie dort und startet den Container neu.
+
+## Mails (`mail`)
+
+LMO verschickt im Container keine echten Mails. Alles, was über PHPs `mail()` gesendet wird (z.B.
+„Liga per E-Mail“ im Admin-Bereich), landet im Postfach unter http://localhost:8025/ – egal an
+welche Adresse. Das Postfach ist nach einem Neustart des Dienstes leer.
+
+## Einstellungen
+
+Als Umgebungsvariable vor dem Befehl oder in einer Datei `.env` im Projektordner:
+
+| Variable | Standard | Bedeutung |
+|---|---|---|
+| `WEB_PORT` | `8082` | Port von LMO im Browser |
+| `MAIL_PORT` | `8025` | Port des Postfachs |
+| `PHP_VERSION` | `8.3` | PHP-Version der Images, danach mit `--build` neu bauen |
+
+```bash
+WEB_PORT=9000 docker compose up -d web
+```
+
+## Tests (`tests`)
+
+```bash
+docker compose run --rm tests # alle Tests
+docker compose run --rm tests --testsuite unit # nur eine Suite: unit, real-data, golden-master
+docker compose run --rm tests --filter LigaTableTest # nur eine Testklasse
+```
+
+Die Tests laufen auf eigenen Kopien von LMO und berühren die Daten des Dienstes `web` nicht.
+Einzelheiten stehen in [TESTING.md](../TESTING.md).
diff --git a/docker/demo/index.php b/docker/demo/index.php
new file mode 100644
index 00000000..228b64b6
--- /dev/null
+++ b/docker/demo/index.php
@@ -0,0 +1,94 @@
+ '1. Bundesliga 2026/27 (laufend, 5. Spieltag)',
+ 'demo-beendet.l98' => '1. Bundesliga 2025/26 (beendet)',
+ 'demo-aelter.l98' => '1. Bundesliga 2024/25 (beendet)',
+];
+$liga = isset($_GET['liga'], $ligen[$_GET['liga']]) ? $_GET['liga'] : 'demo-laufend.l98';
+$q = rawurlencode($liga);
+$alle = implode(',', array_keys($ligen));
+$lmo = '/lmo';
+// Abschnitt => Kacheln [Titel, Beschreibung, Adresse, Hoehe]
+$abschnitte = [
+ 'Eine Liga' => [
+ ['Minitabelle', 'Tabellenausschnitt zum Einbinden in eine eigene Seite. Rechnet mit der classlib.',
+ "$lmo/addon/mini/lmo-minitab.php?mini_liga=$q&mini_template=standard&mini_platz=5&mini_ueber=4&mini_unter=4", 330],
+ ['Spielerstatistik', 'Öffentliche Seite. Spieler und Spalten pflegt man im Admin-Bereich der Liga. Beispieldaten gibt es nur für die laufende Liga.',
+ "$lmo/lmo.php?file=$q&action=spieler", 330],
+ ],
+ 'Eine Liga und mehrere Ligen im Vergleich' => [
+ ['Ticker: eine Liga', 'Lauftext auf der Ligaseite, eingeschaltet über „ticker=1“ in der Ligadatei.',
+ "$lmo/lmo.php?file=$q&action=results", 420],
+ ['Ticker: drei Ligen', 'Direkt aufgerufen mit einer kommagetrennten Liste von Ligen (tickerligen). Jede Liga bringt die Ergebnisse ihres aktuellen Spieltags mit.',
+ "$lmo/addon/ticker/ticker.php?tickerligen=$alle", 420],
+ ['Viewer: eine Liga', 'Ansicht „demo-1liga“: Spiele nach Spieltag. Ansichten bearbeitet man im Admin-Bereich unter den Viewer-Optionen.',
+ "$lmo/addon/viewer/viewer.php?multi=demo-1liga", 420],
+ ['Viewer: drei Ligen', 'Ansicht „demo-3ligen“: dieselbe Darstellung über alle drei Beispielligen.',
+ "$lmo/addon/viewer/viewer.php?multi=demo-3ligen", 420],
+ ['Mini-Spielplan: eine Liga', 'Nächstes Spiel Leverkusen gegen Dortmund und die bisherigen Begegnungen, nur aus der laufenden Liga (mini_withArchiv=0).',
+ "$lmo/addon/mini/lmo-mininext.php?file=demo-laufend.l98&a=1&b=2&mini_withArchiv=0", 300],
+ ['Mini-Spielplan: drei Ligen', 'Dieselbe Paarung, die bisherigen Begegnungen zusätzlich aus den zwei beendeten Saisons im Archivordner (folder=demo-archiv).',
+ "$lmo/addon/mini/lmo-mininext.php?file=demo-laufend.l98&a=1&b=2&folder=demo-archiv", 300],
+ ],
+ 'Nach Datum' => [
+ ['Viewer nach Datum: drei Ligen', 'Ansicht „demo-3ligen-datum“: Spiele 14 Tage vor und nach heute. Die Termine der Beispielligen sind fest, die Liste wird also mit der Zeit leer.',
+ "$lmo/addon/viewer/viewer.php?multi=demo-3ligen-datum", 420],
+ ],
+];
+?>
+
+
+
+
+
+LMO Addons ausprobieren
+
+
+
+
+
" title="">" title=""> ';
@@ -150,7 +182,7 @@
} //Ende der Suche nach Bildern für die Spaltenüberschriften
if ($spieler_extra_sortierspalte == 0) {
?>" title="">" title="">
anpasst, steht im Abschnitt Anpassung des LMO. Zu jedem Link erhalten Ihre Besucher
einen Tooltip, so daß Sie keine weiteren Erklärungen zur Benutzung des Skripts geben müssen.
- Der Adminbereich ist, über lmoadmin.php erreichbar. Beim ersten Start melden Sie sich mit
- dem Namen admin und dem Passwort lmo (Mit L wie Ludwig!) an. Unter Optionen
- können und sollten Sie das dann sofort ändern, damit kein Unbefugter mehr Zugriff hat.
[nach oben]
+ Der Adminbereich ist über lmoadmin.php erreichbar. Beim ersten Aufruf legen Sie dort Ihren
+ Benutzernamen und Ihr Passwort fest (siehe Installation). Rufen Sie lmoadmin.php deshalb
+ direkt nach dem Hochladen auf, damit Ihnen niemand zuvorkommt.[nach oben]
- Update des LMO: Beim Update des LMO 4.0 von älteren LMO4-Versionen (beta/RC1) werden keine Konfigurationsdateien überschrieben,
- Sie können die Dateien über Ihre vorhandenen Datein kopieren. Führen Sie danach unbedingt die Installation neu aus, indem Sie
- den LMO aufrufen. Es empfiehlt sich bei Live-Systemen den LMO für den Zeitpunkt der Installation kurz zu sperren. (z.B. über htaccess).
- Eventuell installierte zusätzliche Addons werden nicht erkannt, diese sind also nach dem Update erneut zu installieren
-
Das ZIP-Archiv wurde bereits von Ihnen entpackt. Achten Sie darauf, dass die Verzeichnisstruktur beim
- Entpacken beibehalten wird.
-
Rufen Sie nun den LMO auf, indem Sie die Adresse zum LMO in Ihren Browser eingeben. Die Installation startet automatisch
- und sorgt für alle Vorraussetzungen, die der Betrieb des LMO erfordert.
-
Im Normalfall ist der LMO nun korrekt installiert. Sollte jedoch ein Fehler aufgetreten sein oder Sie können aus anderen
- Gründen die Installation nicht abschließen, müssen Sie die folgenden Schritte manuell nachvollziehen:
-
Manuelle Installation
-
-
Achtung: Diese Schritte sind nur notwendig, wenn die normale Installation nicht funktioniert!
-
Löschen Sie die Datei install/install.php und kopieren Sie danach den Inhalt
- des Ordners install über Ihr LMO-Verzeichnis
-
Ändern Sie die Datei config/init-parameters.php mit einem normalen Texteditor und tragen Sie dort die
- benötigten Pfade ein. Falls die Datei noch nicht existiert, legen Sie diese mit einem normalen Texteditor an.
-
Die Datei sollte so aussehen: <?php
+
Entpacken Sie das ZIP-Archiv und laden Sie den Ordner lmo auf Ihren Webspace hoch.
+ Achten Sie darauf, dass die Verzeichnisstruktur erhalten bleibt. Einen Installer gibt es nicht mehr.
+
Rufen Sie lmo.php im Browser auf. Der LMO richtet sich dabei selbst ein: Er ermittelt Dateipfad und
+ Webadresse, legt die Konfiguration im Verzeichnis config an (aus den Vorlagen in
+ config-default) und erzeugt die benötigten Verzeichnisse.
+
Rufen Sie sofort danach lmoadmin.php auf. Beim ersten Aufruf legen Sie
+ dort Benutzernamen und Passwort des Hauptadministrators fest (mindestens 8 Zeichen). Ein voreingestelltes Konto gibt es nicht;
+ wer lmoadmin.php als Erster aufruft, legt das Passwort fest.
+
Erscheint die Meldung „Der Ordner config/ ist nicht beschreibbar“, setzen Sie mit Ihrem FTP-Programm die
+ Zugriffsrechte (chmod) und rufen den LMO erneut auf:
+
+
775 oder 777 - die Verzeichnisse config, ligen, output,
+ addon/spieler/stats und addon/tipp/tipps jeweils mit allen Unterverzeichnissen
+
664 oder 666 - die Ligen im Verzeichnis ligen
+
+ Welche Werte nötig sind, hängt vom Server ab; bei den meisten Anbietern ist das heute nicht mehr erforderlich.
+
Nur falls Dateipfad oder Webadresse falsch erkannt werden (z. B. hinter einer besonderen
+ Weiterleitung), können Sie beide fest vorgeben. Legen Sie dazu mit einem Texteditor die Datei
+ config/init-parameters.php an: <?php
$lmo_dateipfad='/absoluter/pfad/zum/lmo'; //Dateipfad zum LMO
- $lmo_url='http://www.adresse.ihres/lmo'; //abolute URL zum LMO
-?>
- Ersetzen Sie die beiden markierten Werte mit dem Dateipfad zum LMO-Verzeichnis
- und mit der Webadresse zum LMO. Falls Sie
- diese Angaben nicht wissen, fragen Sie bitte Ihren Provider.
-
Nun ändern Sie bei folgenden Datei die Zugriffsrechte (chmod):
-
-
Verzeichnisse:
-
777 - das Verzeichnis ligen und alle Unterverzeichnisse
-
777 - das Verzeichnis output
-
777 - das Verzeichnis config und dessen Unterverzeichnis config/viewer
-
777 - das Verzeichnis addon/spieler/stats
-
777 - das Verzeichnis addon/tipp/tipps und alle Unterverzeichnisse
-
Dateien:
-
666 - config/lmo-auth.php
-
666 - addon/tipp/lmo-tippauth.txt
-
666 - Alle Dateien cfg.txt im Verzeichnis config und dessen Unterverzeichnissen
-
666 - alle Ligen im ligen-Verzeichnis und in dessen Unterverzeichnissen
-
666 - alle Dateien in den Verzeichnissen output , addon/spieler/stats und
- tipp/tipps (sofern vorhanden)
-
-
-
Damit ist der Liga Manager Online installiert!
-
-
+ $lmo_url='https://www.adresse.ihres/lmo'; //absolute URL zum LMO
+?>
+
+ Update des LMO: Laden Sie die neuen Dateien über die vorhandenen. Ihre Konfiguration, Ligen und Konten
+ bleiben erhalten. Neue Einstellungen einer neueren Version ergänzt der LMO beim nächsten Aufruf automatisch in Ihren
+ vorhandenen cfg.txt-Dateien, ohne Ihre Werte zu verändern. Einen Ordner install aus einer
+ älteren Version können Sie löschen.
- Wenn Sie den Adminbereich betreten, sehen Sie zuerst eine Eingabemaske, mit der Sie sich beim System anmelden müssen. Nach
- der Installation ist der voreingestellte der Nutzername admin und das Passwort lmo (kleines L, kleines M,
- kleines O) eingestellt. Das sollten Sie aber schnellstmöglich ändern, um Unbefugten den Zutritt zu Ihrem Adminbereich zu
- verwehren.
+ Wenn Sie den Adminbereich betreten, sehen Sie zuerst eine Eingabemaske, mit der Sie sich beim System anmelden müssen. Beim
+ allerersten Aufruf erscheint stattdessen die Ersteinrichtung, in der Sie Benutzernamen und Passwort des
+ Hauptadministrators festlegen; danach sind Sie direkt angemeldet. Weitere Benutzer und Passwortänderungen verwalten Sie unter
+ Optionen.
Wenn Sie eingeloggt sind, haben Sie rechts oben im Menü immer auch einen Befehl Logout, mit dem Sie sich aus
dem Adminbereich ausloggen können. Das sollten Sie der Sicherheit wegen auch immer am Ende einer Sitzung tun.