Create An EAV Based Model In Magento – A Tutorial

back to tech articles
Magento 1.7.0.2 CE. Tea, lots of tea!

First off, this talk is not for beginners. You will need to have a clear grasp of Magento module creation in order to follow along. Here’s an excellent starting point.

Let’s take this slow and we might just learn something. Please read carefully; module and model are not the same thing!

The EAV storage system is very powerful and extremely flexible, but the payoff is complexity. I won’t dive into my own lame description of EAV (Entity-Attribute-Value), I’ll leave that to these folks. But, I will say that EAV is really just a different way of storing data instead of the traditional ‘flat’ database table method. Got it? Ok.

Because EAV is different from a traditional table structure, our model(s) will look a little different compared to what we’re used to. For this talk, I’ll be working in the community code folder. The namespace will be TC (clever, huh?) and the module name is Skeleton.

I have created a public repository on Github for this module. Download the files, or fork it.

The File Structure

Let’s be clear about the file structure. We are going to create several files, along with the ones you might already have as part of your module. I’m going to show you all the files present in my version.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
app
  code
    community
      TC
        Skeleton
          etc
            config.xml
          Helper
            Data.php
          Model
            Resource
              Setup.php
              Skeleton.php
              Skeleton
                Collection.php
            Skeleton.php
          sql
            tc_skeleton_setup
              install-0.1.0.php
  etc
    modules
      TC_Skeleton.xml

That is all we need to convey the basic theory. Obviously, this will need more bits to be useful in any real-world way!

Module File

The module file sets us up, defines our module name, code pool and dependencies.

File: app/etc/modules/TC_Skeleton.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
<config>
    <modules>
        <TC_Skeleton>
            <active>true</active>
            <codePool>community</codePool>
            <depends>
                <Mage_Core />
                <Mage_Eav />
                <Mage_Adminhtml />
            </depends>
        </TC_Skeleton>
    </modules>
</config>

We are dependent upon Magento EAV for this module. We need to ensure that it gets loaded, so the depends node is a good idea.

The Config

I’ll come clean now and confess that I am a bit of a rebel with Magento; I sometimes break with tradition. This is evident in the naming of my files and classes (and methods). Refer to the config.xml file if you get lost later on.

It’s not that drastic; the main thing to look out for is that I’ll often use the whole Namespace_Module instead of just Module as a naming convention.

File: app/code/community/TC/Skeleton/etc/config.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
<?xml version="1.0"?>
<config>
    <modules>
        <TC_Skeleton>
            <version>0.1.0</version>
        </TC_Skeleton>
    </modules>
    <global>
        <models>
            <tc_skeleton>
                <class>TC_Skeleton_Model</class>
                <resourceModel>tc_skeleton_resource</resourceModel>
            </tc_skeleton>
            <tc_skeleton_resource>
                <class>TC_Skeleton_Model_Resource</class>    
                <entities>
                    <skeleton>
                        <table>tc_skeleton_skeleton_entity</table>
                    </skeleton>
                    <skeleton_entity_varchar>
                        <table>tc_skeleton_skeleton_entity_varchar</table>
                    </skeleton_entity_varchar>    
                    <skeleton_entity_text>
                        <table>tc_skeleton_skeleton_entity_text</table>
                    </skeleton_entity_text>                    
                    <skeleton_entity_int>
                        <table>tc_skeleton_skeleton_entity_int</table>
                    </skeleton_entity_int>                    
                </entities>
            </tc_skeleton_resource>
        </models>
        <resources>
            <tc_skeleton_setup>
                <setup>
                    <module>TC_Skeleton</module>
                    <class>TC_Skeleton_Model_Resource_Setup</class>
                </setup>
            </tc_skeleton_setup>
            <tc_skeleton_write>
                <connection>
                    <use>default_write</use>
                </connection>
            </tc_skeleton_write>
            <tc_skeleton_read>
                <connection>
                    <use>default_read</use>
                </connection>
            </tc_skeleton_read>            
        </resources>
    </global>
</config>

Take special note of the models section in the XML above, this definition is different to a traditional model definition.

The Helper

Our helper file is enormous!

File: app/code/community/TC/Skeleton/Helper/Data.php

1
2
class TC_Skeleton_Helper_Data extends Mage_Core_Helper_Data
{}

What?

The Model(s)

So we have our module named TC_Skeleton. Let’s get our hands dirty and create our base model.

File: app/code/community/TC/Skeleton/Model/Skeleton.php

1
2
3
4
5
6
7
8
9
10
11
12
<?php
class TC_Skeleton_Model_Skeleton extends Mage_Core_Model_Abstract
{
    const ENTITY = 'tc_skeleton_skeleton';
    protected $_eventPrefix = 'tc_skeleton';
    protected $_eventObject = 'skeleton';

    function _construct()
    {
        $this->_init('tc_skeleton/skeleton');
    }
}

It looks exactly like a standard model, right? Well, it is, except I’ve added a constant and some variables (lines 4 to 6). The only real difference between this and a flat model structure is that ours will have a resource. Onward!

File: app/code/community/TC/Skeleton/Model/Resource/Skeleton.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class TC_Skeleton_Model_Resource_Skeleton extends Mage_Eav_Model_Entity_Abstract
{
    public function __construct()
    {
        $resource = Mage::getSingleton('core/resource');
       
        $this->setType(TC_Skeleton_Model_Skeleton::ENTITY);

        $this->setConnection(
            $resource->getConnection('tc_skeleton_read'),
            $resource->getConnection('tc_skeleton_write')
        );
    }
}

Ok, there’s our resource. We have made a reference to the constant defined on line 4 of our previous file; it just saves us from typos when re-declaring it. Note which Magento class we are extending: Mage_Eav_Model_Entity_Abstract. This is how we hook into Magento’s EAV structure.

File: app/code/community/TC/Skeleton/Model/Resource/Skeleton/Collection.php

1
2
3
4
5
6
7
class TC_Skeleton_Model_Resource_Skeleton_Collection extends Mage_Eav_Model_Entity_Collection_Abstract
{
    protected function _construct()
    {
        $this->_init('tc_skeleton/skeleton');
    }
}

And that is the collection part done too. We are well on our way to finished.

Database Tables

But hark, we haven’t created a table yet!?! How and where will we save our mind-bending data? Let’s begin our Setup.php file.

File: app/code/community/TC/Skeleton/Model/Resource/Skeleton/Collection.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
class TC_Skeleton_Model_Resource_Setup extends Mage_Eav_Model_Entity_Setup
{

    public function getDefaultEntities() {
        return array(
            TC_Skeleton_Model_Skeleton::ENTITY => array(
                'entity_model' => 'tc_skeleton/skeleton',
                'table' => 'tc_skeleton/skeleton', /* Maps to the config.xml > global > models > tc_skeleton_resource > entities > skeleton */
                'attributes' => array(
                    'first_name' => array(
                        'type' => 'varchar',
                        'label' => 'First name',
                        'input' => 'text',
                        'required' => true,
                        'sort_order' => 10,
                        'position' => 10,
                        'global' => Mage_Catalog_Model_Resource_Eav_Attribute::SCOPE_GLOBAL,
                    ),                    
                    'last_name' => array(
                        'type' => 'varchar',
                        'label' => 'Last name',
                        'input' => 'text',
                        'required' => true,
                        'sort_order' => 20,
                        'position' => 20,
                        'global' => Mage_Catalog_Model_Resource_Eav_Attribute::SCOPE_GLOBAL,
                    ),  
                    'email' => array(
                        'type' => 'varchar',
                        'label' => 'Email',
                        'input' => 'text',
                        'required' => true,
                        'sort_order' => 30,
                        'position' => 30,
                        'global' => Mage_Catalog_Model_Resource_Eav_Attribute::SCOPE_GLOBAL,
                    ),    
                    'address' => array(
                        'type'               => 'text',
                        'label'              => 'Address',
                        'input'              => 'multiline',
                        'sort_order'         => 40,
                        'multiline_count'    => 2,
                        'validate_rules'     => 'a:2:{s:15:"max_text_length";i:255;s:15:"min_text_length";i:1;}',
                        'position'           => 40,
                        'required' => false,
                        'global' => Mage_Catalog_Model_Resource_Eav_Attribute::SCOPE_GLOBAL,
                    ),                    
                    'is_active' => array(
                        'type' => 'int',
                        'label' => 'Is Active',
                        'input' => 'text',
                        'required' => false,                        
                        'sort_order' => 50,
                        'position' => 50,
                        'required' => false,
                        'global' => Mage_Catalog_Model_Resource_Eav_Attribute::SCOPE_GLOBAL,
                    ),
                )
            )
        );
    }
}

The setup file creates our table(s), EAV-style. Now, we have specified columns for our ‘table’, but we haven’t told EAV how those table types are defined. Just like a traditional table, EAV needs to know what type of data each column will hold (INT, VARCHAR, TEXT, etc).

This beast defines our storage types, as well as our reference point:

File: app/code/community/TC/Skeleton/sql/tc_skeleton_setup/install-0.1.0.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
$installer = $this;

$installer->startSetup();

/* Create table 'tc_skeleton/skeleton' */
$table = $installer->getConnection()
    ->newTable($installer->getTable('tc_skeleton/skeleton'))
    ->addColumn('entity_id', Varien_Db_Ddl_Table::TYPE_INTEGER, null, array(
        'identity'  => true,
        'unsigned'  => true,
        'nullable'  => false,
        'primary'   => true,
        ), 'Entity ID')
    ->addColumn('entity_type_id', Varien_Db_Ddl_Table::TYPE_SMALLINT, null, array(
        'unsigned'  => true,
        'nullable'  => false,
        'default'   => '0',
        ), 'Entity Type Id')
    ->addColumn('created_at', Varien_Db_Ddl_Table::TYPE_TIMESTAMP, null, array(
        ), 'Creation Time')
    ->addColumn('updated_at', Varien_Db_Ddl_Table::TYPE_TIMESTAMP, null, array(
        ), 'Update Time')
    ->setComment('TC Skeleton Skeleton Table');
$installer->getConnection()->createTable($table);

/* Create table 'tc_skeleton/skeleton_entity_varchar' */
$table = $installer->getConnection()
    ->newTable($installer->getTable('tc_skeleton/skeleton_entity_varchar'))
    ->addColumn('value_id', Varien_Db_Ddl_Table::TYPE_INTEGER, null, array(
        'identity'  => true,
        'nullable'  => false,
        'primary'   => true,
        ), 'Value Id')
    ->addColumn('entity_type_id', Varien_Db_Ddl_Table::TYPE_SMALLINT, null, array(
        'unsigned'  => true,
        'nullable'  => false,
        'default'   => '0',
        ), 'Entity Type Id')
    ->addColumn('attribute_id', Varien_Db_Ddl_Table::TYPE_SMALLINT, null, array(
        'unsigned'  => true,
        'nullable'  => false,
        'default'   => '0',
        ), 'Attribute Id')
    ->addColumn('store_id', Varien_Db_Ddl_Table::TYPE_SMALLINT, null, array(
        'unsigned'  => true,
        'nullable'  => false,
        'default'   => '0',
        ), 'Store ID')        
    ->addColumn('entity_id', Varien_Db_Ddl_Table::TYPE_INTEGER, null, array(
        'unsigned'  => true,
        'nullable'  => false,
        'default'   => '0',
        ), 'Entity Id')
    ->addColumn('value', Varien_Db_Ddl_Table::TYPE_TEXT, 255, array(
        ), 'Value')
    ->addIndex(
        $installer->getIdxName(
            'tc_skeleton_skeleton_entity_varchar',
            array('entity_id', 'attribute_id', 'store_id'),
            Varien_Db_Adapter_Interface::INDEX_TYPE_UNIQUE
        ),
        array('entity_id', 'attribute_id', 'store_id'),
        array('type' => Varien_Db_Adapter_Interface::INDEX_TYPE_UNIQUE))
    ->addIndex($installer->getIdxName('tc_skeleton_skeleton_entity_varchar', array('attribute_id')),
        array('attribute_id'))
    ->addIndex($installer->getIdxName('tc_skeleton_skeleton_entity_varchar', array('store_id')),
        array('store_id'))
    ->addIndex($installer->getIdxName('tc_skeleton_skeleton_entity_varchar', array('entity_id')),
        array('entity_id'))
    ->addForeignKey(
        $installer->getFkName('tc_skeleton_skeleton_entity_varchar', 'attribute_id', 'eav/attribute', 'attribute_id'),
        'attribute_id', $installer->getTable('eav/attribute'), 'attribute_id',
        Varien_Db_Ddl_Table::ACTION_CASCADE, Varien_Db_Ddl_Table::ACTION_CASCADE)
    ->addForeignKey(
        $installer->getFkName('tc_skeleton_skeleton_entity_varchar', 'entity_id', 'tc_skeleton/skeleton', 'entity_id'),
        'entity_id', $installer->getTable('tc_skeleton/skeleton'), 'entity_id',
        Varien_Db_Ddl_Table::ACTION_CASCADE, Varien_Db_Ddl_Table::ACTION_CASCADE)
    ->addForeignKey(
        $installer->getFkName('tc_skeleton_skeleton_entity_varchar', 'store_id', 'core/store', 'store_id'),
        'store_id', $installer->getTable('core/store'), 'store_id',
        Varien_Db_Ddl_Table::ACTION_CASCADE, Varien_Db_Ddl_Table::ACTION_CASCADE)
    ->setComment('TC Skeleton Skeleton Entity Varchar');
$installer->getConnection()->createTable($table);

/* Create table 'tc_skeleton/skeleton_entity_int' */
$table = $installer->getConnection()
    ->newTable($installer->getTable('tc_skeleton/skeleton_entity_int'))
    ->addColumn('value_id', Varien_Db_Ddl_Table::TYPE_INTEGER, null, array(
        'identity'  => true,
        'nullable'  => false,
        'primary'   => true,
        ), 'Value Id')
    ->addColumn('entity_type_id', Varien_Db_Ddl_Table::TYPE_SMALLINT, null, array(
        'unsigned'  => true,
        'nullable'  => false,
        'default'   => '0',
        ), 'Entity Type Id')
    ->addColumn('attribute_id', Varien_Db_Ddl_Table::TYPE_SMALLINT, null, array(
        'unsigned'  => true,
        'nullable'  => false,
        'default'   => '0',
        ), 'Attribute Id')
    ->addColumn('store_id', Varien_Db_Ddl_Table::TYPE_SMALLINT, null, array(
        'unsigned'  => true,
        'nullable'  => false,
        'default'   => '0',
        ), 'Store ID')        
    ->addColumn('entity_id', Varien_Db_Ddl_Table::TYPE_INTEGER, null, array(
        'unsigned'  => true,
        'nullable'  => false,
        'default'   => '0',
        ), 'Entity Id')
    ->addColumn('value', Varien_Db_Ddl_Table::TYPE_TEXT, 255, array(
        ), 'Value')
    ->addIndex(
        $installer->getIdxName(
            'tc_skeleton_skeleton_entity_int',
            array('entity_id', 'attribute_id', 'store_id'),
            Varien_Db_Adapter_Interface::INDEX_TYPE_UNIQUE
        ),
        array('entity_id', 'attribute_id', 'store_id'),
        array('type' => Varien_Db_Adapter_Interface::INDEX_TYPE_UNIQUE))
    ->addIndex($installer->getIdxName('tc_skeleton_skeleton_entity_int', array('attribute_id')),
        array('attribute_id'))
    ->addIndex($installer->getIdxName('tc_skeleton_skeleton_entity_int', array('store_id')),
        array('store_id'))
    ->addIndex($installer->getIdxName('tc_skeleton_skeleton_entity_int', array('entity_id')),
        array('entity_id'))
    ->addForeignKey(
        $installer->getFkName('tc_skeleton_skeleton_entity_int', 'attribute_id', 'eav/attribute', 'attribute_id'),
        'attribute_id', $installer->getTable('eav/attribute'), 'attribute_id',
        Varien_Db_Ddl_Table::ACTION_CASCADE, Varien_Db_Ddl_Table::ACTION_CASCADE)
    ->addForeignKey(
        $installer->getFkName('tc_skeleton_skeleton_entity_int', 'entity_id', 'tc_skeleton/skeleton', 'entity_id'),
        'entity_id', $installer->getTable('tc_skeleton/skeleton'), 'entity_id',
        Varien_Db_Ddl_Table::ACTION_CASCADE, Varien_Db_Ddl_Table::ACTION_CASCADE)
    ->addForeignKey(
        $installer->getFkName('tc_skeleton_skeleton_entity_int', 'store_id', 'core/store', 'store_id'),
        'store_id', $installer->getTable('core/store'), 'store_id',
        Varien_Db_Ddl_Table::ACTION_CASCADE, Varien_Db_Ddl_Table::ACTION_CASCADE)
    ->setComment('TC Skeleton Skeleton Entity Int');
$installer->getConnection()->createTable($table);

/* Create table 'tc_skeleton/skeleton_entity_text' */
$table = $installer->getConnection()
    ->newTable($installer->getTable('tc_skeleton/skeleton_entity_text'))
    ->addColumn('value_id', Varien_Db_Ddl_Table::TYPE_INTEGER, null, array(
        'identity'  => true,
        'nullable'  => false,
        'primary'   => true,
        ), 'Value Id')
    ->addColumn('entity_type_id', Varien_Db_Ddl_Table::TYPE_SMALLINT, null, array(
        'unsigned'  => true,
        'nullable'  => false,
        'default'   => '0',
        ), 'Entity Type Id')
    ->addColumn('attribute_id', Varien_Db_Ddl_Table::TYPE_SMALLINT, null, array(
        'unsigned'  => true,
        'nullable'  => false,
        'default'   => '0',
        ), 'Attribute Id')
    ->addColumn('store_id', Varien_Db_Ddl_Table::TYPE_SMALLINT, null, array(
        'unsigned'  => true,
        'nullable'  => false,
        'default'   => '0',
        ), 'Store ID')        
    ->addColumn('entity_id', Varien_Db_Ddl_Table::TYPE_INTEGER, null, array(
        'unsigned'  => true,
        'nullable'  => false,
        'default'   => '0',
        ), 'Entity Id')
    ->addColumn('value', Varien_Db_Ddl_Table::TYPE_TEXT, 255, array(
        ), 'Value')
    ->addIndex(
        $installer->getIdxName(
            'tc_skeleton_skeleton_entity_text',
            array('entity_id', 'attribute_id', 'store_id'),
            Varien_Db_Adapter_Interface::INDEX_TYPE_UNIQUE
        ),
        array('entity_id', 'attribute_id', 'store_id'),
        array('type' => Varien_Db_Adapter_Interface::INDEX_TYPE_UNIQUE))
    ->addIndex($installer->getIdxName('tc_skeleton_skeleton_entity_text', array('attribute_id')),
        array('attribute_id'))
    ->addIndex($installer->getIdxName('tc_skeleton_skeleton_entity_text', array('store_id')),
        array('store_id'))
    ->addIndex($installer->getIdxName('tc_skeleton_skeleton_entity_text', array('entity_id')),
        array('entity_id'))
    ->addForeignKey(
        $installer->getFkName('tc_skeleton_skeleton_entity_text', 'attribute_id', 'eav/attribute', 'attribute_id'),
        'attribute_id', $installer->getTable('eav/attribute'), 'attribute_id',
        Varien_Db_Ddl_Table::ACTION_CASCADE, Varien_Db_Ddl_Table::ACTION_CASCADE)
    ->addForeignKey(
        $installer->getFkName('tc_skeleton_skeleton_entity_text', 'entity_id', 'tc_skeleton/skeleton', 'entity_id'),
        'entity_id', $installer->getTable('tc_skeleton/skeleton'), 'entity_id',
        Varien_Db_Ddl_Table::ACTION_CASCADE, Varien_Db_Ddl_Table::ACTION_CASCADE)
    ->addForeignKey(
        $installer->getFkName('tc_skeleton_skeleton_entity_text', 'store_id', 'core/store', 'store_id'),
        'store_id', $installer->getTable('core/store'), 'store_id',
        Varien_Db_Ddl_Table::ACTION_CASCADE, Varien_Db_Ddl_Table::ACTION_CASCADE)
    ->setComment('TC Skeleton Skeleton Entity Text');
$installer->getConnection()->createTable($table);

$installer->endSetup();

$installer->installEntities();

Tidy, isn’t it? We have defined three storage types above, namely int, text and varchar. If you want them, you will need to add more types, such as date/time; this article is already getting too long!

Conclusion

Our module is finished! If we were to install it to a Magento store now – we would see… nothing. Well, that’s not exactly true, there are 4 shiny new database tables:

1
2
3
4
tc_skeleton_skeleton_entity
tc_skeleton_skeleton_entity_int
tc_skeleton_skeleton_entity_text
tc_skeleton_skeleton_entity_varchar

These are the basis of our module and we can now begin to build some functionality around the database. I won’t dive into that now, but you go ahead!

We access our model like so:

1
$skeletonEntry = Mage::getModel('tc_skeleton/skeleton');

Save data to the table(s):

1
2
3
4
5
6
7
$skeletonEntry->setFirstname('Jason');
$skeletonEntry->setLastname('Mark');
$skeletonEntry->setEmail('[email protected]');
$skeletonEntry->setAddress('1 Awesome, Sauce, 1234');
$skeletonEntry->setIsActive('2');
// save
$skeletonEntry->save();

And retrieve like so:

1
Mage::getModel('tc_skeleton/skeleton')->getCollection();

All done. Download the files, or fork it on Github!

Deletion

As a last note, this extension can be deleted by removing all the files as described above. Once that is done, delete our 4 tables in the database and then delete the tc_skeleton row in the core_resource table.

Credit to Branko Ajzele at Inchoo for his excellent pointers.