Composer Path Repositories statt Versions Chaos
Wer mehrere zusammengehörige PHP Pakete pflegt, kennt das Problem: jede kleine Änderung im Kernpaket bedeutet eine neue Version, ein neues Tag, ein composer update im abhängigen Paket, nur um lokal zu testen, ob alles zusammenpasst. Ein PHP Monorepo mit Composer Path Repositories löst genau dieses Problem, ohne die spätere Veröffentlichung auf Packagist zu erschweren.
Inhaltsverzeichnis
- 1. Was ein PHP Monorepo wirklich ist und wann es sich lohnt
- 2. Composer Path Repositories als Fundament
- 3. Verzeichnisstruktur eines PHP Monorepos
- 4. Versionsabhängigkeiten zwischen Paketen
- 5. Tooling: Symplify MonorepoBuilder im Einsatz
- 6. CI Strategie für mehrere Pakete in einem Repository
- 7. Split in eigenständige Packagist Repositories
- 8. Team Workflow: atomare Änderungen über Paketgrenzen hinweg
- 9. Monorepo vs. Multi Repo im direkten Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Was ein PHP Monorepo wirklich ist und wann es sich lohnt
Ein PHP Monorepo ist ein einzelnes Git Repository, das mehrere unabhängige Composer Pakete enthält, jedes mit eigener composer.json, eigenem Namespace und eigener Versionsnummer, aber gemeinsam entwickelt und gemeinsam versioniert in einer einzigen Historie. Der Gegenentwurf ist das Multi Repo Modell, auch Polyrepo genannt: jedes Paket lebt in seinem eigenen Git Repository und wird über Packagist oder ein privates Registry eingebunden. Beide Modelle lösen dasselbe Grundproblem der Paketaufteilung, unterscheiden sich aber massiv im Entwicklungsalltag.
Ein PHP Monorepo lohnt sich, sobald ein Team mehrere eng verwandte Pakete pflegt, die typischerweise gemeinsam verändert werden. Ein internes SDK, das in ein Kernpaket, einen HTTP Client und Test Hilfsmittel aufgeteilt ist, ist ein klassisches Beispiel. Ändert sich eine Schnittstelle im Kernpaket, betrifft das sofort den HTTP Client, und beide Änderungen lassen sich in einem einzigen Commit und einem einzigen Pull Request review, statt über zwei separate Repositories mit zeitversetzten Releases synchronisiert werden zu müssen.
Nicht jedes Szenario profitiert von einem PHP Monorepo. Pakete mit völlig unterschiedlichem Release Rhythmus, externen Beitragenden, die nur an einem einzigen Paket arbeiten sollen, oder komplett getrennten Zielgruppen sind in separaten Repositories oft besser aufgehoben. Die Entscheidung zwischen Monorepo und Multi Repo ist keine reine Geschmacksfrage, sondern hängt direkt davon ab, wie stark die Pakete tatsächlich gemeinsam entwickelt werden.
2. Composer Path Repositories als Fundament
Composer unterstützt neben dem Standardtyp vcs und dem impliziten Packagist Repository auch den Typ path. Ein Path Repository verweist auf ein lokales Verzeichnis relativ zur composer.json und lässt Composer das Paket direkt von dort auflösen, ohne einen Download oder einen Netzwerkzugriff. Genau das ist der technische Kern, der ein PHP Monorepo überhaupt praktikabel macht: alle internen Pakete liegen bereits lokal vor, Composer muss sie nur noch verlinken.
Wichtig ist die Option symlink. Ist sie aktiviert, legt Composer im vendor Verzeichnis einen symbolischen Link auf das Quellverzeichnis an, statt die Dateien zu kopieren. Änderungen im Kernpaket sind dadurch sofort im abhängigen Paket sichtbar, ganz ohne erneutes composer install. Auf Betriebssystemen ohne Symlink Unterstützung, etwa manchen Windows Konfigurationen, fällt Composer automatisch auf Kopieren zurück, was funktional identisch ist, aber Änderungen erst nach einem erneuten Installationslauf zeigt.
{
"name": "mironsoft/http-client",
"type": "library",
"require": {
"php": "^8.4",
"mironsoft/core": "^2.0"
},
"repositories": [
{
"type": "path",
"url": "../core",
"options": {
"symlink": true
}
}
],
"autoload": {
"psr-4": {
"Mironsoft\\HttpClient\\": "src/"
}
}
}
Ein häufiger Stolperstein: Composer bevorzugt bei mehreren passenden Kandidaten das Path Repository gegenüber einer entfernten Quelle, prüft die deklarierte Versionsbedingung dabei aber nicht ignorant. Steht im Kernpaket keine passende Version, etwa weil der Branch noch keine Tags trägt, hilft die Angabe einer expliziten dev Version über die Option version im Repository Eintrag, damit Composer das lokale Paket trotzdem als gültig akzeptiert.
3. Verzeichnisstruktur eines PHP Monorepos
Die etablierte Struktur für ein PHP Monorepo orientiert sich an einem Verzeichnis packages im Wurzelverzeichnis, in dem jedes Paket einen eigenen Unterordner mit eigener composer.json, eigenem src Verzeichnis und eigenen Tests erhält. Das Wurzelverzeichnis selbst enthält eine eigene composer.json, die ausschließlich Entwicklungswerkzeuge wie PHPStan, PHP CS Fixer und PHPUnit als require dev bündelt, damit nicht jedes einzelne Paket dieselben Dev Abhängigkeiten dupliziert.
Namensräume folgen konsequent PSR 4 pro Paket, meist mit dem Paketnamen als zusätzlichem Namespace Segment, etwa Mironsoft\HttpClient für packages/http-client. Diese Trennung sorgt dafür, dass jedes Paket auch nach einem späteren Split in ein eigenes Repository ohne Anpassung am Code funktioniert, weil der Namespace nie vom Monorepo Kontext abhängig war.
#!/usr/bin/env bash
# scaffold-package.sh — create a new package skeleton inside the monorepo
set -euo pipefail
PACKAGE_NAME="$1"
PACKAGE_DIR="packages/${PACKAGE_NAME}"
mkdir -p "${PACKAGE_DIR}/src" "${PACKAGE_DIR}/tests"
cat > "${PACKAGE_DIR}/composer.json" <<JSON
{
"name": "mironsoft/${PACKAGE_NAME}",
"type": "library",
"require": { "php": "^8.4" },
"require-dev": { "phpunit/phpunit": "^11.0" },
"autoload": {
"psr-4": { "Mironsoft\\\\$(echo "$PACKAGE_NAME" | sed -r 's/(^|-)([a-z])/\U\2/g')\\\\": "src/" }
}
}
JSON
echo "[OK] Package skeleton created at ${PACKAGE_DIR}"
4. Versionsabhängigkeiten zwischen Paketen
Sobald ein Paket im PHP Monorepo ein anderes internes Paket voraussetzt, entsteht dieselbe Versionsbedingung wie bei jeder externen Abhängigkeit, etwa require mironsoft/core ^2.0. Weil das Path Repository die Bedingung intern gegen die lokale composer.json des Kernpakets prüft, muss die Versionsnummer im Kernpaket konsistent gepflegt werden, auch wenn während der Entwicklung ohnehin immer der aktuelle Arbeitsstand verwendet wird.
Ein typischer Fehler entsteht, wenn Teams die interne Versionsbedingung vergessen anzupassen, bevor Pakete einzeln veröffentlicht werden. Innerhalb des Monorepos funktioniert alles reibungslos, weil das Path Repository die Versionsbedingung recht großzügig behandelt, sobald ein branch alias vorhanden ist. Nach dem Split in getrennte Packagist Pakete schlägt composer update dann plötzlich fehl, weil die tatsächlich veröffentlichte Version des Kernpakets nicht zur deklarierten Bedingung im abhängigen Paket passt. Ein composer validate und ein composer outdated direkt in der CI Pipeline decken solche Inkonsistenzen zuverlässig auf, bevor sie den Release erreichen.
5. Tooling: Symplify MonorepoBuilder im Einsatz
Für die Orchestrierung eines PHP Monorepos hat sich das Paket symplify/monorepo-builder als De facto Standard etabliert. Es übernimmt drei Kernaufgaben: das Validieren, dass alle internen Versionsbedingungen zueinander passen, das synchronisierte Setzen einer neuen Versionsnummer über alle Pakete hinweg, und das Zusammenführen gemeinsamer composer.json Abschnitte wie require dev, damit nicht jedes Paket dieselben Tool Versionen separat pflegt.
Die Konfiguration erfolgt über eine monorepo-builder.php im Wurzelverzeichnis, in der die Pfade der einzelnen Pakete registriert werden. Der Befehl validate läuft typischerweise als erster Schritt in jeder CI Pipeline eines PHP Monorepos und bricht sofort ab, wenn ein Paket eine veraltete oder inkonsistente interne Abhängigkeit deklariert, lange bevor ein Entwickler das Problem manuell debuggen müsste.
# Validate that all internal composer.json dependencies are consistent
vendor/bin/monorepo-builder validate
# Merge shared require-dev and autoload-dev sections into every package
vendor/bin/monorepo-builder merge
# Bump the version constraint across all packages in one atomic step
vendor/bin/monorepo-builder release 3.1.0 --dry-run
vendor/bin/monorepo-builder release 3.1.0
6. CI Strategie für mehrere Pakete in einem Repository
Ein naiver CI Aufbau für ein PHP Monorepo testet bei jedem Commit alle Pakete vollständig, unabhängig davon, welches Paket sich tatsächlich geändert hat. Bei fünf oder mehr Paketen wird das schnell zum Laufzeitproblem, gerade wenn jedes Paket eine eigene Matrix aus PHP Versionen durchläuft. Die robustere Strategie ermittelt über git diff, welche Verzeichnisse sich seit dem letzten gemeinsamen Commit geändert haben, und startet gezielt nur die betroffenen Job Definitionen.
Für Pull Requests, die mehrere Pakete gleichzeitig betreffen, etwa weil eine Schnittstellenänderung im Kernpaket alle abhängigen Pakete berührt, sollte die Pipeline dennoch konservativ alle abhängigen Pakete mittesten, nicht nur das direkt geänderte. Ein einfacher Abhängigkeitsgraph, gepflegt in derselben monorepo-builder.php Konfiguration, reicht meist aus, um diese Rückwärtsabhängigkeiten automatisch aufzulösen.
# .gitlab-ci.yml — matrix job per package, only for changed directories
stages: [validate, test]
validate:
stage: validate
script:
- composer install --no-progress
- vendor/bin/monorepo-builder validate
test-core:
stage: test
script:
- composer install --working-dir=packages/core
- vendor/bin/phpunit -c packages/core
rules:
- changes: [packages/core/**/*, packages/http-client/**/*]
test-http-client:
stage: test
script:
- composer install --working-dir=packages/http-client
- vendor/bin/phpunit -c packages/http-client
rules:
- changes: [packages/http-client/**/*]
7. Split in eigenständige Packagist Repositories
Auch mit einem PHP Monorepo als internem Entwicklungsmodell erwarten externe Nutzer weiterhin einzelne, fokussierte Composer Pakete auf Packagist, jedes mit eigenem Repository, eigener Release Historie und eigenem Issue Tracker. Diese Anforderung löst der sogenannte Split: ein automatisierter Schritt extrahiert die Commit Historie eines einzelnen packages Unterverzeichnisses in ein eigenständiges, meist read only gepflegtes Ziel Repository.
Symplify MonorepoBuilder liefert dafür eine GitHub Action namens monorepo-split-github-action, die bei jedem Push auf den Hauptbranch automatisch die konfigurierten Unterverzeichnisse in ihre jeweiligen Ziel Repositories pusht, inklusive vollständiger Git Historie für das jeweilige Paket. Entwickler arbeiten ausschließlich im Monorepo, das Split Repository ist reine Distribution und wird nie direkt bearbeitet, um Divergenzen zu vermeiden.
# .github/workflows/split.yml — push each package subdirectory to its own repo
name: Monorepo Split
on:
push:
branches: [main]
jobs:
split:
runs-on: ubuntu-latest
strategy:
matrix:
package:
- local_path: 'packages/core'
split_repository: 'mironsoft/core'
- local_path: 'packages/http-client'
split_repository: 'mironsoft/http-client'
steps:
- uses: actions/checkout@v4
- uses: symplify/monorepo-split-github-action@v2.3
with:
package_directory: ${{ matrix.package.local_path }}
repository_organization: mironsoft
repository_name: ${{ matrix.package.split_repository }}
user_name: mironsoft-bot
user_email: bot@mironsoft.de
8. Team Workflow: atomare Änderungen über Paketgrenzen hinweg
Der größte praktische Vorteil eines PHP Monorepos zeigt sich im Alltag eines Feature Branches, der mehrere Pakete gleichzeitig betrifft. Statt zwei separate Pull Requests in zwei Repositories zu koordinieren und dabei auf die richtige Merge Reihenfolge zu achten, landet die komplette Änderung, Kernpaket und HTTP Client gemeinsam, in einem einzigen Pull Request mit einem einzigen Reviewer Kontext.
Dieser Vorteil verlangt im Gegenzug eine strikte CI Disziplin. Ein fehlerhaftes Paket darf niemals den Merge eines völlig unabhängigen Pakets im selben Monorepo blockieren, sonst kippt der Entwicklungsfluss ins Gegenteil. Die Lösung sind unabhängige Pipeline Stufen pro Paket, kombiniert mit einer klaren Regel, dass ein Branch Schutz nur die tatsächlich betroffenen Job Definitionen als Pflicht Checks voraussetzt, nicht die gesamte Matrix aller Pakete im Repository.
9. Monorepo vs. Multi Repo im direkten Vergleich
Die Wahl zwischen einem PHP Monorepo und getrennten Repositories hängt von der tatsächlichen Kopplung der Pakete ab, nicht von einer generellen Best Practice. Die folgende Tabelle stellt beide Modelle entlang der Kriterien gegenüber, die in der Praxis am häufigsten den Ausschlag geben.
| Kriterium | Multi Repo | PHP Monorepo | Praxisrelevanz |
|---|---|---|---|
| Atomare Änderungen | Über mehrere PRs koordiniert | Ein einziger Commit, ein PR | Hoch bei eng gekoppelten Paketen |
| Externe Sichtbarkeit | Fokussiertes Einzelrepo | Erfordert Split für Nutzer | Wichtig bei externen Beitragenden |
| CI Laufzeit | Klein und isoliert pro Repo | Erfordert selektive Job Auswahl | Relevant ab fünf Paketen aufwärts |
| Onboarding neuer Teammitglieder | Mehrere Repos einzeln klonen | Ein Klon, alles verfügbar | Spart Setup Zeit im Alltag |
| Tooling Reife | Nativer Composer Workflow | Zusätzliches Tool wie MonorepoBuilder nötig | Zusätzliche Lernkurve fürs Team |
In der Praxis entscheidet sich die Wahl meist an einer einzigen Frage: werden die Pakete überwiegend gemeinsam verändert, oder überwiegend unabhängig voneinander. Bei überwiegend gemeinsamer Entwicklung zahlt sich ein PHP Monorepo trotz der zusätzlichen Tooling Komplexität fast immer aus, weil der eingesparte Koordinationsaufwand die Mehrkosten in CI und Split Pipeline deutlich übersteigt.
Mironsoft
PHP Architektur, Paketstrategie und Composer Tooling
Mehrere PHP Pakete in einem sauber strukturierten Monorepo?
Wir analysieren eure bestehende Paketlandschaft, planen die Migration in ein PHP Monorepo mit Path Repositories und richten CI Pipeline sowie Split Automatisierung für Packagist ein.
Architektur Review
Bewertung, ob ein Monorepo oder Multi Repo Modell zur Kopplung eurer Pakete passt
Migration
Path Repositories, MonorepoBuilder Konfiguration und Namespace Umzug ohne Downtime
CI und Split
Selektive Pipelines und automatisierter Split in einzelne Packagist Repositories
10. Zusammenfassung
Ein PHP Monorepo mit mehreren Paketen löst das Grundproblem eng gekoppelter Composer Pakete: statt bei jeder Änderung eine neue Version zu taggen und in abhängigen Paketen manuell nachzuziehen, verlinkt Composer über Path Repositories alle internen Pakete direkt aus dem Arbeitsverzeichnis. Eine klare Verzeichnisstruktur unter packages, konsistente PSR 4 Namensräume pro Paket und ein Tool wie Symplify MonorepoBuilder für Versionsvalidierung und Release Synchronisation bilden das technische Fundament.
Für die CI Pipeline gilt: selektiv testen, was sich geändert hat, aber konservativ alle abhängigen Pakete mittesten, sobald eine gemeinsame Schnittstelle betroffen ist. Der Split in eigenständige Packagist Repositories über automatisierte GitHub Actions stellt sicher, dass externe Nutzer weiterhin fokussierte, einzeln installierbare Pakete vorfinden, während das Team intern in einem einzigen, atomar versionierten Monorepo arbeitet.
PHP Monorepo mit mehreren Paketen — Das Wichtigste auf einen Blick
Path Repositories
Composer verlinkt interne Pakete direkt aus dem Arbeitsverzeichnis, mit symlink Option ohne erneutes Installieren nach jeder Änderung.
Verzeichnisstruktur
Ein packages Verzeichnis mit einem Unterordner pro Paket, jeweils eigene composer.json, eigener PSR 4 Namespace, eigene Tests.
Tooling
Symplify MonorepoBuilder validiert Versionsbedingungen, synchronisiert Releases und führt gemeinsame require dev Abschnitte zusammen.
CI und Split
Selektive Tests nach geänderten Verzeichnissen, automatisierter Split in eigenständige Packagist Repositories per GitHub Action.