Skip to content
beginner Phase 116 · Beginner Projects

Project: Custom Product Attributes

Create custom product attributes with source models, frontend input, and admin configuration

45m
2 problems
Topic Progress 0%

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

0 / 2 solved
Custom Attribute Set

Create a custom attribute set with 5 attributes including dropdown, text, and boolean types.

Attribute Frontend Display

Display custom product attributes on the product page with proper formatting.

Quiz

1. What creates EAV attributes programmatically?

Question 1 options

2. What does a source model provide?

Question 2 options

3. How to display attributes on frontend?

Question 3 options

4. What is attribute scope?

Question 4 options

Flashcards

Question

How to create EAV attributes?

Answer

Use InstallData.php with addAttribute() setup method

Question

What is a source model?

Answer

Provides option values for select/dropdown attributes

Question

What is a backend model?

Answer

Handles validation and data saving logic for attributes

Question

How to display attributes?

Answer

Block class + layout XML + template file

Question

What are attribute scopes?

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