Customer Section Data: Punktestand im Mini-Cart/Checkout per AJAX
Customer Section Data: Punktestand im Mini-Cart/Checkout per AJAX
~6 Min. Lesezeit Zuletzt aktualisiert am 9. August 2026
Kapitel 80-83 haben den Punktestand für externe Clients über REST und GraphQL geöffnet. Für das eigene Hyvä-Frontend selbst ist beides überdimensioniert: Weder der Mini-Cart-Header noch der Checkout-Payment-Schritt aus Kapitel 63/65 sollen bei jedem Seitenaufruf eine eigene GraphQL-Anfrage abschicken, nur um den Punktestand anzuzeigen. Genau für diesen Fall existiert Magentos Customer Section Data-Mechanismus - derselbe, der Warenkorb, Wishlist und Vergleichsliste bereits im Header dieses Themes versorgt.
Wie Customer Section Data funktioniert
Ein zentraler AJAX-Aufruf (customer/section/load, aus dem Magento_Customer-Kern-Modul, hier nicht neu gebaut) lädt beim Seitenaufruf und nach bestimmten, in sections.xml deklarierten Aktionen alle registrierten "Sections" gebündelt nach und legt sie im localStorage ab. Ein private-content-loaded-Event auf window benachrichtigt anschließend jede Alpine-Komponente, die zuhören möchte - exakt das Muster, das header.phtml dieses Themes bereits für Vergleichsliste (initCompareHeader/receiveCompareData) und Wishlist nutzt.
Die neue Section: loyalty_points
sections.xml erklärt, nach welchen Controller-Aktionen die neue loyalty_points-Section (und die Core-cart-Section) als veraltet markiert und beim nächsten Request neu geladen werden - dem redeemenden Redeem\Index-Controller aus Kapitel 50 sowie dem Core-"In den Warenkorb"-Controller, da ein Kauf über den in Kapitel 76 gebauten "Punkte-Paket"-Produkttyp den Punktestand ebenfalls ändert:
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Customer:etc/sections.xsd">
<action name="mironsoft_loyalty/redeem/index">
<section name="loyalty_points"/>
<section name="cart"/>
</action>
<action name="checkout/cart/add">
<section name="loyalty_points"/>
</action>
</config>
Die Datenquelle: CustomerData\PointsBalance
SectionSourceInterface verlangt genau eine Methode - getSectionData(): array - deren Rückgabewert später als JSON im localStorage landet. Bewusst CustomerSession::getCustomerData() statt CustomerRepositoryInterface::getById(): Sections laufen im Storefront-Request-Kontext, wo die Session ohnehin schon geladen ist und ein zusätzlicher Repository-Aufruf nur unnötige Datenbanklast wäre - anders als in den Observern/API-Klassen aus Block 4/10, die keinen Session-Kontext haben.
<?php
declare(strict_types=1);
namespace Mironsoft\Loyalty\CustomerData;
use Magento\Customer\CustomerData\SectionSourceInterface;
use Magento\Customer\Model\Session as CustomerSession;
use Mironsoft\Loyalty\Model\Util\PointsFormatter;
/**
* Exposes the current customer's points balance and tier as customer
* section data - consumed by Alpine components in the mini-cart and
* checkout without a dedicated AJAX round trip.
*/
class PointsBalance implements SectionSourceInterface
{
/**
* @param CustomerSession $customerSession Provides the currently logged-in customer.
*/
public function __construct(
private readonly CustomerSession $customerSession,
) {
}
/**
* @inheritDoc
*/
public function getSectionData(): array
{
if (!$this->customerSession->isLoggedIn()) {
return [
'balance' => 0,
'formatted_balance' => PointsFormatter::formatPoints(0),
'tier' => null,
];
}
$customer = $this->customerSession->getCustomerData();
$attribute = $customer?->getCustomAttribute('loyalty_points_balance');
$balance = $attribute !== null ? (int) $attribute->getValue() : 0;
$tierAttribute = $customer?->getCustomAttribute('loyalty_tier');
$tier = $tierAttribute !== null ? (string) $tierAttribute->getValue() : null;
return [
'balance' => $balance,
'formatted_balance' => PointsFormatter::formatPoints($balance),
'tier' => $tier,
];
}
}
Registrierung: etc/frontend/di.xml
Der Section-Name loyalty_points verbindet sections.xml (oben) mit der PHP-Klasse über SectionPoolInterfaces sectionSourceMap-Argument - dasselbe Muster, mit dem Magento_Checkout intern seine eigene cart-Section registriert:
<?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="Magento\Customer\CustomerData\SectionPoolInterface">
<arguments>
<argument name="sectionSourceMap" xsi:type="array">
<item name="loyalty_points" xsi:type="string">Mironsoft\Loyalty\CustomerData\PointsBalance</item>
</argument>
</arguments>
</type>
</config>
Alpine statt Knockout im übrigen Storefront
Anders als der Checkout-Payment-Schritt aus Kapitel 65 (dort zwingend Knockout, weil Magento_Checkout selbst eine Knockout-SPA ist) läuft der Mini-Cart-Header dieses Themes komplett auf Alpine.js - CLAUDE.mds Vorgabe "kein Knockout.js, kein jQuery" gilt hier uneingeschränkt. Ein Beispiel-Badge, das exakt initCompareHeader/receiveCompareData aus Magento_Theme::html/header.phtml nachbildet:
<div
x-data="initLoyaltyPointsBadge"
@private-content-loaded.window="receiveLoyaltyPointsData"
class="flex items-center gap-1 text-sm text-brand-slate"
>
<span x-text="formattedBalance"></span>
<span x-show="tier" x-text="tier" class="uppercase text-xs font-semibold"></span>
</div>
<script>
function initLoyaltyPointsBadge() {
return {
formattedBalance: '0',
tier: null,
receiveLoyaltyPointsData() {
const data = this.$event.detail.data;
if (data['loyalty_points']) {
this.formattedBalance = data['loyalty_points'].formatted_balance;
this.tier = data['loyalty_points'].tier;
}
}
}
}
document.addEventListener('alpine:init', () => {
Alpine.data('initLoyaltyPointsBadge', initLoyaltyPointsBadge);
}, { once: true });
</script>
<?php $hyvaCsp->registerInlineScript() ?>
this.$event.detail.data['loyalty_points'] greift exakt auf den Rückgabewert von PointsBalance::getSectionData() zu - derselbe Name, den di.xml gerade als Schlüssel der sectionSourceMap registriert hat. Jede spätere Umbenennung der Section muss an beiden Stellen synchron bleiben; eine falsch geschriebene Section im Alpine-Template scheitert dabei still - data['loyalty_points'] ist dann einfach undefined, ohne Fehlermeldung im Browser.
Achtung: Der @hyvaCsp->registerInlineScript()-Aufruf direkt nach dem <script>-Block ist Pflicht (CLAUDE.md), nicht optional - ohne ihn blockiert Hyväs Content-Security-Policy-Modul den Inline-Handler in Produktion, und die Badge-Komponente bleibt dauerhaft bei formattedBalance: '0' hängen, ohne dass die Browser-Konsole einen offensichtlichen Fehler zeigt - nur ein Refused to execute inline script-CSP-Report.
Mit Lese- (REST/GraphQL, Kapitel 80/82), Schreib- (REST/GraphQL, Kapitel 81/83) und jetzt Anzeige-Zugriff (Customer Section Data, dieses Kapitel) sind alle drei praktischen Zugriffswege auf den Punktestand abgedeckt. Kapitel 85 wechselt zur Meta-Ebene: Wie bleiben all diese Verträge über die Zeit stabil, wenn sich das Modul weiterentwickelt?