Company Attribute: B2B-Sonderkonditionen (Company-Attribute-Grundlagen)
Company Attribute: B2B-Sonderkonditionen (Company-Attribute-Grundlagen)
~9 Min. Lesezeit Zuletzt aktualisiert am 9. August 2026
Vier Attribute, vier Mal EavSetup::addAttribute() - so ließe sich Block 3 fortsetzen, wäre company ebenfalls eine EAV-Entität. Ist sie aber nicht: Magento\Company\Model\Company ist ein gewöhnliches AbstractExtensibleModel über der flachen Tabelle company, genau wie Magento\Sales\Model\Order (Kapitel 23) oder ein x-beliebiges eigenes ResourceModel aus Block 1. EavSetup::addAttribute(Company::ENTITY, ...) würde schlicht mit einem Fehler abbrechen, weil eav_entity_type nie einen Eintrag für Companies enthält. Ein B2B-Sonderkonditionen-Feld wie loyalty_tier_override braucht deshalb einen komplett anderen Ansatz: eine echte Spalte plus Extension Attribute.
Warum Company keine EAV-Entität ist
Magento_Company ist Teil von Magento Commerce/B2B und modelliert Firmenkonten: mehrere Kunden gehören zu einer Company, die Company selbst trägt Stammdaten (Name, Steuernummer, Kreditlimit-Bezug bei aktiviertem Magento_CompanyCredit) in einer einzigen flachen Tabellenzeile. Der Grund liegt in der Natur der Daten: Firmendaten variieren nicht von Company zu Company in Struktur, sondern nur im Wert - genau der Fall, für den Kapitel 18 die flache Tabelle gegenüber EAV empfohlen hat.
- Eine neue Spalte in der
company-Tabelle perdb_schema.xml- CLAUDE.mds Vorgabe, deklaratives Schema statt Install-Skripten zu nutzen, gilt hier genauso wie für die Punkte-Ledger-Tabelle in Kapitel 3. - Ein Extension Attribute auf
CompanyInterface, damit der Wert sauber übergetExtensionAttributes()/setExtensionAttributes()erreichbar ist statt über einen "rohen"getData()-Zugriff. - Ein Plugin auf
CompanyRepositoryInterface, das den Spaltenwert beim Laden in das Extension Attribute kopiert und beim Speichern wieder zurück.
Schritt 1: die Spalte in db_schema.xml
<?xml version="1.0"?>
<schema xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd">
<!-- table mironsoft_loyalty_points_ledger: siehe Kapitel 3 -->
<!-- tables mironsoft_loyalty_reward_entity und _varchar/_int/_decimal/_text/_datetime: siehe Kapitel 11 -->
<table name="company" resource="default">
<column xsi:type="varchar" name="loyalty_tier_override" nullable="true" length="32"
comment="Loyalty Tier Override"/>
</table>
</schema>Tipp: declarative schema erlaubt es, eine bereits existierende Tabelle wie company aus einem fremden Modul (Magento_Company) einfach um eine weitere <column> zu ergänzen, ohne die Tabelle im eigenen db_schema.xml neu zu deklarieren - Magento führt beim schema:upgrade-Lauf ein Diff über alle Module hinweg durch.
Schritt 2: Extension Attribute auf CompanyInterface
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:Api/etc/extension_attributes.xsd">
<extension_attributes for="Magento\Company\Api\Data\CompanyInterface">
<attribute code="loyalty_tier_override" type="string"/>
</extension_attributes>
</config>Ohne join-Angabe geht Magento davon aus, dass die Spalte bereits auf der Haupttabelle der Entität liegt - exakt der Fall hier, da Schritt 1 sie direkt in company angelegt hat. Ein join wäre nur nötig, läge der Wert in einer separaten Tabelle.
Schritt 3: Plugin auf CompanyRepositoryInterface
extension_attributes.xml allein füllt das Feld noch nicht automatisch - ein after-Plugin auf get()/getList() kopiert den rohen Spaltenwert (über getData() lesbar, weil Company wie jedes AbstractExtensibleModel die Spalte automatisch mitlädt) in das Extension Attribute; ein before-Plugin auf save() kopiert in umgekehrter Richtung zurück.
<?php
declare(strict_types=1);
namespace Mironsoft\Loyalty\Plugin\Company;
use Magento\Company\Api\CompanyRepositoryInterface;
use Magento\Company\Api\Data\CompanyExtensionFactory;
use Magento\Company\Api\Data\CompanyInterface;
/**
* Bridges the flat loyalty_tier_override column on the company table to the
* CompanyInterface extension attribute, in both read and write direction.
*/
class AddLoyaltyTierOverridePlugin
{
/**
* @param CompanyExtensionFactory $extensionFactory Creates the extension attributes container.
*/
public function __construct(
private readonly CompanyExtensionFactory $extensionFactory
) {
}
/**
* Copies the raw column value into the extension attribute after loading a company.
*
* @param CompanyRepositoryInterface $subject Unused, required by the plugin signature.
* @param CompanyInterface $result The freshly loaded company.
* @return CompanyInterface
*/
public function afterGet(CompanyRepositoryInterface $subject, CompanyInterface $result): CompanyInterface
{
return $this->applyExtensionAttribute($result);
}
/**
* Copies the extension attribute value back onto the plain data array before saving,
* so the resource model persists it as a normal column.
*
* @param CompanyRepositoryInterface $subject Unused, required by the plugin signature.
* @param CompanyInterface $company The company about to be saved.
* @return array<int, CompanyInterface>
*/
public function beforeSave(CompanyRepositoryInterface $subject, CompanyInterface $company): array
{
$extensionAttributes = $company->getExtensionAttributes();
if ($extensionAttributes !== null && $extensionAttributes->getLoyaltyTierOverride() !== null) {
// @phpstan-ignore-next-line CompanyInterface has no setData() in the interface, only on the model.
$company->setData('loyalty_tier_override', $extensionAttributes->getLoyaltyTierOverride());
}
return [$company];
}
/**
* @param CompanyInterface $company The company to enrich with the extension attribute.
* @return CompanyInterface
*/
private function applyExtensionAttribute(CompanyInterface $company): CompanyInterface
{
$extensionAttributes = $company->getExtensionAttributes() ?? $this->extensionFactory->create();
// @phpstan-ignore-next-line CompanyInterface has no getData() in the interface, only on the model.
$extensionAttributes->setLoyaltyTierOverride($company->getData('loyalty_tier_override'));
$company->setExtensionAttributes($extensionAttributes);
return $company;
}
}<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
<type name="Magento\Company\Api\CompanyRepositoryInterface">
<plugin name="mironsoft_loyalty_add_tier_override"
type="Mironsoft\Loyalty\Plugin\Company\AddLoyaltyTierOverridePlugin"/>
</type>
</config>Achtung: getList() lädt in der Regel mehrere Companies auf einmal und ist in dieser Serie bewusst nicht mit einem eigenen Plugin abgedeckt, um das Beispiel überschaubar zu halten - in einem echten Modul müsste ein zweites, sehr ähnliches afterGetList()-Plugin über SearchResultsInterface::getItems() iterieren, sonst zeigt eine Company-Grid-Ansicht das Feld nicht korrekt an.
Das Admin-Formularfeld mit wiederverwendetem Source Model
Company besitzt ein eigenes, per UI Component definiertes Admin-Formular. Ein neues <field> im gleichnamigen UI-Component-File verschmilzt automatisch mit dem Original aus Magento_Company - dieselbe Merge-Logik, die auch reward_listing.xml in Kapitel 16 genutzt hätte, wäre dort ein Grid erweitert statt neu angelegt worden. options referenziert dieselbe LoyaltyTier-Klasse aus Kapitel 21 - Kapitel 24 erklärt im Detail, warum dieselbe Klasse hier problemlos wiederverwendbar ist, obwohl Company keine EAV-Entität ist.
<?xml version="1.0"?>
<form xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd">
<fieldset name="company">
<field name="loyalty_tier_override">
<settings>
<dataType>select</dataType>
<formElement>select</formElement>
<label translate="true">Loyalty Tier Override</label>
<dataScope>loyalty_tier_override</dataScope>
<options class="Mironsoft\Loyalty\Model\Source\LoyaltyTier"/>
</settings>
</field>
</fieldset>
</form>Modul-Abhängigkeit Magento_Company ergänzen
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
<module name="Mironsoft_Loyalty">
<sequence>
<module name="Magento_Customer"/>
<module name="Magento_Sales"/>
<module name="Magento_Eav"/>
<module name="Magento_Catalog"/>
<module name="Magento_Company"/>
</sequence>
</module>
</config>Achtung: Magento_Company ist Teil von Magento Commerce/B2B und in Open Source nicht enthalten - dieses Kapitel setzt voraus, dass B2B-Funktionalität im Shop aktiv ist. Ohne Magento_Company im System schlägt bereits die module.xml-sequence-Deklaration beim setup:upgrade fehl, weil das referenzierte Modul unbekannt ist.
bin/magento setup:upgrade
bin/magento cache:flushDrei völlig unterschiedliche Techniken für vier Attribute: EavSetup für Produkt, Kategorie und Kunde, dann Spalte + Extension Attribute + Plugin für die flache Company-Entität. Kapitel 23 fügt eine vierte Technik hinzu - denn auch Order und Order Item sind flach, aber Magento bietet für sie einen eigenen, spezialisierten Helfer.