Eigene Controller-Struktur: Punkte-Historie-Seite
Eigene Controller-Struktur: Punkte-Historie-Seite
~7 Min. Lesezeit Zuletzt aktualisiert am 9. August 2026
Block 1 bis 5 haben ein vollständiges Backend gebaut: Datenmodell, EAV-Entity, eigene Attribute, Observer, Cron, Plugins und Preferences. Sichtbar war davon bisher nichts - keine einzige Storefront-Seite hat auf all das zugegriffen. Block 6 ändert das: Kapitel 45 bis 54 bauen Controller, einen eigenen Router und die ersten Templates, mit denen ein Kunde seinen Punktestand tatsächlich sieht. Dieses Kapitel startet mit der einfachsten neuen Seite - der Punkte-Historie - und legt dabei die komplette Controller-Verzeichnisstruktur für den gesamten Block an.
Das Controller-Verzeichnis dieses Moduls
Vier Controller-Actions entstehen in Block 6, plus der eigene Router aus Kapitel 46 direkt darüber im Controller/-Namespace (Magento erwartet Router-Klassen nicht in einem Unterordner, im Gegensatz zu Controller-Actions):
Neue Verzeichnisse und Dateien aus Block 6 (ergänzt die Struktur aus Block 1-5)
app/code/Mironsoft/Loyalty/
├── Controller/
│ ├── Router.php # Kapitel 46
│ ├── History/
│ │ └── Index.php # dieses Kapitel
│ ├── Catalog/
│ │ ├── Index.php # Kapitel 49 - Prämienliste
│ │ └── View.php # Kapitel 49 - Prämiendetail
│ └── Redeem/
│ └── Index.php # Kapitel 50 - Prämie einlösen (POST)
└── etc/
└── frontend/
├── routes.xml # Kapitel 46
└── di.xml # Kapitel 46 - RouterList-EintragAccountInterface statt eigener Login-Prüfung
Die Punkte-Historie ist eine reine "Mein Konto"-Seite - ein nicht eingeloggter Besucher darf sie nie zu Gesicht bekommen. Statt in jeder Action manuell CustomerSession::isLoggedIn() zu prüfen und bei Bedarf selbst auf die Login-Seite umzuleiten, reicht es, das leere Marker-Interface Magento\Customer\Controller\AccountInterface zu implementieren. Der Core registriert dafür bereits einen globalen around-Plugin (Magento\Customer\Controller\Plugin\Account, verdrahtet in vendor/magento/module-customer/etc/frontend/di.xml direkt auf das Interface selbst) - jede Controller-Klasse, die AccountInterface implementiert, wird automatisch abgefangen: $session->authenticate() leitet nicht eingeloggte Besucher zur Login-Seite um, bevor execute() überhaupt aufgerufen wird. Genau dieses Interface nutzt jeder Core-Controller unter Mein Konto - von der Bestellübersicht bis zur Adressverwaltung.
<?php
declare(strict_types=1);
namespace Mironsoft\Loyalty\Controller\History;
use Magento\Customer\Controller\AccountInterface;
use Magento\Framework\App\Action\Action;
use Magento\Framework\App\Action\Context;
use Magento\Framework\App\Action\HttpGetActionInterface;
use Magento\Framework\View\Result\Page;
use Magento\Framework\View\Result\PageFactory;
/**
* Renders the customer-facing points history page. Implementing AccountInterface
* (a marker interface with no methods of its own, see
* Magento\Customer\Controller\AccountInterface) makes Magento's own
* Magento\Customer\Controller\Plugin\Account plugin force a login redirect before
* execute() ever runs - no manual session check needed here.
*/
class Index extends Action implements HttpGetActionInterface, AccountInterface
{
/**
* @param Context $context Framework action context (request/response/redirect helpers).
* @param PageFactory $resultPageFactory Builds the full page result handed back to the front controller.
*/
public function __construct(
Context $context,
private readonly PageFactory $resultPageFactory,
) {
parent::__construct($context);
}
/**
* Builds the points history page. The ledger rows themselves are not fetched
* here - the view model wired into the layout (chapter 51) reads
* PointsLedgerRepositoryInterface::getListByCustomerId() (chapter 6) on its own,
* so this controller stays a thin dispatcher per the block/ViewModel split
* explained in chapter 47.
*
* @return Page
*/
public function execute(): Page
{
$resultPage = $this->resultPageFactory->create();
$resultPage->getConfig()->getTitle()->set((string) __('My Points History'));
return $resultPage;
}
}Die interne Route in routes.xml
Damit dieser Controller überhaupt über HTTP erreichbar ist, braucht das Modul eine Route. Wie im echten, bereits im mironsoft-Projekt existierenden Mironsoft\Tutorial-Modul üblich, ist der hier registrierte frontName ein rein technischer, interner Wert - niemals die tatsächlich sichtbare URL. Die hübsche, konfigurierbare Kunden-URL (/treuepraemien/... bzw. /rewards/...) kommt erst in Kapitel 46 über einen eigenen Router hinzu.
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:App/etc/routes.xsd">
<!--
This frontName is an internal routing token only, never exposed to visitors.
Mironsoft\Loyalty\Controller\Router (chapter 46) intercepts the real,
store-configurable /treuepraemien or /rewards path first and explicitly sets
this module name on the request before forwarding, so the standard core router
can resolve the actual controller class on a second dispatch pass - the same
two-pass mechanism Magento\Cms\Controller\Router and this project's own
Mironsoft\Tutorial\Controller\Router use.
-->
<router id="standard">
<route id="mironsoft_loyalty" frontName="mironsoft_loyalty">
<module name="Mironsoft_Loyalty"/>
</route>
</router>
</config>Tipp: Mit nur dieser routes.xml-Datei ist die Historie-Seite schon jetzt unter der technischen, unschönen URL /mironsoft_loyalty/history/index erreichbar - Magentos Standard-Routing-Schema /{frontName}/{controller}/{action} greift ganz normal. Genau das prüft ihr am Ende dieses Kapitels kurz im Browser, bevor Kapitel 46 die hübsche URL ergänzt.
Keine ACL-Ressource für Frontend-Controller
Achtung: Der Reward-Admin-Grid-Controller aus Kapitel 16 (Controller\Adminhtml\Reward\Index) brauchte zwingend const ADMIN_RESOURCE = 'Mironsoft_Loyalty::rewards' und die passende acl.xml-Ressource, sonst hätte jeder Admin-Benutzer Zugriff gehabt. Frontend-Controller wie dieser hier kennen dieses Konzept nicht - Zugriffskontrolle läuft ausschließlich über AccountInterface (eingeloggt oder nicht) statt über granulare Berechtigungen. Eine ADMIN_RESOURCE-Konstante auf einem Frontend-Controller zu ergänzen wäre schlicht wirkungslos.
Tipp: Kapitel 46 baut jetzt den eigentlichen Router, der aus /mironsoft_loyalty/history/index die konfigurierbare, store-abhängige URL /treuepraemien/verlauf (DE) bzw. /rewards/history (EN) macht - inklusive der Prämienkatalog-Startseite und der pro-Prämie-Detailseite.