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.
- A new column in the
companytable viadb_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. - An extension attribute on
CompanyInterface, so the value can be reached cleanly viagetExtensionAttributes()/setExtensionAttributes()instead of a "raw"getData()access. - A plugin on
CompanyRepositoryInterfacethat copies the column value into the extension attribute on load, and back again on save.
Step 1: the column 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: 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
<?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.
<?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() 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.
<?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
<?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:flushThree 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.