PSR-12 im Team durchsetzen: Tools statt Diskussionen im Review
AI generated
<?php
8.4
PHP · Code Style · CI/CD · Tooling
PSR-12 im Team durchsetzen
Tools statt Diskussionen im Review

Wer PSR-12 durchsetzen will, ohne jeden Pull Request mit Kommentaren zu Einrückung und Klammernposition zu blockieren, braucht Automatisierung statt guten Willens. PHP_CodeSniffer und PHP-CS-Fixer, kombiniert mit Pre-Commit-Hooks und CI-Pipeline, beenden Formatierungsdiskussionen im Review dauerhaft, auch in gewachsenen Legacy-Projekten.

15 Min. Lesezeit PHP_CodeSniffer · PHP-CS-Fixer · Pre-Commit · CI PHP 8.4 · Composer · Git Hooks

1. Warum Code-Style-Diskussionen im Review Zeit kosten

In vielen Teams verschlingt ein erheblicher Teil der Review-Kommentare nicht die Logik einer Änderung, sondern ihre Formatierung: fehlende Leerzeile vor einem return, falsche Einrückung nach einem Merge, eine öffnende Klammer in der falschen Zeile. Diese Kommentare sind fachlich meist unstrittig, sie kosten aber Zeit, erzeugen Reibung zwischen Autor und Reviewer, und verzögern den eigentlichen inhaltlichen Review um mehrere Iterationsrunden. Wer PSR-12 durchsetzen will, sollte diese Kategorie von Kommentaren komplett aus dem menschlichen Review entfernen, nicht durch mehr Disziplin, sondern durch Werkzeuge, die den Zustand objektiv und automatisch prüfen.

PSR-12 selbst ist eine Erweiterung des älteren PSR-2-Standards der PHP-FIG und deckt Details wie Einrückung mit vier Leerzeichen, Zeilenlänge, Positionierung von geschweiften Klammern, Leerzeilen zwischen Methoden und die korrekte Reihenfolge von Sichtbarkeitsmodifikatoren ab. Der Standard selbst ist unstrittig und breit akzeptiert, das eigentliche Problem liegt fast nie im Standard, sondern in seiner konsequenten, werkzeuggestützten Durchsetzung über die gesamte Codebasis und alle Teammitglieder hinweg.

Die folgenden Abschnitte zeigen, wie ein Team PSR-12 durchsetzen kann, von der reinen Erkennung über automatisches Auto-Fixing bis zur Absicherung in Pre-Commit-Hooks, CI-Pipeline und Editor, und wie sich das auch auf einer historisch gewachsenen Codebasis einführen lässt, ohne das Team mit einem einzigen riesigen Reformat-Commit zu überfordern.

2. PHP_CodeSniffer: PSR-12 als Ruleset durchsetzen

PHP_CodeSniffer, meist über den Composer-Befehl composer require --dev squizlabs/php_codesniffer installiert, prüft Code gegen ein deklaratives Ruleset und meldet jede Abweichung mit Datei, Zeile und einer eindeutigen Sniff-Kennung. Der eingebaute Standard PSR12 deckt bereits den vollständigen offiziellen Standard ab, ein eigenes phpcs.xml im Projekt-Root erweitert oder verfeinert diesen Standard um projektspezifische Ausnahmen, etwa für generierte Dateien oder Legacy-Verzeichnisse.


<?xml version="1.0"?>
<ruleset name="ProjectPSR12">
    <description>PSR-12 with project-specific exclusions</description>

    <rule ref="PSR12"/>

    <file>src</file>
    <file>tests</file>

    <exclude-pattern>*/var/*</exclude-pattern>
    <exclude-pattern>*/vendor/*</exclude-pattern>
    <exclude-pattern>src/Legacy/*</exclude-pattern>

    <arg name="colors"/>
    <arg value="p"/>
    <arg name="extensions" value="php"/>
</ruleset>

Der Aufruf vendor/bin/phpcs analysiert den konfigurierten Scope und listet jeden Verstoß mit exaktem Fundort auf, praktisch für einen ersten Überblick über den Umfang eines Rollouts. Wer PSR-12 durchsetzen will, ohne jede Regel manuell zu konfigurieren, profitiert davon, dass PSR12 als Standard bereits sämtliche relevanten Sniffs bündelt, einzelne Sniffs lassen sich bei Bedarf über <exclude>-Elemente gezielt deaktivieren, etwa wenn eine Regel mit einer projektspezifischen Konvention kollidiert.

Ein wichtiger Unterschied zu vielen anderen Linting-Tools: PHP_CodeSniffer meldet Verstöße primär, es korrigiert sie standardmäßig nicht. Für automatisches Fixing gibt es das mitgelieferte Schwesterwerkzeug phpcbf (PHP Code Beautifier and Fixer), das denselben Ruleset-Mechanismus nutzt, aber Dateien direkt überschreibt, sofern der jeweilige Sniff eine automatische Korrektur unterstützt. Nicht jeder Sniff ist automatisch fixbar, komplexere strukturelle Verstöße bleiben manuelle Aufgabe.

3. PHP-CS-Fixer: Automatisches Umformatieren statt nur Melden

PHP-CS-Fixer verfolgt von Anfang an einen anderen Ansatz als PHP_CodeSniffer: Es ist primär ein Auto-Fixer, der Code direkt umschreibt, das reine Melden ohne Korrektur ist eher die Ausnahme. Die Konfiguration erfolgt über eine PHP-Datei .php-cs-fixer.php im Projekt-Root, die ein Config-Objekt mit einem Set aktivierter Regeln zurückgibt, entweder als benanntes Regel-Set wie @PSR12 oder als feingranulare Liste einzelner Regeln.


<?php

declare(strict_types=1);

$finder = (new PhpCsFixer\Finder())
    ->in(__DIR__ . '/src')
    ->in(__DIR__ . '/tests')
    ->exclude('Legacy');

return (new PhpCsFixer\Config())
    ->setRules([
        '@PSR12' => true,
        'array_syntax' => ['syntax' => 'short'],
        'declare_strict_types' => true,
        'no_unused_imports' => true,
        'ordered_imports' => ['sort_algorithm' => 'alpha'],
        'single_quote' => true,
        'trailing_comma_in_multiline' => true,
    ])
    ->setFinder($finder)
    ->setRiskyAllowed(true);

Der Aufruf vendor/bin/php-cs-fixer fix überschreibt betroffene Dateien direkt, vendor/bin/php-cs-fixer fix --dry-run --diff zeigt stattdessen nur einen Vorschau-Diff, ohne etwas zu verändern, praktisch für CI-Checks, die einen Build fehlschlagen lassen sollen, ohne Dateien zu modifizieren. Das Flag --risky-allowed aktiviert Regeln, die das Verhalten des Codes theoretisch verändern könnten, etwa den Wechsel von array() zu [], in der Praxis für PSR-12-Konformität aber meist unbedenklich.

Der Unterschied zwischen phpcbf und php-cs-fixer fix liegt vor allem im Umfang des automatisch Korrigierbaren: PHP-CS-Fixer deckt deutlich mehr Regeln mit automatischer Korrektur ab, weil Auto-Fixing von Anfang an das Kernkonzept des Tools ist, während es bei PHP_CodeSniffer eine nachträgliche Ergänzung ist. Viele Teams kombinieren deshalb beide Tools: PHP_CodeSniffer für die Erkennung mit einem breiteren Sniff-Katalog inklusive nicht automatisch fixbarer Architektur-Regeln, PHP-CS-Fixer für das automatische Umformatieren des Großteils der reinen Stilverstöße.

4. Pre-Commit-Hooks: Verstöße vor dem Commit blockieren

Ein Pre-Commit-Hook verhindert, dass nicht konformer Code überhaupt erst ins Repository gelangt, statt ihn erst im Pull Request oder in der CI-Pipeline zu entdecken. Der entscheidende Vorteil gegenüber reiner CI-Prüfung: Der Entwickler bekommt sofortiges Feedback, lokal, bevor ein Commit überhaupt existiert, und die Korrektur passiert im selben Arbeitsschritt statt in einem separaten Fix-up-Commit später.


#!/usr/bin/env bash
# .git/hooks/pre-commit (or managed via a tool like Husky/Captain Hook)
set -euo pipefail

# Only check staged PHP files, not the entire codebase
staged_files=$(git diff --cached --name-only --diff-filter=ACM -- '*.php')

if [ -z "$staged_files" ]; then
    exit 0
fi

echo "Running PHP-CS-Fixer on staged files..."
vendor/bin/php-cs-fixer fix --dry-run --diff $staged_files

if [ $? -ne 0 ]; then
    echo "PSR-12 violations found. Run 'vendor/bin/php-cs-fixer fix' and re-stage."
    exit 1
fi

echo "Running PHP_CodeSniffer on staged files..."
vendor/bin/phpcs $staged_files

Der Hook beschränkt die Prüfung bewusst auf gestagte Dateien, statt jedes Mal die gesamte Codebasis zu scannen, das hält die Laufzeit im Sekundenbereich und macht den Hook praktikabel für den täglichen Gebrauch. Wer PSR-12 durchsetzen will, ohne Entwickler mit langen Wartezeiten bei jedem Commit zu frustrieren, sollte diesen Scope bewusst eng halten und die vollständige Codebasis stattdessen in CI prüfen.

Für Teams mit mehreren Sprachen oder komplexeren Hook-Anforderungen bieten sich Tools wie Captain Hook oder Grumphp an, beide PHP-native Alternativen zu generischen Lösungen wie Husky, die Composer-Skripte direkt als Git-Hooks registrieren und über eine deklarative Konfigurationsdatei verwaltet werden, statt rohe Shell-Skripte manuell zu pflegen.

5. CI-Integration: Der Build als letzte Instanz

Pre-Commit-Hooks lassen sich mit git commit --no-verify umgehen, absichtlich oder aus Versehen, weshalb die CI-Pipeline die eigentliche, nicht umgehbare Durchsetzungsinstanz bleibt. Ein CI-Job, der bei jedem Push und jedem Pull Request läuft, stellt sicher, dass kein nicht konformer Code jemals den Hauptbranch erreicht, unabhängig davon, ob der lokale Hook aktiv war oder nicht.


# .github/workflows/code-style.yml
name: Code Style

on: [push, pull_request]

jobs:
    phpcs:
        runs-on: ubuntu-latest
        steps:
            - uses: actions/checkout@v4
            - uses: shivammathur/setup-php@v2
              with:
                  php-version: '8.4'
            - run: composer install --prefer-dist --no-progress
            - name: Check PSR-12 compliance
              run: vendor/bin/phpcs --report=checkstyle
            - name: Check PHP-CS-Fixer rules
              run: vendor/bin/php-cs-fixer fix --dry-run --diff

Das Flag --report=checkstyle gibt Ergebnisse im Checkstyle-XML-Format aus, das viele CI-Systeme und Code-Review-Tools direkt als Inline-Kommentare im Diff darstellen können, statt nur eine rohe Konsolenausgabe zu liefern. Wer PSR-12 durchsetzen als harte CI-Gate umsetzt, sollte den Job unbedingt als Required Check im Pull-Request-Workflow markieren, damit ein Merge ohne bestandenen Style-Check technisch gar nicht möglich ist, statt nur eine unverbindliche Warnung anzuzeigen.

Für schnellere Feedback-Zyklen lohnt sich ein separater, schneller Job ausschließlich für geänderte Dateien im Pull Request, kombiniert mit einem vollständigen, aber selteneren Scan der gesamten Codebasis, etwa nächtlich, um schleichende Abweichungen durch fehlerhafte Konfigurationsänderungen frühzeitig zu erkennen.

6. Editor-Integration: Format-on-Save in PhpStorm und VS Code

Die wirksamste Automatisierung ist die, die ein Entwickler nie bewusst wahrnimmt: Format-on-Save formatiert Code automatisch beim Speichern, sodass ein Entwickler PSR-12-Konformität praktisch nie manuell herstellen muss. PhpStorm unterstützt PHP-CS-Fixer nativ über eine Einstellung unter Settings → PHP → Quality Tools → PHP-CS-Fixer, dort wird der Pfad zur ausführbaren Datei und zur .php-cs-fixer.php-Konfiguration hinterlegt, danach lässt sich "Run on save" für den aktuellen Dateibereich aktivieren.

In VS Code übernimmt die Erweiterung junstyle.php-cs-fixer dieselbe Aufgabe, konfiguriert über editor.formatOnSave: true in Kombination mit "[php]": { "editor.defaultFormatter": "junstyle.php-cs-fixer" } in den Workspace-Settings. Wichtig für Teamkonsistenz: Diese Editor-Einstellungen sollten projektbezogen in .vscode/settings.json versioniert werden, statt sich auf individuelle, lokale Konfiguration jedes Entwicklers zu verlassen, sonst formatiert jeder Editor nach eigenen, potenziell abweichenden Regeln.

Format-on-Save reduziert die Zahl der Verstöße, die überhaupt erst den Pre-Commit-Hook erreichen, drastisch, weil der Code bereits beim Speichern korrekt formatiert wird. Wer PSR-12 durchsetzen auf allen drei Ebenen, Editor, Pre-Commit-Hook und CI, konsequent umsetzt, erlebt in der Praxis kaum noch einen tatsächlichen CI-Fehlschlag wegen Formatierung, weil die früheren Stufen die meisten Fälle bereits vorher abfangen.

7. Rollout auf Legacy-Code ohne Big-Bang-Reformat

Ein bestehendes, mehrere Jahre gewachsenes Projekt direkt mit einem einzigen riesigen Reformat-Commit auf PSR-12 zu bringen, klingt verlockend, erzeugt aber einen Diff mit tausenden geänderten Zeilen, der git blame für praktisch die gesamte Historie unbrauchbar macht und Merge-Konflikte in jedem parallel laufenden Feature-Branch provoziert. Eine schrittweise Migration ist in fast allen Fällen die bessere Strategie.

Git bietet dafür seit Version 2.23 eine gezielte Lösung: .git-blame-ignore-revs listet Commit-Hashes auf, die git blame standardmäßig überspringt, konfiguriert über git config blame.ignoreRevsFile .git-blame-ignore-revs. Ein einmaliger, vollständiger Reformat-Commit landet in dieser Datei, git blame zeigt danach für jede betroffene Zeile weiterhin den ursprünglichen, fachlichen Autor an, statt pauschal den Reformat-Commit.


# One-time full reformat, then register the commit to be ignored by blame
vendor/bin/php-cs-fixer fix
git add -A
git commit -m "style: apply PSR-12 formatting across the codebase"

# Add the resulting commit hash to .git-blame-ignore-revs
echo "$(git rev-parse HEAD)  # PSR-12 mass reformat" >> .git-blame-ignore-revs
git add .git-blame-ignore-revs
git commit -m "chore: ignore PSR-12 reformat commit in git blame"

# Each contributor configures git locally (or via .gitconfig checked into
# the repo root and referenced via includeIf)
git config blame.ignoreRevsFile .git-blame-ignore-revs

Alternativ, für Projekte, bei denen selbst ein einmaliger Reformat-Commit zu riskant erscheint, etwa wegen laufender Release-Branches, bietet sich ein Baseline-Ansatz analog zu PHPStan an: Ein initialer phpcs-Lauf über die gesamte Codebasis wird als Referenzzustand gespeichert, die CI-Prüfung schlägt danach nur fehl, wenn neue oder geänderte Dateien neue Verstöße einführen, unverändert bleibender Legacy-Code wird vorerst toleriert und schrittweise bei ohnehin anstehenden Änderungen mitkorrigiert.

In der Praxis bewährt sich häufig eine Kombination: Neue Module und aktiv weiterentwickelte Verzeichnisse werden sofort vollständig auf PSR-12 durchsetzen, klar abgegrenzte, kaum noch angefasste Legacy-Bereiche bleiben über exclude-pattern zunächst ausgenommen und werden bei Gelegenheit, etwa im Rahmen eines ohnehin geplanten Refactorings, nachgezogen.

8. Was danach im Review noch übrig bleibt

Nach vollständiger Automatisierung verschwinden Kommentare zu Einrückung, Klammernposition und Leerzeilen komplett aus dem Review, das gewonnene Zeitbudget verschiebt sich vollständig auf fachliche Fragen: Ist die gewählte Architektur sinnvoll, sind Edge Cases abgedeckt, ist der Name einer Methode selbsterklärend. Diese Verschiebung ist der eigentliche Wert der Automatisierung, nicht die Formatierung selbst, sondern die dadurch frei werdende Aufmerksamkeit für Dinge, die kein Tool automatisch prüfen kann.

Ein sekundärer, oft unterschätzter Effekt: Neue Teammitglieder müssen den unternehmensinternen Style nicht mehr aus Beispielcode oder mündlicher Überlieferung ableiten, die Konfigurationsdatei selbst ist die verbindliche, ausführbare Dokumentation des Styles. Das reduziert Einarbeitungszeit und verhindert Situationen, in denen unterschiedliche Teammitglieder unterschiedliche, informelle Vorstellungen vom "richtigen" Stil vertreten.

9. PHP_CodeSniffer vs. PHP-CS-Fixer im Vergleich

Beide Tools ergänzen sich in der Praxis eher, als dass eines das andere vollständig ersetzt. Die folgende Übersicht zeigt die wichtigsten Unterschiede für die Entscheidung, welches Tool in welcher Rolle zum Einsatz kommt.

Dimension PHP_CodeSniffer PHP-CS-Fixer
Primäres Konzept Erkennung und Meldung von Verstößen Automatisches Umschreiben von Code
Auto-Fix-Abdeckung Teilweise, über separates phpcbf Umfangreich, Kernfunktion
Regel-Katalog Sehr breit, auch Architektur-Sniffs Primär reine Formatierung
Konfigurationsformat XML-Ruleset PHP-Datei mit Config-Objekt
CI-Freundlichkeit Checkstyle-Report für Diff-Kommentare --dry-run --diff für nicht-blockierende Prüfung

Die produktivste Kombination für die meisten Teams: PHP-CS-Fixer für automatisches Formatieren im Editor und im Pre-Commit-Hook, PHP_CodeSniffer für die vollständige Regelprüfung in CI, inklusive Sniffs, die über reine Formatierung hinausgehen, etwa Nutzung veralteter Funktionen oder fehlende Sichtbarkeitsmodifikatoren.

10. Zusammenfassung

PSR-12 durchsetzen gelingt nicht durch mehr Disziplin oder ausführlichere Style-Guides, sondern durch Werkzeuge, die den Zustand objektiv prüfen und wo möglich automatisch korrigieren. PHP_CodeSniffer mit einem phpcs.xml-Ruleset deckt die Erkennung ab, PHP-CS-Fixer mit .php-cs-fixer.php übernimmt das automatische Umschreiben des Großteils der Verstöße. Pre-Commit-Hooks fangen Probleme lokal ab, bevor sie überhaupt committet werden, die CI-Pipeline bleibt die letzte, nicht umgehbare Instanz.

Editor-Integration mit Format-on-Save reduziert die Zahl der überhaupt entstehenden Verstöße auf ein Minimum, ein Rollout über .git-blame-ignore-revs oder eine Baseline-Strategie ermöglicht die Migration bestehender, gewachsener Codebasen ohne einen riskanten Big-Bang-Commit. Der eigentliche Gewinn zeigt sich im Review: Kommentare zu Formatierung verschwinden vollständig, die gewonnene Aufmerksamkeit fließt in fachliche Fragen, die kein Tool automatisch beantworten kann.

PSR-12 im Team durchsetzen - Das Wichtigste auf einen Blick

Erkennung und Fix

PHP_CodeSniffer mit phpcs.xml für Erkennung, PHP-CS-Fixer mit .php-cs-fixer.php für automatisches Umschreiben.

Drei Verteidigungslinien

Editor (Format-on-Save), Pre-Commit-Hook (nur gestagte Dateien), CI-Pipeline (harte, nicht umgehbare Prüfung).

Legacy-Rollout

Einmaliger Reformat-Commit plus .git-blame-ignore-revs, oder Baseline-Ansatz für risikoscheue Projekte.

Gewonnene Zeit

Review-Kommentare zu Formatierung entfallen vollständig, Aufmerksamkeit verschiebt sich auf Architektur und Logik.

11. FAQ: PSR-12 im Team durchsetzen

1Wie kann ich PSR-12 im Team durchsetzen?
Über automatisierte Tools: PHP_CodeSniffer für Erkennung, PHP-CS-Fixer für Auto-Fix, abgesichert durch Pre-Commit-Hooks und CI.
2Was unterscheidet PHP_CodeSniffer und PHP-CS-Fixer?
PHP_CodeSniffer meldet primär, mit phpcbf als separatem Fixer. PHP-CS-Fixer ist von Grund auf ein Auto-Fixer mit breiterer Korrekturabdeckung.
3Wie richte ich einen Pre-Commit-Hook ein?
Bash-Skript, das nur gestagte PHP-Dateien prüft, oder Tools wie Captain Hook bzw. Grumphp für deklarative Konfiguration.
4Kann ein Pre-Commit-Hook umgangen werden?
Ja, mit --no-verify. Deshalb bleibt CI die eigentliche, nicht umgehbare Durchsetzungsinstanz.
5Wie führe ich PSR-12 in Legacy-Code ein?
Reformat-Commit plus .git-blame-ignore-revs, oder Baseline-Ansatz, der nur neue Verstöße blockiert.
6Was macht .git-blame-ignore-revs?
Listet Commits, die git blame überspringt, sodass ein Reformat-Commit nicht die ursprüngliche Autorenschaft überschreibt.
7Wie richte ich Format-on-Save in PhpStorm ein?
Unter Quality Tools, PHP-CS-Fixer den Pfad zur ausführbaren Datei hinterlegen, dann Run on save aktivieren.
8Sollte der Style-Check in CI blockierend sein?
Ja, als Required Check, sonst bleibt PSR-12 eine unverbindliche Empfehlung statt einer durchgesetzten Regel.
9Ersetzt das den Code-Review komplett?
Nein, es entfernt nur Formatierungskommentare. Architektur, Edge Cases und Namensgebung bleiben menschliche Review-Aufgabe.
10Brauche ich beide Tools gleichzeitig?
Nicht zwingend, aber die Kombination aus PHP-CS-Fixer für Auto-Fix und PHP_CodeSniffer für zusätzliche Sniffs ist in der Praxis verbreitet.

Mironsoft

Code-Qualität, Tooling und CI-Pipelines für PHP-Teams

Noch immer Formatierungsdiskussionen im Code-Review?

Wir richten PHP_CodeSniffer und PHP-CS-Fixer sauber ein, integrieren Pre-Commit-Hooks und CI-Checks und begleiten den Rollout auf eurer bestehenden Codebasis, ohne einen riskanten Big-Bang-Reformat.

Tooling-Setup

phpcs.xml und .php-cs-fixer.php passend zu eurer bestehenden Codebasis konfiguriert

Legacy-Rollout

Reformat-Strategie mit git-blame-ignore-revs oder Baseline-Ansatz, risikoarm umgesetzt

CI-Integration

Pre-Commit-Hooks und Required Checks, die Style-Verstöße dauerhaft aus dem Review entfernen