EAV and product attributes
Products in Magento have no single table with "color", "size", "warranty" columns. Every property is an attribute, and new attributes are added without changing the database structure. This storage model is called EAV: entity, attribute, value.
How it works
| Table | What it stores |
|---|---|
catalog_product_entity | The products themselves: ID, SKU, type |
eav_attribute | Attribute definitions: code, type, label |
catalog_product_entity_varchar, _int, _decimal, _text, _datetime | Values: product ID, attribute ID, store view, value |
Thanks to the store view column, a product name or description can differ between languages. You do not work with these tables directly — values are read and written through the product.
An attribute can be created in the admin panel: Stores → Attributes → Product. But then it exists only on that server. To make an attribute part of a module, create it with a data patch.
A patch with an attribute
Let's give products a warranty period in months:
// app/code/Acme/Hello/Setup/Patch/Data/AddWarrantyAttribute.php
namespace Acme\Hello\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;
class AddWarrantyAttribute implements DataPatchInterface
{
public function __construct(
private ModuleDataSetupInterface $setup,
private EavSetupFactory $eavSetupFactory,
) {}
public function apply(): self
{
$eavSetup = $this->eavSetupFactory->create(['setup' => $this->setup]);
$eavSetup->addAttribute(Product::ENTITY, 'warranty_months', [
'type' => 'int',
'label' => 'Warranty, months',
'input' => 'text',
'required' => false,
'global' => ScopedAttributeInterface::SCOPE_GLOBAL,
'user_defined' => true,
'visible_on_front' => true,
'used_in_product_listing' => true,
'group' => 'General',
]);
return $this;
}
public static function getDependencies(): array
{
return [];
}
public function getAliases(): array
{
return [];
}
}Run bin/magento setup:upgrade. Open any product in the admin panel: the form now has a "Warranty, months" field. An attribute label is written in English and translated in the admin panel or with a translation file.
Attribute options
| Option | What it sets |
|---|---|
type | Which value table to use: int, varchar, decimal, text, datetime |
input | The form field: text, textarea, select, boolean, date, price |
global | The value scope: global, website or store view |
user_defined | The attribute was not created by the core and can be deleted in the admin panel |
visible_on_front | Show it in the specification table on the product page |
used_in_product_listing | Load the value in product lists |
group | The section of the product form where the field goes |
Reading the value
// app/code/Acme/Hello/Model/ShopInfo.php
public function warranty(string $sku): int
{
$product = $this->productRepository->get($sku);
return (int) $product->getData('warranty_months');
}The method returns the product's warranty period, or zero if the field is empty. A product obtained from the repository contains all attributes.
Writing the value
// app/code/Acme/Hello/Model/ShopInfo.php
public function setWarranty(string $sku, int $months): void
{
$product = $this->productRepository->get($sku);
$product->setCustomAttribute('warranty_months', $months);
$this->productRepository->save($product);
}After saving, the value is visible in the product form in the admin panel and, thanks to visible_on_front, on the product page in the specifications tab.
Common mistake
The type and input options do not agree: for example, a select dropdown is declared with the varchar type, or a price with the int type. Values are saved into the wrong table, and catalog filters and sorting work incorrectly. Use int for select and boolean, decimal for price, and varchar for a short string.