Adding a Product Attribute
A data patch that creates a dropdown product attribute with options, shows it on the storefront, and makes it filterable.
$eavSetup->addAttribute($entityTypeId, $code, array $attr)A product attribute is created in code by a data patch: a class in Setup/Patch/Data/ that implements Magento\Framework\Setup\Patch\DataPatchInterface and runs once. Inside it, Magento\Eav\Setup\EavSetup::addAttribute() writes the definition.
The array keys are short names
The third argument's keys are not column names. A property mapper translates them, and the defaults matter: required defaults to 1, global to Global scope, and filterable, visible_on_front and used_in_product_listing all to 0. Omitting required gives you a mandatory field on every product form.
Storefront and layered navigation
visible_on_front puts the value in the product page's attribute list. used_in_product_listing makes it available on category and search listings. Layered navigation, the filter sidebar on a category page, needs filterable set to 1 (Filterable, with results) or 2 (no results), and an input type of select, multiselect or boolean. A text field cannot be a filter.
Then run the commands
bin/magento setup:upgrade
bin/magento indexer:reindex catalog_product_attribute catalogsearch_fulltext
bin/magento cache:flush
setup:upgrade applies the patch. The reindex is what makes the filter appear in the sidebar; without it the attribute exists and the sidebar does not change. In production mode, add bin/magento setup:di:compile, because the patch class is new.
Example
<?php
// app/code/Vendor/Catalog/Setup/Patch/Data/AddCountryOfOrigin.php
declare(strict_types=1);
namespace Vendor\Catalog\Setup\Patch\Data;
use Magento\Catalog\Api\Data\ProductAttributeInterface;
use Magento\Eav\Model\Entity\Attribute\ScopedAttributeInterface;
use Magento\Eav\Model\Entity\Attribute\Source\Table;
use Magento\Eav\Setup\EavSetup;
use Magento\Eav\Setup\EavSetupFactory;
use Magento\Framework\Setup\ModuleDataSetupInterface;
use Magento\Framework\Setup\Patch\DataPatchInterface;
class AddCountryOfOrigin implements DataPatchInterface
{
public function __construct(
private readonly ModuleDataSetupInterface $moduleDataSetup,
private readonly EavSetupFactory $eavSetupFactory
) {
}
public function apply(): void
{
/** @var EavSetup $eavSetup */
$eavSetup = $this->eavSetupFactory->create(['setup' => $this->moduleDataSetup]);
$eavSetup->addAttribute(
ProductAttributeInterface::ENTITY_TYPE_CODE,
'country_of_origin',
[
'label' => 'Country of Origin',
'type' => 'int',
'input' => 'select',
'source' => Table::class,
'global' => ScopedAttributeInterface::SCOPE_GLOBAL,
'required' => false,
'user_defined' => true,
'visible_on_front' => true,
'used_in_product_listing' => true,
'filterable' => 1,
'group' => 'Product Details',
'option' => ['values' => ['Portugal', 'Italy', 'Japan']],
]
);
}
public static function getDependencies(): array
{
return [];
}
public function getAliases(): array
{
return [];
}
}When to use it
- Adding a Country of Origin dropdown that merchants pick from and shoppers filter by.
- Shipping an attribute with a module so every environment gets it without a manual admin step.
- Turning an existing text attribute into a filter in the category sidebar.
More examples
What 'group' does, written out longhand
The shorthand reads well but is wider than it looks: with a 'group' key, addAttribute() walks every row of eav_attribute_set for the entity type and assigns the attribute in each. Omit the key and call addAttributeGroup() plus addAttributeToSet() yourself when the field belongs to one set only, which is the usual case for a module that adds fields for a single product range.
<?php
// app/code/Vendor/Catalog/Setup/Patch/Data/AssignToOneSetOnly.php
// Fragment of apply(), after addAttribute() has run without a 'group' key.
declare(strict_types=1);
$entityType = ProductAttributeInterface::ENTITY_TYPE_CODE;
// Shorthand, inside the addAttribute() array:
// 'group' => 'Product Details'
//
// It is the same as this loop, which addAttribute() runs for you
// over EVERY attribute set of the entity type:
$eavSetup->addAttributeGroup($entityType, 'Default', 'Product Details');
$eavSetup->addAttributeToSet(
$entityType,
'Default',
'Product Details',
'country_of_origin',
100
);
// Spelling it out is how you assign the attribute to ONE set.
// 'group' => ... would add it to all of them, including sets
// another extension created.Make the patch revertable
PatchRevertableInterface adds revert(), which runs when the module is uninstalled with bin/magento module:uninstall. Implement it for anything a merchant could reasonably want undone. Leave it off for a one-way data migration, where removing the attribute would destroy data the patch did not create.
<?php
// app/code/Vendor/Catalog/Setup/Patch/Data/AddCountryOfOrigin.php
// Added to the class in the main example.
declare(strict_types=1);
use Magento\Framework\Setup\Patch\PatchRevertableInterface;
// class AddCountryOfOrigin implements DataPatchInterface, PatchRevertableInterface
public function revert(): void
{
/** @var EavSetup $eavSetup */
$eavSetup = $this->eavSetupFactory->create(['setup' => $this->moduleDataSetup]);
$eavSetup->removeAttribute(
ProductAttributeInterface::ENTITY_TYPE_CODE,
'country_of_origin'
);
}
// bin/magento module:uninstall Vendor_Catalog
// calls revert() before removing the module's files.
//
// removeAttribute() deletes the definition AND every value row
// for it. There is no undo, so take a database dump first.Make an existing attribute filterable
updateAttribute() takes the storage column name, is_filterable, rather than the addAttribute() shorthand filterable. Use it to adjust a core or third-party attribute instead of recreating it. The product EAV indexer only indexes attributes where is_filterable, is_filterable_in_search or is_visible_in_advanced_search is greater than zero, which is why the reindex is not optional here.
<?php
// app/code/Vendor/Catalog/Setup/Patch/Data/MakeManufacturerFilterable.php
// Fragment of apply().
declare(strict_types=1);
$entityType = ProductAttributeInterface::ENTITY_TYPE_CODE;
// updateAttribute($entityTypeId, $id, $field, $value = null, $sortOrder = null)
$eavSetup->updateAttribute($entityType, 'manufacturer', 'is_filterable', 1);
$eavSetup->updateAttribute($entityType, 'manufacturer', 'used_in_product_listing', 1);
// Then:
// bin/magento setup:upgrade
// bin/magento indexer:reindex catalog_product_attribute catalogsearch_fulltext
// bin/magento cache:flush
Discussion