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

Product Attribute: Creating loyalty_points_multiplier on the Product

Product Attribute: Creating loyalty_points_multiplier on the Product

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

Block 2 built a completely new EAV entity with the reward entity - its own tables, its own entity type, its own model/resource model pair. Block 3 needs none of that: catalog_product already exists as a fully-fledged EAV entity, complete with an eav_entity_type row, attribute sets, and the familiar value tables like catalog_product_entity_decimal and friends. A new product attribute is therefore far simpler than a new entity: just a single call to EavSetup::addAttribute() on an already existing entity type code.

No new entity type - just an attribute

loyalty_points_multiplier (type decimal, default 1.0) determines the factor a product's base point count gets multiplied by - chapter 5 already reserved the $productMultiplier parameter in PointsCalculator::calculatePoints() exactly for this attribute. A value of 2.0 means "double points", a value of 0.0 means "this product earns no points at all, even when bought".

app/code/Mironsoft/Loyalty/Setup/Patch/Data/InstallProductLoyaltyAttribute.php
<?php

declare(strict_types=1);

namespace Mironsoft\Loyalty\Setup\Patch\Data;

use Magento\Catalog\Model\Product;
use Magento\Eav\Model\Entity\Attribute\ScopedAttributeInterface;
use Magento\Eav\Setup\EavSetupFactory;
use Magento\Framework\Setup\ModuleDataSetupInterface;
use Magento\Framework\Setup\Patch\DataPatchInterface;

/**
 * Registers the loyalty_points_multiplier decimal attribute on catalog_product.
 */
class InstallProductLoyaltyAttribute implements DataPatchInterface
{
    /**
     * @param ModuleDataSetupInterface $moduleDataSetup Provides the setup connection for the patch.
     * @param EavSetupFactory $eavSetupFactory Creates the EavSetup helper used to register the attribute.
     */
    public function __construct(
        private readonly ModuleDataSetupInterface $moduleDataSetup,
        private readonly EavSetupFactory $eavSetupFactory
    ) {
    }

    /**
     * Adds the loyalty_points_multiplier attribute to the existing catalog_product entity.
     *
     * @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(Product::ENTITY, 'loyalty_points_multiplier', [
            'type' => 'decimal',
            'label' => 'Loyalty Points Multiplier',
            'input' => 'text',
            'required' => false,
            'default' => '1.0000',
            'global' => ScopedAttributeInterface::SCOPE_GLOBAL,
            'group' => 'General',
            'sort_order' => 100,
            'visible' => true,
            'user_defined' => true,
            'apply_to' => '',
            'used_in_product_listing' => true,
        ]);

        $this->moduleDataSetup->getConnection()->endSetup();
    }

    /**
     * @return array<int, string>
     */
    public static function getDependencies(): array
    {
        return [];
    }

    /**
     * @return array<int, string>
     */
    public function getAliases(): array
    {
        return [];
    }
}

The most important array keys at a glance

  • type - the backend_type, determines which value table (catalog_product_entity_decimal) the value gets written to, the exact concept from chapter 14.
  • apply_to - an empty string means "visible for all product types"; a comma-separated list like 'simple,virtual,configurable' would, for instance, hide the attribute on grouped products.
  • global - the scope, deliberately SCOPE_GLOBAL as a starting point; chapter 25 explains why this specific attribute later gets changed to SCOPE_WEBSITE.
  • used_in_product_listing - lets the product collection loader in the frontend/layered-navigation context load the value efficiently without an explicit addAttributeToSelect() call.

Achtung: addAttribute() assigns a new attribute to the Default attribute set only by default. If the shop already has additional attribute sets (e.g. for different product categories), loyalty_points_multiplier does not automatically appear there - that requires an additional loop over $eavSetup->getAllAttributeSetIds(Product::ENTITY) with one addAttributeToSet() call each, or a manual assignment in the admin under Stores > Attribute Set.

Adding the Magento_Catalog module dependency

catalog_product comes from Magento_Catalog - the same reasoning as in chapter 2 (Magento_Customer, Magento_Sales) and chapter 11 (Magento_Eav) applies here too: without a sequence entry there's no guarantee catalog_product already exists when this patch runs.

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"/>
        </sequence>
    </module>
</config>
bin/magento setup:upgrade
bin/magento indexer:reindex catalog_product_attribute
bin/magento cache:flush

Tipp: After every new or changed catalog attribute, a targeted reindex of catalog_product_attribute is worth it instead of a full indexer:reindex - a noticeable time difference on large catalogs, and bin/cache-clean (CLAUDE.md) then handles the Hyvä watcher part if frontend templates are affected too.

With loyalty_points_multiplier in place on the product, chapter 20 follows the same pattern for the category - with one important difference in the default scope that chapter 25 picks up later.