Customer Attribute: Punktestand und Treue-Stufe am Kunden
Customer Attribute: Punktestand und Treue-Stufe am Kunden
~8 Min. Lesezeit Zuletzt aktualisiert am 9. August 2026
customer ist die dritte Kern-Entität in diesem Block, die bereits als EAV geführt wird - genau wie Produkt und Kategorie. Zwei neue Attribute kommen hinzu: loyalty_points_balance (Typ int, Default 0) als schneller, direkt lesbarer Punktestand, und loyalty_tier (Typ varchar, Dropdown mit den drei Werten bronze/silver/gold) als aktuelle Treue-Stufe - beide leiten sich aus dem Punkte-Ledger (Kapitel 3-4) ab, werden aber als redundante, schnell lesbare Kopie direkt am Kunden gepflegt, damit Checkout und Kontoseite sie ohne zusätzliche Ledger-Abfrage anzeigen können.
Redundanz ist hier bewusst
loyalty_points_balance ist streng genommen aus dem Ledger ableitbar (SUM(points) aller Einträge eines Kunden), wird hier aber trotzdem als eigenes Attribut gepflegt - derselbe Denkfehler wäre es, das balance_after -Feld aus Kapitel 3 für überflüssig zu halten. Beide Felder sind ein bewusster Denormalisierungs-Kompromiss: eine Konto-Übersichtsseite, die bei jedem Aufruf über Tausende Ledger-Zeilen aggregieren müsste, skaliert schlechter als ein einzelnes SELECT auf ein EAV-Attribut. Kapitel 30 (Block 4) zeigt den Observer, der Ledger und Kunden-Attribut bei jeder Punktebuchung synchron hält.
Der Setup-Patch für beide Attribute
<?php
declare(strict_types=1);
namespace Mironsoft\Loyalty\Setup\Patch\Data;
use Magento\Customer\Model\Customer;
use Magento\Eav\Setup\EavSetupFactory;
use Magento\Framework\Setup\ModuleDataSetupInterface;
use Magento\Framework\Setup\Patch\DataPatchInterface;
use Mironsoft\Loyalty\Model\Source\LoyaltyTier;
/**
* Registers loyalty_points_balance and loyalty_tier on the customer entity.
*/
class InstallCustomerLoyaltyAttributes implements DataPatchInterface
{
/**
* @param ModuleDataSetupInterface $moduleDataSetup Provides the setup connection for the patch.
* @param EavSetupFactory $eavSetupFactory Creates the EavSetup helper used to register attributes.
*/
public function __construct(
private readonly ModuleDataSetupInterface $moduleDataSetup,
private readonly EavSetupFactory $eavSetupFactory
) {
}
/**
* Adds the points balance and tier attributes to the existing customer entity.
*
* @return void
*/
public function apply(): void
{
$this->moduleDataSetup->getConnection()->startSetup();
/** @var \Magento\Eav\Setup\EavSetup $eavSetup */
$eavSetup = $this->eavSetupFactory->create(['setup' => $this->moduleDataSetup]);
$eavSetup->addAttribute(Customer::ENTITY, 'loyalty_points_balance', [
'type' => 'int',
'label' => 'Loyalty Points Balance',
'input' => 'text',
'required' => false,
'default' => '0',
'visible' => true,
'system' => false,
'user_defined' => true,
'position' => 200,
]);
$eavSetup->addAttribute(Customer::ENTITY, 'loyalty_tier', [
'type' => 'varchar',
'label' => 'Loyalty Tier',
'input' => 'select',
'source' => LoyaltyTier::class,
'required' => false,
'default' => LoyaltyTier::TIER_BRONZE,
'visible' => true,
'system' => false,
'user_defined' => true,
'position' => 210,
]);
$this->moduleDataSetup->getConnection()->endSetup();
}
/**
* @return array<int, string>
*/
public static function getDependencies(): array
{
return [];
}
/**
* @return array<int, string>
*/
public function getAliases(): array
{
return [];
}
}Achtung: Magento_Customer steht bereits seit Kapitel 2 in der module.xml-sequence - hier ist keine Ergänzung nötig. Leicht zu übersehen ist dagegen system => false: Ohne diesen Schlüssel behandelt Magento neue Kundenattribute mitunter als Systemattribut, was sie im Formular-Editor unter Stores > Customer Attributes als schreibgeschützt und nicht löschbar erscheinen lässt.
loyalty_tier braucht ein Source Model
input => 'select' ohne source würde ein leeres Dropdown ergeben - Magento weiß ohne ein Source Model schlicht nicht, welche Optionen es anzeigen soll. Anders als bei RewardType in Kapitel 13, das nur von einem einzigen Attribut genutzt wird, liegt LoyaltyTier bewusst nicht unter Model/Reward/Source/, sondern unter dem entitätsneutralen Model/Source/ - Kapitel 22 verwendet exakt dieselbe Klasse für loyalty_tier_override auf der Company-Entität wieder.
<?php
declare(strict_types=1);
namespace Mironsoft\Loyalty\Model\Source;
use Magento\Eav\Model\Entity\Attribute\Source\AbstractSource;
/**
* Source model for the bronze/silver/gold loyalty tier dropdown. Shared between
* the customer attribute loyalty_tier (chapter 21) and the company attribute
* loyalty_tier_override (chapter 22) - see chapter 24 for the full rationale.
*/
class LoyaltyTier extends AbstractSource
{
/**
* @var string
*/
public const TIER_BRONZE = 'bronze';
/**
* @var string
*/
public const TIER_SILVER = 'silver';
/**
* @var string
*/
public const TIER_GOLD = 'gold';
/**
* Returns the dropdown options shown in admin forms and grid filters.
*
* @return array<int, array{value: string, label: string}>
*/
public function getAllOptions(): array
{
if ($this->_options === null) {
$this->_options = [
['value' => self::TIER_BRONZE, 'label' => __('Bronze')],
['value' => self::TIER_SILVER, 'label' => __('Silver')],
['value' => self::TIER_GOLD, 'label' => __('Gold')],
];
}
return $this->_options;
}
}Tipp: Die drei Konstanten TIER_BRONZE/TIER_SILVER/TIER_GOLD sind bewusst wortgleich mit den Konstanten PointsCalculator::TIER_BRONZE & Co. aus Kapitel 5 - beide Klassen kennen sich nicht gegenseitig (das Source Model ist reines Anzeige-/Formular-Konzept, der Service reine Berechnungslogik), aber dieselben String-Werte 'bronze'/'silver'/'gold' stellen sicher, dass determineTier()-Rückgabewerte direkt als loyalty_tier-Attributwert gespeichert werden können, ohne Übersetzungstabelle.
bin/magento setup:upgrade
bin/magento indexer:reindex customer_grid
bin/magento cache:flushAchtung: Ein direkter $customer->setLoyaltyPointsBalance()-Aufruf in Controller- oder Observer-Code funktioniert erst nach setup:upgrade und einem vollständigen Objekt-Neuaufbau (neuer Request bzw. bin/magento cache:flush) - der magische Getter/Setter-Mechanismus von AbstractModel liest die verfügbaren Attribute aus dem EAV-Cache, der sonst noch den alten Attributstand kennt.
Mit Produkt-, Kategorie- und Kunden-Attribut ist die EAV-Seite von Block 3 abgeschlossen. Kapitel 22 wechselt bewusst die Technik: Company ist keine EAV-Entität, und loyalty_tier_override braucht deshalb einen völlig anderen Ansatz.