Attribute Setup
Module Structure
Vendor/CustomAttributes/
├── Setup/
│ ├── InstallData.php
│ └── UpgradeData.php
├── Model/
│ └── Source/
│ ├── Color.php
│ └── Size.php
├── Observer/
│ └── CatalogProductLoadAfter.php
├── Plugin/
│ └── CatalogProduct/
│ └── ViewPlugin.php
└── etc/
├── module.xml
├── registration.php
└── di.xml
InstallData.php
<?php
namespace Vendor\CustomAttributes\Setup;
use Magento\Framework\Setup\InstallDataInterface;
use Magento\Framework\Setup\ModuleContextInterface;
use Magento\Framework\Setup\ModuleDataSetupInterface;
use Magento\Eav\Model\Entity\Attribute\ScopeInterface;
use Magento\Catalog\Model\Product;
class InstallData implements InstallDataInterface
{
public function install(ModuleDataSetupInterface $setup, ModuleContextInterface $context)
{
$setup->startSetup();
// Custom Color Attribute
$this->createAttribute($setup, [
'attribute_code' => 'custom_color',
'type' => 'select',
'label' => 'Color',
'input' => 'select',
'source' => \Vendor\CustomAttributes\Model\Source\Color::class,
'required' => false,
'sort_order' => 10,
'global' => ScopeInterface::SCOPE_STORE,
'group' => 'General'
]);
// Custom Size Attribute
$this->createAttribute($setup, [
'attribute_code' => 'custom_size',
'type' => 'select',
'label' => 'Size',
'input' => 'select',
'source' => \Vendor\CustomAttributes\Model\Source\Size::class,
'required' => false,
'sort_order' => 20,
'global' => ScopeInterface::SCOPE_STORE,
'group' => 'General'
]);
// Custom Material Attribute (text)
$this->createAttribute($setup, [
'attribute_code' => 'custom_material',
'type' => 'text',
'label' => 'Material',
'input' => 'text',
'required' => false,
'sort_order' => 30,
'global' => ScopeInterface::SCOPE_STORE,
'group' => 'General'
]);
// Custom Handmade Attribute (boolean)
$this->createAttribute($setup, [
'attribute_code' => 'custom_handmade',
'type' => 'int',
'label' => 'Handmade',
'input' => 'boolean',
'source' => \Magento\Eav\Model\Entity\Attribute\Source\Boolean::class,
'required' => false,
'sort_order' => 40,
'global' => ScopeInterface::SCOPE_STORE,
'group' => 'General'
]);
$setup->endSetup();
}
private function createAttribute($setup, $config)
{
$eavConfig = \Magento\Framework\App\ObjectManager::getInstance()
->create(\Magento\Eav\Model\Config::class);
$entityTypeId = $eavConfig->getEntityType(
Product::ENTITY
)->getId();
$attribute = $setup->getAttribute($entityTypeId, $config['attribute_code']);
if (!$attribute) {
$setup->addAttribute(
Product::ENTITY,
$config['attribute_code'],
[
'type' => $config['type'],
'label' => $config['label'],
'input' => $config['input'],
'source' => $config['source'] ?? null,
'required' => $config['required'],
'sort_order' => $config['sort_order'],
'global' => $config['global'],
'group' => $config['group'],
'visible_on_front' => true,
'is_used_in_grid' => true,
'is_filterable_in_grid' => true
]
);
}
}
}
UpgradeData.php (for adding options)
<?php
namespace Vendor\CustomAttributes\Setup;
use Magento\Framework\Setup\UpgradeDataInterface;
use Magento\Framework\Setup\ModuleContextInterface;
use Magento\Framework\Setup\ModuleDataSetupInterface;
use Magento\Eav\Model\Config as EavConfig;
use Magento\Catalog\Model\Product;
class UpgradeData implements UpgradeDataInterface
{
public function __construct(
private EavConfig $eavConfig
) {}
public function upgrade(ModuleDataSetupInterface $setup, ModuleContextInterface $context)
{
$setup->startSetup();
if (version_compare($context->getVersion(), '1.0.1', '<')) {
$this->addColorOptions($setup);
}
$setup->endSetup();
}
private function addColorOptions($setup)
{
$entityTypeId = $this->eavConfig->getEntityType(
Product::ENTITY
)->getId();
$attribute = $setup->getAttribute($entityTypeId, 'custom_color');
if ($attribute) {
$options = [
['label' => 'Red', 'value' => 'red'],
['label' => 'Blue', 'value' => 'blue'],
['label' => 'Green', 'value' => 'green'],
['label' => 'Black', 'value' => 'black'],
['label' => 'White', 'value' => 'white']
];
foreach ($options as $option) {
$setup->addAttributeOption([
'attribute_id' => $attribute->getId(),
'value' => [
'option' => [
$option['value']
]
]
]);
}
}
}
}
Source Models
Color Source Model
<?php
namespace Vendor\CustomAttributes\Model\Source;
use Magento\Eav\Model\Entity\Attribute\Source\AbstractSource;
class Color extends AbstractSource
{
public function toOptionArray(): array
{
return [
['value' => 'red', 'label' => __('Red')],
['value' => 'blue', 'label' => __('Blue')],
['value' => 'green', 'label' => __('Green')],
['value' => 'black', 'label' => __('Black')],
['value' => 'white', 'label' => __('White')],
['value' => 'yellow', 'label' => __('Yellow')],
['value' => 'purple', 'label' => __('Purple')]
];
}
}
Size Source Model
<?php
namespace Vendor\CustomAttributes\Model\Source;
use Magento\Eav\Model\Entity\Attribute\Source\AbstractSource;
class Size extends AbstractSource
{
public function toOptionArray(): array
{
return [
['value' => 'xs', 'label' => __('XS')],
['value' => 's', 'label' => __('S')],
['value' => 'm', 'label' => __('M')],
['value' => 'l', 'label' => __('L')],
['value' => 'xl', 'label' => __('XL')],
['value' => 'xxl', 'label' => __('XXL')]
];
}
}
Dynamic Source Model (from database)
<?php
namespace Vendor\CustomAttributes\Model\Source;
use Magento\Eav\Model\Entity\Attribute\Source\AbstractSource;
use Vendor\CustomAttributes\Model\ResourceModel\Option\CollectionFactory;
class DynamicOptions extends AbstractSource
{
public function __construct(
private CollectionFactory $optionCollectionFactory
) {}
public function toOptionArray(): array
{
$collection = $this->optionCollectionFactory->create();
$options = [];
foreach ($collection as $option) {
$options[] = [
'value' => $option->getId(),
'label' => $option->getName()
];
}
return $options;
}
}
Backend Model
<?php
namespace Vendor\CustomAttributes\Model\Backend;
use Magento\Eav\Model\Entity\Attribute\Backend\AbstractBackend;
class ValidateColor extends AbstractBackend
{
public function validate($object)
{
$value = $object->getData($this->getAttribute()->getAttributeCode());
if ($value && !in_array($value, ['red', 'blue', 'green', 'black', 'white'])) {
throw new \Magento\Framework\Exception\LocalizedException(
__('Invalid color value: %1', $value)
);
}
return $this;
}
}
Frontend Model
<?php
namespace Vendor\CustomAttributes\Model\Frontend;
use Magento\Eav\Model\Entity\Attribute\Frontend\AbstractFrontend;
class ColorSwatch extends AbstractFrontend
{
public function output($object)
{
$value = $object->getData($this->getAttribute()->getAttributeCode());
$colors = [
'red' => '#FF0000',
'blue' => '#0000FF',
'green' => '#00FF00',
'black' => '#000000',
'white' => '#FFFFFF'
];
$color = $colors[$value] ?? '#CCCCCC';
return sprintf(
'<span class="color-swatch" style="background-color: %s;"></span> %s',
$color,
$value
);
}
}
Frontend Display
Product View Plugin
<?php
namespace Vendor\CustomAttributes\Plugin\CatalogProduct;
class ViewPlugin
{
public function afterGetAdditionalData(
$subject,
$result
) {
// Add custom attributes to additional data
$customAttributes = [
'custom_color',
'custom_size',
'custom_material',
'custom_handmade'
];
foreach ($customAttributes as $attributeCode) {
$value = $subject->getData($attributeCode);
if ($value !== null) {
$result[$attributeCode] = $value;
}
}
return $result;
}
}
Layout XML
<!-- view/frontend/layout/catalog_product_view.xml -->
<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
<body>
<referenceContainer name="product.info.main">
<block class="Vendor\CustomAttributes\Block\Product\CustomAttributes"
name="custom.attributes"
template="Vendor_CustomAttributes::product/attributes.phtml"
after="product.info.price"
ifconfig="custom_attributes/show_on_product_page"/>
</referenceContainer>
</body>
</page>
Block Class
<?php
namespace Vendor\CustomAttributes\Block\Product;
use Magento\Framework\View\Element\Template;
use Magento\Catalog\Model\Product;
class CustomAttributes extends Template
{
public function getProduct(): Product
{
return $this->getData('product');
}
public function getCustomAttributes(): array
{
$product = $this->getProduct();
$attributes = [];
$attributeCodes = ['custom_color', 'custom_size', 'custom_material', 'custom_handmade'];
foreach ($attributeCodes as $code) {
$attribute = $product->getResource()->getAttribute($code);
if ($attribute && $product->getData($code)) {
$attributes[] = [
'label' => $attribute->getStoreLabel(),
'value' => $this->getAttributeValue($product, $attribute),
'code' => $code
];
}
}
return $attributes;
}
private function getAttributeValue(Product $product, $attribute)
{
if ($attribute->getSourceModel()) {
return $attribute->getSource()->getOptionText($product->getData($attribute->getAttributeCode()));
}
return $product->getData($attribute->getAttributeCode());
}
}
Template
<!-- view/frontend/templates/product/attributes.phtml -->
<?php
/** @var \Vendor\CustomAttributes\Block\Product\CustomAttributes $block */
$attributes = $block->getCustomAttributes();
?>
<?php if (!empty($attributes)): ?>
<div class="custom-product-attributes">
<h3><?= __('Product Details') ?></h3>
<table class="attributes-table">
<?php foreach ($attributes as $attribute): ?>
<tr>
<th><?= $block->escapeHtml($attribute['label']) ?>:</th>
<td><?= $block->escapeHtml($attribute['value']) ?></td>
</tr>
<?php endforeach; ?>
</table>
</div>
<?php endif; ?>
CSS
/* view/frontend/web/css/custom-attributes.css */
.custom-product-attributes {
margin: 20px 0;
padding: 20px;
background: #f9f9f9;
border-radius: 8px;
}
.custom-product-attributes h3 {
margin-bottom: 15px;
color: #333;
}
.attributes-table {
width: 100%;
border-collapse: collapse;
}
.attributes-table th,
.attributes-table td {
padding: 10px;
text-align: left;
border-bottom: 1px solid #ddd;
}
.attributes-table th {
width: 30%;
font-weight: 600;
color: #666;
}
.color-swatch {
display: inline-block;
width: 20px;
height: 20px;
border-radius: 50%;
margin-right: 5px;
border: 1px solid #ccc;
}
Admin and Testing
Admin Configuration
Assign Attributes to Set
1. Go to Stores > Attributes > Product
2. Find custom_color, click Edit
3. Set "Default Label": Color
4. Set "Storefront Properties":
- Visible on Catalog Pages on Storefront: Yes
- Used in Product Listing: Yes
5. Save Attribute
6. Go to Stores > Attributes > Attribute Set
7. Edit "Default" set
8. Drag custom attributes to desired group
9. Save
Configure Product
1. Go to Catalog > Products
2. Edit or create a product
3. Find custom attributes in the form
4. Set values:
- Color: Red
- Size: M
- Material: Cotton
- Handmade: Yes
5. Save Product
Testing
Unit Test
<?php
namespace Vendor\CustomAttributes\Test\Unit\Model\Source;
use PHPUnit\Framework\TestCase;
use Vendor\CustomAttributes\Model\Source\Color;
class ColorTest extends TestCase
{
private $color;
protected function setUp(): void
{
$this->color = new Color();
}
public function testToOptionArray()
{
$options = $this->color->toOptionArray();
$this->assertIsArray($options);
$this->assertNotEmpty($options);
$values = array_column($options, 'value');
$this->assertContains('red', $values);
$this->assertContains('blue', $values);
}
public function testGetOptionByValue()
{
$options = $this->color->toOptionArray();
$option = $this->color->getOptionByValue('red');
$this->assertNotNull($option);
}
}
Integration Test
<?php
namespace Vendor\CustomAttributes\Test\Integration\Model;
use Magento\TestFramework\Helper\Bootstrap;
use PHPUnit\Framework\TestCase;
use Magento\Catalog\Model\ProductFactory;
class ProductAttributeTest extends TestCase
{
public function testSaveProductWithCustomAttributes()
{
$productFactory = Bootstrap::getObjectManager()->create(ProductFactory::class);
$product = $productFactory->create();
$product->setName('Test Product');
$product->setSku('test-product');
$product->setPrice(29.99);
$product->setCustomColor('red');
$product->setCustomSize('m');
$product->setCustomMaterial('Cotton');
$product->setCustomHandmade(1);
$product->setStatus(1);
$product->setWebsiteIds([1]);
$product->save();
$this->assertNotNull($product->getId());
$this->assertEquals('red', $product->getCustomColor());
$this->assertEquals('m', $product->getCustomSize());
}
}
Frontend Test
# Test product page
php bin/magento catalogsearch:reindex
php bin/magento cache:flush
# Check product page
# Visit: https://example.com/test-product.html
# Verify custom attributes display correctly
Troubleshooting
- Attribute not showing: Check attribute is active and visible
- Source model error: Verify class path and interface
- Values not saving: Check backend model and data type
- Layout not applying: Verify layout XML file name
Practice Problems
Create a custom attribute set with 5 attributes including dropdown, text, and boolean types.
Display custom product attributes on the product page with proper formatting.
Quiz
1. What creates EAV attributes programmatically?
2. What does a source model provide?
3. How to display attributes on frontend?
4. What is attribute scope?
Flashcards
Question
How to create EAV attributes?
Click to reveal answer
Answer
Use InstallData.php with addAttribute() setup method
Question
What is a source model?
Click to reveal answer
Answer
Provides option values for select/dropdown attributes
Question
What is a backend model?
Click to reveal answer
Answer
Handles validation and data saving logic for attributes
Question
How to display attributes?
Click to reveal answer
Answer
Block class + layout XML + template file
Question
What are attribute scopes?
Click to reveal answer
Answer
Global, Website, Store - determines value granularity
Revision Notes
Key Takeaways
- 1. InstallData.php creates EAV attributes with addAttribute()
- 2. Source models provide dropdown options via toOptionArray()
- 3. Backend models handle validation, Frontend models handle display
- 4. Attributes must be assigned to attribute set and group
- 5. Display on frontend: Block + layout XML + template
- 6. Use Setup classes for versioned attribute changes
Interview Tips
- • How do you create a custom product attribute?
- • Explain the difference between source, backend, and frontend models
- • How do you display custom attributes on the product page?
- • What are attribute scopes and when to use each?
- • How do you migrate attribute changes between environments?
Cheat Sheet
Product Attributes Cheat Sheet
Create Attribute:
- InstallData.php: addAttribute()
- UpgradeData.php: addAttributeOption()
Models:
- Source: option values (toOptionArray)
- Backend: validation, save logic
- Frontend: display formatting
Display:
- Block: get attribute values
- Layout: add block to page
- Template: render HTML
Admin:
- Stores > Attributes > Product
- Stores > Attributes > Attribute Set
- Assign to groups
Scope:
- Global: same everywhere
- Website: per website
- Store: per store view