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