Attributes
What an attribute definition contains: code, backend type, input type, and the optional source, backend and frontend models.
$this->eavConfig->getAttribute('catalog_product', 'color')->getBackendType()An attribute is one named field, defined once and shared by every product. The definition lives in two tables. eav_attribute holds the parts every entity type shares. catalog_eav_attribute holds the catalog-only parts, such as is_filterable and used_in_product_listing. The second one is called the additional table, and the entity type's row in eav_entity_type names it.
The four things that matter
attribute_code is the identifier you use in code. It starts with a letter and is at most 60 characters, because a flat-table column name cannot exceed 64 in MySQL.
backend_type is one of static, varchar, int, decimal, text or datetime. It decides which value table holds the data.
frontend_input is the control the admin form renders: text, textarea, texteditor, select, multiselect, boolean, date, price, media_image.
Three optional classes attach behaviour:
- Source model supplies the options for a select or multiselect.
- Backend model validates and transforms the value around load, save and delete.
- Frontend model formats the value for display.
Scope
is_global is 1 for Global, 0 for Store View and 2 for Website. A store-view attribute keeps one value row per store_id; a global one keeps a single row with store_id 0.
After editing a source or backend class, run bin/magento cache:clean eav. A brand new class also needs bin/magento setup:di:compile before it resolves in production mode.
Example
<?php
// app/code/Vendor/Catalog/Model/Product/Attribute/Source/Origin.php
declare(strict_types=1);
namespace Vendor\Catalog\Model\Product\Attribute\Source;
use Magento\Eav\Model\Entity\Attribute\Source\AbstractSource;
class Origin extends AbstractSource
{
private const OPTIONS = [
'pt' => 'Portugal',
'it' => 'Italy',
'jp' => 'Japan',
];
/**
* @return array<int, array<string, mixed>>
*/
public function getAllOptions()
{
if ($this->_options === null) {
$this->_options = [];
foreach (self::OPTIONS as $value => $label) {
$this->_options[] = ['value' => $value, 'label' => __($label)];
}
}
return $this->_options;
}
}
// Referenced from a data patch as:
// 'source' => Origin::class
// which is shorthand for the same string written out:
// 'source' => 'Vendor\Catalog\Model\Product\Attribute\Source\Origin'When to use it
- Deciding between a source model and stored options for a dropdown field.
- Enforcing a format rule on a product field everywhere it can be saved.
- Working out why a store view shows the default value instead of its own.
More examples
A backend model that validates on save
Attached with 'backend' => Isbn::class when the attribute is created. The check then runs wherever the product is saved: the admin form, an import, the REST API, a cron job. Reach for a backend model instead of a plugin or an observer when the rule belongs to the field rather than to one screen. The parameter stays untyped because the parent method is untyped; widening it would break the inherited signature.
<?php
// app/code/Vendor/Catalog/Model/Product/Attribute/Backend/Isbn.php
declare(strict_types=1);
namespace Vendor\Catalog\Model\Product\Attribute\Backend;
use Magento\Eav\Model\Entity\Attribute\Backend\AbstractBackend;
use Magento\Framework\Exception\LocalizedException;
class Isbn extends AbstractBackend
{
/**
* @param \Magento\Framework\DataObject $object
* @return bool
* @throws LocalizedException
*/
public function validate($object)
{
$code = $this->getAttribute()->getAttributeCode();
$value = $object->getData($code);
if ($value !== null && $value !== '' && preg_match('/^\d{13}$/', (string) $value) !== 1) {
throw new LocalizedException(
__('The ISBN must be 13 digits.')
);
}
return parent::validate($object);
}
}Read a definition at runtime
ProductAttributeRepositoryInterface::get() takes the attribute code, not an id, and throws NoSuchEntityException when the code does not exist. Use this when you are debugging someone else's attribute and need to know whether a value will arrive as a string, an option id or a decimal before you write code against it.
<?php
// app/code/Vendor/Catalog/Model/AttributeReport.php
declare(strict_types=1);
namespace Vendor\Catalog\Model;
use Magento\Catalog\Api\ProductAttributeRepositoryInterface;
class AttributeReport
{
public function __construct(
private readonly ProductAttributeRepositoryInterface $attributeRepository
) {
}
/**
* @return array<string, mixed>
*/
public function describe(string $attributeCode): array
{
$attribute = $this->attributeRepository->get($attributeCode);
return [
'code' => $attribute->getAttributeCode(),
'backend_type' => $attribute->getBackendType(),
'frontend_input' => $attribute->getFrontendInput(),
'source_model' => $attribute->getSourceModel(),
'required' => (bool) $attribute->getIsRequired(),
'scope_global' => (int) $attribute->getIsGlobal() === 1,
];
}
}
Discussion