Semantic Versioning für PHP-Pakete: Versionsnummern richtig vergeben
AI generated
<?php
8.4
PHP 8.4 · Composer · SemVer · Packagist
Semantic Versioning für PHP-Pakete
Versionsnummern richtig vergeben

Eine falsch vergebene Versionsnummer bricht in Sekunden hunderte fremde Installationen, ohne dass der Maintainer davon zunächst etwas merkt. Semantic Versioning ist die Vereinbarung, auf die sich das gesamte Composer-Ökosystem verlässt: MAJOR für Breaking Changes, MINOR für neue, abwärtskompatible Funktionen, PATCH für Bugfixes. Dieser Artikel erklärt die Regeln im Detail, zeigt Composer-Constraints und automatisierte Versionsermittlung mit echtem PHP-8.4-Code.

16 Min. Lesezeit MAJOR.MINOR.PATCH · Caret · Tilde PHP 8.4 · Composer 2.x · Packagist

1. Was Semantic Versioning löst und warum Composer darauf besteht

Semantic Versioning, kurz SemVer, löst ein sehr konkretes Problem: Ohne verbindliche Regeln kann eine Versionsnummer alles oder nichts bedeuten, und niemand außer dem Maintainer selbst weiß, ob ein Update sicher ist. SemVer definiert stattdessen ein festes Vertragsformat, MAJOR.MINOR.PATCH, bei dem jede Stelle eine klar definierte Bedeutung trägt. Ein Nutzer, der die Regeln kennt, kann allein aus der Versionsnummer ablesen, ob ein Update risikofrei, funktional erweiternd oder potenziell brechend ist, ohne den Changelog lesen zu müssen.

Composer setzt Semantic Versioning nicht nur voraus, sondern baut seinen gesamten Constraint-Mechanismus darauf auf. Der Caret-Operator ^1.2.0 etwa erlaubt automatisch alle Updates, die laut SemVer garantiert abwärtskompatibel sind, verweigert aber jedes Update, das eine neue Major-Version einführen würde. Diese Automatisierung funktioniert nur, wenn sich Paket-Maintainer tatsächlich an das Versprechen halten, das hinter Semantic Versioning steht, sonst installiert Composer scheinbar sichere Updates, die in Wahrheit die Anwendung brechen.

Für ein einzelnes Projekt mag eine unsaubere Versionierung noch verkraftbar sein, weil ein Entwickler den Changelog ohnehin manuell liest. Sobald ein Paket aber von Dutzenden oder Hunderten fremden Projekten genutzt wird, verlässt sich jedes davon automatisiert auf Semantic Versioning, ohne dass ein Mensch jedes Update einzeln prüft. Genau diese Automatisierung ist der eigentliche Wert von SemVer, und genau sie bricht, sobald ein Maintainer die Regeln auch nur einmal ignoriert.

2. Aufbau einer SemVer-Nummer: MAJOR, MINOR, PATCH

Eine vollständige SemVer-Nummer besteht aus drei durch Punkte getrennten, nicht-negativen Ganzzahlen: MAJOR.MINOR.PATCH, etwa 2.4.1. Die MAJOR-Zahl wird erhöht, sobald eine inkompatible Änderung an der öffentlichen API vorgenommen wird, also jede Änderung, die bestehenden, korrekten Aufrufercode zum Bruch bringen kann. Ein entfernter öffentlicher Parameter, ein geändertes Rückgabetyp-Verhalten oder eine umbenannte Klasse sind klassische Gründe für eine neue MAJOR-Version in konsequentem Semantic Versioning.

Die MINOR-Zahl steigt, wenn neue Funktionalität abwärtskompatibel hinzukommt, etwa eine zusätzliche öffentliche Methode oder ein neuer optionaler Parameter mit Standardwert. Bestehender Code, der gegen die vorherige MINOR-Version geschrieben wurde, muss unverändert weiterlaufen. Die PATCH-Zahl schließlich ist ausschließlich für abwärtskompatible Bugfixes reserviert, niemals für neue Funktionalität. Wer in einem PATCH-Release versehentlich eine neue Methode ergänzt, verletzt Semantic Versioning, selbst wenn die Änderung technisch harmlos wirkt.

Solange die MAJOR-Zahl 0 ist, gilt bei Semantic Versioning eine Sonderregel: Die gesamte API gilt als instabil, und selbst die MINOR-Zahl darf Breaking Changes einführen. Das ist der Grund, warum viele frische Pakete bewusst bei 0.x.y starten, bis die öffentliche API sich in der Praxis bewährt hat. Der Sprung auf 1.0.0 ist damit ein bewusstes Versprechen an alle Nutzer, dass die API ab diesem Zeitpunkt Stabilität verspricht.


#!/usr/bin/env bash
# Semantic Versioning decision in practice, before tagging a release

# PATCH: bugfix only, no API change at all
git tag -a v2.4.2 -m "Fix rounding error in currency conversion"

# MINOR: new, backward-compatible feature
git tag -a v2.5.0 -m "Add optional locale parameter to formatted()"

# MAJOR: removed a deprecated method, breaking change
git tag -a v3.0.0 -m "Remove deprecated Money::fromFloat(), use fromCents() instead"

3. Versionsconstraints in composer.json: Caret, Tilde, Wildcard

Composer übersetzt Semantic Versioning in konkrete Installationsregeln über Constraint-Operatoren in composer.json. Der Caret-Operator ^2.4.1 ist die heute empfohlene Standardform: Er erlaubt jedes Update innerhalb derselben MAJOR-Version, solange diese größer als 0 ist, blockiert aber jeden Sprung auf eine neue MAJOR-Version. Für Pakete mit MAJOR-Version 0 verhält sich der Caret-Operator strenger und erlaubt nur Updates innerhalb derselben MINOR-Version, weil bei 0.x-Paketen laut Semantic Versioning auch MINOR-Sprünge brechend sein dürfen.

Der Tilde-Operator ~2.4.1 ist konservativer und erlaubt nur Updates der letzten angegebenen Stelle, hier also Patch-Updates bis vor 2.5.0. Wird die MINOR-Stelle weggelassen, etwa ~2.4, erlaubt Tilde MINOR-Updates bis vor 3.0.0. Dieser feine Unterschied wird oft verwechselt, führt aber in der Praxis zu unterschiedlich strengen Update-Grenzen und sollte bewusst gewählt werden, je nachdem wie viel Vertrauen in die Semantic-Versioning-Disziplin des jeweiligen Pakets besteht.

Wildcard-Constraints wie 2.4.* und explizite Bereichsangaben wie >=2.4.0 <3.0.0 bieten weitere Präzision für Sonderfälle, etwa wenn ein Paket bekanntermaßen zwischen zwei bestimmten Versionen einen problematischen Zwischenstand hat. In der überwältigenden Mehrheit der Fälle reicht der Caret-Operator, weil er dem eigentlichen Versprechen von Semantic Versioning, kompatible Updates automatisch, brechende Updates niemals, am direktesten entspricht.


{
    "require": {
        "php": "^8.2",
        "mironsoft/money-value": "^2.4",
        "psr/log": "^3.0 || ^2.0 || ^1.1",
        "symfony/console": "~7.1.0",
        "some/legacy-package": ">=2.4.0 <3.0.0"
    }
}

4. Breaking Changes erkennen und korrekt einordnen

Nicht jede offensichtliche Codeänderung ist auch eine Breaking Change im Sinne von Semantic Versioning, und nicht jede unscheinbare Änderung ist harmlos. Das Entfernen einer öffentlichen Methode ist immer eine Breaking Change. Weniger offensichtlich: Wird der Rückgabetyp einer Methode von string auf string|null erweitert, ist das für Aufrufer, die bisher blind mit dem Rückgabewert weiterarbeiten, ebenfalls eine Breaking Change, auch wenn die Signatur formal nur erweitert wurde.

Änderungen an internen, nicht als public deklarierten Klassen zählen dagegen nicht als Breaking Change, solange die öffentliche API unverändert bleibt. Genau deshalb lohnt sich eine bewusst schmale public API: Je weniger Code öffentlich exponiert ist, desto größer ist der Spielraum für interne Refactorings, ohne die MAJOR-Zahl erhöhen zu müssen. Ein Paket, das versehentlich zu viele interne Details öffentlich macht, zwingt sich selbst zu häufigeren Major-Releases, als eigentlich nötig wäre.

Auch Änderungen am Verhalten ohne Signaturänderung zählen bei konsequentem Semantic Versioning: Wirft eine Methode neu eine Exception, die vorher nie geworfen wurde, kann bestehender Aufrufercode, der diese Exception nicht abfängt, zur Laufzeit brechen. Solche verhaltensbezogenen Breaking Changes werden in der Praxis am häufigsten übersehen, weil sie sich statischer Analyse entziehen und nur durch sorgfältige Changelog-Pflege und manuelle Prüfung erkennbar werden.

5. Pre-Release- und Build-Metadaten richtig nutzen

Semantic Versioning erlaubt zusätzliche Pre-Release-Kennungen nach einem Bindestrich, etwa 3.0.0-beta.1 oder 3.0.0-rc.2. Diese Kennungen markieren eine Version, die formal bereits die nächste Stufe erreicht hat, aber noch nicht als stabil gilt. Composer installiert solche Pre-Releases standardmäßig nicht, es sei denn, ein Nutzer setzt minimum-stability explizit herab oder verlangt die Version mit einem @beta-Stability-Flag direkt an der Anforderung.

Build-Metadaten nach einem Plus-Zeichen, etwa 3.0.0+build.20260730, tragen zusätzliche Information, die für den Versionsvergleich selbst irrelevant ist. Zwei Versionen, die sich nur in den Build-Metadaten unterscheiden, gelten laut Semantic Versioning-Spezifikation als gleichrangig, Composer ignoriert diesen Teil beim Constraint-Abgleich vollständig. In der PHP-Paketwelt kommen Build-Metadaten selten vor, weil Git-Tags selbst bereits als eindeutiger Identifikator eines Commits dienen.

Ein sinnvoller Einsatz von Pre-Releases: Vor einem größeren MAJOR-Release, etwa dem Wechsel von PHP 8.3 auf PHP 8.4 als Mindestanforderung, veröffentlicht man mehrere rc-Versionen, die interessierte Nutzer bewusst gegen ihre eigene Test-Suite laufen lassen können, bevor die finale, stabile Version erscheint. Diese Praxis reduziert das Risiko unentdeckter Breaking Changes erheblich, ohne die reguläre Semantic-Versioning-Kette zu verletzen.


#!/usr/bin/env bash
set -euo pipefail

# Publish a release candidate before the final major version
git tag -a v3.0.0-rc.1 -m "Release candidate: PHP 8.4 baseline"
git push origin v3.0.0-rc.1

# Interested users can opt in explicitly:
# composer require mironsoft/money-value:3.0.0-rc.1

# Once validated, tag the stable release
git tag -a v3.0.0 -m "Stable release: PHP 8.4 baseline"
git push origin v3.0.0

6. Composers eigener Versionsvergleichsalgorithmus

Composer normalisiert jede Versionsnummer intern, bevor ein Vergleich stattfindet, und behandelt dabei Sonderfälle, die reines Semantic Versioning so nicht kennt. Ein Präfix v, wie in v2.4.1, wird beim Vergleich ignoriert, ist aber als Konvention für Git-Tags weit verbreitet und funktional gleichwertig zu 2.4.1. Composer akzeptiert außerdem verkürzte Versionsnummern wie 2.4 und ergänzt die fehlende PATCH-Stelle intern mit 0.

Für den eigentlichen Vergleich zerlegt Composer die Versionsnummer in ihre numerischen Bestandteile und vergleicht sie stellenweise von links nach rechts, MAJOR zuerst, dann MINOR, dann PATCH. Stabilitäts-Suffixe wie -dev, -alpha, -beta, -RC und schließlich die stabile Version ohne Suffix bilden eine feste, aufsteigende Reihenfolge, sodass 2.0.0-beta1 laut Semantic-Versioning-Logik zuverlässig als kleiner als 2.0.0 gilt.

Ein oft übersehener Sonderfall: Composer behandelt dev-main und ähnliche Branch-Aliase außerhalb der regulären SemVer-Ordnung als eigene Stabilitätsstufe, die standardmäßig als am wenigsten stabil gilt. Wer in require versehentlich einen Branch-Namen statt eines Tags referenziert, verlässt damit implizit die Garantien von Semantic Versioning, weil sich der Inhalt eines Branches jederzeit ändern kann, ohne dass sich die Versionsangabe selbst ändert.

7. Automatisierte Versionsermittlung mit Conventional Commits

Conventional Commits ergänzen Semantic Versioning um eine maschinenlesbare Commit-Konvention: fix: für Bugfixes, feat: für neue Funktionen, und ein BREAKING CHANGE:-Vermerk im Commit-Body oder ein Ausrufezeichen nach dem Typ, etwa feat!:, für Breaking Changes. Tools wie semantic-release oder das PHP-Pendant conventional-changelog lesen diese Commit-Historie automatisch aus und leiten daraus die nächste korrekte Versionsnummer ab, ohne dass ein Mensch manuell entscheiden muss.

Der Vorteil dieser Automatisierung liegt nicht nur in gesparter Zeit, sondern in erzwungener Konsistenz: Ein automatisiertes Tool wendet die Regeln von Semantic Versioning mechanisch an, ohne die menschliche Tendenz, eine Breaking Change aus Bequemlichkeit als MINOR-Release zu deklarieren. Voraussetzung ist allerdings, dass jeder Commit tatsächlich korrekt klassifiziert wird, ein falsch getaggter Commit-Typ propagiert sich sonst direkt in eine falsche Versionsnummer.

In der CI-Pipeline lässt sich dieser Prozess vollständig automatisieren: Nach jedem Merge in den Hauptbranch analysiert ein Job die Commit-Historie seit dem letzten Tag, bestimmt die nächste Version nach Semantic Versioning und erstellt Tag sowie Changelog-Eintrag automatisch. Für Teams mit hoher Release-Frequenz reduziert das den manuellen Aufwand erheblich, ohne die Verlässlichkeit der Versionsnummern zu gefährden.

8. Typische SemVer-Fehler in der Praxis

Der häufigste Fehler bei Semantic Versioning ist, eine Breaking Change versehentlich als MINOR- oder sogar PATCH-Release zu veröffentlichen, meist weil die Änderung dem Maintainer selbst harmlos erschien. Ein zweiter typischer Fehler betrifft PATCH-Releases, die eigentlich neue Funktionalität enthalten, etwa weil ein Bugfix nebenbei eine zusätzliche öffentliche Methode einführt. Beides untergräbt das Vertrauen, auf dem Composers automatisierte Updates beruhen.


<?php

declare(strict_types=1);

/**
 * WRONG: this method signature change is a breaking change,
 * but shipping it as v2.5.1 (a patch release) violates Semantic Versioning.
 * Existing callers that rely on the exact return type will fail.
 */
final class LegacyExample
{
    // Before v2.5.0: public function total(): float
    // After v2.5.1 (WRONG, should be v3.0.0):
    public function total(): string
    {
        return number_format($this->amount, 2);
    }
}

/**
 * RIGHT: add a new method alongside the existing one (v2.5.0, a minor
 * release), deprecate the old one, and only remove it in v3.0.0.
 */
final class CorrectExample
{
    public function total(): float
    {
        return $this->amount;
    }

    /** @deprecated since 2.5.0, use total() and format the result yourself */
    public function formattedTotal(): string
    {
        return number_format($this->amount, 2);
    }
}

Ein dritter Fehler betrifft transitive Abhängigkeiten: Ein Paket, das intern eine fremde Bibliothek mit einer zu weiten Constraint wie * referenziert, kann durch ein Breaking-Change-Update dieser Abhängigkeit brechen, ohne selbst eine neue Version veröffentlicht zu haben. Konsequentes Semantic Versioning verlangt deshalb auch bei eigenen Abhängigkeiten präzise Constraints, nicht nur bei der eigenen, nach außen kommunizierten Versionsnummer.

9. Constraint-Syntax im direkten Vergleich

Composer bietet mehrere Constraint-Operatoren mit unterschiedlich strengen Update-Grenzen an. Die folgende Übersicht zeigt, welcher Operator welchen Teil der SemVer-Nummer für Updates freigibt und wann sich der jeweilige Operator eignet.

Constraint Beispiel Erlaubte Updates Empfehlung
Caret ^ ^2.4.1 Alle 2.x.y, kein 3.0.0 Standardwahl für die meisten Abhängigkeiten
Tilde ~ ~2.4.1 Nur 2.4.x, kein 2.5.0 Bei weniger vertrauenswürdigen Paketen
Wildcard * 2.4.* Nur 2.4.x, identisch zu Tilde hier Selten nötig, Caret ist klarer
Bereich >=2.4.0 <3.0.0 Explizit definierter Bereich Nur für dokumentierte Sonderfälle
Exakt 2.4.1 Keine, komplett fixiert Nur bei bekannt instabilen Paketen

Der Caret-Operator deckt in der Praxis die überwiegende Mehrheit sinnvoller Fälle ab, weil er exakt der Garantie entspricht, die Semantic Versioning verspricht: kompatible Updates automatisch, brechende Updates niemals ungefragt. Exakte Versionsfixierung sollte die Ausnahme bleiben, weil sie Sicherheitsupdates blockiert, die als PATCH-Release eigentlich risikofrei einspielbar wären.

Mironsoft

PHP-Architektur, Release-Prozesse und Paket-Pflege

Ein Update hat gerade heimlich die Produktion gebrochen?

Wir richten saubere Semantic-Versioning-Prozesse für eure PHP-Pakete ein, automatisieren die Versionsermittlung über Conventional Commits und sorgen dafür, dass Composer-Updates zuverlässig das tun, was ihre Versionsnummer verspricht.

Versionierungs-Audit

Bestehende Release-Historie auf SemVer-Verstöße prüfen

Release-Automatisierung

Conventional Commits und automatisierte Versionsermittlung in der CI

Composer-Beratung

Constraint-Strategie für Abhängigkeiten im ganzen Projekt festlegen

10. Zusammenfassung

Semantic Versioning ist das Vertragsformat, auf dem Composers gesamte automatisierte Update-Logik beruht: MAJOR für Breaking Changes, MINOR für abwärtskompatible neue Funktionen, PATCH für reine Bugfixes. Der Caret-Operator übersetzt dieses Versprechen direkt in eine Constraint-Regel und erlaubt genau die Updates, die laut SemVer sicher sein sollten. Pre-Release-Kennungen wie -beta und -rc ermöglichen kontrolliertes Testen vor einem größeren Release, ohne die reguläre Versionskette zu verletzen.

Wer Semantic Versioning konsequent einhält, auch bei unscheinbaren Verhaltensänderungen und internen Abhängigkeiten, gibt jedem Nutzer eines Pakets die Möglichkeit, Updates blind und automatisiert einzuspielen. Automatisierte Versionsermittlung über Conventional Commits reduziert menschliche Fehleinschätzungen zusätzlich und macht die Einhaltung der Regeln zu einem technischen statt einem rein disziplinären Prozess.

Semantic Versioning für PHP-Pakete: Das Wichtigste auf einen Blick

Aufbau

MAJOR.MINOR.PATCH: Breaking Change, neue Funktion, Bugfix. Klare, verbindliche Bedeutung jeder Stelle.

Constraints

Caret ^ als Standard, erlaubt kompatible Updates, blockiert neue Major-Versionen automatisch.

Pre-Releases

-beta, -rc für kontrolliertes Testen vor einem größeren Release, Composer installiert sie standardmäßig nicht.

Häufigster Fehler

Breaking Change versehentlich als MINOR oder PATCH deklariert, bricht Automatisierung fremder Projekte.

11. FAQ: Semantic Versioning für PHP-Pakete

1Was bedeutet Semantic Versioning konkret?
MAJOR.MINOR.PATCH: Breaking Change, neue abwärtskompatible Funktion, reiner Bugfix. Versionsnummer signalisiert das Update-Risiko.
2Unterschied zwischen Caret und Tilde?
Caret erlaubt Updates innerhalb der Major-Version, Tilde nur innerhalb der letzten angegebenen Stelle, also deutlich enger.
3Warum Sonderregel bei 0.x.y?
Die API gilt als instabil, selbst Minor-Sprünge dürfen brechen. 1.0.0 ist das erste Stabilitätsversprechen.
4Erweiterter Rückgabetyp: Breaking Change?
Ja, wenn er bestehenden Aufrufercode brechen kann, etwa string zu string|null, das erfordert eine neue Major-Version.
5Installiert Composer Pre-Releases automatisch?
Nein, nur bei gesenkter minimum-stability oder expliziter Stability-Flag-Anforderung.
6Was sind Conventional Commits?
Eine Commit-Konvention mit fix:, feat: und BREAKING CHANGE:, aus der Tools automatisch die nächste Version ableiten.
7Bugfix versehentlich mit Feature vermischt?
Verletzt SemVer, Nutzer mit reinen Patch-Updates erhalten ungewollt neue Funktionalität, die sie nicht erwarten.
8Wie vergleicht Composer Versionen?
Normalisiert Präfixe, vergleicht MAJOR, MINOR, PATCH stellenweise, Stabilitäts-Suffixe bilden eine feste Reihenfolge.
9Zählen Build-Metadaten beim Vergleich?
Nein, Versionen mit unterschiedlichen Build-Metadaten gelten als gleichrangig, Composer ignoriert diesen Teil.
10Immer mit exakter Version fixieren?
In der Regel nein, das blockiert auch sichere Patch-Updates. Caret ist für die meisten Abhängigkeiten die bessere Wahl.