Defining Custom EAV Attributes for Rewards (Attribute Setup Script)
Defining Custom EAV Attributes for Rewards (Attribute Setup Script)
~8 Min. Lesezeit Zuletzt aktualisiert am August 9, 2026
Six tables (chapter 11) and a model/resource model pair (chapter 12) aren't enough yet - as long as eav_entity_type doesn't know about mironsoft_loyalty_reward and eav_attribute doesn't know any of the six reward properties, there's simply nothing for AbstractEntity to load. This chapter registers both via data patches - Magento's modern, declarative replacement for the old InstallData/UpgradeData scripts, in the same spirit as CLAUDE.md's rule of using db_schema.xml instead of install scripts for the schema itself.
First patch: registering the entity type
EavSetup::addEntityType() creates the row in eav_entity_type and links it to the resource model, attribute model, and main table. If the entity type is new, the method also automatically creates an attribute set named Default with a group named General - both are entirely sufficient for rewards, a custom set isn't needed here.
<?php
declare(strict_types=1);
namespace Mironsoft\Loyalty\Setup\Patch\Data;
use Magento\Eav\Model\Entity\Attribute as EavAttribute;
use Magento\Eav\Model\ResourceModel\Entity\Attribute\Collection as EavAttributeCollection;
use Magento\Eav\Setup\EavSetupFactory;
use Magento\Framework\Setup\ModuleDataSetupInterface;
use Magento\Framework\Setup\Patch\DataPatchInterface;
use Mironsoft\Loyalty\Model\Reward;
use Mironsoft\Loyalty\Model\ResourceModel\Reward as RewardResource;
/**
* Registers the mironsoft_loyalty_reward EAV entity type, including its
* automatically created "Default" attribute set and "General" group.
*/
class InstallRewardEntityType implements DataPatchInterface
{
/**
* @param ModuleDataSetupInterface $moduleDataSetup Provides the setup connection for the patch.
* @param EavSetupFactory $eavSetupFactory Creates the EavSetup helper used to register the entity type.
*/
public function __construct(
private readonly ModuleDataSetupInterface $moduleDataSetup,
private readonly EavSetupFactory $eavSetupFactory
) {
}
/**
* Adds the eav_entity_type row for rewards.
*
* @return void
*/
public function apply(): void
{
$this->moduleDataSetup->getConnection()->startSetup();
/** @var \Magento\Eav\Setup\EavSetup $eavSetup */
$eavSetup = $this->eavSetupFactory->create(['setup' => $this->moduleDataSetup]);
$eavSetup->addEntityType(Reward::ENTITY, [
'entity_model' => RewardResource::class,
'attribute_model' => EavAttribute::class,
'table' => 'mironsoft_loyalty_reward_entity',
'entity_attribute_collection' => EavAttributeCollection::class,
]);
$this->moduleDataSetup->getConnection()->endSetup();
}
/**
* @return array<int, string>
*/
public static function getDependencies(): array
{
return [];
}
/**
* @return array<int, string>
*/
public function getAliases(): array
{
return [];
}
}Second patch: the six attributes
The second patch depends on the first via getDependencies() - the entity type has to exist before attributes can be assigned to it. EavSetup::addAttribute() expects the entity type code, the attribute code, and an array with type (the backend_type, see chapter 14), input (the admin form field), required, and further fields.
<?php
declare(strict_types=1);
namespace Mironsoft\Loyalty\Setup\Patch\Data;
use Magento\Eav\Model\Entity\Attribute\Source\Boolean as BooleanSource;
use Magento\Eav\Setup\EavSetupFactory;
use Magento\Framework\Setup\ModuleDataSetupInterface;
use Magento\Framework\Setup\Patch\DataPatchInterface;
use Mironsoft\Loyalty\Model\Reward;
use Mironsoft\Loyalty\Model\Reward\Source\RewardType;
/**
* Registers the six custom EAV attributes of the mironsoft_loyalty_reward entity:
* title, description, points_cost, discount_value, reward_type, is_active.
*/
class InstallRewardAttributes 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 all six reward attributes to the "General" group of the "Default" attribute set.
*
* @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(Reward::ENTITY, 'title', [
'type' => 'varchar',
'label' => 'Title',
'input' => 'text',
'required' => true,
'sort_order' => 10,
'group' => 'General',
]);
$eavSetup->addAttribute(Reward::ENTITY, 'description', [
'type' => 'text',
'label' => 'Description',
'input' => 'textarea',
'required' => false,
'sort_order' => 20,
'group' => 'General',
]);
$eavSetup->addAttribute(Reward::ENTITY, 'points_cost', [
'type' => 'int',
'label' => 'Points Cost',
'input' => 'text',
'required' => true,
'sort_order' => 30,
'group' => 'General',
]);
$eavSetup->addAttribute(Reward::ENTITY, 'discount_value', [
'type' => 'decimal',
'label' => 'Discount Value',
'input' => 'text',
'required' => false,
'sort_order' => 40,
'group' => 'General',
]);
$eavSetup->addAttribute(Reward::ENTITY, 'reward_type', [
'type' => 'varchar',
'label' => 'Reward Type',
'input' => 'select',
'source' => RewardType::class,
'required' => true,
'sort_order' => 50,
'group' => 'General',
]);
$eavSetup->addAttribute(Reward::ENTITY, 'is_active', [
'type' => 'int',
'label' => 'Is Active',
'input' => 'boolean',
'source' => BooleanSource::class,
'required' => false,
'default' => '1',
'sort_order' => 60,
'group' => 'General',
]);
$this->moduleDataSetup->getConnection()->endSetup();
}
/**
* @return array<int, string>
*/
public static function getDependencies(): array
{
return [InstallRewardEntityType::class];
}
/**
* @return array<int, string>
*/
public function getAliases(): array
{
return [];
}
}The source model for reward_type
reward_type is defined as a dropdown (input => 'select') and needs a source model for that, providing the option list - the same role chapter 24 in block 3 plays for the loyalty tier dropdown on the customer.
<?php
declare(strict_types=1);
namespace Mironsoft\Loyalty\Model\Reward\Source;
use Magento\Eav\Model\Entity\Attribute\Source\AbstractSource;
/**
* Source model for the reward_type dropdown attribute.
*/
class RewardType extends AbstractSource
{
/**
* @var string
*/
public const TYPE_DISCOUNT = 'discount';
/**
* @var string
*/
public const TYPE_FREE_PRODUCT = 'free_product';
/**
* @var string
*/
public const TYPE_FREE_SHIPPING = 'free_shipping';
/**
* Returns the dropdown options shown in the admin form and used for grid filtering.
*
* @return array<int, array{value: string, label: string}>
*/
public function getAllOptions(): array
{
if ($this->_options === null) {
$this->_options = [
['value' => self::TYPE_DISCOUNT, 'label' => __('Discount')],
['value' => self::TYPE_FREE_PRODUCT, 'label' => __('Free Product')],
['value' => self::TYPE_FREE_SHIPPING, 'label' => __('Free Shipping')],
];
}
return $this->_options;
}
}bin/magento setup:upgrade
bin/magento cache:flushTipp: Data patches automatically run exactly once on every setup:upgrade - Magento tracks executed patches in the patch_list table. A later change to apply() no longer affects already installed environments; attribute changes after the first release need a new, additional patch.
Achtung: getDependencies() is a static method - an easy-to-miss difference from getAliases(), which is an instance method. A PHPStan level 5 run over Setup/Patch/Data/ makes such signature mismatches immediately visible.
With the entity type registered and six attributes in place, a fully functional EAV entity now exists. Chapter 14 takes a closer look at the four backend_type values actually in use.