Headless Digital 6 min readmagentodev-workflow

Add an eav attribute to products in magento 2

The original video this piece is based on.

This pulls together a run of videos I recorded in 2021, and a follow up in 2023, about adding your own attributes to products and customers in Magento. The code in those was written against the 2.4 line at the time, so read the class names and the script types as the shape of the idea rather than something to paste straight in. The thinking holds up well. The exact mechanism has moved on since, and I will tell you where.

What EAV actually is, and why it costs you

EAV stands for entity, attribute, value. Instead of one wide table with a column for every product field, Magento splits a product across a set of tables by data type. The product is the entity, each field like name or colour is an attribute, and the stored content is the value, which sits in whichever table matches its type. That is why you cannot just open a products table and read a row. There isn't one. A single product is stitched together from several tables at read time.

The upside is real. A shop owner can add a new product attribute in the admin without anyone touching the database schema, and that flexibility is a big part of why Magento won the mid market. The downside is just as real. Every read is a pile of joins, the data model is hard to reason about, and performance work often comes down to flattening EAV back into something wide, which is what the indexers do. Powerful, and heavy. Both things are true at once.

Adding your own attribute

The job in the first video was a multi select on the product page. A merchant ticks options like vegan friendly or made in UK in the admin, and the matching icons show on the front end of that product. To create an attribute you used to write an InstallData or UpgradeData script in your module's Setup folder, bump the module's setup_version, and run setup:upgrade so it hit the database once.

Here is the honest update, because this is the part that has dated. InstallData and UpgradeData tied to setup_version are the old way. From 2.3 onward Magento uses declarative schema for structure and data patches for content. So a new attribute today belongs in a patch class that implements DataPatchInterface, not an InstallData script. The good news is that the actual EAV setup call inside the patch is almost identical to the one in the old videos, which is why they still teach you the important bit. Just wrap it in a patch on anything current, and let the patch registry track what has run rather than a version number.

Source models, where the options live

A plain text attribute is easy. A select or multi select needs somewhere to get its choices from, and that somewhere is a source model. You point the attribute's source at a class that extends Magento's abstract source class and implements getAllOptions, which returns an array of label and value pairs. The label is what a person reads, the value is what gets stored.

getAllOptions is worth properly understanding because Magento leans on it everywhere, from category settings to product attributes. My method in the video was the same one I still use. Find an existing core attribute that already does roughly what you want, open its source model, and copy the structure rather than inventing your own. Reconnaissance in the core is most of the skill. You can build the options array by hand for a fixed list, or load it from a collection or a factory if the choices come from elsewhere. Start hard coded, prove it saves, then load it from data if you actually need to.

The two table trap

This is the bit that catches people out, and it is the whole reason the 2023 follow up exists. An attribute's definition and its option values live in different tables and reference each other by id. Those ids get handed out in the order things hit the database. On your local an option might land as id 768. On staging, where another developer added their own attributes in a different order, the exact same option might be 771. Same code, different number, depending on history.

So do not wire front end logic to an option's numeric value if you can avoid it. In the follow up I had a little jQuery show and hide a text field depending on a dropdown selection, and I deliberately matched on the visible label text rather than the value id, precisely because the id was not safe to trust across environments. It is a touch more code and a lot less pain when the same module runs on three databases that grew up differently. If you ever see a feature that works on local and silently breaks on staging, mismatched option ids are the first thing I would check.

Getting it onto the front end

Displaying a custom attribute on the product page is the same three moves as any Magento front end change. Layout XML to attach a block to the page, a block class to fetch what you need, and a phtml template to render it. Turn on developer mode and template hints so you can see exactly which template owns the area you are targeting, then add your block next to it.

One shortcut worth knowing. If you only need to render a value the customer typed into a field, you often do not need a block at all, because the field name carries straight through to the saved entity and the template can read it. For anything computed or looked up, do it in the block and keep logic out of the template. And if you do reach for jQuery in a phtml file, require jQuery at the top of your script before you use the dollar, every time. Half the front end bugs I debugged in those years were that.

Why I keep telling this story

Count the moving parts to add one field. An attribute, a source model, a data patch, layout XML, a block, a template, and some thought about indexing so it shows in the grids. That ceremony is genuine engineering, and it is also a fair slice of why I moved a lot of my work to headless, which I went into in why I moved from Magento to headless. Learn this properly anyway. Most live Magento stores are extended in exactly this way, the discipline of reading core before you write makes you sharper wherever you end up, and you cannot make an honest call about leaving a platform until you understand how it really works.