Configuring Attribute Visibility and Scope Correctly (Website/Store/Global)
Configuring Attribute Visibility and Scope Correctly (Website/Store/Global)
~7 Min. Lesezeit Zuletzt aktualisiert am August 9, 2026
Chapter 19 created loyalty_points_multiplier with SCOPE_GLOBAL, chapter 20 created loyalty_bonus_category directly with SCOPE_WEBSITE - both decisions were announced but not explained in depth. This chapter delivers that explanation, and shows how to change the scope of an already existing attribute afterward, without recreating the attribute definition itself.
The three scope levels
\Magento\Eav\Model\Entity\Attribute\ScopedAttributeInterface defines three constants that apply equally to product, category, and customer attributes:
SCOPE_GLOBAL(value0) - a single value for the entire installation, regardless of website or store view. Changes take effect immediately across all stores.SCOPE_WEBSITE(value1) - its own value per website; all store views of the same website share this value.SCOPE_STORE(value2) - its own value per store view, the finest granularity available.
Achtung: The naming is slightly misleading for customer attributes: in the customer attribute admin form, the dropdown option for SCOPE_WEBSITE is simply labeled "Website", but internally it's mapped through ScopedAttributeInterface exactly the same way as for products - the same mechanism, not two different ones.
Why loyalty_points_multiplier needs website scope
This project itself provides the best example: CLAUDE.md describes a dual-vendor workflow where the same module is operated in parallel under two brand names (Mironsoft/Abrams) - in practice, often as two websites on the same Magento installation. A global points multiplier would force both brands to run exactly the same points generosity, even though the business behind them can differ. A subsequent data patch changes the scope without deleting and recreating the attribute:
<?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;
/**
* Changes loyalty_points_multiplier from global to website scope, so the
* Mironsoft and Abrams websites (see CLAUDE.md dual-vendor workflow) can run
* different point multipliers.
*/
class UpdateProductLoyaltyAttributeScope implements DataPatchInterface
{
/**
* @param ModuleDataSetupInterface $moduleDataSetup Provides the setup connection for the patch.
* @param EavSetupFactory $eavSetupFactory Creates the EavSetup helper used to update the attribute.
*/
public function __construct(
private readonly ModuleDataSetupInterface $moduleDataSetup,
private readonly EavSetupFactory $eavSetupFactory
) {
}
/**
* Switches the attribute's is_global property to website scope.
*
* @return void
*/
public function apply(): void
{
$this->moduleDataSetup->getConnection()->startSetup();
/** @var \Magento\Eav\Setup\EavSetup $eavSetup */
$eavSetup = $this->eavSetupFactory->create(['setup' => $this->moduleDataSetup]);
$eavSetup->updateAttribute(
Product::ENTITY,
'loyalty_points_multiplier',
'is_global',
ScopedAttributeInterface::SCOPE_WEBSITE
);
$this->moduleDataSetup->getConnection()->endSetup();
}
/**
* @return array<int, string>
*/
public static function getDependencies(): array
{
return [\Mironsoft\Loyalty\Setup\Patch\Data\InstallProductLoyaltyAttribute::class];
}
/**
* @return array<int, string>
*/
public function getAliases(): array
{
return [];
}
}Achtung: updateAttribute() expects the new value of the database column is_global directly as its fourth parameter, not an associative array like addAttribute(). A common mistake is passing ['is_global' => ...] instead - this doesn't fail with an exception, it silently stores a wrong (because cast) value.
Changing scope with existing data
Switching from global to website or store scope on an attribute already in production use isn't purely cosmetic: the single existing value (store_id = 0) stays in place as the fallback for every scope that hasn't set its own value - exactly the store_id = 0 behavior chapter 18 described for rewards applies identically to product, category, and customer attributes. Only an actual, targeted save with a differing store context creates an additional row.
No scope concept on flat entities
loyalty_tier_override (company, chapter 22) and loyalty_points_earned (sales, chapter 23), by contrast, have no ScopedAttributeInterface mechanism at all - a flat column stores exactly one value per row, regardless of website or store. For company this makes sense anyway (a company belongs, by nature, to exactly one B2B context); for sales attributes the question is moot, since every order is already permanently tied to a single store the moment it exists. Anyone needing "scope-like" behavior on a flat entity anyway (e.g. different company conditions per website) would have to model that themselves - for instance via an additional mapping table.
bin/magento setup:upgrade
bin/magento indexer:reindex catalog_product_attribute
bin/magento cache:flushTipp: The configured scope of an attribute can always be looked up in the admin under Stores > Attributes > Product (or Category/Customer) under "Scope" - a quick way to check, before a bugfix, whether an unexpected value is actually caused by a misconfigured scope rather than the underlying business logic.
With scope and visibility settled, chapter 26 goes one step further: backend models, which don't just store an attribute value but actively compute or validate it on save.