MultiPurposeBlog 1.1.15 Help

Frontend

Client-seitige Referenz für Entwickler: der Aufbau des Admin-Moduls leoparden-blog, die abstrakten TypeScript-Basisklassen, von denen die Social-Media-Sub-Plugins ihren Provider ableiten, sowie die CMS-Element- Registrierung und die Storefront-Assets.

Admin-Bundle

Das Administrations-Bundle liegt unter src/Resources/app/administration/. Einstieg ist src/main.ts, das zwei Module lädt:

import './module/leoparden-blog'; import './module/sw-cms';

Das npm-Paket exportiert unter dem Alias @leoparden/multi-purpose-blog-administration/… — daran erkennt man in den Sub-Plugins die Imports auf die Basisklassen.

Modul leoparden-blog

module/leoparden-blog/index.ts registriert das Vue-Modul über Module.register('leoparden-blog', …):

  • Typ/Entity: type: 'plugin', entity: 'leoparden_blog_item'

  • Navigation: Eintrag unter sw-content (Menü Inhalte → Blog)

  • Routen: index (Liste), detail/:id und create — beide mit Kind-Routen general und content (die beiden Editor-Tabs)

  • Store: Shopware.Store.register({ id: 'leoparden-blog', … }) hält den aktuell bearbeiteten blogItem

  • CMS-Seitentyp: cmsPageTypeService.register({ name: 'blog', … })

Die Komponenten werden per Lazy-Import registriert, z. B.:

Component.register('leoparden-blog-index', () => import('./page/leoparden-blog-index')); Component.register('leoparden-blog-detail', () => import('./page/leoparden-blog-detail')); Component.register('leoparden-blog-general', () => import('./views/leoparden-blog-general')); Component.register('leoparden-blog-content', () => import('./views/leoparden-blog-content'));

Verzeichnisstruktur des Moduls:

Ordner

Inhalt

page/

leoparden-blog-index (Liste), leoparden-blog-detail (Editor-Rahmen)

views/

Tab-Inhalte: -general, -content, -social-media, -not-implemented

component/

Layout-Karte, Social-Media-Komponenten, sw-custom-field-set-detail-base-Override

service/

Basisklassen PageProvider/oAuthProvider + Helfer (Account, Page, Post, Cache, Notifier)

mixin/, helper/

tabMixin, registerModule, getStoreName, …

snippet/

de-DE.json/en-GB.json (Präfix plugin.leoparden.MultiPurposeBlog.*)

Abstrakte Basisklassen für Sub-Plugins

Der Kern der Erweiterbarkeit sind zwei abstrakte TS-Klassen in module/leoparden-blog/service/. Ein Sub-Plugin (Meta, LinkedIn, Brevo) leitet seinen Provider davon ab.

oAuthProvider

service/oAuthProvider.ts — Basis für Authentifizierung und Account-Verwaltung (extends Notifier). Sie kapselt:

  • den OAuth-Login-Flow gegen die Auth-Bridge (Events loginSuccess, loginFailed, logoutSuccess, accountLoaded)

  • ein typisiertes Event-System (oAuthEventMap) inkl. der Upload-Fortschritts- Events (progress_open/progress_message/progress_closed, über @microsoft/fetch-event-source an den ProxyController-SSE-Stream gekoppelt)

  • den proxy()-Aufruf gegen den serverseitigen ProxyController

  • einen Cache und die Provider-Identität (get name() = Klassenname)

Abstrakt zu implementieren u. a.: accountType, getAccount().

Konto-Identität und Kontowechsel

Der Datensatz leoparden_social_media_account gehört dem Shopware-Benutzer (eine Zeile je Benutzer und Provider), nicht dem Social-Media-Konto. Welches Konto tatsächlich verbunden ist, sagt allein das über getAccount() geholte Profil. Daraus folgt der Lebenszyklus, auf den sich jedes Sub-Plugin verlassen kann:

Ereignis

Was die Basisklasse tut

logout()

verwirft Token, loggedIn, Profil (customFields = {}) und den Proxy-Cache — auch den der Provider am selben Konto —, löst die Seiten vom Account und leert die Seitenauswahl

frische Autorisierung (login(challenge) → verify)

onAuthorizationChanged(): leert den Cache, setzt reloadAccountFromApi und verwirft das Ergebnis der Zugriffsprüfung; das Profil wird einmal neu geholt

Profil-Id ≠ gespeicherte Id

accountIdentityChanged (vor accountLoaded) → PageProvider::discardPages()

  • getAccount() wird bewusst selten aufgerufen — die Profil-APIs sind hart limitiert. Ein normaler checkLogin() mit gültigem Token holt kein Profil.

  • Seiten-Datensätze werden nie gelöscht, nur die m:n-Verknüpfung zum Account (accountPagesRepository, verschachtelte Route /leoparden-social-media-account/<id>/pages). Ein Speichern der Association legt nur an; Löschen geht ausschließlich über diese Route. Meldet sich dasselbe Konto später wieder an, findet getPagesProxy() die Seiten über customFields wieder.

  • Ein Unter-Provider (Instagram hängt am Konto von Facebook: oAuthProvider liefert "Facebook", dataProvider "Instagram") teilt Account, Token und Seitenauswahl mit dem Eltern-Provider, hat aber einen eigenen Cache und einen eigenen Event-Bus — emit() erreicht nur die Listener der emittierenden Instanz. Cache-übergreifend löst das clearCaches() über accountSiblings; alles Weitere (eigener post-Store, Ansichtszustand) muss der Unter-Provider selbst am Eltern-Bus mithören.

PageProvider

service/index.ts — PageProvider extends oAuthProvider. Ergänzt die Verwaltung von Seiten und Posts eines Providers und die Verzahnung mit dem aktuellen Blog-Beitrag:

  • Zugriff auf den aktuellen Beitrag (get blogItem() aus dem Store) und dessen Medien (get blogItemMedia() — extrahiert Media-IDs aus der slotConfig)

  • Ableitung von Default-Content/-Medien aus dem Beitrag für neue Posts (getDefaultPost(), getDefaultContent(), getDefaultMedia())

  • Seiten-/Post-Synchronisation zwischen Provider-API und DAL (getPagesProxy(), getCurrentPostProxy())

  • Link-Vorschau-Parsing (parseLink()) über den Server-Proxy

Von der Subklasse zu implementieren: postType, pageType, maxContentLength, postUrl, getPages(), getPost(), sendPost(), updatePost(), deletePost(), uploadMedia(), refreshCurrentPage().

Vertrag von getPages(): die Antwort ist die Zugriffsliste

getPages() muss genau die Seiten liefern, die das gerade autorisierte Konto bespielen darf — nicht mehr und nicht weniger. Die Basisklasse behandelt die Antwort als Zugriffsliste: was darin fehlt, wird über die m:n-Route vom Konto gelöst und verschwindet aus der Auswahl (reconcilePageAccess()).

Daraus folgen drei Regeln für eine Implementierung:

  • Nie die gespeicherten Seiten ins Ergebnis mischen. Sie dürfen als Zwischenspeicher für Anzeigedaten dienen (ein Detail-Call weniger), aber nie bestimmen, welche Seiten im Ergebnis stehen.

  • Im Fehlerfall werfen, nicht leer zurückgeben. Eine leere Liste ist die Aussage „dieses Konto darf gar nichts mehr" und löst alle Seiten. Wirft getPages(), bleibt dagegen alles unangetastet.

  • Nicht aus dem Proxy-Cache antworten. proxy() kennt dafür die fetch-üblichen Modi: {cache: 'reload'} holt neu und legt die Antwort wieder ab, {cache: 'no-store'} holt neu und legt sie gar nicht erst ab. (Facebook und Brevo umgehen den Cache stattdessen historisch mit einem time-Parameter in der URL.)

Wann geprüft wird, entscheidet shouldCheckPageAccess(): solange keine Seite gespeichert ist immer, sonst einmal je Admin-Sitzung — manche Provider zählen jeden Aufruf hart gegen ein Kontingent. Nach einer frischen Autorisierung setzt onAuthorizationChanged() die Marke zurück.

Weitere Bausteine: linkedPages (alle verknüpften Seiten, ungefiltert) gegenüber pages (nur die bestätigten), identifyPage() (die providerseitige Kennung aus pageIdentifier — bewusst ohne Rückfall auf die DAL-Uuid) und das Ereignis pageAccessRevoked.

CMS-Integration (sw-cms)

module/sw-cms/ registriert Blöcke und Elemente für die Erlebniswelten:

  • Block blog (blocks/blog/…) — Container für die Blog-Elemente

  • Element blog-listing (elements/blog-listing/…) — Component, Config und Preview; Config nutzt das cms-element-Mixin und initElementConfig('blog-listing')

  • Element blog-item (elements/blog-item/…) — analog für die Einzel-Vorschau

Die Element-Typen (blog-listing, blog-item) korrespondieren 1:1 mit den serverseitigen Resolvern (siehe Backend). Die zugehörigen Storefront- Templates liegen unter src/Resources/views/storefront/element/.

Storefront-Assets

Das Storefront-Bundle (src/Resources/app/storefront/) enthält im Basis-Plugin keine eigenen JavaScript-Plugins, sondern nur Styles (src/scss/base.scss). Die Storefront-Ausgabe erfolgt vollständig über Twig-Templates unter src/Resources/views/storefront/ (Seite, Block, Elemente, Meta-Layout).

Die Admin-Tests und das Entitäten-Schema

Das gemeinsame Prüfgerüst erzeugt sein Entitäten-Schema aus dem laufenden Shop (bin/console framework:schema). Darin stehen nur die Entitäten der Plugins, die dort installiert und aktiv sind — auf jedem Rechner, auf dem dieses Plugin nicht installiert ist, fehlten also leoparden_blog_* und leoparden_social_media_*, und alle Specs starben im Mock-Repository mit Cannot read properties of undefined (reading 'entity'). Die Meldung nennt weder die Entität noch die Ursache.

Die Definitionen liegen deshalb eingefroren neben den Tests:

Datei

Inhalt

test/fixtures/schema.json

die neun eigenen Entitäten, vollständig

test/fixtures/schema-extensions.json

nur die Felder, die dieses Plugin an cms_page, media, language und user ergänzt

Die Kern-Entitäten werden nicht ersetzt, sondern ergänzt (jest.init.ts). Würde man sie vollständig einfrieren, trüge das Fixture die Erweiterungen jedes anderen Plugins mit, das beim Erzeugen zufällig aktiv war — und die Abhängigkeit wäre nur verschoben.

Auffrischen

ddev exec bash -c 'cd custom/plugins/multi-purpose-blog \ && APP_ENV=test KERNEL_CLASS="Shopware\Core\Kernel" php src/Test/Administration/dump-schema.php'

Gelesen wird aus der Testdatenbank, in der der PHPUnit-Bootstrap das Plugin ohnehin installiert hält — der Entwicklungs-Shop muss es dafür nicht tragen.

Last modified: 12 September 2026