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

Attribute Source Models for Dropdown Lists

Attribute Source Models for Dropdown Lists

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

LoyaltyTier has already appeared twice in this block: as the source class of the EAV attribute loyalty_tier in chapter 21, and as the options class of the company form field loyalty_tier_override in chapter 22 - even though customer is an EAV entity and company isn't. This chapter explains why the exact same class works in both completely different places, and goes deeper into how source models work in general - beyond the simple repetition from chapter 13.

One interface, two usage contexts

The common denominator is \Magento\Framework\Data\OptionSourceInterface with its single method toOptionArray(): array. \Magento\Eav\Model\Entity\Attribute\Source\AbstractSource, which LoyaltyTier extends, already implements this interface - internally, toOptionArray() simply calls getAllOptions() and reshapes the result. An EAV attribute's source array key (chapter 21) expects a class with exactly this base; a UI component form field (chapter 22) only requires any class implementing OptionSourceInterface - AbstractSource automatically satisfies both requirements at once.

interface OptionSourceInterface
{
    /**
     * @return array<int, array{value: string, label: string}>
     */
    public function toOptionArray(): array;
}

The built-in alternative: table source without a custom class

For a single dropdown that's never reused elsewhere, a shorter variant using a direct 'option' key in the addAttribute() array would also be an option instead of a custom class - Magento then manages the option values itself in eav_attribute_option/eav_attribute_option_value and even makes them editable in the admin via Store > Attributes UI, with no PHP class at all.

$eavSetup->addAttribute(Customer::ENTITY, 'loyalty_tier', [
    'type' => 'varchar',
    'input' => 'select',
    'option' => [
        'values' => [
            'bronze' => 'Bronze',
            'silver' => 'Silver',
            'gold' => 'Gold',
        ],
    ],
    // ...
]);

Achtung: This shorter variant was deliberately not the choice for loyalty_tier in chapter 21: it only works for real EAV attributes and wouldn't have been available at all for the company form field in chapter 22 - a dedicated source model class was the only option reusable in both places. For values only needed in a single place (e.g. is_active in chapter 13, solved there via BooleanSource instead of option), the choice is mostly a matter of taste.

Caching inside the source model

LoyaltyTier's getAllOptions() (chapter 21) checks $this->_options === null before rebuilding the array - a convention AbstractSource itself prescribes. Without this check, every call (potentially several times per page load, e.g. once for the grid display and once for a form rebuild) would needlessly create a new array - negligible for three static values, but a real difference for a dynamic option list computed from ledger data.

addValueSortToCollection() for grid filters

AbstractSource also offers addValueSortToCollection(), which allows sorting by the option label instead of the stored raw value - relevant as soon as an admin grid should be sortable by loyalty_tier and "Bronze" should appear alphabetically before "Gold", even though the internal value bronze happens to already sort before gold anyway. With differently named internal values (e.g. tier_1/tier_2/tier_3), the difference between raw-value sorting and label sorting would actually be visible.

Don't forget translation

__('Bronze'), __('Silver'), __('Gold') in chapter 21 aren't just a style convention: without the __() wrapper, the labels would never be recognized as translatable strings by an i18n CSV translation (block 11, chapters 89-90) and would stay identical in the English/German admin store even if a de_DE.csv contained a translation for them.

Tipp: A source model that dynamically loads options from the database (e.g. a list of active rewards, conceivable in principle for chapter 13) should never load in its own constructor - the access strictly belongs in getAllOptions(), so the null caching from this chapter kicks in and the class doesn't trigger a database query on every object instantiation, even when the options end up never being queried at all.

With source model reusability settled, chapter 25 turns to the second major configuration axis of every attribute: scope - who sees a value and who's allowed to set it, website by website, store view by store view.