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

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.

  1. Eine neue Spalte in der company-Tabelle per db_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.
  2. Ein Extension Attribute auf CompanyInterface, damit der Wert sauber über getExtensionAttributes()/setExtensionAttributes() erreichbar ist statt über einen "rohen" getData()-Zugriff.
  3. 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

app/code/Mironsoft/Loyalty/etc/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

app/code/Mironsoft/Loyalty/etc/extension_attributes.xml
<?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.

app/code/Mironsoft/Loyalty/Plugin/Company/AddLoyaltyTierOverridePlugin.php
<?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;
    }
}
app/code/Mironsoft/Loyalty/etc/di.xml
<?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.

app/code/Mironsoft/Loyalty/view/adminhtml/ui_component/company_form.xml
<?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

app/code/Mironsoft/Loyalty/etc/module.xml
<?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:flush

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