Eigene Checkout-Schritte in Magento 2 mit Hyvä hinzufügen
AI generated
M2
di.xml
Magento 2 · Hyvä Checkout · Magewire · Alpine.js
Eigene Checkout-Schritte in Magento 2
sauber mit Hyvä Checkout registrieren

Ein zusätzlicher Checkout-Schritt, etwa für eine Geschenkverpackungs-Auswahl oder eine Alters-Verifizierung, lässt sich in Magento 2 nicht einfach per Template-Override einbauen. Mit Hyvä Checkout und Magewire wird ein eigener Checkout-Schritt zu einer sauber registrierten, serverseitig validierten Komponente, ohne KnockoutJS und ohne fragile jsLayout-Merges.

17 Min. Lesezeit StepPool · Magewire Component · Alpine.js Magento 2.4.8-p4 · PHP 8.4

1. Warum ein eigener Checkout-Schritt nötig wird

Der native Magento-Checkout besteht aus zwei fest verdrahteten Schritten: Versand und Zahlung. Sobald ein Projekt mehr braucht, etwa eine Auswahl der Lieferzeitfensters, eine Zustimmung zu Sonderbedingungen oder eine Altersverifizierung bei bestimmten Produktkategorien, reicht dieses starre Zwei-Schritte-Modell nicht mehr aus. Ein zusätzlicher Checkout-Schritt ist dann keine kosmetische Änderung, sondern eine strukturelle Erweiterung des Bestellprozesses, die sauber in die bestehende Step-Pipeline eingehängt werden muss.

In einem klassischen Luma-Setup bedeutet das, tief in die KnockoutJS-Komponentenstruktur des Checkouts einzugreifen, jsLayout-Arrays per LayoutProcessor zu manipulieren und eigene Knockout-Templates zu registrieren. Für ein Hyvä-Projekt, das laut Projektkonvention explizit ohne KnockoutJS und ohne UI-Components auskommt, ist das kein gangbarer Weg. Genau hier setzt Hyvä Checkout an: Ein neuer Checkout-Schritt wird dort als PHP-Klasse mit einer Magewire-Komponente und einem Alpine.js-Template definiert, komplett ohne Knockout-Abhängigkeit.

Dieser Artikel zeigt den vollständigen Weg von der Idee bis zum produktiv laufenden Checkout-Schritt: die Architekturunterschiede zwischen Luma und Hyvä Checkout, die Modulstruktur für einen eigenen Schritt, die Registrierung in der Step-Pipeline, das Alpine.js-Template, die serverseitige Validierung, bedingte Sichtbarkeit sowie die Persistenz eigener Daten in der Quote. Der Fokus liegt durchgehend auf Magento 2.4.8-p4 mit PHP 8.4 und Constructor Property Promotion.

2. Checkout-Architektur: Luma-KnockoutJS versus Hyvä Checkout

Der native Magento-Checkout unter checkout_index_index.xml rendert eine einzige Single-Page-Application aus KnockoutJS-Komponenten, deren Struktur über ein verschachteltes jsLayout-Array beschrieben wird. Jeder Checkout-Schritt ist dort ein Knoten in diesem Baum, mit eigenem Knockout-Template, eigenem ViewModel und eigener Sichtbarkeitslogik über Magento_Checkout/js/model/step-navigator. Neue Schritte hinzuzufügen bedeutet, einen LayoutProcessorInterface-Plugin zu schreiben, der das jsLayout-Array zur Laufzeit ergänzt, und ein RequireJS-Modul zu registrieren, das als Knockout-Komponente lädt.

Hyvä Checkout ersetzt diese komplette Architektur durch Magewire, eine serverseitig gerenderte Komponentenschicht nach dem Vorbild von Livewire, kombiniert mit Alpine.js für rein clientseitige Interaktionen wie Akkordeons oder Toggle-Zustände. Ein Checkout-Schritt ist in diesem Modell keine Knockout-Komponente mehr, sondern eine PHP-Klasse, die Zustand hält, Validierung ausführt und ein .phtml-Template mit Alpine-Direktiven rendert. Der Roundtrip zwischen Frontend und Backend läuft über Magewire-Requests, nicht über die klassischen Checkout-REST-Endpunkte.

Für Projekte mit der Vorgabe kein Knockout.js, kein jQuery, keine UI-Components ist das der einzig konsistente Weg, einen zusätzlichen Checkout-Schritt zu bauen, ohne die Hyvä-Prinzipien zu verletzen. Die folgenden Abschnitte bauen konsequent auf diesem Magewire-Modell auf und zeigen, wie ein eigener Schritt als Mironsoft-Modul strukturiert wird.

3. Einen eigenen Checkout-Schritt als Modul definieren

Ein eigener Checkout-Schritt beginnt mit einer PHP-Klasse, die den Step-Vertrag von Hyvä Checkout implementiert. Diese Klasse liefert einen eindeutigen Code, eine Sortierposition, eine Sichtbarkeitsbedingung und eine Referenz auf die zugehörige Magewire-Komponente, die den eigentlichen Inhalt rendert. Wichtig für die Wartbarkeit: Die Step-Klasse selbst enthält keine Template-Logik, sie beschreibt nur Metadaten, während die Magewire-Komponente den Zustand und die Interaktion übernimmt.

Im Beispielprojekt handelt es sich um einen Checkout-Schritt für ein Lieferzeitfenster: Der Kunde wählt zwischen mehreren verfügbaren Zeitslots, bevor er zur Zahlung weitergeleitet wird. Die Klasse implementiert getCode(), getSortOrder() und isVisible(), wobei isVisible() beispielsweise prüft, ob das Versandverfahren überhaupt Zeitfenster unterstützt.


<?php

declare(strict_types=1);

namespace Mironsoft\CheckoutDeliverySlot\Model\Step;

use Hyva\Checkout\Model\CheckoutStepInterface;
use Magento\Checkout\Model\Session as CheckoutSession;
use Magento\Quote\Model\Quote;

/**
 * Custom checkout step that lets the customer pick a delivery time slot
 * before proceeding to the payment step.
 */
class DeliverySlotStep implements CheckoutStepInterface
{
    private const STEP_CODE = 'delivery-slot';
    private const SORT_ORDER = 15;

    /**
     * @param CheckoutSession $checkoutSession
     */
    public function __construct(
        private readonly CheckoutSession $checkoutSession
    ) {
    }

    /**
     * Unique identifier used by the step pool and by Magewire wire keys.
     *
     * @return string
     */
    public function getCode(): string
    {
        return self::STEP_CODE;
    }

    /**
     * Position between shipping (10) and payment (20) in the step pipeline.
     *
     * @return int
     */
    public function getSortOrder(): int
    {
        return self::SORT_ORDER;
    }

    /**
     * Only show this checkout step when the selected shipping method
     * actually supports delivery time slots.
     *
     * @return bool
     */
    public function isVisible(): bool
    {
        $quote = $this->checkoutSession->getQuote();
        $shippingMethod = (string) $quote->getShippingAddress()->getShippingMethod();

        return str_starts_with($shippingMethod, 'timeslotcarrier_');
    }
}

Diese Trennung zwischen Step-Metadaten und Magewire-Komponente zahlt sich vor allem bei Tests aus: isVisible() lässt sich isoliert mit einem präparierten Quote-Objekt testen, ohne dass ein Alpine.js-Template oder ein Magewire-Roundtrip beteiligt sein muss. Ein eigener Checkout-Schritt bleibt dadurch auch bei wachsender Komplexität nachvollziehbar.

4. Den Checkout-Schritt in der Step-Pipeline registrieren

Nach der Step-Klasse folgt die Registrierung über di.xml. Hyvä Checkout verwaltet die Reihenfolge aller Schritte über eine Step-Pool-Konfiguration, in die eigene Module ihren Checkout-Schritt per Named Argument eintragen. Diese Registrierung ist rein deklarativ und erfordert keine Änderung an Kernklassen, was Upgrades unproblematisch macht, weil kein Preference und kein Core-Override entsteht.

Neben der Registrierung des Schritts selbst muss auch die zugehörige Magewire-Komponente bekannt gemacht werden, damit sie beim Aufruf der Checkout-Seite geladen und in den Komponentenbaum eingehängt wird. Beide Registrierungen gehören zusammen in dieselbe di.xml, damit ein Reviewer beim Lesen des Moduls sofort sieht, welcher Schritt welche Komponente rendert.


<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <type name="Hyva\Checkout\Model\CheckoutStepPool">
        <arguments>
            <argument name="steps" xsi:type="array">
                <item name="delivery_slot" xsi:type="object">Mironsoft\CheckoutDeliverySlot\Model\Step\DeliverySlotStep</item>
            </argument>
        </arguments>
    </type>

    <type name="Hyva\Checkout\Model\Magewire\ComponentPool">
        <arguments>
            <argument name="components" xsi:type="array">
                <item name="delivery_slot" xsi:type="string">Mironsoft\CheckoutDeliverySlot\Model\Magewire\DeliverySlotSelector</item>
            </argument>
        </arguments>
    </type>
</config>

Ein häufiger Fehler bei der Registrierung eines eigenen Checkout-Schritts: Die Sortierposition kollidiert mit einem bereits vorhandenen Schritt, wodurch die Reihenfolge im Frontend nicht deterministisch wird. Es lohnt sich, alle registrierten Sortierpositionen einmalig aufzulisten, etwa mit einem kleinen CLI-Befehl, der die Step-Pool-Konfiguration ausliest, bevor ein neuer Schritt seine Position bekommt.

5. Alpine.js-Template und Magewire-Zustand verbinden

Die eigentliche Benutzeroberfläche des Checkout-Schritts lebt im .phtml-Template der Magewire-Komponente. Anders als bei einer klassischen Alpine-Komponente ohne Magewire hält x-data hier nur rein clientseitigen UI-Zustand, etwa welches Akkordeon gerade geöffnet ist. Die eigentlichen Geschäftsdaten, also der ausgewählte Zeitslot, werden über Magewire-Bindings mit dem PHP-Backend synchronisiert und dort validiert, bevor der Checkout in den nächsten Schritt wechselt.

Diese Aufteilung ist bewusst: Alpine.js übernimmt reine Präsentationslogik ohne Server-Roundtrip, Magewire übernimmt alles, was Validierung, Persistenz oder Geschäftslogik betrifft. Für einen Checkout-Schritt mit mehreren auswählbaren Zeitslots bedeutet das: Der Klick auf einen Slot löst einen Magewire-Call aus, der serverseitig prüft, ob der Slot noch verfügbar ist, und erst danach den Zustand im Frontend aktualisiert.


<div x-data="{ expanded: true }" class="border border-slate-200 rounded-xl p-4">
    <button type="button" @click="expanded = !expanded" class="flex items-center justify-between w-full font-semibold">
        <span>Lieferzeitfenster wählen</span>
        <svg x-show="!expanded" class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
        </svg>
    </button>

    <div x-show="expanded" class="mt-4 space-y-2">
        <template x-for="slot in $wire.entangle('availableSlots')" :key="slot.id">
            <label class="flex items-center gap-3 p-3 rounded-lg border cursor-pointer"
                   :class="$wire.entangle('selectedSlotId').get() === slot.id ? 'border-orange-500 bg-orange-50' : 'border-slate-200'">
                <input type="radio" name="delivery_slot"
                       @change="$wire.selectSlot(slot.id)"
                       :checked="$wire.entangle('selectedSlotId').get() === slot.id">
                <span x-text="slot.label"></span>
            </label>
        </template>
    </div>
</div>

Die Methode selectSlot() ist auf der Magewire-Komponente selbst als PHP-Methode implementiert, prüft dort die Verfügbarkeit des Slots gegen den aktuellen Lagerbestand des Versanddienstleisters und aktualisiert bei Erfolg den internen Zustand der Komponente. Der eigentliche Checkout-Schritt bleibt dadurch konsistent, selbst wenn zwei Kunden gleichzeitig denselben knappen Zeitslot auswählen wollen.

6. Serverseitige Validierung vor dem Schrittwechsel

Ein Checkout-Schritt ohne serverseitige Validierung ist nur eine hübsche Oberfläche. Bevor Hyvä Checkout den Wechsel zum nächsten Schritt erlaubt, ruft die Step-Pipeline eine Validierungsmethode der zugehörigen Magewire-Komponente auf. Erst wenn diese Methode keinen Fehler zurückgibt, wird der interne Fortschritt aktualisiert und der Kunde sieht den nächsten Schritt. Diese Validierung läuft vollständig serverseitig und kann deshalb nicht durch deaktiviertes JavaScript oder manipulierte Client-Requests umgangen werden.

Für den Lieferzeitfenster-Checkout-Schritt bedeutet das konkret: Bevor der Kunde zur Zahlung wechseln darf, prüft die Komponente erneut, ob der ausgewählte Slot noch existiert und ob er noch demselben Versandverfahren zugeordnet ist, das der Kunde im vorherigen Schritt gewählt hat. Ändert sich zwischen Auswahl und Bestätigung etwas, etwa weil der Slot durch einen anderen Kunden ausgebucht wurde, bricht die Validierung mit einer klaren Fehlermeldung ab, statt eine inkonsistente Bestellung zuzulassen.


<?php

declare(strict_types=1);

namespace Mironsoft\CheckoutDeliverySlot\Model\Magewire;

use Hyva\Checkout\Model\Magewire\Component\EvaluationResultFactory;
use Hyva\Checkout\Model\Magewire\Component\EvaluationResultInterface;
use Magento\Checkout\Model\Session as CheckoutSession;
use Mironsoft\CheckoutDeliverySlot\Api\SlotAvailabilityCheckerInterface;

/**
 * Server side validation executed before the checkout step pipeline
 * advances from the delivery slot step to the payment step.
 */
class DeliverySlotValidator
{
    /**
     * @param CheckoutSession $checkoutSession
     * @param SlotAvailabilityCheckerInterface $availabilityChecker
     * @param EvaluationResultFactory $resultFactory
     */
    public function __construct(
        private readonly CheckoutSession $checkoutSession,
        private readonly SlotAvailabilityCheckerInterface $availabilityChecker,
        private readonly EvaluationResultFactory $resultFactory
    ) {
    }

    /**
     * Re-check slot availability right before the step transition is allowed.
     *
     * @param int $selectedSlotId
     * @return EvaluationResultInterface
     */
    public function evaluate(int $selectedSlotId): EvaluationResultInterface
    {
        $quote = $this->checkoutSession->getQuote();

        if (!$this->availabilityChecker->isAvailable($selectedSlotId, (int) $quote->getId())) {
            return $this->resultFactory->createError(
                'Das gewählte Lieferzeitfenster ist inzwischen ausgebucht. Bitte einen anderen Slot wählen.'
            );
        }

        return $this->resultFactory->createSuccess();
    }
}

Wichtig ist, dass diese Validierung ausschließlich in der Magewire-Komponente lebt und nicht zusätzlich im Alpine-Template dupliziert wird. Eine clientseitige Vorab-Prüfung ist als reine Komfortfunktion völlig legitim, ersetzt aber niemals die serverseitige Prüfung, die als einzige tatsächlich über den Fortschritt im Checkout-Schritt entscheidet.

7. Reihenfolge, Sichtbarkeit und bedingtes Überspringen

Nicht jeder Kunde soll jeden Checkout-Schritt sehen. Die isVisible()-Methode aus der Step-Klasse entscheidet zur Laufzeit, ob ein Schritt überhaupt in der Pipeline auftaucht. Für den Lieferzeitfenster-Schritt bedeutet das: Kunden, die ein Versandverfahren ohne Zeitfenster gewählt haben, überspringen den Schritt komplett, ohne dass die Pipeline dafür Sonderfälle im Frontend abbilden muss.

Die Sortierposition steuert zusätzlich, an welcher Stelle zwischen den nativen Schritten Versand und Zahlung ein eigener Checkout-Schritt eingefügt wird. Bei mehreren eigenen Schritten aus unterschiedlichen Modulen lohnt sich eine feste Konvention, etwa Zehnerschritte zwischen den nativen Sortierpositionen, damit später ein weiterer Schritt eingeschoben werden kann, ohne bestehende Positionen zu verschieben. Fehlt diese Konvention, verschiebt jede neue Erweiterung potenziell die Reihenfolge aller anderen Schritte.

Für komplexere Bedingungen, etwa einen Checkout-Schritt, der nur für bestimmte Kundengruppen oder nur bei bestimmten Produktkategorien im Warenkorb erscheinen soll, empfiehlt sich, die Sichtbarkeitslogik in einen eigenen, testbaren Service auszulagern, statt sie direkt in isVisible() zu verschachteln. Die Step-Klasse ruft diesen Service dann nur noch auf, bleibt selbst aber schlank und gut lesbar.


#!/usr/bin/env bash
# List all registered checkout step sort positions before adding a new one,
# to avoid accidental collisions between modules
set -euo pipefail

bin/magento dev:di:info Hyva\\Checkout\\Model\\CheckoutStepPool \
  | grep -A1 "steps" \
  | sort -k2 -n

echo "Verify the new sort order does not collide with an existing step."

8. Eigene Daten in der Quote persistieren

Damit die Auswahl aus einem eigenen Checkout-Schritt die Zahlungsseite und später die Bestellung erreicht, müssen die Daten in der Quote persistiert werden. Die sauberste Methode dafür sind Extension Attributes auf Magento\Quote\Api\Data\CartExtensionInterface, deklariert über extension_attributes.xml und gesichert über eine eigene db_schema.xml-Tabelle mit Fremdschlüssel auf quote. Damit steht der gewählte Zeitslot über den gesamten weiteren Bestellprozess zur Verfügung, inklusive der Übernahme in die finale Order beim Platzieren der Bestellung.

Ein häufig übersehener Punkt: Extension Attributes der Quote werden beim Übergang zur Order nicht automatisch übernommen. Dafür braucht es einen Plugin auf Magento\Quote\Model\QuoteManagement::submit(), der die eigenen Daten explizit von der Quote in die Order kopiert. Ohne dieses Plugin verliert der Checkout die im eigenen Checkout-Schritt gesammelten Informationen genau in dem Moment, in dem sie für Fulfillment und Versand relevant werden.

9. Checkout-Schritt-Ansätze im Vergleich

Je nach Projektsetup und Zielarchitektur gibt es unterschiedliche Wege, einen zusätzlichen Checkout-Schritt umzusetzen. Für Hyvä-Projekte ist die Wahl in der Praxis fast immer eindeutig, die folgende Tabelle zeigt trotzdem die Kompromisse der Alternativen.

Ansatz Technologie Hyvä-Kompatibilität Empfehlung
LayoutProcessor + jsLayout KnockoutJS, RequireJS Verletzt kein-Knockout-Vorgabe Nur bei reinem Luma-Fallback
Hyvä Checkout Step PHP, Magewire, Alpine.js Vollständig kompatibel Standardweg für dieses Projekt
Eigene Standalone-Seite vor Checkout Controller, CMS-Block Kompatibel, aber kein echter Step Nur bei sehr einfachen Zwischenschritten
Modal statt Step Alpine.js, kein Server-Roundtrip nötig Kompatibel Bei rein bestätigenden Zusatzschritten

Die Entscheidung zwischen einem echten Checkout-Schritt und einem einfachen Modal hängt davon ab, ob eine serverseitige Validierung mit eigenem Fortschrittsstatus nötig ist. Braucht die Erweiterung nur eine Bestätigung ohne Persistenz, reicht ein Alpine-Modal innerhalb eines bestehenden Schritts, ohne die Step-Pipeline überhaupt zu berühren.

Mironsoft

Magento 2 Checkout-Entwicklung mit Hyvä Checkout und Magewire

Ein zusätzlicher Checkout-Schritt für euren Bestellprozess?

Wir konzipieren und implementieren eigene Checkout-Schritte in Magento 2 mit Hyvä Checkout, von der Step-Registrierung über Alpine.js-Templates bis zur serverseitigen Validierung und Datenpersistenz in der Quote.

Step-Konzeption

Sichtbarkeit, Sortierung und Datenmodell für neue Checkout-Schritte planen

Magewire-Komponenten

Serverseitige Validierung und Alpine.js-Interaktion sauber trennen

Quote & Order

Extension Attributes von der Quote sauber in die Order übernehmen

10. Zusammenfassung

Eigene Checkout-Schritte in Magento 2 lassen sich in einem Hyvä-Projekt nicht sinnvoll über die klassische KnockoutJS-Architektur bauen, weil das den Grundprinzipien des Themes widerspricht. Hyvä Checkout ersetzt diesen Weg durch Magewire-Komponenten mit Alpine.js-Templates: Eine Step-Klasse liefert Code, Sortierung und Sichtbarkeit, eine Magewire-Komponente hält Zustand und Validierung, ein Alpine-Template übernimmt reine Präsentationslogik. Die Registrierung läuft vollständig über di.xml, ohne Core-Override.

Der entscheidende Qualitätsfaktor bei jedem eigenen Checkout-Schritt ist die serverseitige Validierung unmittelbar vor dem Wechsel zum nächsten Schritt, kombiniert mit einer sauberen Übernahme der gesammelten Daten von der Quote in die finale Order. Wer diese beiden Punkte konsequent umsetzt, bekommt einen Checkout-Schritt, der sich nahtlos in die native Step-Pipeline einfügt und auch bei parallelen Bestellungen konsistent bleibt.

Eigene Checkout-Schritte in Magento 2, das Wichtigste auf einen Blick

Architektur

Hyvä Checkout ersetzt KnockoutJS-Steps durch Magewire-Komponenten mit Alpine.js-Templates, ohne UI-Components.

Registrierung

Step und Komponente ausschließlich über di.xml im Step-Pool eintragen, kein Preference nötig.

Validierung

Serverseitige Prüfung in der Magewire-Komponente entscheidet über den Schrittwechsel, nie das Alpine-Template allein.

Datenpersistenz

Extension Attributes auf der Quote plus explizites Plugin für die Übernahme in die Order beim Submit.

11. FAQ: Eigene Checkout-Schritte in Magento 2

1Wie füge ich einen eigenen Checkout-Schritt hinzu?
Step-Klasse plus Magewire-Komponente mit Alpine.js-Template implementieren, beide über di.xml im Step-Pool registrieren.
2Warum keine KnockoutJS-Steps bei Hyvä?
Hyvä setzt konsequent auf Alpine.js statt Knockout und UI-Components, LayoutProcessor-Ansätze würden das unterlaufen.
3Step-Klasse vs. Magewire-Komponente?
Step-Klasse liefert nur Metadaten, Magewire-Komponente hält Zustand, Validierung und Template-Rendering.
4Wie steuere ich die Sichtbarkeit?
Über isVisible in der Step-Klasse, komplexere Bedingungen in einen eigenen testbaren Service auslagern.
5Wo läuft die Validierung?
Ausschließlich serverseitig in der Magewire-Komponente, Alpine-Prüfungen sind nur Komfort, kein Ersatz.
6Kollision bei Sortierpositionen?
Feste Konvention mit Zehnerschritten zwischen nativen Positionen verhindert nicht-deterministische Reihenfolge.
7Wie kommen Daten in die Bestellung?
Extension Attributes auf der Quote plus Plugin auf QuoteManagement::submit für die Übernahme in die Order.
8Gleichzeitige Auswahl derselben Ressource?
Serverseitige Validierung prüft Verfügbarkeit direkt vor dem Schrittwechsel erneut und bricht bei Konflikt klar ab.
9Auch als einfaches Modal möglich?
Ja, ohne serverseitige Validierung reicht ein Alpine.js-Modal innerhalb eines bestehenden Schritts.
10Müssen Core-Dateien überschrieben werden?
Nein, die komplette Registrierung läuft über di.xml und eigene Klassen, ohne Preferences oder Template-Overrides.