Sales Attribute: erzielte Punkte an Order und Order Item speichern
Sales Attribute: erzielte Punkte an Order und Order Item speichern
~8 Min. Lesezeit Zuletzt aktualisiert am 9. August 2026
loyalty_points_earned (Typ int) hält fest, wie viele Punkte eine konkrete Bestellung beziehungsweise eine einzelne Bestellposition tatsächlich eingebracht hat - das "Ergebnis" von PointsCalculator::calculatePoints() aus Kapitel 5, dauerhaft an genau der Bestellung festgemacht, die es ausgelöst hat. Anders als die Company-Lösung aus Kapitel 22 kommt dieses Kapitel ohne Extension Attribute und Plugin aus - Magento bringt für Sales-Entitäten ein eigenes, dediziertes Werkzeug mit.
Auch Order und Order Item sind nicht EAV
Bis einschließlich Magento 1 waren Bestellungen tatsächlich EAV-Entitäten - eine der bekanntesten Performance-Bremsen der alten Plattform. Magento 2 hat sales_order, sales_order_item und die übrigen Sales-Entitäten von Anfang an als flache Tabellen konzipiert, exakt aus den Gründen, die Kapitel 18 allgemein für flache Tabellen genannt hat: hohe Schreibfrequenz (jede Bestellung, jede Rechnung), feste Spaltenstruktur. Trotzdem heißt der Mechanismus zum Hinzufügen neuer Felder bis heute "Sales Attribute" - ein historisches Überbleibsel des Namens, keine Aussage über die tatsächliche Speicherung.
SalesSetup statt EavSetup
\Magento\Sales\Setup\SalesSetup erbt zwar von EavSetup und bietet dieselbe addAttribute()-Methode an wie in den Kapiteln 19-21 - intern verhält sie sich für Order/Order-Item aber vollkommen anders: Statt Zeilen in eav_attribute und einer Wertetabelle anzulegen, fügt sie der jeweiligen Tabelle (sales_order/sales_order_item) direkt eine echte ALTER TABLE ... ADD COLUMN-Spalte hinzu. Der Aufruf sieht identisch aus wie bei einem Produkt-Attribut, das Ergebnis in der Datenbank ist aber ein flaches Feld, kein EAV-Wert.
<?php
declare(strict_types=1);
namespace Mironsoft\Loyalty\Setup\Patch\Data;
use Magento\Framework\Setup\ModuleDataSetupInterface;
use Magento\Framework\Setup\Patch\DataPatchInterface;
use Magento\Sales\Setup\SalesSetupFactory;
/**
* Adds loyalty_points_earned as a flat column to sales_order and sales_order_item,
* via the Sales module's dedicated SalesSetup helper rather than EavSetup.
*/
class InstallSalesLoyaltyAttributes implements DataPatchInterface
{
/**
* @param ModuleDataSetupInterface $moduleDataSetup Provides the setup connection for the patch.
* @param SalesSetupFactory $salesSetupFactory Creates the SalesSetup helper used to add sales attributes.
*/
public function __construct(
private readonly ModuleDataSetupInterface $moduleDataSetup,
private readonly SalesSetupFactory $salesSetupFactory
) {
}
/**
* Adds loyalty_points_earned to both the order and the order item entity.
*
* @return void
*/
public function apply(): void
{
$this->moduleDataSetup->getConnection()->startSetup();
/** @var \Magento\Sales\Setup\SalesSetup $salesSetup */
$salesSetup = $this->salesSetupFactory->create(['setup' => $this->moduleDataSetup]);
$attributeConfig = [
'type' => 'int',
'label' => 'Loyalty Points Earned',
'input' => 'text',
'required' => false,
'default' => '0',
'visible' => false,
'system' => false,
];
$salesSetup->addAttribute('order', 'loyalty_points_earned', $attributeConfig);
$salesSetup->addAttribute('order_item', 'loyalty_points_earned', $attributeConfig);
$this->moduleDataSetup->getConnection()->endSetup();
}
/**
* @return array<int, string>
*/
public static function getDependencies(): array
{
return [];
}
/**
* @return array<int, string>
*/
public function getAliases(): array
{
return [];
}
}Achtung: Die Entity-Type-Codes 'order' und 'order_item' sind String-Literale, keine Klassenkonstanten wie Product::ENTITY - ein Tippfehler wie 'orders' erzeugt keinen sofortigen Fehler beim Patch-Lauf, sondern bricht erst tief in SalesSetup mit einer kryptischen "Unknown entity type"-Exception ab, weil intern eine feste Zuordnung von Code zu Tabellenname existiert.
Zugriff ohne Extension Attribute
Weil loyalty_points_earned eine echte Spalte ist, funktioniert der Zugriff bereits mit einfachem getData()/setData() - \Magento\Sales\Model\Order und \Magento\Sales\Model\Order\Item nutzen den magischen Getter/Setter-Mechanismus von AbstractModel, ganz ohne extension_attributes.xml: $order->getLoyaltyPointsEarned() funktioniert nach einem einzigen setup:upgrade sofort, im Gegensatz zur Company-Lösung aus Kapitel 22, die genau deshalb ein Plugin brauchte, weil CompanyInterface keine eigenen Getter/Setter für Fremdfelder erlaubt.
Ein echter Zugriff über REST oder GraphQL - etwa damit ein externes System sieht, wie viele Punkte eine Bestellung eingebracht hat - braucht dagegen sehr wohl eine eigene extension_attributes.xml auf OrderInterface/OrderItemInterface, denn die Service-Contract-Schicht kennt nur, was über Interface oder Extension Attribute erreichbar ist. Block 10 (Kapitel 79-87) greift das wieder auf, sobald die REST-/GraphQL-Schicht für das Modul entsteht.
Order Grid nicht automatisch mit dabei
Die neue Spalte landet ausschließlich in sales_order/sales_order_item, nicht in der separaten, denormalisierten sales_order_grid-Tabelle, aus der das Admin-Bestellungsraster liest. Damit loyalty_points_earned dort sichtbar wird, müsste die Spalte zusätzlich per db_schema.xml auf sales_order_grid ergänzt und über einen Plugin/Observer auf den Grid-Sync-Mechanismus befüllt werden - ein zusätzlicher Schritt, der bewusst nicht Teil dieses Kapitels ist.
bin/magento setup:upgrade
bin/magento cache:flushTipp: visible => false ist hier bewusst gewählt: loyalty_points_earned ist ein reines Ergebnisfeld, das ein Observer (Kapitel 30) automatisch beim Bestellabschluss befüllt - anders als loyalty_points_multiplier am Produkt braucht es kein Admin-Formularfeld zum manuellen Bearbeiten.
Vier Attribute, drei grundverschiedene Techniken: EavSetup für echte EAV-Entitäten (Produkt, Kategorie, Kunde), Spalte + Extension Attribute + Plugin für eine flache Entität ohne eigenen Sales-Helfer (Company), und SalesSetup für die flachen, aber speziell unterstützten Sales-Entitäten. Kapitel 24 wechselt die Perspektive: weg von der Speicherung, hin zu den Source Models, die loyalty_tier und loyalty_tier_override ihre Dropdown-Optionen geben.