MultiPurposeBlog 1.1.15 Help

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:

leoparden_blog_item (BlogItemDefinition) # active, location, schema_type, media_id (FK), cms_page_id (FK), # release_at, release_until ├── leoparden_blog_item_translation (übersetzbar: slotConfig, metaTitle, │ metaDescription, keywords, customFields) ├── categories (M:N via leoparden_blog_category_mapping) │ └── leoparden_blog_item_category (BlogItemCategoryDefinition: name) ├── media (M:1 → Shopware media) ├── cmsPage (M:1 → Shopware cms_page) └── posts (1:N → leoparden_blog_item_post, für Sub-Plugins)

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

leoparden_blog_item

BlogItemDefinition

leoparden_blog_item.repository

leoparden_blog_item_translation

BlogItemTranslationDefinition

–

leoparden_blog_item_category

BlogItemCategoryDefinition

leoparden_blog_item_category.repository

leoparden_blog_category_mapping

BlogItemCategoryMappingDefinition

–

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

Migration1670332694CreateBlogItem

Blog-Item, Übersetzung, Kategorie, Media-Mapping

Migration1716390249createCategoryMapping

leoparden_blog_category_mapping

Migration1716390250createPost

Social-Media-Tabellen (leoparden_social_media_*)

Migration1716390250addCustomFields

Custom-Field-Sets

Migration1751636640addCmsPageFields

CMS-Page-Anbindung

Migration1785402000AddSchemaType

Spalte schema_type (nullable, Default NewsArticle)

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

#[Route(path: '/blog/{blogItemId}', name: 'frontend.blog.page', defaults: ['_routeScope' => ['storefront']], methods: ['GET'])] public function detail(string $blogItemId, Request $request, SalesChannelContext $context): Response

BlogController::detail delegiert an den BlogPageLoader. Dieser:

  1. baut über den GenericPageLoader eine BlogPage (extends Page),

  2. lädt den Beitrag mit active = true, Media und Kategorien — über den BlogPageCriteriaEvent erweiterbar,

  3. löst bei gesetztem cmsPageId die CMS-Page mit der beitragsindividuellen slotConfig und einem EntityResolverContext auf,

  4. befüllt die Meta-Informationen ({Shopname} | metaTitle, Description mit {{ … }}-Platzhalter-Auflösung, Keywords),

  5. 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 nach releaseAt, begrenzt die Anzahl und wendet (sofern „unveröffentlichte" nicht aktiv) den BlogItemAvailableFilter an.

  • BlogItemCmsElementResolver (getType() = 'blog-item') — spiegelt bei benachbartem blog-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

/verify, /challenge, /refresh

POST

Handshake gegen die Auth-Bridge (https://auth-bridge.die-leoparden.de)

/redirect

GET

Baut die OAuth-Autorisierungs-URL des Providers

/upload/{mediaId}

*

Nimmt einen Upload an, legt eine UploadMessage an und dispatcht sie

/upload-progress/{uploadId}

GET

Streamt den Upload-Fortschritt als Server-Sent-Events

Abstrakt und von jeder Subklasse zu implementieren:

abstract public function proxy(string $path, Request $request, Context $context): Response; abstract public function handleUpload(UploadMessage $message): void;

Upload-Messenger

Der Upload-Weg entkoppelt den langlaufenden Medien-Transfer vom Request:

  1. ProxyController::upload() erzeugt eine UploadMessage(mediaId, handler: get_class($this), data, headers, endOffset) und dispatcht sie über den MessageBusInterface. UploadMessage implementiert AsyncMessageInterface, läuft also im Worker.

  2. Der UploadMessageHandler (#[AsMessageHandler]) erhält alle mit leoparden.blog.uploader getaggten Services und ruft handleUpload() genau des Handlers auf, dessen Klassenname message->getHandler() entspricht.

  3. 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, sobald startOffset === 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

BlogSchemaBuilder

Beitrag → NewsArticle/BlogPosting/Article

BlogItemListSchemaBuilder

Beitragsliste → ItemList (Kurzform, nur Position und URL)

BlogSchemaStruct

Ergebnis: Rohdaten (getData()) und fertiges JSON (getJson())

JsonLdEncoder

die einzige json_encode-Stelle, siehe unten

ContentLanguage

inLanguage aus der Kontext-Sprache

PublisherResolverInterface/ThemePublisherResolver

Herausgeber aus Shopname und Theme-Logo

BlogItemSchemaType

Enum + Whitelist für den Typ

BlogItemReleaseWindow

PHP-Gegenstück zu BlogItemAvailableFilter

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:

<service id="Acme\Blog\RedaktionsPublisherResolver" decorates="Leoparden\MultiPurposeBlog\Content\Blog\Schema\PublisherResolverInterface"> <argument type="service" id="Acme\Blog\RedaktionsPublisherResolver.inner"/> </service>

resolve() liefert eine SchemaOrganization (Name, optional URL und Logo).

Service-Tags im Überblick

Tag

Verwendung

shopware.entity.definition

DAL-Definitionen

shopware.entity.extension

Core-Entity-Erweiterungen

shopware.cms.data_resolver

Blog-CMS-Element-Resolver

shopware.seo_url.route

BlogSeoRoute

leoparden.blog.uploader

Sub-Plugin-Upload-Handler (vom UploadMessageHandler + UploaderChain konsumiert)

kernel.event_subscriber

DynamicSeoUrlPageSubscriber, UserLoadedSubscriber

Last modified: 09 September 2026