vom naiven Warenkorb-Button zur geprüften Teil-Bestellung
Eine robuste Reorder-Funktion tut mehr, als Bestellpositionen blind in den Warenkorb zu kopieren: Sie prüft vor dem Klick, ob Produkte noch verkäuflich sind, zeigt fehlende Artikel transparent an und lässt Kunden über einen Alpine.js-Bestätigungsdialog bewusst entscheiden, welche Positionen tatsächlich erneut bestellt werden, sauber über Controller, ViewModel, CSRF-Schutz und CSP-konforme Skripte abgesichert.
Inhaltsverzeichnis
- 1. Warum "alle Artikel in den Warenkorb legen" nicht reicht
- 2. Bestellhistorie und Bestellansicht in Hyvä: wo der Reorder-Button sitzt
- 3. Modularchitektur: eigenes Modul statt Core-Override
- 4. Controller: Bestellung laden und sellable Items ermitteln
- 5. ViewModel: Verfügbarkeit vor dem Klick prüfen
- 6. Template: Button und Inline-Warnung im Bestellverlauf
- 7. Alpine.js-Bestätigungsdialog für den Teil-Reorder
- 8. CSP-Konformität und Store-View-Konfiguration
- 9. Reorder-Funktion im Vergleich: Patterns gegenübergestellt
- 10. Zusammenfassung
- 11. FAQ
1. Warum "alle Artikel in den Warenkorb legen" nicht reicht
Die naheliegende Idee für eine Reorder-Funktion klingt simpel: Kunde klickt auf "Erneut bestellen", alle Positionen der alten Bestellung landen im Warenkorb, fertig. In der Praxis hält diese Annahme dem ersten echten Testfall selten stand. Produkte werden zwischen Bestellung und Reorder-Versuch deaktiviert oder komplett gelöscht, Lagerbestände sind erschöpft, Konfigurationsoptionen eines konfigurierbaren Produkts existieren in dieser Kombination nicht mehr, und Preise haben sich seit dem letzten Kauf geändert. Jeder dieser Fälle bricht eine naive Implementierung an einer anderen Stelle, meist lautlos.
Besonders unangenehm wird es, wenn eine Reorder-Funktion Fehler verschluckt, statt sie zu melden: Der Kunde landet im Warenkorb, zählt nur drei statt fünf erwarteter Positionen, und muss selbst herausfinden, welche zwei fehlen und warum. Das erzeugt Support-Anfragen und Vertrauensverlust genau an der Stelle, an der ein Wiederkauf eigentlich reibungslos verlaufen sollte. Die folgenden Abschnitte bauen deshalb eine Reorder-Funktion, die Verfügbarkeit vor dem Klick prüft, Probleme transparent benennt und dem Kunden eine bewusste Entscheidung über eine Teil-Bestellung ermöglicht.
2. Bestellhistorie und Bestellansicht in Hyvä: wo der Reorder-Button sitzt
Magento_Sales rendert die Liste vergangener Bestellungen über order/history.phtml und die Detailansicht einer einzelnen Bestellung über order/view.phtml. Beide Templates enthalten in der Standardauslieferung bereits einen Reorder-Link, der auf die Route sales/order/reorder zeigt und von Magento\Sales\Controller\Order\Reorder verarbeitet wird. Dieser Core-Controller iteriert die Bestellpositionen und ruft für jede einzelne Magento\Checkout\Model\Cart::addOrderItem() auf, ganz ohne vorherige Prüfung auf Verkäuflichkeit. Genau dieser Mechanismus ist die Wurzel der im ersten Abschnitt beschriebenen Probleme, er ist keine theoretische Annahme, sondern der tatsächliche Standardweg.
In Luma wird der Reorder-Link zusätzlich von einem Knockout-Widget mit Bestätigungsdialog umgeben. Hyvä ersetzt dieses Widget vollständig durch reines phtml-Markup mit Tailwind-Klassen, ohne Knockout.js, ohne UI-Components und ohne jQuery-Abhängigkeit. Für den eigenen Reorder-Button bedeutet das: Statt den Core-Controller weiterzuverwenden und nur den Bestätigungsdialog zu ersetzen, ist der sauberere Weg ein eigener Controller, der von Anfang an Verkäuflichkeit prüft, statt Fehler erst im Warenkorb sichtbar zu machen.
3. Modularchitektur: eigenes Modul statt Core-Override
Ein eigenes Modul, hier als Mironsoft_Reorder bezeichnet, ersetzt über Layout-XML den Standard-Reorder-Link in order/view.phtml und order/history.phtml durch einen eigenen Block mit eigener Route. Das vermeidet jeden Core-Override von Magento_Sales-Templates und bleibt bei jedem Magento-Update kompatibel, weil ausschließlich additiv über referenceBlock und eine neue Controller-Klasse gearbeitet wird. Die neue Route, etwa reorder/order/confirm, ersetzt den bisherigen GET-Link durch ein POST-Formular, was für den späteren CSRF-Schutz über den form_key unumgänglich ist.
Die Modulstruktur folgt dem üblichen Hyvä-Aufbau: ein Controller im Namespace Mironsoft\Reorder\Controller\Order, ein ViewModel unter Mironsoft\Reorder\ViewModel, ein system.xml für die Store-View-Konfiguration und eine acl.xml für die Admin-Berechtigung der Einstellungen. Die eigentliche Reorder-Funktion steckt dabei fast vollständig in Controller und ViewModel, das Template bleibt reine Präsentationsschicht und bindet lediglich das Ergebnis der Verfügbarkeitsprüfung an Alpine.js.
4. Controller: Bestellung laden und sellable Items ermitteln
Der Controller lädt die Bestellung über OrderRepositoryInterface, prüft, dass die Bestellung tatsächlich dem eingeloggten Kunden gehört, und ermittelt anschließend für jede Position, ob das zugehörige Produkt noch verkäuflich ist. Für die sellable Items wird der BuyRequest der ursprünglichen Bestellposition wiederverwendet, ein DataObject mit Menge und gewählten Optionen, das Magento\Sales\Model\Order\Item::getBuyRequest() bereits mitbringt. Die aktive Kunden-Quote wird über CartRepositoryInterface geladen oder bei Bedarf neu angelegt, dann über addProduct() um die geprüften Positionen ergänzt.
<?php
declare(strict_types=1);
namespace Mironsoft\Reorder\Controller\Order;
use Magento\Customer\Controller\AbstractAccount;
use Magento\Customer\Controller\AccountInterface;
use Magento\Customer\Model\Session as CustomerSession;
use Magento\Framework\App\Action\Context;
use Magento\Framework\App\Action\HttpPostActionInterface;
use Magento\Framework\Controller\Result\RedirectFactory;
use Magento\Framework\Controller\ResultInterface;
use Magento\Framework\Exception\LocalizedException;
use Magento\Quote\Api\CartManagementInterface;
use Magento\Quote\Api\CartRepositoryInterface;
use Magento\Sales\Api\OrderRepositoryInterface;
use Mironsoft\Reorder\Model\SellableItemFilter;
/**
* Adds still-sellable items from a past order back into the customer's active quote.
*/
class Confirm extends AbstractAccount implements HttpPostActionInterface, AccountInterface
{
/**
* @param Context $context Request/response context required by AbstractAccount.
* @param CustomerSession $customerSession Current logged-in customer session.
* @param OrderRepositoryInterface $orderRepository Loads the source order by id.
* @param CartRepositoryInterface $cartRepository Loads and persists the active quote.
* @param CartManagementInterface $cartManagement Creates a quote if none exists yet.
* @param SellableItemFilter $sellableItemFilter Splits order items into sellable and skipped.
* @param RedirectFactory $redirectFactory Builds the redirect result after processing.
*/
public function __construct(
Context $context,
private readonly CustomerSession $customerSession,
private readonly OrderRepositoryInterface $orderRepository,
private readonly CartRepositoryInterface $cartRepository,
private readonly CartManagementInterface $cartManagement,
private readonly SellableItemFilter $sellableItemFilter,
private readonly RedirectFactory $redirectFactory
) {
parent::__construct($context);
}
/**
* Reorder the selected, still-sellable items into the active customer quote.
*
* @return ResultInterface
*/
public function execute(): ResultInterface
{
$orderId = (int) $this->getRequest()->getParam('order_id');
$selectedItemIds = (array) $this->getRequest()->getParam('items', []);
$customerId = (int) $this->customerSession->getCustomerId();
$order = $this->orderRepository->get($orderId);
// Guard: never let a customer reorder another customer's order
if ((int) $order->getCustomerId() !== $customerId) {
throw new LocalizedException(__('This order does not belong to your account.'));
}
$result = $this->sellableItemFilter->filter($order->getItems(), $selectedItemIds);
$quoteId = $this->cartManagement->getCartForCustomer($customerId)->getId();
$quote = $this->cartRepository->get($quoteId);
foreach ($result->getSellableItems() as $orderItem) {
$buyRequest = $orderItem->getBuyRequest();
$quote->addProduct($orderItem->getProduct(), $buyRequest);
}
$quote->setTotalsCollectedFlag(false);
$this->cartRepository->save($quote);
$resultRedirect = $this->redirectFactory->create();
if ($result->getSkippedCount() > 0) {
$this->messageManager->addWarningMessage(
__('%1 of %2 items could not be reordered and were skipped.', $result->getSkippedCount(), $result->getTotalCount())
);
}
$this->messageManager->addSuccessMessage(__('The available items have been added to your cart.'));
return $resultRedirect->setPath('checkout/cart');
}
}
Wichtig ist die Guard-Klausel gegen fremde Bestellungen: Ohne den Vergleich der Kunden-ID ließe sich über eine manipulierte order_id theoretisch jede beliebige Bestellung eines anderen Kunden reorderen, ein klassischer Insecure-Direct-Object-Reference-Fehler. Die eigentliche Trennung von sellable und nicht-sellable Positionen liegt bewusst nicht im Controller selbst, sondern in einer eigenen SellableItemFilter-Klasse, die dieselbe Prüfung auch dem ViewModel im nächsten Abschnitt zur Verfügung stellt, ohne Logik zu duplizieren.
5. ViewModel: Verfügbarkeit vor dem Klick prüfen
Die eigentliche Stärke einer sauberen Reorder-Funktion zeigt sich nicht erst im Controller, sondern schon beim Rendern der Bestellansicht. Ein ViewModel, das ArgumentInterface implementiert, führt dieselbe Verfügbarkeitsprüfung wie der Controller bereits vor dem Klick durch und liefert dem Template ein strukturiertes Ergebnis, aus dem sich eine Inline-Warnung wie "2 von 5 Artikeln sind nicht mehr verfügbar" direkt ableiten lässt.
<?php
declare(strict_types=1);
namespace Mironsoft\Reorder\ViewModel;
use Magento\Catalog\Api\ProductRepositoryInterface;
use Magento\Framework\Exception\NoSuchEntityException;
use Magento\Framework\View\Element\Block\ArgumentInterface;
use Magento\InventorySalesApi\Api\IsProductSalableInterface;
use Magento\Sales\Api\Data\OrderInterface;
use Magento\Store\Model\StoreManagerInterface;
/**
* Pre-checks saleability of order items so the order view can warn before reorder.
*/
class ReorderAvailability implements ArgumentInterface
{
/**
* @param ProductRepositoryInterface $productRepository Loads the current product state.
* @param IsProductSalableInterface $isProductSalable Checks saleable status per sales channel.
* @param StoreManagerInterface $storeManager Resolves the current website for the stock check.
*/
public function __construct(
private readonly ProductRepositoryInterface $productRepository,
private readonly IsProductSalableInterface $isProductSalable,
private readonly StoreManagerInterface $storeManager
) {
}
/**
* Build a per-item availability report for the given order.
*
* @param OrderInterface $order Order whose items should be pre-checked.
* @return array<int, array{item_id: int, name: string, sellable: bool, reason: string|null}>
*/
public function getAvailabilityReport(OrderInterface $order): array
{
$websiteId = (int) $this->storeManager->getWebsite()->getId();
$report = [];
foreach ($order->getItems() as $item) {
if ($item->getParentItem() !== null) {
continue; // Skip child rows of configurable/bundle items
}
$sellable = false;
$reason = null;
try {
$product = $this->productRepository->getById((int) $item->getProductId());
$sellable = $product->isSalable()
&& $this->isProductSalable->execute((string) $product->getSku(), $websiteId);
$reason = $sellable ? null : 'out_of_stock_or_disabled';
} catch (NoSuchEntityException) {
$reason = 'product_deleted';
}
$report[] = [
'item_id' => (int) $item->getItemId(),
'name' => (string) $item->getName(),
'sellable' => $sellable,
'reason' => $reason,
];
}
return $report;
}
/**
* Number of order items that are no longer sellable.
*
* @param OrderInterface $order Order whose items should be counted.
* @return int
*/
public function getUnavailableCount(OrderInterface $order): int
{
return count(array_filter($this->getAvailabilityReport($order), static fn (array $row): bool => !$row['sellable']));
}
}
Der try/catch-Block auf NoSuchEntityException deckt den Fall ab, dass ein Produkt zwischen Bestellung und Aufruf der Bestellansicht komplett gelöscht wurde, ein Fall, den isSalable() allein nicht behandeln kann, weil dafür überhaupt kein Produkt mehr geladen werden kann. Die IsProductSalableInterface aus dem Inventory-Modul berücksichtigt zusätzlich Multi-Source-Bestände je Website, was ein simpler Blick auf StockItem::getIsInStock() nicht leisten würde.
Das Template ruft getAvailabilityReport() auf, kodiert das Ergebnis als JSON und übergibt es an die Alpine.js-Komponente. Beispielhaft sieht die Struktur für eine Bestellung mit fünf Positionen, von denen zwei nicht mehr verkäuflich sind, so aus:
[
{ "item_id": 4501, "name": "Cotton T-Shirt, Größe M", "sellable": true, "reason": null },
{ "item_id": 4502, "name": "Cotton T-Shirt, Größe XXL", "sellable": false, "reason": "out_of_stock_or_disabled" },
{ "item_id": 4503, "name": "Running Shoes Model 2024", "sellable": false, "reason": "product_deleted" },
{ "item_id": 4504, "name": "Wool Socks 3-Pack", "sellable": true, "reason": null },
{ "item_id": 4505, "name": "Baseball Cap", "sellable": true, "reason": null }
]
6. Template: Button und Inline-Warnung im Bestellverlauf
Im Template von order/view.phtml ersetzt der eigene Block den Standard-Reorder-Link. Das ViewModel liefert die Verfügbarkeitsdaten, die direkt als JSON in x-data geschrieben werden, damit die Alpine-Komponente ohne zusätzlichen Ajax-Request startet. Die Inline-Warnung erscheint nur, wenn tatsächlich Positionen fehlen, und benennt die genaue Anzahl, statt den Kunden pauschal zu warnen.
<?php
/** @var \Mironsoft\Reorder\ViewModel\ReorderAvailability $viewModel */
$viewModel = $block->getData('view_model');
/** @var \Magento\Sales\Api\Data\OrderInterface $order */
$order = $block->getOrder();
$report = $viewModel->getAvailabilityReport($order);
$unavailableCount = $viewModel->getUnavailableCount($order);
?>
<div class="reorder-function-wrapper"
x-data="reorderModal(<?= /* @noEscape */ json_encode($report) ?>, <?= (int) $order->getEntityId() ?>)">
<template x-if="unavailableCount > 0">
<div class="bg-amber-50 border border-amber-200 rounded-lg px-4 py-2 mb-3 text-sm text-amber-800">
<span x-text="unavailableCount + ' <?= $escaper->escapeHtml(__('of')) ?> ' + items.length + ' <?= $escaper->escapeHtml(__('items are no longer available')) ?>'"></span>
</div>
</template>
<button type="button" @click="open = true"
class="inline-flex items-center gap-2 bg-slate-800 text-white font-semibold px-4 py-2 rounded-lg hover:bg-slate-700 transition-colors">
<?= $escaper->escapeHtml(__('Reorder')) ?>
</button>
<!-- Alpine.js confirmation modal, see next section for the component logic -->
<div x-show="open" x-cloak class="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div class="bg-white rounded-2xl p-6 max-w-md w-full mx-4">
<p class="font-bold text-lg mb-4"><?= $escaper->escapeHtml(__('Confirm reorder')) ?></p>
<template x-for="item in items" :key="item.item_id">
<label class="flex items-center gap-3 py-2 border-b border-slate-100">
<input type="checkbox" x-model="selected" :value="item.item_id" :disabled="!item.sellable">
<span :class="item.sellable ? 'text-slate-800' : 'text-slate-400 line-through'" x-text="item.name"></span>
</label>
</template>
<form method="post" action="<?= $escaper->escapeUrl($block->getUrl('reorder/order/confirm')) ?>" @submit="submitting = true">
<input type="hidden" name="form_key" value="<?= $escaper->escapeHtmlAttr($block->getFormKey()) ?>">
<input type="hidden" name="order_id" value="<?= (int) $order->getEntityId() ?>">
<template x-for="itemId in selected" :key="itemId">
<input type="hidden" name="items[]" :value="itemId">
</template>
<p x-show="selected.length === 0" x-text="validationMessage" class="text-red-600 text-sm mb-3"></p>
<div class="flex gap-3 mt-4">
<button type="button" @click="open = false" class="px-4 py-2 rounded-lg border border-slate-300"><?= $escaper->escapeHtml(__('Cancel')) ?></button>
<button type="submit" :disabled="selected.length === 0 || submitting"
class="px-4 py-2 rounded-lg bg-slate-800 text-white font-semibold disabled:opacity-50">
<span x-show="!submitting"><?= $escaper->escapeHtml(__('Confirm reorder')) ?></span>
<span x-show="submitting" x-text="'<?= $escaper->escapeHtml(__('Processing...')) ?>'"></span>
</button>
</div>
</form>
</div>
</div>
</div>
Bemerkenswert an diesem Template ist, dass keine einzige Mustache-Doppelklammer auftaucht, obwohl Alpine.js dafür bekannt ist. Statt der üblichen Mustache-Schreibweise um item.name steht überall x-text="item.name", weil Magento-Templates doppelte geschweifte Klammern selbst als Direktive interpretieren würden und ein Konflikt mit der Rendering-Pipeline entstünde. Diese Konvention ist in jeder Reorder-Funktion, die auf Hyvä aufsetzt, nicht optional, sondern Voraussetzung für funktionierendes Markup.
7. Alpine.js-Bestätigungsdialog für den Teil-Reorder
Die Alpine-Komponente reorderModal übernimmt drei Aufgaben: das Öffnen und Schließen des Dialogs, die Vorauswahl aller sellable Positionen als bereits markiert, und eine clientseitige Validierung, die den Submit-Button deaktiviert, solange keine einzige Position ausgewählt ist. Der eigentliche POST erfolgt als normales Formular mit form_key, nicht als Fetch-Request, damit der Dialog auch ohne JavaScript als einfacher Redirect funktioniert und progressive Enhancement erhalten bleibt.
// File: app/code/Mironsoft/Reorder/view/frontend/web/js/reorder-modal.js
document.addEventListener('alpine:init', () => {
Alpine.data('reorderModal', (report, orderId) => ({
items: report,
orderId: orderId,
open: false,
submitting: false,
// Pre-select every sellable item id, skip the rest by default
selected: report.filter((row) => row.sellable).map((row) => row.item_id),
get unavailableCount() {
return this.items.filter((row) => !row.sellable).length;
},
get validationMessage() {
return this.selected.length === 0
? 'Please select at least one item to reorder.'
: '';
},
toggleAll(checked) {
this.selected = checked
? this.items.filter((row) => row.sellable).map((row) => row.item_id)
: [];
}
}));
});
Weil das Formular normal per POST abgeschickt wird, übernimmt Magentos eigenes Session-Message-System die Erfolgs- und Fehlermeldung nach dem Redirect, Alpine muss dafür keinen eigenen Zustand nach dem Absenden verwalten. Die einzige clientseitige Zustandslogik bleibt bewusst schlank: submitting verhindert einen versehentlichen Doppel-Klick, und validationMessage verhindert einen Leer-Submit, bevor überhaupt ein Request das Backend erreicht. Diese Aufgabenteilung, Alpine für sofortiges Feedback im Browser, Magento-Messages für das Ergebnis nach dem Redirect, hält den Bestätigungsdialog einfach wartbar.
8. CSP-Konformität und Store-View-Konfiguration
Jedes Inline-<script> in einem Hyvä-Template muss unmittelbar danach mit $hyvaCsp->registerInlineScript() registriert werden, sonst blockiert die Content-Security-Policy das Skript lautlos im Browser. Da die Alpine-Komponente hier bereits als externe Datei unter view/frontend/web/js/reorder-modal.js ausgeliefert wird, entfällt diese Registrierung für die Komponente selbst, weil Hyväs CSP ausschließlich Inline-Skripte einschränkt. Sollte die Reorder-Funktion stattdessen mit einem Inline-<script>-Block direkt im Template arbeiten, ist der Aufruf von registerInlineScript() nach jedem einzelnen Block zwingend, sonst bleibt der Dialog optisch vorhanden, aber komplett unreaktiv, ein Fehler, der in der lokalen Entwicklung ohne aktivierte CSP leicht übersehen wird.
Ob die Reorder-Funktion überhaupt aktiv ist, sollte pro Store View konfigurierbar sein, etwa weil ein B2B-Store sie nutzt, ein anderer Store sie aus Prozessgründen deaktiviert lassen will. Ein system.xml mit einem Feld unter mironsoft_reorder/general/enabled und Scope website deckt das ab, ausgelesen über ScopeConfigInterface::isSetFlag() im ViewModel, das dann auch die Sichtbarkeit des Buttons selbst steuert. Die zugehörige acl.xml regelt lediglich, welche Administratoren diese Konfiguration im Backend unter Stores > Configuration ändern dürfen, das betrifft ausschließlich das Backend und hat mit der Sichtbarkeit im Frontend nichts zu tun.
9. Reorder-Funktion im Vergleich: Patterns gegenübergestellt
Die folgende Tabelle stellt für jede zentrale Teilaufgabe den naiven, in der Praxis brüchigen Ansatz dem empfohlenen Muster für eine robuste Reorder-Funktion gegenüber. Die rechte Spalte entspricht dem, was in den vorherigen Abschnitten Schritt für Schritt aufgebaut wurde.
| Aufgabe | Naiver / brüchiger Ansatz | Empfohlenes Hyvä-Pattern | Vorteil |
|---|---|---|---|
| Artikel in den Warenkorb | Cart::addOrderItem() ungeprüft in Schleife |
CartRepositoryInterface + Sellable-Check pro Item |
Nur verkäufliche Artikel landen im Warenkorb |
| Nicht verfügbare Produkte | Stiller Skip, Kunde bemerkt es erst im Warenkorb | ViewModel-Prüfung vor dem Klick mit Inline-Warnung | Transparenz vor der Aktion, kein Rätselraten |
| Geänderte Optionen | getBuyRequest() blind übernehmen |
Optionen gegen aktuellen Produktstatus prüfen | Keine kaputten Warenkorbzeilen |
| Bestätigung vor Aktion | Sofortiger Redirect ohne Rückfrage | Alpine.js-Modal mit Teil-Reorder-Auswahl | Kunde entscheidet bewusst über jede Position |
| CSRF-Schutz | Reorder per GET-Link ohne Formular | POST-Formular mit form_key |
Kein Cart-Mutation-Angriff über simplen Link |
Auffällig ist, dass keines der empfohlenen Muster nennenswert mehr Aufwand bedeutet als der naive Weg, es ist im Wesentlichen derselbe Code, nur an der richtigen Stelle ergänzt: eine Prüfung vor dem Hinzufügen, eine Rückfrage vor dem Redirect, ein Formular statt eines Links. Wer diese fünf Punkte konsequent umsetzt, reduziert Support-Anfragen rund um den Wiederkauf-Prozess spürbar, weil Kunden Probleme sehen, bevor sie überhaupt entstehen.
10. Zusammenfassung
Eine robuste Reorder-Funktion in Hyvä ersetzt den naiven Core-Mechanismus, der Bestellpositionen ungeprüft in den Warenkorb kopiert, durch eine mehrstufige Prüfung: Ein ViewModel meldet bereits beim Rendern der Bestellansicht, welche Positionen nicht mehr verkäuflich sind. Ein Alpine.js-Dialog lässt den Kunden bewusst entscheiden, welche verfügbaren Positionen tatsächlich erneut bestellt werden sollen. Ein Controller mit CartRepositoryInterface übernimmt ausschließlich die geprüften, sellable Positionen in die aktive Quote, abgesichert per form_key gegen CSRF und mit klarer Kunden-ID-Prüfung gegen fremde Bestellungen.
Zwei Details werden in der Praxis am häufigsten übersehen: $hyvaCsp->registerInlineScript() nach jedem Inline-Skript, ohne den die Content-Security-Policy die Interaktivität lautlos blockiert, und die konsequente Vermeidung von Mustache-Doppelklammern zugunsten von x-text, da Magento doppelte geschweifte Klammern selbst interpretiert. Wer Reorder-Funktion, ViewModel-Verfügbarkeitsprüfung und Alpine-Bestätigung als Einheit denkt statt als drei getrennte Aufgaben, baut einen Wiederkauf-Prozess, der auch bei geänderten Produktdaten zuverlässig bleibt.
Reorder-Funktion in Hyvä: das Wichtigste auf einen Blick
Naiver Ansatz vermeiden
Der Core-Reorder-Link kopiert Positionen ungeprüft und meldet Probleme erst im Warenkorb, nie vor dem Klick.
Controller & Quote
CartRepositoryInterface plus getBuyRequest() übernehmen nur sellable Positionen in die aktive Quote.
ViewModel-Check
Verfügbarkeit wird vor dem Klick geprüft, die Inline-Warnung nennt die genaue Anzahl fehlender Artikel.
Alpine, CSP & ACL
Bestätigungsdialog mit x-text, form_key-Formular, registerInlineScript(), Toggle per Store View.
11. FAQ: Reorder-Funktion in Hyvä implementieren
1Was ist eine Reorder-Funktion in Hyvä genau?
2Warum reicht der Standard-Reorder-Link nicht?
3Wie wird fehlende Verfügbarkeit erkannt?
4Was passiert bei geänderten Optionen?
5Wie funktioniert der Bestätigungsdialog?
6Warum keine Mustache-Doppelklammern?
7Wie ist CSRF-Schutz umgesetzt?
8Was, wenn registerInlineScript() fehlt?
9Wie schalte ich sie pro Store View ab?
10Auch für Gäste möglich?
Mironsoft
Hyvä-Entwicklung, Customer-Account-Funktionen und Checkout-nahe Features
Eine robuste Reorder-Funktion für Ihren Hyvä-Shop?
Wir implementieren Controller, ViewModel-Verfügbarkeitsprüfung und Alpine.js-Bestätigungsdialog, CSP-konform und ohne ein einziges Core-Template zu überschreiben.
Konzeption
Analyse der bestehenden Bestellhistorie und Planung des Wiederkauf-Ablaufs inklusive Teil-Reorder
Umsetzung
Modul, Controller, ViewModel und Alpine.js-Komponenten nach Hyvä-Standard
CSP & ACL
CSP-konforme Skripte, Store-View-Konfiguration und Formkey-Absicherung