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

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.

app/code/Mironsoft/Loyalty/Setup/Patch/Data/InstallRewardEntityType.php
<?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.

app/code/Mironsoft/Loyalty/Setup/Patch/Data/InstallRewardAttributes.php
<?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.

app/code/Mironsoft/Loyalty/Model/Reward/Source/RewardType.php
<?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:flush

Tipp: 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.