Zum Inhalt springen
Zurück zum BlogBackend

Shopware App-Server: Bestelldaten verarbeiten

Wie ein App-Server über das Event order.written Bestelldaten abruft und weiterverarbeitet, inklusive Custom Fields und eigenem Admin-Modul.

Nico Schnaß· 8 Min. Lesezeit

Ein Shopware-App-Server kommt an Bestelldaten, indem er den Webhook order.written abonniert, daraus die ID der Bestellung liest und die Bestellung anschließend über die Admin API lädt. Dieser Beitrag zeigt den Weg Schritt für Schritt mit dem Symfony App-Bundle, inklusive Custom Fields und einem eigenen Modul in der Administration. Dasselbe Vorgehen funktioniert für jede Entität, die über die Admin API verfügbar ist und ein passendes Event besitzt.

Ein App-Server ist eine eigene Webanwendung außerhalb von Shopware, die Webhooks des Shops empfängt und über die Admin API mit ihm kommuniziert. Welche Events Shopware als Webhook verschickt, steht in der Webhook-Events-Referenz.

App per manifest.xml registrieren

Minimale manifest.xml anlegen

Um eine App in Shopware zu registrieren, genügt eine manifest.xml. Sie benötigt mindestens folgende Inhalte:

manifest.xml
<?xml version="1.0" encoding="UTF-8"?>
<manifest xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:noNamespaceSchemaLocation="https://raw.githubusercontent.com/shopware/shopware/trunk/src/Core/Framework/App/Manifest/Schema/manifest-3.0.xsd">
    <meta>
        <name>MyExampleApp</name>
        <label>Example App</label>
        <description>Example app for processing order data</description>
        <author>Jane Doe</author>
        <copyright>Example Company</copyright>
        <version>1.0.0</version>
        <icon></icon>
        <license>MIT</license>
    </meta>
    <setup>
        <registrationUrl>https://app-server.example.com/app/lifecycle/register</registrationUrl>
        <secret>my-app-secret</secret>
    </setup>
</manifest>

Die Werte secret, registrationUrl und der App-Name müssen an den verwendeten App-Server angepasst werden. Sobald der App-Server korrekt eingerichtet ist, kann die App in Shopware installiert und aktiviert werden.

Webhook für order.written registrieren

Doku: Webhooks

Um auf das Beispiel-Event order.written reagieren zu können, wird zunächst die Berechtigung order:read benötigt. Anschließend kann das gewünschte Event samt Ziel-URL definiert werden:

manifest.xml
<permissions>
    <permission>order:read</permission>
</permissions>
<webhooks>
    <webhook name="orderWritten" url="https://app-server.example.com/upload/order.written" event="order.written"/>
</webhooks>

Custom Fields an der Bestellung anlegen

Doku: Custom Fields

Custom Fields werden im Manifest über <custom-fields> definiert. Ein Set enthält Name, Labels, zugehörige Entitäten und die Felder. Das Beispiel in diesem Beitrag ist eine Wiedervorlage: Mitarbeitende markieren in der Bestellung, dass sie bis zu einem Datum erneut bearbeitet werden muss. Das folgende Set hängt dafür drei Felder an die Bestellung (<order/>), das Admin-Modul weiter unten zählt die fälligen Wiedervorlagen.

manifest.xml
<custom-fields>
    <custom-field-set>
        <name>resubmission</name>

        <label>Resubmission</label>
        <label lang="de-DE">Wiedervorlage</label>

        <related-entities>
            <order/>
        </related-entities>

        <fields>
            <bool name="resubmission_active">
                <label>Resubmission active</label>
                <label lang="de-DE">Wiedervorlage aktiv</label>
            </bool>
            <datetime name="resubmission_date">
                <label>Resubmission Date</label>
                <label lang="de-DE">Wiedervorlage Datum</label>
            </datetime>
            <text-area name="resubmission_note">
                <label>Note</label>
                <label lang="de-DE">Notiz</label>
            </text-area>
        </fields>
    </custom-field-set>
</custom-fields>

Webhook im App-Server verarbeiten

Webhook-Endpunkt im Controller anlegen

Doku: Symfony App-Server aufsetzen

Im App-Server wird ein POST-Endpunkt bereitgestellt, der die eingehende WebhookAction verarbeitet. Das Symfony App-Bundle prüft vor dem Aufruf des Controllers die Signatur im Header shopware-shop-signature mit dem Secret des Shops; ungültige Anfragen erreichen den Controller nicht.

WebhookController.php
#[AsController]
class WebhookController {
    #[Route('/upload/order.written', methods: ['POST'])]
    public function handle(WebhookAction $webhook): Response
    {
    }
}

Primärschlüssel aus dem Webhook auslesen

Shopware sendet im Webhook-Payload lediglich den Primärschlüssel der betroffenen Entität. Dieser kann wie folgt ausgelesen werden:

WebhookController.php
$payload = $webhook->payload;
$firstEvent = $payload[0] ?? null;
$primaryKey = $firstEvent['primaryKey'] ?? null;
if (\is_array($primaryKey)) {
    $orderId = $primaryKey[0] ?? null;
} else {
    $orderId = $primaryKey;
}

Da der App-Server nur die ID erhält, müssen weitere Daten über die Admin API nachgeladen werden. Der Payload ist eine Liste; das Beispiel verarbeitet nur den ersten Eintrag. order.written wird bei jedem Schreibvorgang auf die Bestellung ausgelöst, nicht nur beim Anlegen.

Access Token für die Admin API holen

Endpoint-Doku: Authentifizierung

Um Daten aus der Admin API abzurufen, wird zunächst ein Access Token per OAuth erzeugt. Die nötigen Zugangsdaten (client_id und client_secret) übergibt Shopware bei der Registrierung der App an den App-Server; das App-Bundle speichert sie zum Shop.

WebhookController.php
public function __construct(private readonly HttpClientInterface $httpClient)
{}

/* Existing Code */

$shop = $webhook->shop;
$shopUrl = \rtrim($shop->getShopUrl(), '/');

$tokenResponse = $this->httpClient->request('POST', $shopUrl.'/api/oauth/token',
    [
        'body' => [
            'grant_type' => 'client_credentials',
            'client_id' => $shop->getShopClientId(),
            'client_secret' => $shop->getShopClientSecret(),
        ]
    ]
);
$token = $tokenResponse->toArray()['access_token'] ?? null;

Bestelldaten abrufen

Endpoint-Doku: Order-Suche

Mit dem Token wird die Bestellung über die Suche der Admin API geladen. Der Body muss als JSON gesendet werden ('json' statt 'body'). Ohne associations liefert die Suche nur die Felder der Bestellung selbst; Positionen und Kunde werden ausdrücklich angefordert:

WebhookController.php
$response = $this->httpClient->request('POST', $shopUrl.'/api/search/order',
    [
        'headers' => ['Authorization' => 'Bearer '.$token],
        'json' => [
            'ids' => [$orderId],
            'associations' => [
                'lineItems' => [],
                'orderCustomer' => [],
            ],
        ]
    ]
);

$order = $response->toArray()['data'][0] ?? null;

$order enthält danach die Bestellung als Array mit Positionen (lineItems) und Kundendaten (orderCustomer). Die Custom Fields stehen unter customFields, zum Beispiel $order['customFields']['resubmission_date'].

Einfacher geht es mit der ClientFactory aus dem App-SDK: Sie holt und cacht das Access Token selbst, der Token-Request von oben entfällt.

WebhookController.php
public function __construct(private readonly ClientFactory $clientFactory)
{}

/* Existing Code */

$client = $this->clientFactory->createSimpleClient($webhook->shop);
$response = $client->post($shopUrl.'/api/search/order', [
    'ids' => [$orderId],
    'associations' => ['lineItems' => [], 'orderCustomer' => []],
]);

$order = $response->json()['data'][0] ?? null;

Custom Module in der Administration

Doku: Custom Modules

Ein Custom Module ist ein IFrame, das in der Shopware-Administration als eigene Seite eingebunden wird. Der Inhalt kommt vollständig vom App-Server. Im Beispiel zeigt das Modul „Wiedervorlagen“ unter Bestellungen an, wie viele Wiedervorlagen fällig sind.

Modul im Manifest registrieren

In der manifest.xml wird das Modul mit folgendem Abschnitt registriert:

manifest.xml
<admin>
    <module name="resubmissions"
            source="https://app-server.example.com/module/resubmissions"
            parent="sw-order"
            position="50">
                <label>Resubmissions</label>
                <label lang="de-DE">Wiedervorlagen</label>
    </module>
</admin>

Der Wert von source muss auf die entsprechende Route im App-Server verweisen, über die das Modul geladen wird.

Controller rendert das Modul-Template

Im App-Server genügt ein normaler Symfony-Controller. Das App-Bundle prüft auch hier die Signatur der Anfrage, bevor es die ModuleAction übergibt. Der Controller zählt über die Admin API alle Bestellungen mit aktiver Wiedervorlage, deren Datum erreicht ist, und übergibt die Zahl als dueCount an das Template:

ModuleController.php
class ModuleController extends AbstractController
{
    public function __construct(private readonly ClientFactory $clientFactory)
    {}

    #[Route('/module/resubmissions')]
    public function show(ModuleAction $module): Response
    {
        $client = $this->clientFactory->createSimpleClient($module->shop);
        $shopUrl = \rtrim($module->shop->getShopUrl(), '/');

        $response = $client->post($shopUrl.'/api/search/order', [
            'limit' => 1,
            'total-count-mode' => 1,
            'filter' => [
                ['type' => 'equals', 'field' => 'customFields.resubmission_active', 'value' => true],
                ['type' => 'range', 'field' => 'customFields.resubmission_date', 'parameters' => [
                    'lte' => (new \DateTimeImmutable())->format(\DATE_ATOM),
                ]],
            ],
        ]);

        return $this->render('module/resubmissions.html.twig', [
            'dueCount' => $response->json()['total'] ?? 0,
        ]);
    }
}

Mit 'total-count-mode' => 1 liefert die Suche die genaue Gesamtzahl im Feld total; 'limit' => 1 hält die Antwort klein, weil die Bestellungen selbst nicht gebraucht werden.

Template: IFrame als geladen melden

Das Template muss Shopware per postMessage melden, dass die Seite geladen ist; erst dann zeigt die Administration den IFrame an. Die Meldung wird genau einmal gesendet:

module/resubmissions.html.twig
<script>
    window.addEventListener('load', () => {
        window.parent.postMessage('sw-app-loaded', '*');
    });
</script>

Styling mit dem Meteor Design System

Das Meteor Design System ist das Design System der Shopware-Administration. Damit sieht das Modul aus wie der Rest der Administration, und die Admin Components stehen zur Verfügung.

Meteor Components mit Symfony UX Vue

Es lassen sich auch die Meteor Components mithilfe von Symfony UX Vue nutzen. Dazu müssen sowohl Symfony UX Vue als auch die Meteor Component Library gemäß der jeweiligen Dokumentation installiert werden. Das Beispiel setzt Webpack Encore voraus; require.context gibt es nur dort, nicht im AssetMapper. Zusätzlich wird für die Nutzung der Components das Plugin Vue I18n benötigt:

Terminal
npm install vue-i18n@11

Die app.js registriert die Vue-Controller und initialisiert Vue I18n für jede Komponente:

assets/app.js
import { registerVueControllerComponents } from '@symfony/ux-vue';
//import i18n
import { createI18n } from 'vue-i18n'

import './stimulus_bootstrap.js';
import "@shopware-ag/meteor-component-library/styles.css";
import "@shopware-ag/meteor-component-library/font.css";

//i18n initialisieren
document.addEventListener('vue:before-mount', (event) => {
    const i18n = createI18n({
        legacy: false,
        locale: 'de',
        fallbackLocale: 'en',
    })

    const {
        componentName, // The Vue component's name
        component, // The resolved Vue component
        props, // The props that will be injected to the component
        app, // The Vue application instance
    } = event.detail;

    app.use(i18n);
});

registerVueControllerComponents(require.context('./vue/controllers', true, /\.vue$/));

Danach lassen sich Vue-Controller mit Meteor Components schreiben. Jede verwendete Komponente wird importiert:

assets/vue/controllers/mt-card.vue
<template>
    <mt-card :isLoading="false" :large="false">
        <mt-text>
            {{ firstText }}
        </mt-text>
        <br />
        <template #title>
            {{ title }}
        </template>
    </mt-card>
</template>

<script setup>
import { MtCard, MtText } from "@shopware-ag/meteor-component-library";

const props = defineProps({
    firstText: {
        type: String,
        required: true,
    },
    title: {
        type: String,
        required: true,
    },
});
</script>

Im Twig-Template wird die Vue-Komponente über vue_component eingebunden:

module/resubmissions.html.twig
<div
    {{ vue_component('mt-card', {
        firstText: dueCount ~ ' Bestellungen',
        title: 'Fällige Wiedervorlagen',
    }) }}
></div>

Häufige Fragen

Welche Berechtigung braucht der Webhook order.written?

Die App benötigt die Berechtigung order:read. Sie wird in der manifest.xml unter <permissions> eingetragen.

Warum enthält der Webhook nur die ID der Bestellung?

Shopware sendet im Webhook-Payload lediglich den Primärschlüssel der betroffenen Entität. Alle weiteren Daten lädt der App-Server über die Admin API nach.

Woher bekommt der App-Server die Zugangsdaten für die Admin API?

Shopware übergibt client_id und client_secret bei der Registrierung der App, das App-Bundle speichert sie zum Shop. Damit erzeugt der App-Server per OAuth über /api/oauth/token ein Access Token, oder die ClientFactory des SDK übernimmt das.

Was ist ein Custom Module?

Ein Custom Module ist ein IFrame, das in der Shopware-Administration als eigene Seite eingebunden wird. Die Seite muss per postMessage('sw-app-loaded', '*') melden, dass sie geladen ist.

Prüft der App-Server die Signatur des Webhooks?

Ja, mit dem Symfony App-Bundle automatisch. Es prüft den Header shopware-shop-signature mit dem Secret des Shops, bevor der Controller die WebhookAction erhält.

Warum kommt order.written mehrmals an?

Das Event wird bei jedem Schreibvorgang auf die Bestellung ausgelöst, nicht nur beim Anlegen. Der App-Server muss deshalb damit rechnen, dieselbe Bestellung mehrfach zu erhalten.

Geschrieben vonNico SchnaßFull Stack Developer und Self-Hosting-Enthusiast.
@MobilGame06