von der Idee bis zu Packagist
Wer denselben Hilfscode in mehreren Projekten kopiert, verliert früher oder später den Überblick, welche Version wo läuft. Ein eigenes Composer-Paket löst dieses Problem: eine composer.json, ein PSR-4-Namespace, eine Versionsnummer pro Git-Tag und eine einzige Quelle der Wahrheit. Dieser Artikel begleitet den kompletten Weg von der ersten Verzeichnisstruktur über Git-Tags bis zur Registrierung auf Packagist, mit echtem, nachvollziehbarem PHP-8.4-Code.
Inhaltsverzeichnis
- 1. Warum ein eigenes Composer-Paket, statt Code zu kopieren
- 2. Grundstruktur: composer.json richtig aufsetzen
- 3. Namespace, PSR-4 und Verzeichnisstruktur festlegen
- 4. Versionierung und Git-Tags für das Paket
- 5. Paket bei Packagist registrieren und Webhook einrichten
- 6. README, LICENSE und Metadaten für Endnutzer
- 7. CI-Pipeline: Tests, PHPStan und Code-Style vor jedem Release
- 8. Pflege nach der Veröffentlichung: Issues, PRs und Deprecations
- 9. Eigenes Paket im Vergleich zu Copy-Paste und Monorepo
- 10. Zusammenfassung
- 11. FAQ
1. Warum ein eigenes Composer-Paket, statt Code zu kopieren
In fast jedem PHP-Team entsteht irgendwann derselbe Hilfscode mehrfach: eine Money-Value-Klasse, ein kleiner HTTP-Wrapper, ein Set von Validierungsregeln. Wird dieser Code per Copy-Paste in mehrere Projekte übernommen, existiert er faktisch als mehrere unabhängige Kopien, die sich unbemerkt auseinanderentwickeln. Ein Bugfix in Projekt A erreicht Projekt B nur, wenn sich jemand aktiv daran erinnert, ihn manuell nachzuziehen. Ein eigenes Composer-Paket löst genau dieses Problem, indem es den Code an einer einzigen Stelle hält und über eine Versionsnummer kontrolliert an alle abhängigen Projekte verteilt.
Der zweite Vorteil eines eigenen Composer-Pakets liegt in der erzwungenen Trennung von Verantwortlichkeiten. Sobald Code in einem eigenen Repository mit eigener composer.json lebt, muss er zwangsläufig unabhängig von einer konkreten Anwendung funktionieren, ohne stillschweigende Annahmen über globale Zustände, Autoloading-Reihenfolgen oder Framework-spezifische Hilfsfunktionen. Diese Isolation deckt Kopplungsprobleme auf, die in einem monolithischen Projekt oft jahrelang unbemerkt bleiben, weil der Code nie außerhalb seines gewohnten Kontexts ausgeführt wird.
Nicht jeder Code eignet sich für ein eigenes Composer-Paket. Stark projektspezifische Geschäftslogik, die eng an ein einzelnes Domänenmodell gekoppelt ist, gehört in der Regel nicht in ein wiederverwendbares Paket. Kandidaten sind stattdessen generische, fachlich neutrale Bausteine: Value Objects, HTTP-Clients, Formatierungs- und Validierungslogik, kleine Adapter für externe APIs. Wer diese Grenze früh zieht, vermeidet ein Paket, das in Wahrheit nur für ein einziges Projekt sinnvoll ist und trotzdem den vollen Pflegeaufwand einer öffentlichen Bibliothek verursacht.
2. Grundstruktur: composer.json richtig aufsetzen
Die composer.json ist die zentrale Beschreibung jedes Composer-Pakets und entscheidet, wie Name, Abhängigkeiten und Autoloading nach außen sichtbar werden. Der Feldname name folgt zwingend dem Schema vendor/paket in Kleinbuchstaben, wobei der Vendor-Teil meist den Firmen- oder GitHub-Namen widerspiegelt. Das Feld type sollte bei einer wiederverwendbaren Bibliothek explizit library lauten, das ist der Standardwert, den Composer erwartet, wenn ein Paket als Abhängigkeit in andere Projekte installiert werden soll, statt selbst ein eigenständiges Projekt zu sein.
Besonders wichtig für ein eigenes Composer-Paket ist eine präzise require-Sektion. Jede tatsächlich genutzte Abhängigkeit gehört explizit hinein, auch wenn sie transitiv über eine andere Bibliothek bereits verfügbar wäre, weil sich das jederzeit ändern kann und das eigene Paket sonst plötzlich ohne erkennbaren Grund bricht. Die PHP-Versionsanforderung selbst gehört ebenfalls in require, etwa "php": "^8.2", damit Composer inkompatible Installationen von vornherein ablehnt, statt erst zur Laufzeit mit einem Syntaxfehler zu scheitern.
Die autoload-Sektion verweist über PSR-4 auf das Quellverzeichnis, während eine getrennte autoload-dev-Sektion ausschließlich für Tests gilt und in Produktions-Installationen komplett fehlt. Diese Trennung ist bei einem Composer-Paket besonders wichtig, weil ein Endnutzer, der das Paket per composer require installiert, niemals Testklassen oder Fixtures im eigenen vendor-Verzeichnis sehen sollte.
{
"name": "mironsoft/money-value",
"description": "Immutable money value object for PHP 8.4 with currency-safe arithmetic",
"type": "library",
"license": "MIT",
"keywords": ["php", "money", "value-object", "currency"],
"authors": [
{ "name": "Mironsoft", "homepage": "https://mironsoft.de" }
],
"require": {
"php": "^8.2",
"ext-bcmath": "*"
},
"require-dev": {
"phpunit/phpunit": "^11.0",
"phpstan/phpstan": "^1.11"
},
"autoload": {
"psr-4": { "Mironsoft\\MoneyValue\\": "src/" }
},
"autoload-dev": {
"psr-4": { "Mironsoft\\MoneyValue\\Tests\\": "tests/" }
},
"minimum-stability": "stable",
"prefer-stable": true
}
3. Namespace, PSR-4 und Verzeichnisstruktur festlegen
Der Namespace eines Composer-Pakets sollte den Vendor-Namen aus der composer.json widerspiegeln, damit Nutzer allein am Namespace erkennen, woher eine Klasse stammt. Eine Verzeichnisstruktur mit src/ für Produktionscode und tests/ für Tests hat sich als De-facto-Standard durchgesetzt, weil sie sowohl von Menschen als auch von Tools wie PHPStan und PHPUnit ohne zusätzliche Konfiguration verstanden wird. Innerhalb von src/ spiegelt die Verzeichnisstruktur exakt den Namespace unterhalb des PSR-4-Präfixes wider, das ist keine Empfehlung, sondern eine technische Voraussetzung des Autoloadings.
Ein häufiger Anfängerfehler bei einem neuen eigenen Composer-Paket ist, zu viele öffentliche Klassen und zu wenig durchdachte API-Grenzen zu exponieren. Wer von Anfang an konsequent zwischen einer schmalen, dokumentierten public API und internen Implementierungsdetails unterscheidet, etwa über final-Klassen und ein bewusst kleines Set exportierter Interfaces, spart sich spätere Breaking Changes, wenn interne Details doch angepasst werden müssen. Genau diese Disziplin unterscheidet ein Paket, das über Jahre stabil bleibt, von einem, das bei jedem internen Refactoring eine neue Major-Version erzwingt.
<?php
declare(strict_types=1);
namespace Mironsoft\MoneyValue;
/**
* Immutable money value object, part of the package's small public API.
* Internal helpers stay in Mironsoft\MoneyValue\Internal and are not
* covered by the package's backward compatibility promise.
*/
final readonly class Money
{
public function __construct(
private int $amountInCents,
private string $currencyCode,
) {
}
public function add(self $other): self
{
if ($this->currencyCode !== $other->currencyCode) {
throw new CurrencyMismatchException($this->currencyCode, $other->currencyCode);
}
return new self($this->amountInCents + $other->amountInCents, $this->currencyCode);
}
public function formatted(): string
{
return number_format($this->amountInCents / 100, 2) . ' ' . $this->currencyCode;
}
}
4. Versionierung und Git-Tags für das Paket
Composer liest Versionen eines Composer-Pakets primär aus Git-Tags, nicht aus einem Feld in der composer.json selbst. Ein Tag wie v1.2.0 oder 1.2.0 markiert einen unveränderlichen Snapshot des Repositories, den Packagist erkennt und als installierbare Version anbietet. Ohne konsequente Tags kann Composer zwar auch auf Branches oder Commit-Hashes zurückgreifen, das erzeugt aber instabile Installationen, weil sich der Inhalt eines Branches jederzeit ändern kann, während ein Tag per Definition fixiert bleibt.
Für ein eigenes Composer-Paket empfiehlt sich ein fester Release-Ablauf: erst alle Tests und die statische Analyse grün, dann ein aussagekräftiger Changelog-Eintrag, danach der Tag, und erst zum Schluss der Push zum Remote-Repository. Wird ein bereits veröffentlichter Tag nachträglich verschoben, weil ein Fehler im Release entdeckt wurde, kann das bei Nutzern, die bereits gegen den alten Commit-Hash installiert haben, zu widersprüchlichen Zuständen führen. Ein fehlerhafter Tag sollte deshalb nie verschoben, sondern immer durch einen neuen Patch-Release ersetzt werden.
#!/usr/bin/env bash
set -euo pipefail
# Full release flow for a Composer package
composer test
composer phpstan
composer cs-check
# Tag the release, annotated tags keep author and message
git tag -a v1.2.0 -m "Add currency conversion helper"
git push origin v1.2.0
# Verify what Packagist will see for this tag
git show v1.2.0 --stat
5. Paket bei Packagist registrieren und Webhook einrichten
Packagist ist das zentrale, öffentliche Repository für Composer-Pakete und wird von Composer standardmäßig ohne weitere Konfiguration abgefragt. Die Registrierung eines eigenen Pakets läuft über ein Formular auf packagist.org, in das lediglich die Git-Repository-URL eingetragen wird. Packagist liest daraufhin die composer.json aus dem Repository, extrahiert Name, Beschreibung und verfügbare Versionen aus den vorhandenen Tags und macht das Paket sofort per composer require vendor/paket installierbar.
Ohne zusätzlichen Schritt aktualisiert Packagist ein Composer-Paket aber nur periodisch, was bei einem frischen Release zu unnötiger Wartezeit führt. Der GitHub-Webhook unter den Repository-Einstellungen behebt das: Bei jedem Push, insbesondere bei jedem neuen Tag, benachrichtigt GitHub Packagist aktiv, und die neue Version steht innerhalb von Sekunden zur Installation bereit. Für GitLab und Bitbucket existieren äquivalente Webhook-Integrationen, die denselben Zweck erfüllen.
Wer ein Paket dauerhaft nicht mehr pflegt, sollte es auf Packagist explizit als abandoned markieren, optional mit einem Verweis auf einen Nachfolger. Composer zeigt Nutzern beim Installieren eines als verlassen markierten Pakets eine deutliche Warnung an, das ist fairer gegenüber der Community als ein stillschweigend unverändertes, aber faktisch totes Composer-Paket.
6. README, LICENSE und Metadaten für Endnutzer
Ein Composer-Paket ohne README ist für potenzielle Nutzer praktisch unauffindbar, selbst wenn die Installation über Packagist technisch funktioniert. Ein brauchbares README beschreibt in wenigen Sätzen den Zweck des Pakets, zeigt ein minimales Installations- und Verwendungsbeispiel direkt am Anfang und verweist erst danach auf ausführlichere Dokumentation. Badges für Build-Status, Testabdeckung und unterstützte PHP-Versionen geben potenziellen Nutzern innerhalb weniger Sekunden ein Gefühl für die Qualität und Aktualität des Projekts.
Die Datei LICENSE ist für jedes öffentlich installierbare Composer-Paket praktisch Pflicht, MIT ist im PHP-Ökosystem der mit Abstand verbreitetste Standard, weil sie kommerzielle Nutzung ohne Einschränkung erlaubt. Fehlt eine explizite Lizenzdatei, ist rechtlich unklar, ob und wie ein Paket überhaupt genutzt werden darf, was viele Unternehmen aus Compliance-Gründen von der Installation abhält, unabhängig davon, wie gut der Code tatsächlich ist.
Über .gitattributes lässt sich außerdem steuern, welche Dateien beim Erzeugen eines Release-Archivs überhaupt in das an Composer-Nutzer ausgelieferte Paket gelangen. Tests, CI-Konfiguration und Dokumentationsquellen gehören ins Repository, aber nicht in jede Installation eines Endnutzers, weil sie dort nur unnötig Speicherplatz belegen und keinen funktionalen Mehrwert bieten.
#!/usr/bin/env bash
# Create .gitattributes so release archives stay lean
cat > .gitattributes << 'EOF'
/tests export-ignore
/.github export-ignore
/phpunit.xml.dist export-ignore
/phpstan.neon export-ignore
/.gitattributes export-ignore
/.gitignore export-ignore
EOF
git add .gitattributes
git commit -m "Exclude dev files from release archives"
7. CI-Pipeline: Tests, PHPStan und Code-Style vor jedem Release
Ein Composer-Paket ohne automatisierte CI-Pipeline verlässt sich vollständig auf die Disziplin des Maintainers, jeden Test manuell vor jedem Release auszuführen. Das funktioniert eine Weile, bricht aber zuverlässig zusammen, sobald mehrere Personen am selben Paket mitarbeiten oder ein einzelner Maintainer unter Zeitdruck einen Schritt überspringt. Eine GitHub-Actions-Pipeline, die bei jedem Push und jedem Pull Request automatisch PHPUnit, PHPStan und einen Code-Style-Check ausführt, macht diese Disziplin technisch erzwingbar statt einer bloßen Empfehlung.
Für ein eigenes Composer-Paket, das mehrere PHP-Versionen unterstützen will, lohnt sich eine Testmatrix, die dieselbe Test-Suite gegen jede unterstützte Version laufen lässt. So fällt ein Kompatibilitätsproblem mit einer älteren oder neueren PHP-Version sofort in der Pipeline auf, statt erst dann, wenn ein Nutzer mit genau dieser Version eine Fehlermeldung meldet. Ergänzend prüft ein Composer-Validierungsschritt, ob die composer.json selbst syntaktisch und semantisch korrekt ist, bevor überhaupt ein Test ausgeführt wird.
name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
php: ['8.2', '8.3', '8.4']
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
coverage: none
- run: composer validate --strict
- run: composer install --no-progress --prefer-dist
- run: vendor/bin/phpstan analyse --level=8
- run: vendor/bin/php-cs-fixer fix --dry-run --diff
- run: vendor/bin/phpunit
8. Pflege nach der Veröffentlichung: Issues, PRs und Deprecations
Die Veröffentlichung eines Composer-Pakets ist kein Endpunkt, sondern der Beginn einer laufenden Pflegeverantwortung. Sobald andere Projekte gegen eine bestimmte Version des Pakets entwickeln, wird jede Änderung an der public API zu einer Entscheidung mit realen Konsequenzen für fremden Code. Ein Issue-Tracker, der tatsächlich beobachtet wird, und eine klare Reaktionszeit auf Pull Requests unterscheiden ein lebendiges Paket von einem, das nach der ersten Version faktisch verwaist.
Wird eine öffentliche Methode eines Composer-Pakets künftig entfernt, sollte sie zunächst über @deprecated-Annotationen und, wo möglich, per trigger_error(..., E_USER_DEPRECATED) markiert werden, mindestens eine Minor-Version lang, bevor sie in einer neuen Major-Version tatsächlich verschwindet. Dieses Vorgehen gibt Nutzern Zeit, ihre Integration anzupassen, statt bei einem Update ohne Vorwarnung mit einem Fatal Error konfrontiert zu werden. Ein gepflegtes Changelog dokumentiert jede Deprecation nachvollziehbar für alle, die das Paket in Produktion einsetzen.
9. Eigenes Paket im Vergleich zu Copy-Paste und Monorepo
Nicht jede Situation rechtfertigt ein vollständig eigenständiges Composer-Paket mit eigenem Repository und eigener Release-Pipeline. Die folgende Übersicht vergleicht die drei gängigsten Strategien für geteilten PHP-Code und zeigt, wann welche davon den geringsten Aufwand bei ausreichender Robustheit bietet.
| Strategie | Wiederverwendbarkeit | Pflegeaufwand | Wann sinnvoll |
|---|---|---|---|
| Copy-Paste | Keine, Kopien driften auseinander | Praktisch keiner, aber teuer bei Bugfixes | Nur bei einmaligem, sehr kleinem Snippet |
| Paket im Monorepo | Gut innerhalb derselben Organisation | Gering, ein Repository, ein CI-Lauf | Mehrere Anwendungen im selben Unternehmen |
| Eigenes Composer-Paket | Sehr hoch, über Firmengrenzen hinweg | Höher, eigene Pipeline und Versionierung | Wiederverwendung über Teams oder öffentlich |
Für Code, der ausschließlich innerhalb einer einzigen Organisation zwischen wenigen Projekten geteilt wird, ist ein Paket innerhalb eines Monorepos oft die pragmatischere Wahl, weil eine einzige CI-Pipeline und ein einziger Versionsstand genügen. Ein eigenes Composer-Paket mit eigenem Repository lohnt sich, sobald externe Teams, andere Firmen oder die Öffentlichkeit den Code nutzen sollen, weil nur dann die zusätzliche Isolation und die eigene Release-Disziplin ihren vollen Nutzen entfalten.
Mironsoft
PHP-Architektur, Composer-Tooling und Paket-Strategien
Code, der in fünf Projekten kopiert wird, statt als Paket zu leben?
Wir extrahieren geteilten PHP-Code in saubere, versionierte Composer-Pakete, richten CI-Pipelines und Packagist-Anbindung ein und etablieren eine tragfähige Release-Disziplin für euer Team.
Paket-Extraktion
Geteilten Code identifizieren und in ein eigenständiges Composer-Paket überführen
Release-Automatisierung
CI-Pipeline mit Tests, PHPStan und automatischer Packagist-Anbindung
PHP-8.4-Beratung
Moderne Sprachfeatures und PSR-Standards in eigenen Bibliotheken etablieren
10. Zusammenfassung
Ein eigenes Composer-Paket lohnt sich immer dann, wenn Code in mehreren Projekten oder von mehreren Teams genutzt wird und eine einzige Quelle der Wahrheit wichtiger ist als der zusätzliche Pflegeaufwand einer eigenen Release-Pipeline. Der Weg dorthin ist klar strukturiert: eine saubere composer.json mit präzisen Abhängigkeiten, eine PSR-4-konforme Verzeichnisstruktur mit schmaler public API, konsequente Git-Tags nach Semantic Versioning und eine Registrierung bei Packagist inklusive Webhook für sofortige Verfügbarkeit neuer Releases.
README, LICENSE und eine automatisierte CI-Pipeline sind kein optionales Beiwerk, sondern Grundvoraussetzung dafür, dass ein Composer-Paket von anderen überhaupt vertrauensvoll genutzt wird. Wer zusätzlich eine klare Deprecation-Politik pflegt und Issues sowie Pull Requests aktiv bearbeitet, verwandelt ein einmaliges Release in eine langfristig verlässliche Abhängigkeit für alle, die darauf aufbauen.
Eigene Composer-Pakete veröffentlichen: Das Wichtigste auf einen Blick
Grundstruktur
composer.json mit name, type: library, präziser require-Sektion und PSR-4-Autoloading.
Versionierung
Git-Tags nach Semantic Versioning, niemals verschieben, Fehler per neuem Patch-Release korrigieren.
Packagist
Repository-URL registrieren, GitHub-Webhook für sofortige Aktualisierung nach jedem Tag einrichten.
Qualitätssicherung
CI-Pipeline mit Testmatrix über mehrere PHP-Versionen, PHPStan und Code-Style-Check vor jedem Release.