Monorepo mit mehreren PHP Paketen verwalten: Composer Path Repositories
AI generated
<?php
8.4
PHP · Composer · Paket Ökosystem
Monorepo mit mehreren PHP Paketen verwalten
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.

18 Min. Lesezeit Composer · Path Repository · MonorepoBuilder PHP 8.4 · Composer 2.x

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.

11. FAQ: PHP Monorepo mit mehreren Paketen

1Was ist ein PHP Monorepo genau?
Ein einzelnes Git Repository mit mehreren unabhängigen Composer Paketen, jedes mit eigener composer.json und eigenem Namespace, gemeinsam entwickelt in einer Historie.
2Wie verlinkt Composer Pakete im Monorepo?
Über den Repository Typ path mit Option symlink true. Composer erstellt einen symbolischen Link im vendor Verzeichnis statt zu kopieren.
3Braucht jedes Paket eine eigene composer.json?
Ja, jedes Paket hat eine eigene composer.json mit eigenem Namen, PSR 4 Zuordnung und Abhängigkeiten, unabhängig von der Root composer.json.
4Was macht Symplify MonorepoBuilder?
Validiert interne Versionsbedingungen, setzt Versionen synchron über alle Pakete und führt gemeinsame composer.json Abschnitte zusammen.
5Wie teste ich nur geänderte Pakete in der CI?
Mit einer changes Regel in GitLab CI oder paths Filter in GitHub Actions, die Jobs nur bei Änderungen im jeweiligen packages Unterverzeichnis startet.
6Wie kommen Pakete einzeln auf Packagist?
Über einen Split Schritt mit der monorepo-split-github-action, die die Historie jedes Unterverzeichnisses in ein eigenes Ziel Repository pusht.
7Muss das Split Repository bearbeitet werden?
Nein, es ist reine Distribution und sollte niemals direkt bearbeitet werden, um Divergenzen zum Monorepo zu vermeiden.
8Was passiert bei einem fehlerhaften Paket?
Mit unabhängigen Pipeline Stufen pro Paket blockiert ein Fehler nicht automatisch andere Pakete, solange der Branch Schutz nur betroffene Checks verlangt.
9Ab wie vielen Paketen lohnt sich ein Monorepo?
Ab drei bis vier eng gekoppelten Paketen mit häufigen gemeinsamen Änderungen überwiegt der Koordinationsvorteil den zusätzlichen Tooling Aufwand meist deutlich.
10Geht ein Monorepo auch ohne MonorepoBuilder?
Ja, mit reinen Path Repositories und manuellen Tags, aber der manuelle Aufwand für Versionsvalidierung und Split steigt deutlich mit der Paketanzahl.