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.