Anatomie eines Plugins
Was jede Datei tut, und welche du wirklich brauchst.
AcmeShipping/
├── composer.json # Paket-Metadaten, Typ octibiz-plugin
├── plugin.json # Manifest, das der Installer vor dem Start liest
├── CHANGELOG.md # oberster Abschnitt entspricht der Version im Descriptor
├── src/
│ ├── AcmeShippingPlugin.php # das Bundle
│ ├── AcmeShippingDescriptor.php # die Selbstbeschreibung
│ ├── Entity/ # Datenmodell, automatisch angemeldet
│ ├── ApiResource/ # API-Ressourcen, automatisch gefunden
│ ├── State/ # Lieferanten und Verarbeiter dazu
│ ├── Security/ # Voter und Rechte-Vertrag
│ ├── Catalog/ # Standarddaten, Demo, Verweise
│ ├── Migrations/ # eigene, additive Migrationen
│ ├── Service/ Command/ EventListener/ Controller/ # nach Bedarf
│ └── Resources/icon.svg # Pflicht
├── templates/ # optional, eigener Vorlagen-Namensraum
└── frontend/ # optional
├── plugin.manifest.ts
├── locales/de.ts, en.ts
└── views/ components/
Was Pflicht ist
Gemessen an allen ausgelieferten Plugins sind sieben Dinge in jedem einzelnen vorhanden: composer.json, plugin.json, CHANGELOG.md, das Bundle, der Descriptor, ein Ordner Catalog/ und das Symbol.
Alles andere ist optional. Das kleinste ausgelieferte Plugin besteht aus zehn Dateien.
Kein Plugin hat eine services.yaml. Dienste werden gefunden, nicht angemeldet.
Die Bundle-Klasse
final class AcmeShippingPlugin extends AbstractPlugin
{
}
Ein leerer Rumpf ist gültig und der Normalfall. Die Basisklasse übernimmt:
- Alle Dienste unter
src/finden und verdrahten. src/Entity/als Datenmodell anmelden.src/Migrations/als Migrationspfad anmelden.templates/als eigenen Vorlagen-Namensraum anmelden.src/ApiResource/wird von der API-Schicht selbst gefunden.
Es gibt vier Stellen, an denen du eingreifen kannst, und alle sind optional:
| Methode | Wofür |
|---|---|
serviceBinds() | Feste Werte in Konstruktoren binden |
serviceAliases() | Eine Vertragsstelle des Kerns übernehmen |
publicApiPathPatterns() | Endpunkte ohne Anmeldepflicht |
prependExtension() | Eigenes Nachrichten-Routing, eigene Begrenzer |
Wenn du prependExtension() überschreibst, ruf parent:: auf. Sonst fällt lautlos aus, was die Basis erledigt. Das ist der teuerste Fehler in diesem System, weil er auf einer bestehenden Entwickler-Datenbank unsichtbar bleibt: Die Tabellen sind ja schon da. Erst eine Frischinstallation legt keine einzige mehr an.
Der Descriptor
Die einzige Pflicht-Implementierung. Sie beantwortet sieben Fragen:
final class AcmeShippingDescriptor implements PluginDescriptorInterface
{
public const VERSION = '0.1.0';
public function getPluginKey(): string { return 'acme.shipping'; }
public function getDisplayName(): string { return 'Acme Versand'; }
public function getVersion(): string { return self::VERSION; }
public function getTrustTier(): string { return self::TRUST_COMMUNITY; }
public function getBundleClass(): string { return AcmeShippingPlugin::class; }
public function getModuleFlag(): string { return 'module.acme-shipping'; }
/** Jedes neue API-Praefix gehoert hier hinein. */
public function getRouteSegments(): array { return ['acme-shipping']; }
}
Drei Verbraucher lesen ihn: die Routen-Sperre, das Inventar in der Oberfläche und die Befehle des Lebenszyklus.
const VERSION ist die maßgebliche Quelle. Dieselbe Nummer steht in beiden Paketdateien und im Änderungsprotokoll. Ein Prüfer hält sie zusammen; der Versions-Befehl zieht alle mit.
getRouteSegments() ist die Zeile, die am häufigsten vergessen wird. Führst du ein neues API-Präfix ein und trägst es hier nicht nach, greift für dieses Segment keine Sperre. Die Route bleibt erreichbar, auch wenn das Modul aus ist.
Meist implementiert der Descriptor zusätzlich den Abhängigkeits-Vertrag:
public function getDependencies(): array
{
return ['octi.projects'];
}
Warum es zwei Manifest-Dateien gibt
Das ist die häufigste Verständnisfrage, und die Antwort ist eine zeitliche.
plugin.json liest der Installer, bevor die Anwendung startet. Er muss den Namensraum registrieren und das Bundle laden können, und dafür braucht er die Angaben, bevor es einen PHP-Descriptor gibt, den er fragen könnte.
{
"name": "acme.shipping",
"version": "0.1.0",
"namespace": "Acme\\Shipping\\",
"bundle": "Acme\\Shipping\\AcmeShippingPlugin",
"moduleFlag": "module.acme-shipping",
"routeSegments": ["acme-shipping"],
"hasFrontend": true,
"spiVersion": "3.15"
}
Der Namensraum braucht den abschließenden Rückstrich. name und moduleFlag müssen mit dem Descriptor übereinstimmen.
composer.json trägt die Anzeige-Metadaten:
{
"name": "acme/octibiz-shipping",
"version": "0.1.0",
"type": "octibiz-plugin",
"require": { "php": ">=8.4" },
"extra": {
"octibiz": {
"pluginKey": "acme.shipping",
"technicalName": "AcmeShipping",
"label": "Acme Versand",
"icon": "src/Resources/icon.svg",
"trustTier": "community"
}
}
}
Das Symbol wird als eingebettete Grafik ausgeliefert, damit die Oberfläche es ohne zweiten Abruf zeigt.
Die Vertragsversion
spiVersion nennt die Vertragsfläche, gegen die du gebaut hast. Bei der Installation aus einem Paket prüft das System sie vor jedem Seiteneffekt:
- Andere Hauptversion — harter Abbruch. Die Zusage der Rückwärtskompatibilität gilt nur
innerhalb einer Hauptlinie.
- Neuere Nebenversion als die Instanz — ebenfalls Abbruch. Du hast gegen etwas gebaut, das es
hier noch nicht gibt.
- Fehlt die Angabe — Installation mit Warnung im Protokoll.
Der Weg über den Paket-Manager prüft sie nicht. Dort bist du selbst verantwortlich.
Nicht im Plugin
Zwei Dinge, die man dort vermutet und die woanders liegen:
- Backend-Übersetzungen. Sie liegen zentral, nicht im Plugin. Nur die Übersetzungen der
Oberfläche gehören ins Plugin. Siehe Übersetzungen.
- PHP-Tests. Sie liegen zentral. Ein Test im Plugin-Ordner wird nie ausgeführt. Siehe
Weiter
- Backend — Datenmodell, API, Rechte
- Oberfläche — Ansichten und Andockstellen
- Verteilung — Paket bauen, Versionen führen