Composer Patches in Laravel: Vendor-Code fixen ohne Fork

7 Min. Lesezeit Read in English

Kurz gesagt

Ein Bugfix in vendor/ direkt zu editieren bringt nichts, weil der Ordner bei jedem composer install neu aufgebaut wird und Änderungen dort nur lokal existieren. Mit dem Plugin cweagans/composer-patches legst du deinen Fix stattdessen als versionierte .patch-Datei im Repository ab, die automatisch bei jedem Composer-Lauf wieder eingespielt wird. Für größere Änderungen ist ein Patch der falsche Weg – dann gehört der Fix als Pull Request stromaufwärts zum Paket.

Ein Fremdpaket hat einen Bug. Der Fix ist eine einzige Zeile in vendor/. Du änderst sie, alles läuft, du bist zufrieden. Beim nächsten composer update ist die Zeile wieder weg.

Genau hier stehen die meisten Laravel-Teams irgendwann. Zwischen “schnell in vendor/ reinhacken” und “das ganze Paket forken” liegt ein sauberer Mittelweg: ein Patch, der bei jedem Composer-Lauf automatisch wieder angewendet wird.

Warum vendor/ kein Ort für Fixes ist

vendor/ gehört Composer, nicht dir. Der Ordner steht in .gitignore, wird nicht committet und bei jedem composer install neu aus den Paketquellen aufgebaut. Eine Änderung dort lebt nur auf deinem Rechner.

Das fällt spätestens im Team auf. Deine Kollegin zieht den Branch, ihr vendor/ kennt deinen Fix nicht. Die CI baut frisch, der Fix fehlt, der Build ist grün, das Feature trotzdem kaputt. Ein Bugfix, der nicht in Git steht, ist kein Bugfix. Er ist eine Zeitbombe.

Composer Patches: der saubere Mittelweg

Das Plugin cweagans/composer-patches löst das Problem an der richtigen Stelle. Du legst deinen Fix als .patch-Datei ins Repository. Das Plugin spielt diesen Patch nach jedem composer install und composer update automatisch wieder ein, direkt nachdem das Paket entpackt wurde.

Installation:

composer require cweagans/composer-patches

Die Zuordnung von Paket zu Patch steht in der composer.json. Die Patch-Dateien selbst liegen üblicherweise in einem patches/-Ordner:

{
    "extra": {
        "patches": {
            "acme/pdf-generator": {
                "Null-Check beim Rendern leerer Seiten": "patches/pdf-generator-empty-page.patch"
            }
        }
    }
}

Beides – die composer.json-Zuordnung und die .patch-Datei – wird committet. Jeder im Team und jede CI-Umgebung bekommt den Fix damit automatisch.

Neu in 2.0: patches.json und ein Lockfile

Im Oktober 2025 ist cweagans/composer-patches 2.0 erschienen. Zwei Änderungen sind für Teams relevant.

Erstens eine eigene patches.json. Statt die Patch-Definitionen in der composer.json zu führen, kannst du sie in eine separate Datei auslegen. Das hält die composer.lock ruhig: Neue Patches lösen keine Änderung am Lockfile mehr aus.

Zweitens ein patches.lock.json. Diese Datei ist das Gegenstück zu Composers eigenem Lockfile. Sie hält für jeden angewendeten Patch eine SHA-256-Prüfsumme fest. Damit installiert jede Umgebung nachweislich denselben Patch-Stand. Ein still veränderter oder verrutschter Patch fällt auf, statt unbemerkt durchzurutschen.

Unter der Haube wendet 2.0 Patches jetzt über Git an, nicht mehr über das System-patch. Das Verhalten ist damit über Betriebssysteme hinweg gleich. Deine bestehende extra.patches-Konfiguration aus der 1.x-Zeit läuft weiter – der Umstieg ist rückwärtskompatibel.

Einen Patch erstellen

Ein Patch ist ein Unified Diff. git diff hilft hier allerdings nicht: vendor/ steht in der .gitignore, Git kennt die Dateien dort also gar nicht. Du vergleichst deshalb mit einer Kopie des Originals, und zwar direkt im Paketordner, damit die Pfade im Patch zum Paket passen:

cd vendor/acme/pdf-generator
cp src/Renderer.php src/Renderer.php.orig
# jetzt den Fix in src/Renderer.php einbauen
diff -u --label a/src/Renderer.php --label b/src/Renderer.php \
  src/Renderer.php.orig src/Renderer.php > ../../../patches/pdf-generator-empty-page.patch
rm src/Renderer.php.orig

Danach machst du deine Änderung in vendor/ rückgängig und lässt composer install laufen. Der Patch wird sauber wieder eingespielt. Bei 2.0 trägst du einen neuen Patch zusätzlich mit composer patches-relock in die patches.lock.json ein und spielst ihn mit composer patches-repatch ein; das löscht die gepatchten Pakete in vendor/ und installiert sie neu.

Bei mehreren Änderungen wird das Diffen von Hand mühsam. symplify/vendor-patches nimmt dir das ab: Du legst von jeder Datei, die du änderst, eine Kopie mit der Endung .old an, bearbeitest dann die Vendor-Datei, und ein Befehl scannt vendor/, schreibt die Diffs raus und trägt die Zuordnung in die composer.json ein.

composer require symplify/vendor-patches --dev
vendor/bin/vendor-patches generate

Die üblichen Stolperfallen

Patches sind versionsgebunden. Sie hängen an konkreten Zeilennummern und Umgebungen. Nach einem Paket-Update kann derselbe Patch schon nicht mehr passen.

Problem Ursache Fix
Patch läuft nach Update nicht mehr Zeilen im Paket haben sich verschoben Bei 1.x "composer-exit-on-patch-failure": true setzen, damit der Composer-Lauf laut abbricht statt still weiterzumachen. Bei 2.x bricht Composer ab, wenn kein Patcher den Patch anwenden kann
Patch findet die Datei nicht Falscher -p/Path-Strip-Level; die a/- und b/-Präfixe im Diff passen nicht zum Pfad Diff prüfen: relativ zum Paket-Root erzeugen, nicht zum Projekt-Root
Patch wird riesig Du baust eigentlich ein Feature ein, kein Fix Nicht patchen – siehe unten

Bei 1.x ist der wichtigste Schalter composer-exit-on-patch-failure. Ohne ihn schluckt Composer einen fehlgeschlagenen Patch und baut weiter. Dann hast du einen grünen Build ohne den Fix. Genau den Zustand, den du vermeiden wolltest. In 2.x gibt es diese Option nicht mehr: Dort bricht der Lauf ab, wenn ein Patch nicht angewendet werden kann.

Wann du nicht patchen solltest

Ein Patch ist für kleine Deltas gedacht: ein Null-Check, ein falscher Default, eine vergessene Escape-Funktion. Sobald du mehrere Methoden umschreibst oder Verhalten hinzufügst, ist der Patch das falsche Werkzeug.

Dann gehört der Fix stromaufwärts. Öffne einen Pull Request beim Paket. Bis der gemerged ist, überbrückt ein Patch die Wartezeit. Danach fliegt er wieder raus. Ein Patch ist eine Brücke, kein Zuhause.

Was du für dein Laravel-Projekt mitnimmst

  • halte Fixes an Fremdpaketen in Git, nicht in einem lokalen vendor/;
  • ein Patch bleibt ein kleines, klar umrissenes Delta;
  • nutze in neuen Projekten patches.json plus Lockfile für eine ruhige composer.lock;
  • sorge dafür, dass veraltete Patches laut scheitern (1.x: composer-exit-on-patch-failure: true, 2.x: ist Standard);
  • geh stromaufwärts und mach einen PR, sobald der Patch wächst.

Wenn dein Laravel-Projekt an solchen Stellen sauber bleiben soll, bauen wir Laravel-Projekte und individuelle Software, bei denen Paketpflege kein Zufall ist.

Häufige Fragen

Warum sollte man vendor/ nicht direkt bearbeiten?

Weil der Ordner Composer gehört, nicht dir. Er steht in der .gitignore, wird nicht committet und bei jedem composer install neu aus den Paketquellen aufgebaut. Eine Änderung dort lebt nur auf deinem Rechner. Im Team zieht die Kollegin den Branch ohne deinen Fix, die CI baut frisch ohne ihn – der Build ist grün, das Feature trotzdem kaputt. Ein Bugfix, der nicht in Git steht, ist keiner.

Wie patcht man ein Composer-Paket in Laravel sauber?

Mit dem Plugin cweagans/composer-patches. Du legst deinen Fix als .patch-Datei ins Repository (üblicherweise in einem patches/-Ordner) und trägst die Zuordnung von Paket zu Patch in der composer.json ein. Das Plugin spielt den Patch nach jedem composer install und composer update automatisch wieder ein, direkt nachdem das Paket entpackt wurde. Die .patch-Datei erzeugst du als Unified Diff, also mit git diff.

Was ist neu in cweagans/composer-patches 2.0?

Drei Dinge: eine eigene patches.json, sodass neue Patches keine Änderung an der composer.lock mehr auslösen; ein patches.lock.json, das je Patch eine SHA-256-Prüfsumme festhält, damit jede Umgebung nachweislich denselben Patch-Stand installiert; und die Anwendung über Git statt über das System-patch, wodurch das Verhalten über Betriebssysteme hinweg gleich ist. Bestehende extra.patches-Konfiguration läuft rückwärtskompatibel weiter.

Wann sollte man ein Paket nicht patchen?

Ein Patch ist für kleine Deltas gedacht – ein Null-Check, ein falscher Default, eine vergessene Escape-Funktion. Sobald du mehrere Methoden umschreibst oder Verhalten hinzufügst, ist der Patch das falsche Werkzeug. Dann gehört der Fix stromaufwärts als Pull Request beim Paket; ein Patch überbrückt nur die Wartezeit, bis der PR gemerged ist. Ein Patch ist eine Brücke, kein Zuhause.

Tobias Bobreck
Tobias Bobreck

Sr. Full-Stack Developer

Frontend & Backend. Entwickelt skalierbare Webanwendungen mit Fokus auf saubere Architektur und Wartbarkeit.