Backend
Server-seitige Referenz für Entwickler: Datenmodell, Migrationen, die Storefront-Route sowie das Erweiterungs-Framework (ProxyController + Upload-Messenger), auf dem die Social-Media-Sub-Plugins aufsetzen.
Datenmodell
Kern des Plugins sind der Blog-Beitrag und seine Kategorien:
Die DAL-Definitionen liegen unter src/Content/BlogItem/, src/Content/BlogItemCategory/ und den Aggregate/-Unterordnern. Wichtige Entity-Namen und Repository-IDs:
Entity-Name | Definition | Repository-ID |
|---|---|---|
|
|
|
|
| – |
|
|
|
|
| – |
Ergänzt wird das Modell durch shopware.entity.extension-Services (UserExtension, LanguageExtension, MediaExtension, CmsPageExtension), die Assoziationen an Shopware-Core-Entities anhängen.
Migrationen
Alle Migrationen unter src/Migration/ laufen bei der Plugin-Aktivierung:
Migration | Zweck |
|---|---|
| Blog-Item, Übersetzung, Kategorie, Media-Mapping |
|
|
| Social-Media-Tabellen ( |
| Custom-Field-Sets |
| CMS-Page-Anbindung |
| Spalte |
Der eigene CMS-Seitentyp blog wird nicht per Migration, sondern in MultiPurposeBlog::install()/::uninstall() verwaltet (Umschalten zwischen blog und landingpage über die CSS-Marker-Klasse was-blog-page).
Storefront-Route und Page-Loader
BlogController::detail delegiert an den BlogPageLoader. Dieser:
baut über den
GenericPageLoadereineBlogPage(extends Page),lädt den Beitrag mit
active = true, Media und Kategorien — über denBlogPageCriteriaEventerweiterbar,löst bei gesetztem
cmsPageIddie CMS-Page mit der beitragsindividuellenslotConfigund einemEntityResolverContextauf,befüllt die Meta-Informationen (
{Shopname} | metaTitle, Description mit{{ … }}-Platzhalter-Auflösung, Keywords),dispatcht abschließend den
BlogPageLoadedEvent.
Fehlt der Beitrag (oder ist er inaktiv), wird eine BlogItemNotFoundException geworfen.
SEO-URLs
BlogSeoRoute (Tag shopware.seo_url.route) registriert die Route frontend.blog.page mit dem Template /blog/{{ blogItem.translated.metaTitle }}. Der DynamicSeoUrlPageSubscriber hört auf die Write-/Delete-Events des Beitrags und ruft SeoUrlUpdater::update() — SEO-URLs bleiben so stets aktuell. Derselbe Subscriber entfernt beim Speichern verwaiste Kategorien.
CMS-Element-Resolver
Zwei AbstractCmsElementResolver (Tag shopware.cms.data_resolver) versorgen die Storefront-Elemente mit Daten:
BlogListingCmsElementResolver(getType() = 'blog-listing') — filtert nach Kategorie, sortiert nachreleaseAt, begrenzt die Anzahl und wendet (sofern „unveröffentlichte" nicht aktiv) denBlogItemAvailableFilteran.BlogItemCmsElementResolver(getType() = 'blog-item') — spiegelt bei benachbartemblog-listing-Slot dessen Konfiguration, sonst die manuell gewählten Beiträge.
Der BlogItemAvailableFilter (extends AndFilter) kapselt die Sichtbarkeitslogik (active, releaseAt <= now, releaseUntil >= now).
Erweiterungs-Framework für Sub-Plugins
Abstrakter ProxyController
src/Controller/ProxyController.php ist die abstrakte Basis jedes Social-Media-Sub-Plugin-Controllers (implements UploaderInterface). Sie ist per Klassen-Attribut unter /api/blog/default gemountet und liest die OAuth-Konfiguration (_provider, _oauth_url, _oauth_scope, _oauth_client_id) aus dem #[Route]-Attribut der konkreten Subklasse.
Bereits implementierte Endpunkte (im ApiRouteScope):
Route | Methode | Aufgabe |
|---|---|---|
| POST | Handshake gegen die Auth-Bridge ( |
| GET | Baut die OAuth-Autorisierungs-URL des Providers |
| * | Nimmt einen Upload an, legt eine |
| GET | Streamt den Upload-Fortschritt als Server-Sent-Events |
Abstrakt und von jeder Subklasse zu implementieren:
Upload-Messenger
Der Upload-Weg entkoppelt den langlaufenden Medien-Transfer vom Request:
ProxyController::upload()erzeugt eineUploadMessage(mediaId, handler: get_class($this), data, headers, endOffset)und dispatcht sie über denMessageBusInterface.UploadMessageimplementiertAsyncMessageInterface, läuft also im Worker.Der
UploadMessageHandler(#[AsMessageHandler]) erhält alle mitleoparden.blog.uploadergetaggten Services und rufthandleUpload()genau des Handlers auf, dessen Klassennamemessage->getHandler()entspricht.Der Fortschritt wird per
updateProgress()als serialisierte Datei (upload_progress_<id>.json) im Flysystem abgelegt;currentUploadProgress()liest sie im Sekundentakt und schließt den Stream, sobaldstartOffset === endOffset.
Ein Sub-Plugin registriert seinen Controller also mit dem Tag leoparden.blog.uploader, damit der Handler ihn findet. Der UploaderPass (Compiler-Pass, registriert in MultiPurposeBlog::build()) sammelt dieselben getaggten Services zusätzlich in der UploaderChain.
Strukturierte Daten (schema.org)
Die Klassen unter src/Content/Blog/Schema/ bauen das JSON-LD der Detailseite und der Listen. Der BlogPageLoader ruft sie auf; ein Event-Subscriber wäre hier die falsche Naht, weil load() jede Exception in eine BlogItemNotFoundException verwandelt — ein Fehler in der Auszeichnung würde den Beitrag also in einen 404 kippen.
Klasse | Aufgabe |
|---|---|
| Beitrag → |
| Beitragsliste → |
| Ergebnis: Rohdaten ( |
| die einzige |
|
|
| Herausgeber aus Shopname und Theme-Logo |
| Enum + Whitelist für den Typ |
| PHP-Gegenstück zu |
Die Seite stellt das Ergebnis über BlogPage::getSchema() bereit; gerendert wird es in storefront/layout/blog-schema.html.twig bzw. storefront/element/cms-element-blog-listing-schema.html.twig. Beide Partials sind absichtlich winzig und rufen keine Shopware-Twig-Funktion auf — nur so sind sie mit einer nackten Twig\Environment testbar, und ein Theme, das die Listendarstellung überschreibt, löscht die Auszeichnung nicht versehentlich mit.
Erweiterungspunkt
Wer einen anderen Herausgeber ausgeben will — eine Dachmarke, eine Redaktion oder später einmal echte Autoren —, dekoriert PublisherResolverInterface:
resolve() liefert eine SchemaOrganization (Name, optional URL und Logo).
Service-Tags im Überblick
Tag | Verwendung |
|---|---|
| DAL-Definitionen |
| Core-Entity-Erweiterungen |
| Blog-CMS-Element-Resolver |
|
|
| Sub-Plugin-Upload-Handler (vom |
|
|