Magento 2 Experten — Hyvä Theme, Tailwind CSS & SEO aus einer Hand ›

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-Eintrag

AccountInterface 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.

app/code/Mironsoft/Loyalty/Controller/History/Index.php
<?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.

app/code/Mironsoft/Loyalty/etc/frontend/routes.xml
<?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.