ViewModel für den Punktestand auf der Kontoseite
ViewModel für den Punktestand auf der Kontoseite
~6 Min. Lesezeit Zuletzt aktualisiert am 9. August 2026
Kapitel 47 hat die Entscheidung erklärt - dieses Kapitel baut den ersten echten ViewModel dieses Blocks. PointsBalance zeigt Punktestand und Treue-Stufe an und wird gleich zweimal wiederverwendet: als eigenständiges Widget (dieses Kapitel, verdrahtet in Kapitel 51) und ein zweites Mal im Kundenkonto-Dashboard (Kapitel 52) - ohne eine einzige Zeile Code zu duplizieren.
Die ViewModel-Klasse
<?php
declare(strict_types=1);
namespace Mironsoft\Loyalty\ViewModel;
use Magento\Customer\Api\CustomerRepositoryInterface;
use Magento\Customer\Api\Data\CustomerInterface;
use Magento\Customer\Model\Session as CustomerSession;
use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\Exception\NoSuchEntityException;
use Magento\Framework\View\Element\Block\ArgumentInterface;
use Mironsoft\Loyalty\Model\Config\LoyaltyConfig;
use Mironsoft\Loyalty\Model\Util\PointsFormatter;
/**
* Exposes the logged-in customer's points balance and loyalty tier to the storefront.
* Deliberately reusable across two independent layout locations (chapter 51's
* dedicated widget, chapter 52's account dashboard) - a plain PHP object with no
* Template\Context coupling can be wired into any number of blocks unmodified,
* exactly the point made in chapter 47.
*/
class PointsBalance implements ArgumentInterface
{
/**
* Lazily loaded and cached for the lifetime of this instance, since a template
* commonly calls several getters (balance, tier, formatted balance) per request.
*
* @var CustomerInterface|null
*/
private ?CustomerInterface $customer = null;
/**
* @param CustomerSession $customerSession Provides the logged-in customer ID.
* @param CustomerRepositoryInterface $customerRepository Loads the customer's custom attributes (chapter 21).
* @param LoyaltyConfig $loyaltyConfig Confirms the loyalty program is enabled before showing anything.
*/
public function __construct(
private readonly CustomerSession $customerSession,
private readonly CustomerRepositoryInterface $customerRepository,
private readonly LoyaltyConfig $loyaltyConfig,
) {
}
/**
* Whether the balance widget should render at all: the loyalty program must be
* enabled for the current website (chapter 7) AND the visitor must be logged in.
*
* @return bool
*/
public function canShow(): bool
{
return $this->loyaltyConfig->isEnabled() && $this->customerSession->isLoggedIn();
}
/**
* Returns the raw points balance, 0 for a guest or a customer with no history yet.
*
* @return int
*/
public function getPointsBalance(): int
{
$attribute = $this->getCustomer()?->getCustomAttribute('loyalty_points_balance');
return $attribute !== null ? (int) $attribute->getValue() : 0;
}
/**
* Returns the points balance formatted with a thousands separator, e.g. "1.250".
*
* @return string
*/
public function getFormattedPointsBalance(): string
{
return PointsFormatter::formatPoints($this->getPointsBalance());
}
/**
* Returns the customer's current loyalty tier, defaulting to bronze if unset.
*
* @return string
*/
public function getTier(): string
{
$attribute = $this->getCustomer()?->getCustomAttribute('loyalty_tier');
return $attribute !== null ? (string) $attribute->getValue() : 'bronze';
}
/**
* Loads and caches the current customer via the repository - never via
* CustomerSession::getCustomer(), which returns a legacy \Magento\Customer\Model\Customer
* instance whose getData()/setData() cannot see EAV custom attributes reliably
* (chapter 30 explains getCustomAttribute() vs. getData() in full).
*
* @return CustomerInterface|null Null for a guest or if the customer record vanished mid-session.
*/
private function getCustomer(): ?CustomerInterface
{
if ($this->customer !== null) {
return $this->customer;
}
if (!$this->customerSession->isLoggedIn()) {
return null;
}
try {
$this->customer = $this->customerRepository->getById(
(int) $this->customerSession->getCustomerId()
);
} catch (NoSuchEntityException|LocalizedException) {
return null;
}
return $this->customer;
}
}Keine di.xml-Präferenz nötig
Anders als ein Service Contract (Kapitel 6) braucht ArgumentInterface keinen <preference>-Eintrag - es ist ein reines Marker-Interface ohne eigene Methoden, das dem Layout-XML-Parser nur signalisiert, dass diese Klasse als view_model-Argument erlaubt ist. Die eigentliche Objekterzeugung läuft über den ganz normalen Object Manager mit Constructor Injection, ausgelöst durch das <argument name="view_model" xsi:type="object">-Tag im Layout - vollständig in Kapitel 51.
Tipp: Ein kurzer Vorgeschmack auf die Layout-Verdrahtung, damit dieser ViewModel nicht isoliert wirkt:
<block class="Magento\Framework\View\Element\Template"
name="loyalty.points.balance"
template="Mironsoft_Loyalty::widget/points-balance.phtml">
<arguments>
<argument name="view_model" xsi:type="object">Mironsoft\Loyalty\ViewModel\PointsBalance</argument>
</arguments>
</block>Im Template dann schlicht: $viewModel = $block->getViewModel();, gefolgt von $viewModel->canShow() als Guard-Klausel - Kapitel 53 zeigt das vollständige Template.
Achtung: canShow() vor jedem Rendering-Zweig im Template zu prüfen ist kein optionales Detail: Wird der Punktestand ohne diese Prüfung angezeigt, sieht ein Kunde auf einem Website, auf dem das Programm gerade deaktiviert wurde (Kapitel 7, mironsoft_loyalty/general/enabled), trotzdem einen (veralteten oder irreführenden) Punktestand - genau der Konfigurationswert, den bereits der Router aus Kapitel 46 respektiert.
Tipp: getCustomer() lädt den Kunden maximal einmal pro Request und Instanz - ruft das Template getPointsBalance() und getTier() beide auf, feuert CustomerRepositoryInterface::getById() nur einmal. Ein kleines, aber lohnendes Detail, sobald ein ViewModel mehrfach pro Seite aufgerufen wird, wie es in Kapitel 52 der Fall sein wird.