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

Company Attribute: B2B Special Conditions (Company Attribute Basics)

Company Attribute: B2B Special Conditions (Company Attribute Basics)

~9 Min. Lesezeit Zuletzt aktualisiert am August 9, 2026

Four attributes, four calls to EavSetup::addAttribute() - that's how block 3 could continue if company were also an EAV entity. It isn't: Magento\Company\Model\Company is an ordinary AbstractExtensibleModel over the flat company table, just like Magento\Sales\Model\Order (chapter 23) or any custom resource model from block 1. EavSetup::addAttribute(Company::ENTITY, ...) would simply fail with an error, because eav_entity_type never has a row for companies. A B2B special-conditions field like loyalty_tier_override therefore needs a completely different approach: a real column plus an extension attribute.

Why company is not an EAV entity

Magento_Company is part of Magento Commerce/B2B and models company accounts: multiple customers belong to a company, and the company itself carries master data (name, tax id, credit-limit reference when Magento_CompanyCredit is active) in a single flat table row. The reason lies in the nature of the data: company data doesn't vary in structure from company to company, only in value - exactly the case chapter 18 recommended a flat table over EAV for.

  1. A new column in the company table via db_schema.xml - CLAUDE.md's rule of using declarative schema instead of install scripts applies here just as much as it did for the points ledger table in chapter 3.
  2. An extension attribute on CompanyInterface, so the value can be reached cleanly via getExtensionAttributes()/setExtensionAttributes() instead of a "raw" getData() access.
  3. A plugin on CompanyRepositoryInterface that copies the column value into the extension attribute on load, and back again on save.

Step 1: the column 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: see chapter 3 -->
    <!-- tables mironsoft_loyalty_reward_entity and _varchar/_int/_decimal/_text/_datetime: see chapter 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 lets you extend an already existing table like company from a foreign module (Magento_Company) with just another <column>, without re-declaring the whole table in your own db_schema.xml - Magento runs a diff across all modules during schema:upgrade.

Step 2: extension attribute on 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>

Without a join attribute, Magento assumes the column already lives on the entity's main table - exactly the case here, since step 1 added it directly to company. A join would only be needed if the value lived in a separate table.

Step 3: plugin on CompanyRepositoryInterface

extension_attributes.xml alone doesn't automatically populate the field - an after plugin on get()/getList() copies the raw column value (readable via getData(), since Company automatically loads the column just like any AbstractExtensibleModel) into the extension attribute; a before plugin on save() copies it back in the opposite direction.

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() typically loads several companies at once and is deliberately not covered by its own plugin in this series, to keep the example manageable - in a real module, a second, very similar afterGetList() plugin would have to iterate over SearchResultsInterface::getItems(), otherwise a company grid view wouldn't display the field correctly.

The admin form field, reusing the source model

Company has its own admin form defined via a UI component. A new <field> in the same-named UI component file merges automatically with the original from Magento_Company - the same merge logic that reward_listing.xml in chapter 16 would have used had it extended a grid instead of creating a new one. options references the same LoyaltyTier class from chapter 21 - chapter 24 explains in detail why this same class is trivially reusable here even though company isn't an EAV entity.

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>

Adding the Magento_Company module dependency

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 is part of Magento Commerce/B2B and not included in Open Source - this chapter assumes B2B functionality is active in the shop. Without Magento_Company in the system, the module.xml sequence declaration itself fails during setup:upgrade, because the referenced module is unknown.

bin/magento setup:upgrade
bin/magento cache:flush

Three completely different techniques for four attributes: EavSetup for product, category, and customer, then column + extension attribute + plugin for the flat company entity. Chapter 23 adds a fourth technique - because order and order item are flat too, but Magento offers them their own, specialized helper.