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

Model, ResourceModel und Collection für den Punkte-Ledger schreiben

Model, ResourceModel und Collection für den Punkte-Ledger schreiben

~7 Min. Lesezeit Zuletzt aktualisiert am 9. August 2026

Mit der Tabelle aus Kapitel 3 steht die Datenbasis - jetzt braucht es die drei Klassen, über die der restliche PHP-Code auf sie zugreift: das Model für einen einzelnen Ledger-Eintrag, das ResourceModel für Lesen und Schreiben, und die Collection für Mengenabfragen mit Filtern und Sortierung. Dieses Kapitel hält alle drei bewusst schlank - Kapitel 6 rüstet das Model später um ein Interface auf, sobald der Repository-Gedanke eingeführt wird.

Model

Das Model erbt von AbstractModel und verbindet sich in _construct() mit seinem ResourceModel - mehr braucht es an dieser Stelle noch nicht.

app/code/Mironsoft/Loyalty/Model/PointsLedger.php
<?php

declare(strict_types=1);

namespace Mironsoft\Loyalty\Model;

use Magento\Framework\Model\AbstractModel;
use Mironsoft\Loyalty\Model\ResourceModel\PointsLedger as PointsLedgerResource;

/**
 * Points ledger entity model, represents a single, immutable ledger entry.
 */
class PointsLedger extends AbstractModel
{
    /**
     * Binds the model to its resource model.
     *
     * @return void
     */
    protected function _construct(): void
    {
        $this->_init(PointsLedgerResource::class);
    }
}

ResourceModel

Das ResourceModel kennt Tabellenname und Primärschlüsselspalte - beides kommt direkt aus db_schema.xml.

app/code/Mironsoft/Loyalty/Model/ResourceModel/PointsLedger.php
<?php

declare(strict_types=1);

namespace Mironsoft\Loyalty\Model\ResourceModel;

use Magento\Framework\Model\ResourceModel\Db\AbstractDb;

/**
 * Resource model for the points ledger, maps the entity onto
 * mironsoft_loyalty_points_ledger.
 */
class PointsLedger extends AbstractDb
{
    /**
     * Initializes the main table and primary key column.
     *
     * @return void
     */
    protected function _construct(): void
    {
        $this->_init('mironsoft_loyalty_points_ledger', 'ledger_id');
    }
}

Collection

Die Collection bindet Model und ResourceModel zusammen und bekommt zwei kleine, aber wichtige Zusatzmethoden: addCustomerFilter() filtert auf einen einzelnen Kunden, addNewestFirstOrder() sortiert nach Erstellungsdatum absteigend. Beide werden ab Kapitel 6 mehrfach wiederverwendet.

app/code/Mironsoft/Loyalty/Model/ResourceModel/PointsLedger/Collection.php
<?php

declare(strict_types=1);

namespace Mironsoft\Loyalty\Model\ResourceModel\PointsLedger;

use Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection;
use Mironsoft\Loyalty\Model\PointsLedger as PointsLedgerModel;
use Mironsoft\Loyalty\Model\ResourceModel\PointsLedger as PointsLedgerResource;

/**
 * Collection of points ledger entries.
 */
class Collection extends AbstractCollection
{
    /**
     * Binds the collection to its model and resource model pair.
     *
     * @return void
     */
    protected function _construct(): void
    {
        $this->_init(PointsLedgerModel::class, PointsLedgerResource::class);
    }

    /**
     * Restricts the collection to ledger entries of a single customer.
     *
     * @param int $customerId Customer entity ID.
     * @return $this
     */
    public function addCustomerFilter(int $customerId): self
    {
        $this->addFieldToFilter('customer_id', ['eq' => $customerId]);

        return $this;
    }

    /**
     * Orders the collection by creation date, most recent entry first.
     *
     * @return $this
     */
    public function addNewestFirstOrder(): self
    {
        $this->setOrder('created_at', self::SORT_ORDER_DESC);

        return $this;
    }
}

Achtung: addFieldToFilter('customer_id', $customerId) ohne das ['eq' => ...]-Array funktioniert in vielen Fällen zufällig auch, weil Magento intern versucht, den Wert zu interpretieren - verlässlich ist das aber nicht, und PHPStan Level 5 markiert die nackte Skalar-Form in diesem Projekt als Fehler. Die Array-Form ist außerdem die einzige, die zuverlässig zwischen "gleich", "ungleich", "größer als" und weiteren Operatoren unterscheidet.

Warum hier noch kein Interface?

CLAUDE.md verlangt Service Contracts und Repositories - aber ein Interface für eine einzelne Klasse zu schreiben, bevor überhaupt eine zweite Implementierung oder ein Repository existiert, wäre verfrühte Abstraktion. Kapitel 5 baut zunächst den PointsCalculator-Service, der komplett unabhängig von diesen drei Klassen funktioniert. Erst Kapitel 6 führt PointsLedgerInterface und PointsLedgerRepositoryInterface ein - und rüstet PointsLedger dann in einem Schritt vollständig um.