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:
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/:idundcreate— beide mit Kind-Routengeneralundcontent(die beiden Editor-Tabs)Store:
Shopware.Store.register({ id: 'leoparden-blog', … })hält den aktuell bearbeitetenblogItemCMS-Seitentyp:
cmsPageTypeService.register({ name: 'blog', … })
Die Komponenten werden per Lazy-Import registriert, z. B.:
Verzeichnisstruktur des Moduls:
Ordner | Inhalt |
|---|---|
|
|
| Tab-Inhalte: |
| Layout-Karte, Social-Media-Komponenten, |
| Basisklassen |
|
|
|
|
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-sourcean denProxyController-SSE-Stream gekoppelt)den
proxy()-Aufruf gegen den serverseitigenProxyControllereinen
Cacheund 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 |
|---|---|
| verwirft Token, |
frische Autorisierung ( |
|
Profil-Id ≠ gespeicherte Id |
|
getAccount()wird bewusst selten aufgerufen — die Profil-APIs sind hart limitiert. Ein normalercheckLogin()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, findetgetPagesProxy()die Seiten übercustomFieldswieder.Ein Unter-Provider (Instagram hängt am Konto von Facebook:
oAuthProviderliefert"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 dasclearCaches()überaccountSiblings; alles Weitere (eigenerpost-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 derslotConfig)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 einemtime-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-ElementeElement
blog-listing(elements/blog-listing/…) — Component, Config und Preview; Config nutzt dascms-element-Mixin undinitElementConfig('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 |
|---|---|
| die neun eigenen Entitäten, vollständig |
| nur die Felder, die dieses Plugin an |
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
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.