How to create an EAV entity?

Part 1

For the purpose of this demo I'm going to create a module, let's name it Easylife_News that contains an entity named Article.
The module comes with complete CRUD code for backend, including the section for managing attributes, and some simple frontend listing and view page for each article. It also ads a menu item in the top menu to the listing of articles.
It also contains URL rewrites, RSS feed and breadcrumbs for frontend. This entity will have 4 attributes: Title, Short Description, Description, Publish Date in addition to the system ones (created_at, updated_at, status, in_rss, url_key).
The module should contain the following files.

app/etc/module/Easylife_News.xml - the declaration module:

<?xml version="1.0"?>
<config>
    <modules>
        <Easylife_News>
            <active>true</active>
            <codePool>local</codePool>
            <depends>
                <Mage_Catalog /><!-- some classes extend one class from the catalog module to avoid duplicating it -->
             </depends>
        </Easylife_News>
    </modules>
</config>

app/code/local/Easylife/News/etc/config.xml - the configuration file

<?xml version="1.0"?>
<config>
    <modules>
        <Easylife_News>
            <version>1.0.0</version>
        </Easylife_News>
    </modules>
    <global>
        <resources>
            <easylife_news_setup>
                <setup>
                    <module>Easylife_News</module>
                    <class>Easylife_News_Model_Resource_Setup</class>
                </setup>
            </easylife_news_setup>
        </resources>
        <blocks>
            <easylife_news>
                <class>Easylife_News_Block</class>
            </easylife_news>
        </blocks>
        <helpers>
            <easylife_news>
                <class>Easylife_News_Helper</class>
            </easylife_news>
        </helpers>
        <models>
            <easylife_news>
                <class>Easylife_News_Model</class>
                <resourceModel>easylife_news_resource</resourceModel>
            </easylife_news>
            <easylife_news_resource>
                <class>Easylife_News_Model_Resource</class>
                <entities>
                    <article>
                        <table>easylife_news_article</table>
                    </article>
                    <eav_attribute>
                        <table>easylife_news_eav_attribute</table>
                    </eav_attribute>
                </entities>
            </easylife_news_resource>
        </models>
        <events>
            <controller_front_init_routers><!-- event for custom router - url rewrites -->
                <observers>
                    <easylife_news>
                        <class>Easylife_News_Controller_Router</class>
                        <method>initControllerRouters</method>
                    </easylife_news>
                </observers>
            </controller_front_init_routers>
        </events>
    </global>
    <adminhtml>
        <layout>
            <updates>
                <easylife_news>
                    <file>easylife_news.xml</file>
                </easylife_news>
            </updates>
        </layout>
        <translate>
            <modules>
                <Easylife_News>
                    <files>
                        <default>Easylife_News.csv</default>
                    </files>
                </Easylife_News>
            </modules>
        </translate>
        <global_search>
            <article>
                <class>easylife_news/adminhtml_search_article</class>
                <acl>easylife_news</acl>
            </article>
        </global_search>
    </adminhtml>
    <admin>
        <routers>
            <adminhtml>
                <args>
                    <modules>
                        <Easylife_News before="Mage_Adminhtml">Easylife_News_Adminhtml</Easylife_News>
                    </modules>
                </args>
            </adminhtml>
        </routers>
    </admin>
    <frontend>
        <events>
            <page_block_html_topmenu_gethtml_before><!-- add link to top menu -->
                <observers>
                    <easylife_news>
                        <class>easylife_news/observer</class>
                        <method>addItemsToTopmenuItems</method>
                    </easylife_news>
                </observers>
            </page_block_html_topmenu_gethtml_before>
        </events>

        <routers>
            <easylife_news>
                <use>standard</use>
                <args>
                    <module>Easylife_News</module>
                    <frontName>news</frontName>
                </args>
            </easylife_news>
        </routers>
        <layout>
            <updates>
                <easylife_news>
                    <file>easylife_news.xml</file>
                </easylife_news>
            </updates>
        </layout>
        <translate>
            <modules>
                <Easylife_News>
                    <files>
                        <default>Easylife_News.csv</default>
                    </files>
                </Easylife_News>
            </modules>
        </translate>
    </frontend>
    <default>
        <easylife_news>
            <article>
                <breadcrumbs>1</breadcrumbs>
                <url_prefix>article</url_prefix>
                <url_suffix>html</url_suffix>
                <rss>1</rss>
                <meta_title>Articles</meta_title>
            </article>
        </easylife_news>
    </default>
</config>

app/code/local/Easylife/News/etc/adminhtml.xml - the admin acl and menu file.

<?xml version="1.0"?>
<config>
    <acl>
        <resources>
            <admin>
                <children>
                    <system>
                        <children>
                            <config>
                                <children>
                                    <easylife_news translate="title" module="easylife_news">
                                        <title>News</title>
                                    </easylife_news>
                                </children>
                            </config>
                        </children>
                    </system>
                    <cms>
                        <children>
                            <easylife_news translate="title" module="easylife_news">
                                <title>News</title>
                                    <children>
                                        <article translate="title" module="easylife_news">
                                            <title>Article</title>
                                            <sort_order>0</sort_order>
                                        </article>
                                        <article_attributes translate="title" module="easylife_news">
                                            <title>Manage Article attributes</title>
                                            <sort_order>7</sort_order>
                                        </article_attributes>
                                    </children>
                                </easylife_news>
                            </children>
                        </cms>

                </children>
            </admin>
        </resources>
    </acl>
    <menu>
        <cms><!-- I added the admin menu under CMS. Feel free to move it. -->
            <children>
                <easylife_news translate="title" module="easylife_news">
                    <title>News</title>
                    <sort_order>17</sort_order>
                    <children>
                        <article translate="title" module="easylife_news">
                            <title>Article</title>
                            <action>adminhtml/news_article</action>
                            <sort_order>0</sort_order>
                        </article>
                        <article_attributes translate="title" module="easylife_news">
                            <title>Manage Article Attributes</title>
                            <action>adminhtml/news_article_attribute</action>
                            <sort_order>7</sort_order>
                        </article_attributes>
                    </children>
                </easylife_news>
            </children>
        </cms>

    </menu>
</config>

app/code/local/Easylife/News/etc/system.xml - the system configuration file. It allows you to manage a few settings like SEO for the articles list, breadcrumbs, RSS enable/disable, url prefix and suffix

<?xml version="1.0"?>
<config>
    <tabs>
        <easylife translate="label" module="easylife_news">
            <label>News</label>
            <sort_order>2000</sort_order>
        </easylife>
    </tabs>
    <sections>
        <easylife_news translate="label" module="easylife_news">
            <class>separator-top</class>
            <label>News</label>
            <tab>easylife</tab>
            <frontend_type>text</frontend_type>
            <sort_order>100</sort_order>
            <show_in_default>1</show_in_default>
            <show_in_website>1</show_in_website>
            <show_in_store>1</show_in_store>
            <groups>
                <article translate="label" module="easylife_news">
                    <label>Article</label>
                    <frontend_type>text</frontend_type>
                    <sort_order>0</sort_order>
                    <show_in_default>1</show_in_default>
                    <show_in_website>1</show_in_website>
                    <show_in_store>1</show_in_store>
                    <fields>
                        <breadcrumbs translate="label">
                            <label>Use Breadcrumbs</label>
                            <frontend_type>select</frontend_type>
                            <source_model>adminhtml/system_config_source_yesno</source_model>
                            <sort_order>10</sort_order>
                            <show_in_default>1</show_in_default>
                            <show_in_website>1</show_in_website>
                            <show_in_store>1</show_in_store>
                        </breadcrumbs>
                        <url_prefix translate="label comment">
                            <label>URL prefix</label>
                            <frontend_type>text</frontend_type>
                            <sort_order>20</sort_order>
                            <show_in_default>1</show_in_default>
                            <show_in_website>1</show_in_website>
                            <show_in_store>1</show_in_store>
                            <comment>Leave empty for no prefix</comment>
                        </url_prefix>
                        <url_suffix translate="label comment">
                            <label>Url suffix</label>
                            <frontend_type>text</frontend_type>
                            <sort_order>30</sort_order>
                            <show_in_default>1</show_in_default>
                            <show_in_website>1</show_in_website>
                            <show_in_store>1</show_in_store>
                            <comment>What goes after the dot. Leave empty for no suffix.</comment>
                        </url_suffix>
                        <rss translate="label">
                            <label>Enable rss</label>
                            <frontend_type>select</frontend_type>
                            <source_model>adminhtml/system_config_source_yesno</source_model>
                            <sort_order>40</sort_order>
                            <show_in_default>1</show_in_default>
                            <show_in_website>1</show_in_website>
                            <show_in_store>1</show_in_store>
                        </rss>
                        <meta_title translate="label">
                            <label>Meta title for articles list page</label>
                            <frontend_type>text</frontend_type>
                            <sort_order>50</sort_order>
                            <show_in_default>1</show_in_default>
                            <show_in_website>1</show_in_website>
                            <show_in_store>1</show_in_store>
                        </meta_title>
                        <meta_description translate="label">
                            <label>Meta description for articles list page</label>
                            <frontend_type>textarea</frontend_type>
                            <sort_order>60</sort_order>
                            <show_in_default>1</show_in_default>
                            <show_in_website>1</show_in_website>
                            <show_in_store>1</show_in_store>
                        </meta_description>
                        <meta_keywords translate="label">
                            <label>Meta keywords for articles list page</label>
                            <frontend_type>textarea</frontend_type>
                            <sort_order>70</sort_order>
                            <show_in_default>1</show_in_default>
                            <show_in_website>1</show_in_website>
                            <show_in_store>1</show_in_store>
                        </meta_keywords>
                    </fields>
                </article>
            </groups>
        </easylife_news>
    </sections>
</config>

app/code/local/Easylife/News/sql/easylife_news_setup/install-1.0.0.php - the install script

<?php
$this->startSetup();
//create the entity table
$table = $this->getConnection()
    ->newTable($this->getTable('easylife_news/article'))
    ->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('attribute_set_id', Varien_Db_Ddl_Table::TYPE_SMALLINT, null, array(
        'unsigned'  => true,
        'nullable'  => false,
        'default'   => '0',
        ), 'Attribute Set 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')
    ->addIndex($this->getIdxName('easylife_news/article', array('entity_type_id')),
        array('entity_type_id'))
    ->addIndex($this->getIdxName('easylife_news/article', array('attribute_set_id')),
        array('attribute_set_id'))
    ->addForeignKey(
        $this->getFkName(
            'easylife_news/article',
            'attribute_set_id',
            'eav/attribute_set',
            'attribute_set_id'
        ),
        'attribute_set_id', $this->getTable('eav/attribute_set'), 'attribute_set_id',
        Varien_Db_Ddl_Table::ACTION_CASCADE, Varien_Db_Ddl_Table::ACTION_CASCADE)
    ->addForeignKey($this->getFkName('easylife_news/article', 'entity_type_id', 'eav/entity_type', 'entity_type_id'),
        'entity_type_id', $this->getTable('eav/entity_type'), 'entity_type_id',
        Varien_Db_Ddl_Table::ACTION_CASCADE, Varien_Db_Ddl_Table::ACTION_CASCADE)
    ->setComment('Article Table');
$this->getConnection()->createTable($table);

//create the attribute values tables (int, decimal, varchar, text, datetime)
$articleEav = array();
$articleEav['int'] = array(
    'type'      => Varien_Db_Ddl_Table::TYPE_INTEGER,
    'length'    => null,
    'comment'   => 'Article Datetime Attribute Backend Table'
);

$articleEav['varchar'] = array(
    'type'      => Varien_Db_Ddl_Table::TYPE_TEXT,
    'length'    => 255,
    'comment'   => 'Article Varchar Attribute Backend Table'
);

$articleEav['text'] = array(
    'type'      => Varien_Db_Ddl_Table::TYPE_TEXT,
    'length'    => '64k',
    'comment'   => 'Article Text Attribute Backend Table'
);

$articleEav['datetime'] = array(
    'type'      => Varien_Db_Ddl_Table::TYPE_DATETIME,
    'length'    => null,
    'comment'   => 'Article Datetime Attribute Backend Table'
);

$articleEav['decimal'] = array(
    'type'      => Varien_Db_Ddl_Table::TYPE_DECIMAL,
    'length'    => '12,4',
    'comment'   => 'Article Datetime Attribute Backend Table'
);

foreach ($articleEav as $type => $options) {
    $table = $this->getConnection()
        ->newTable($this->getTable(array('easylife_news/article', $type)))
        ->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', $options['type'], $options['length'], array(
            ), 'Value')
        ->addIndex(
            $this->getIdxName(
                array('easylife_news/article', $type),
                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($this->getIdxName(array('easylife_news/article', $type), array('store_id')),
            array('store_id'))
        ->addIndex($this->getIdxName(array('easylife_news/article', $type), array('entity_id')),
            array('entity_id'))
        ->addIndex($this->getIdxName(array('easylife_news/article', $type), array('attribute_id')),
            array('attribute_id'))
        ->addForeignKey(
            $this->getFkName(
                array('easylife_news/article', $type),
                'attribute_id',
                'eav/attribute',
                'attribute_id'
            ),
            'attribute_id', $this->getTable('eav/attribute'), 'attribute_id',
            Varien_Db_Ddl_Table::ACTION_CASCADE, Varien_Db_Ddl_Table::ACTION_CASCADE)
        ->addForeignKey(
            $this->getFkName(
                array('easylife_news/article', $type),
                'entity_id',
                'easylife_news/article',
                'entity_id'
            ),
            'entity_id', $this->getTable('easylife_news/article'), 'entity_id',
            Varien_Db_Ddl_Table::ACTION_CASCADE, Varien_Db_Ddl_Table::ACTION_CASCADE)
        ->addForeignKey($this->getFkName(array('easylife_news/article', $type), 'store_id', 'core/store', 'store_id'),
            'store_id', $this->getTable('core/store'), 'store_id',
            Varien_Db_Ddl_Table::ACTION_CASCADE, Varien_Db_Ddl_Table::ACTION_CASCADE)
        ->setComment($options['comment']);
    $this->getConnection()->createTable($table);
}
//crete the news_eav_attribute (for additional attribute settings)
$table = $this->getConnection()
    ->newTable($this->getTable('easylife_news/eav_attribute'))
    ->addColumn('attribute_id', Varien_Db_Ddl_Table::TYPE_INTEGER, null, array(
        'identity'  => true,
        'nullable'  => false,
        'primary'   => true,
        ), 'Attribute ID')
    ->addColumn('is_global', Varien_Db_Ddl_Table::TYPE_INTEGER, null, array(), 'Attribute scope')
    ->addColumn('position', Varien_Db_Ddl_Table::TYPE_INTEGER, null, array(), 'Attribute position')
    ->addColumn('is_wysiwyg_enabled', Varien_Db_Ddl_Table::TYPE_INTEGER, null, array(), 'Attribute uses WYSIWYG')
    ->addColumn('is_visible', Varien_Db_Ddl_Table::TYPE_INTEGER, null, array(), 'Attribute is visible')
    ->setComment('News attribute table');
$this->getConnection()->createTable($table);

$this->installEntities();
$this->endSetup();

Models app/code/local/Easylife/News/Model/Observer.php - add the new menu item on the top menu:

<?php
class Easylife_News_Model_Observer {
    public function addItemsToTopmenuItems($observer) {
        $menu = $observer->getMenu();
        $tree = $menu->getTree();
        $action = Mage::app()->getFrontController()->getAction()->getFullActionName();

        $articleNodeId = 'article';
        $data = array(
            'name' => Mage::helper('easylife_news')->__('Articles'),
            'id' => $articleNodeId,
            'url' => Mage::helper('easylife_news/article')->getArticlesUrl(),
            'is_active' => ($action == 'easylife_news_article_index' || $action == 'easylife_news_article_view')
        );
        $articleNode = new Varien_Data_Tree_Node($data, 'id', $tree, $menu);
        $menu->addChild($articleNode);
        return $this;
    }
}

app/code/local/Easylife/News/Model/Article.php - the article main model

<?php
class Easylife_News_Model_Article
    extends Mage_Catalog_Model_Abstract {
    const ENTITY    = 'easylife_news_article';
    const CACHE_TAG = 'easylife_news_article';
    protected $_eventPrefix = 'easylife_news_article';
    protected $_eventObject = 'article';
    public function _construct(){
        parent::_construct();
        $this->_init('easylife_news/article');
    }
    protected function _beforeSave(){
        parent::_beforeSave();
        $now = Mage::getSingleton('core/date')->gmtDate();
        if ($this->isObjectNew()){
            $this->setCreatedAt($now);
        }
        $this->setUpdatedAt($now);
        return $this;
    }
    public function getArticleUrl(){
        if ($this->getUrlKey()){
            $urlKey = '';
            if ($prefix = Mage::getStoreConfig('easylife_news/article/url_prefix')){
                $urlKey .= $prefix.'/';
            }
            $urlKey .= $this->getUrlKey();
            if ($suffix = Mage::getStoreConfig('easylife_news/article/url_suffix')){
                $urlKey .= '.'.$suffix;
            }
            return Mage::getUrl('', array('_direct'=>$urlKey));
        }
        return Mage::getUrl('easylife_news/article/view', array('id'=>$this->getId()));
    }
    public function checkUrlKey($urlKey, $active = true){
        return $this->_getResource()->checkUrlKey($urlKey, $active);
    }

    public function getDescription(){ //this needs to be implemented for all WYSIWYG attributes
        $description = $this->getData('description');
        $helper = Mage::helper('cms');
        $processor = $helper->getBlockTemplateProcessor();
        $html = $processor->filter($description);
        return $html;
    }
    public function getDefaultAttributeSetId() {
        return $this->getResource()->getEntityType()->getDefaultAttributeSetId();
    }
    public function getAttributeText($attributeCode) {
        $text = $this->getResource()
            ->getAttribute($attributeCode)
            ->getSource()
            ->getOptionText($this->getData($attributeCode));
        if (is_array($text)){
            return implode(', ',$text);
        }
        return $text;
    }
    public function getDefaultValues() {
        $values = array();
        $values['status'] = 1;
        $values['in_rss'] = 1;
        return $values;
    }
}

app/code/local/Easylife/News/Model/Resource/Article.php - the resource model for the article

<?php
class Easylife_News_Model_Resource_Article
    extends Mage_Catalog_Model_Resource_Abstract {
    public function __construct() {
        $resource = Mage::getSingleton('core/resource');
        $this->setType('easylife_news_article')
            ->setConnection(
                $resource->getConnection('article_read'),
                $resource->getConnection('article_write')
            );

    }
    public function getMainTable() {
        return $this->getEntityTable();
    }
    public function checkUrlKey($urlKey, $storeId, $active = true){
        $stores = array(Mage_Core_Model_App::ADMIN_STORE_ID, $storeId);
        $select = $this->_initCheckUrlKeySelect($urlKey, $stores);
        if (!$select){
            return false;
        }
        $select->reset(Zend_Db_Select::COLUMNS)
            ->columns('e.entity_id')
            ->limit(1);
        return $this->_getReadAdapter()->fetchOne($select);
    }
    protected function _initCheckUrlKeySelect($urlKey, $store){
        $urlRewrite = Mage::getModel('eav/config')->getAttribute('easylife_news_article', 'url_key');
        if (!$urlRewrite || !$urlRewrite->getId()){
            return false;
        }
        $table = $urlRewrite->getBackend()->getTable();
        $select = $this->_getReadAdapter()->select()
            ->from(array('e' => $table))
            ->where('e.attribute_id = ?', $urlRewrite->getId())
            ->where('e.value = ?', $urlKey)
            ->where('e.store_id IN (?)', $store)
            ->order('e.store_id DESC');
        return $select;
    }
}

app/code/local/Easylife/News/Model/Resource/Article/Collection.php - the collection model

<?php
class Easylife_News_Model_Resource_Article_Collection
    extends Mage_Catalog_Model_Resource_Collection_Abstract {
    protected function _construct(){
        parent::_construct();
        $this->_init('easylife_news/article');
    }
    protected function _toOptionArray($valueField='entity_id', $labelField='title', $additional=array()){
        $this->addAttributeToSelect('title');
        return parent::_toOptionArray($valueField, $labelField, $additional);
    }
    protected function _toOptionHash($valueField='entity_id', $labelField='title'){
        $this->addAttributeToSelect('title');
        return parent::_toOptionHash($valueField, $labelField);
    }
    public function getSelectCountSql(){
        $countSelect = parent::getSelectCountSql();
        $countSelect->reset(Zend_Db_Select::GROUP);
        return $countSelect;
    }
}

The code is too long to fit in one answer. The rest will follow....wait for it.


Part 2.

app/code/local/Easylife/News/Model/Attribute.php - the model for the article attributes

<?php
class Easylife_News_Model_Attribute
    extends Mage_Eav_Model_Entity_Attribute {
    const SCOPE_STORE                           = 0;
    const SCOPE_GLOBAL                          = 1;
    const SCOPE_WEBSITE                         = 2;
    const MODULE_NAME                           = 'Easylife_News';
    const ENTITY                                = 'easylife_news_eav_attribute';
    protected $_eventPrefix                     = 'easylife_news_entity_attribute';
    protected $_eventObject                     = 'attribute';
    static protected $_labels                   = null;
    protected function _construct(){
        $this->_init('easylife_news/attribute');
    }
    protected function _beforeSave(){
        $this->setData('modulePrefix', self::MODULE_NAME);
        if (isset($this->_origData['is_global'])) {
            if (!isset($this->_data['is_global'])) {
                $this->_data['is_global'] = self::SCOPE_GLOBAL;
            }
        }
        if ($this->getFrontendInput() == 'textarea') {
            if ($this->getIsWysiwygEnabled()) {
                $this->setIsHtmlAllowedOnFront(1);
            }
        }
        return parent::_beforeSave();
    }
    protected function _afterSave(){
        Mage::getSingleton('eav/config')->clear();
        return parent::_afterSave();
    }
    public function getIsGlobal(){
        return $this->_getData('is_global');
    }
    public function isScopeGlobal(){
        return $this->getIsGlobal() == self::SCOPE_GLOBAL;
    }
    public function isScopeWebsite(){
        return $this->getIsGlobal() == self::SCOPE_WEBSITE;
    }
    public function isScopeStore(){
        return !$this->isScopeGlobal() && !$this->isScopeWebsite();
    }
    public function getStoreId(){
        $dataObject = $this->getDataObject();
        if ($dataObject) {
            return $dataObject->getStoreId();
        }
        return $this->getData('store_id');
    }
    public function getSourceModel(){
        $model = $this->getData('source_model');
        if (empty($model)) {
            if ($this->getBackendType() == 'int' && $this->getFrontendInput() == 'select') {
                return $this->_getDefaultSourceModel();
            }
        }
        return $model;
    }
    public function getFrontendLabel(){
        return $this->_getData('frontend_label');
    }
    protected function _getLabelForStore(){
        return $this->getFrontendLabel();
    }
    public static function initLabels($storeId = null){
        if (is_null(self::$_labels)) {
            if (is_null($storeId)) {
                $storeId = Mage::app()->getStore()->getId();
            }
            $attributeLabels = array();
            $attributes = Mage::getResourceSingleton('catalog/product')->getAttributesByCode();
            foreach ($attributes as $attribute) {
                if (strlen($attribute->getData('frontend_label')) > 0) {
                    $attributeLabels[] = $attribute->getData('frontend_label');
                }
            }
            self::$_labels = Mage::app()->getTranslator()->getResource()->getTranslationArrayByStrings($attributeLabels, $storeId);
        }
    }
    public function _getDefaultSourceModel(){
        return 'eav/entity_attribute_source_table';
    }
}

app/code/local/Easylife/News/Model/Resource/Attribute.php - the attribute resource model

<?php
class Easylife_News_Model_Resource_Attribute
    extends Mage_Eav_Model_Resource_Entity_Attribute {
    protected  function _afterSave(Mage_Core_Model_Abstract $object){
        $setup       = Mage::getModel('eav/entity_setup', 'core_write');
        $entityType  = $object->getEntityTypeId();
        $setId       = $setup->getDefaultAttributeSetId($entityType);
        $groupId     = $setup->getDefaultAttributeGroupId($entityType);
        $attributeId = $object->getId();
        $sortOrder   = $object->getPosition();

        $setup->addAttributeToGroup($entityType, $setId, $groupId, $attributeId, $sortOrder);
        return parent::_afterSave($object);
    }
}

app/code/local/Easylife/News/Model/Resource/Eav/Attribute.php - the attribute EAV resource model

<?php
class Easylife_News_Model_Resource_Eav_Attribute
    extends Mage_Eav_Model_Entity_Attribute {
    const MODULE_NAME   = 'Easylife_News';
    const ENTITY        = 'easylife_news_eav_attribute';
    protected $_eventPrefix = 'easylife_news_entity_attribute';
    protected $_eventObject = 'attribute';
    static protected $_labels = null;
    protected function _construct() {
        $this->_init('easylife_news/attribute');
    }
    public function isScopeStore() {
        return $this->getIsGlobal() == Mage_Catalog_Model_Resource_Eav_Attribute::SCOPE_STORE;
    }
    public function isScopeWebsite() {
        return $this->getIsGlobal() == Mage_Catalog_Model_Resource_Eav_Attribute::SCOPE_WEBSITE;
    }
    public function isScopeGlobal() {
        return (!$this->isScopeStore() && !$this->isScopeWebsite());
    }
    public function getBackendTypeByInput($type) {
        switch ($type){
            case 'file':
                //intentional fallthrough
            case 'image':
                return 'varchar';
                break;
            default:
                return parent::getBackendTypeByInput($type);
            break;
        }
    }
    protected function _beforeDelete(){
        if (!$this->getIsUserDefined()){
            throw new Mage_Core_Exception(Mage::helper('easylife_news')->__('This attribute is not deletable'));
        }
        return parent::_beforeDelete();
    }
}

app/code/local/Easylife/News/Model/Resource/Article/Collection.php - attribute collection model

<?php
class Easylife_News_Model_Resource_Article_Attribute_Collection
    extends Mage_Eav_Model_Resource_Entity_Attribute_Collection {
    protected function _initSelect() {
            $this->getSelect()->from(array('main_table' => $this->getResource()->getMainTable()))
                ->where('main_table.entity_type_id=?', Mage::getModel('eav/entity')->setType('easylife_news_article')->getTypeId())
                ->join(
                    array('additional_table' => $this->getTable('easylife_news/eav_attribute')),
                    'additional_table.attribute_id=main_table.attribute_id'
                );
        return $this;
    }
    public function setEntityTypeFilter($typeId) {
        return $this;
    }
    public function addVisibleFilter() {
        return $this->addFieldToFilter('additional_table.is_visible', 1);
    }
    public function addEditableFilter() {
        return $this->addFieldToFilter('additional_table.is_editable', 1);
    }
}

app/code/local/Easylife/News/Model/Resource/Setup.php - the setup resource model - used for adding the entities and attribtues:

<?php
class Easylife_News_Model_Resource_Setup
    extends Mage_Catalog_Model_Resource_Setup {
    public function getDefaultEntities(){
    $entities = array();
        $entities['easylife_news_article'] = array(
            'entity_model'                  => 'easylife_news/article',
            'attribute_model'               => 'easylife_news/resource_eav_attribute',
            'table'                         => 'easylife_news/article',
            'additional_attribute_table'    => 'easylife_news/eav_attribute',
            'entity_attribute_collection'   => 'easylife_news/article_attribute_collection',
            'attributes'        => array(
                    'title' => array(
                        'group'          => 'General',
                        'type'           => 'varchar',
                        'backend'        => '',
                        'frontend'       => '',
                        'label'          => 'Title',
                        'input'          => 'text',
                        'source'         => '',
                        'global'         => Mage_Catalog_Model_Resource_Eav_Attribute::SCOPE_STORE,
                        'required'       => '1',
                        'user_defined'   => false,
                        'default'        => '',
                        'unique'         => false,
                        'position'       => '10',
                        'note'           => '',
                        'visible'        => '1',
                        'wysiwyg_enabled'=> '0',
                    ),
                    'short_description' => array(
                        'group'          => 'General',
                        'type'           => 'text',
                        'backend'        => '',
                        'frontend'       => '',
                        'label'          => 'Short description',
                        'input'          => 'textarea',
                        'source'         => '',
                        'global'         => Mage_Catalog_Model_Resource_Eav_Attribute::SCOPE_STORE,
                        'required'       => '1',
                        'user_defined'   => true,
                        'default'        => '',
                        'unique'         => false,
                        'position'       => '20',
                        'note'           => '',
                        'visible'        => '1',
                        'wysiwyg_enabled'=> '0',
                    ),
                    'description' => array(
                        'group'          => 'General',
                        'type'           => 'text',
                        'backend'        => '',
                        'frontend'       => '',
                        'label'          => 'Description',
                        'input'          => 'textarea',
                        'source'         => '',
                        'global'         => Mage_Catalog_Model_Resource_Eav_Attribute::SCOPE_STORE,
                        'required'       => '1',
                        'user_defined'   => true,
                        'default'        => '',
                        'unique'         => false,
                        'position'       => '30',
                        'note'           => '',
                        'visible'        => '1',
                        'wysiwyg_enabled'=> '1',
                    ),
                    'publish_date' => array(
                        'group'          => 'General',
                        'type'           => 'datetime',
                        'backend'        => 'eav/entity_attribute_backend_datetime',
                        'frontend'       => '',
                        'label'          => 'Publish Date',
                        'input'          => 'date',
                        'source'         => '',
                        'global'         => Mage_Catalog_Model_Resource_Eav_Attribute::SCOPE_GLOBAL,
                        'required'       => '1',
                        'user_defined'   => true,
                        'default'        => '',
                        'unique'         => false,
                        'position'       => '40',
                        'note'           => '',
                        'visible'        => '1',
                        'wysiwyg_enabled'=> '0',
                    ),
                    'status' => array(
                        'group'          => 'General',
                        'type'           => 'int',
                        'backend'        => '',
                        'frontend'       => '',
                        'label'          => 'Enabled',
                        'input'          => 'select',
                        'source'         => 'eav/entity_attribute_source_boolean',
                        'global'         => Mage_Catalog_Model_Resource_Eav_Attribute::SCOPE_STORE,
                        'required'       => '',
                        'user_defined'   => false,
                        'default'        => '',
                        'unique'         => false,
                        'position'       => '50',
                        'note'           => '',
                        'visible'        => '1',
                        'wysiwyg_enabled'=> '0',
                    ),
                    'url_key' => array(
                        'group'          => 'General',
                        'type'           => 'varchar',
                        'backend'        => 'easylife_news/article_attribute_backend_urlkey',
                        'frontend'       => '',
                        'label'          => 'URL key',
                        'input'          => 'text',
                        'source'         => '',
                        'global'         => Mage_Catalog_Model_Resource_Eav_Attribute::SCOPE_STORE,
                        'required'       => '',
                        'user_defined'   => false,
                        'default'        => '',
                        'unique'         => false,
                        'position'       => '60',
                        'note'           => '',
                        'visible'        => '1',
                        'wysiwyg_enabled'=> '0',
                    ),
                    'in_rss' => array(
                        'group'          => 'General',
                        'type'           => 'int',
                        'backend'        => '',
                        'frontend'       => '',
                        'label'          => 'In RSS',
                        'input'          => 'select',
                        'source'         => 'eav/entity_attribute_source_boolean',
                        'global'         => Mage_Catalog_Model_Resource_Eav_Attribute::SCOPE_STORE,
                        'required'       => '',
                        'user_defined'   => false,
                        'default'        => '',
                        'unique'         => false,
                        'position'       => '70',
                        'note'           => '',
                        'visible'        => '1',
                        'wysiwyg_enabled'=> '0',
                    ),
                    'meta_title' => array(
                        'group'          => 'General',
                        'type'           => 'varchar',
                        'backend'        => '',
                        'frontend'       => '',
                        'label'          => 'Meta title',
                        'input'          => 'text',
                        'source'         => '',
                        'global'         => Mage_Catalog_Model_Resource_Eav_Attribute::SCOPE_STORE,
                        'required'       => '',
                        'user_defined'   => false,
                        'default'        => '',
                        'unique'         => false,
                        'position'       => '80',
                        'note'           => '',
                        'visible'        => '1',
                        'wysiwyg_enabled'=> '0',
                    ),
                    'meta_keywords' => array(
                        'group'          => 'General',
                        'type'           => 'text',
                        'backend'        => '',
                        'frontend'       => '',
                        'label'          => 'Meta keywords',
                        'input'          => 'textarea',
                        'source'         => '',
                        'global'         => Mage_Catalog_Model_Resource_Eav_Attribute::SCOPE_STORE,
                        'required'       => '',
                        'user_defined'   => false,
                        'default'        => '',
                        'unique'         => false,
                        'position'       => '90',
                        'note'           => '',
                        'visible'        => '1',
                        'wysiwyg_enabled'=> '0',
                    ),
                    'meta_description' => array(
                        'group'          => 'General',
                        'type'           => 'text',
                        'backend'        => '',
                        'frontend'       => '',
                        'label'          => 'Meta description',
                        'input'          => 'textarea',
                        'source'         => '',
                        'global'         => Mage_Catalog_Model_Resource_Eav_Attribute::SCOPE_STORE,
                        'required'       => '',
                        'user_defined'   => false,
                        'default'        => '',
                        'unique'         => false,
                        'position'       => '100',
                        'note'           => '',
                        'visible'        => '1',
                        'wysiwyg_enabled'=> '0',
                    ),

                )
        );
        return $entities;
    }
}

app/code/local/Easylife/News/Model/Article/Attribute/Backend/File.php - backend for file attributes if you decide to add one

<?php
class Easylife_News_Model_Article_Attribute_Backend_File
    extends Mage_Eav_Model_Entity_Attribute_Backend_Abstract {
    public function afterSave($object){
        $value = $object->getData($this->getAttribute()->getName());

        if (is_array($value) && !empty($value['delete'])) {
            $object->setData($this->getAttribute()->getName(), '');
            $this->getAttribute()->getEntity()
                ->saveAttribute($object, $this->getAttribute()->getName());
            return;
        }

        $path = Mage::helper('easylife_news/article')->getFileBaseDir();

        try {
            $uploader = new Varien_File_Uploader($this->getAttribute()->getName());
            //set allowed file extensions if you need
            //$uploader->setAllowedExtensions(array('mp4', 'mov', 'f4v', 'flv', '3gp', '3g2', 'mp3', 'aac', 'm4a', 'swf'));
            $uploader->setAllowRenameFiles(true);
            $uploader->setFilesDispersion(true);
            $result = $uploader->save($path);
            $object->setData($this->getAttribute()->getName(), $result['file']);
            $this->getAttribute()->getEntity()->saveAttribute($object, $this->getAttribute()->getName());
        } catch (Exception $e) {
            if ($e->getCode() != 666){
                //throw $e;
            }
            return;
        }
    }
}

app/code/local/Easylife/News/Model/Article/Attribute/Backend/Image.php - backend for image attributes if you decide to add one

<?php
class Easylife_News_Model_Article_Attribute_Backend_Image
    extends Mage_Eav_Model_Entity_Attribute_Backend_Abstract {
    public function afterSave($object){
        $value = $object->getData($this->getAttribute()->getName());

        if (is_array($value) && !empty($value['delete'])) {
            $object->setData($this->getAttribute()->getName(), '');
            $this->getAttribute()->getEntity()
                ->saveAttribute($object, $this->getAttribute()->getName());
            return;
        }

        $path = Mage::helper('easylife_news/article_image')->getImageBaseDir();

        try {
            $uploader = new Varien_File_Uploader($this->getAttribute()->getName());
            $uploader->setAllowRenameFiles(true);
            $uploader->setFilesDispersion(true);
            $result = $uploader->save($path);
            $object->setData($this->getAttribute()->getName(), $result['file']);
            $this->getAttribute()->getEntity()->saveAttribute($object, $this->getAttribute()->getName());
        } catch (Exception $e) {
            if ($e->getCode() != 666){
                //throw $e;
            }
            return;
        }
    }
}

app/code/local/Easylife/News/Model/Article/Attribute/Backend/Urlkey.php - backend for url key attribute

<?php
class Easylife_News_Model_Article_Attribute_Backend_Urlkey
    extends Mage_Eav_Model_Entity_Attribute_Backend_Abstract {
    public function beforeSave($object) {
        $attributeName = $this->getAttribute()->getName();
        $urlKey = $object->getData($attributeName);
        if ($urlKey == '') {
            $urlKey = $object->getTitle();
        }
        $urlKey = $this->formatUrlKey($urlKey);
        $validKey = false;
        while (!$validKey) {
            $entityId = Mage::getResourceModel('easylife_news/article')->checkUrlKey($urlKey, $object->getStoreId(), false);
            if ($entityId == $object->getId() || empty($entityId)) {
                $validKey = true;
            }
            else {
                $parts = explode('-', $urlKey);
                $last = $parts[count($parts) - 1];
                if (!is_numeric($last)){
                    $urlKey = $urlKey.'-1';
                }
                else {
                    $suffix = '-'.($last + 1);
                    unset($parts[count($parts) - 1]);
                    $urlKey = implode('-', $parts).$suffix;
                }
            }
        }
        $object->setData($attributeName, $urlKey);
        return $this;
    }
    public function formatUrlKey($str) {
        $urlKey = preg_replace('#[^0-9a-z]+#i', '-', Mage::helper('catalog/product_url')->format($str));
        $urlKey = strtolower($urlKey);
        $urlKey = trim($urlKey, '-');
        return $urlKey;
    }
}

app/code/local/Easylife/News/Model/Adminhtml/Search/Article.php - model that handles the admin global search

<?php
class Easylife_News_Model_Adminhtml_Search_Article
    extends Varien_Object {
    public function load(){
        $arr = array();
        if (!$this->hasStart() || !$this->hasLimit() || !$this->hasQuery()) {
            $this->setResults($arr);
            return $this;
        }
        $collection = Mage::getResourceModel('easylife_news/article_collection')
            ->addAttributeToFilter('title', array('like' => $this->getQuery().'%'))
            ->setCurPage($this->getStart())
            ->setPageSize($this->getLimit())
            ->load();
        foreach ($collection->getItems() as $article) {
            $arr[] = array(
                'id'=> 'article/1/'.$article->getId(),
                'type'  => Mage::helper('easylife_news')->__('Article'),
                'name'  => $article->getTitle(),
                'description'   => $article->getTitle(),
                'url' => Mage::helper('adminhtml')->getUrl('*/news_article/edit', array('id'=>$article->getId())),
            );
        }
        $this->setResults($arr);
        return $this;
    }
}

We're done with the models. Let's go on to blocks.
app/code/local/Easylife/News/Block/Rss.php - the general rss block

<?php 
class Easylife_News_Block_Rss
    extends Mage_Core_Block_Template {
    protected $_feeds = array();
    public function addFeed($label, $url, $prepare = false) {
        $link = ($prepare ? $this->getUrl($url) : $url);
        $feed = new Varien_Object();
        $feed->setLabel($label);
        $feed->setUrl($link);
        $this->_feeds[$link] = $feed;
        return $this;
    }
    public function getFeeds() {
        return $this->_feeds;
    }
}

app/code/local/Easylife/News/Block/Adminhtml/Article.php - the main article block

<?php
class Easylife_News_Block_Adminhtml_Article
    extends Mage_Adminhtml_Block_Widget_Grid_Container {
    public function __construct(){
        $this->_controller         = 'adminhtml_article';
        $this->_blockGroup         = 'easylife_news';
        parent::__construct();
        $this->_headerText         = Mage::helper('easylife_news')->__('Article');
        $this->_updateButton('add', 'label', Mage::helper('easylife_news')->__('Add Article'));

        $this->setTemplate('easylife_news/grid.phtml');
    }
}

app/code/local/Easylife/News/Block/Adminhtml/Article/Edit.php - the admin edit form

<?php
class Easylife_News_Block_Adminhtml_Article_Edit
    extends Mage_Adminhtml_Block_Widget_Form_Container {
    public function __construct(){
        parent::__construct();
        $this->_blockGroup = 'easylife_news';
        $this->_controller = 'adminhtml_article';
        $this->_updateButton('save', 'label', Mage::helper('easylife_news')->__('Save Article'));
        $this->_updateButton('delete', 'label', Mage::helper('easylife_news')->__('Delete Article'));
        $this->_addButton('saveandcontinue', array(
            'label'        => Mage::helper('easylife_news')->__('Save And Continue Edit'),
            'onclick'    => 'saveAndContinueEdit()',
            'class'        => 'save',
        ), -100);
        $this->_formScripts[] = "
            function saveAndContinueEdit(){
                editForm.submit($('edit_form').action+'back/edit/');
            }
        ";
    }
    public function getHeaderText(){
        if( Mage::registry('current_article') && Mage::registry('current_article')->getId() ) {
            return Mage::helper('easylife_news')->__("Edit Article '%s'", $this->escapeHtml(Mage::registry('current_article')->getTitle()));
        }
        else {
            return Mage::helper('easylife_news')->__('Add Article');
        }
    }
}

Looks like there will be a part 3.....


Part 3.
app/code/local/Easylife/News/Block/Adminhtml/Article/Grid.php - the admin grid

<?php
class Easylife_News_Block_Adminhtml_Article_Grid
    extends Mage_Adminhtml_Block_Widget_Grid {
    public function __construct(){
        parent::__construct();
        $this->setId('articleGrid');
        $this->setDefaultSort('entity_id');
        $this->setDefaultDir('ASC');
        $this->setSaveParametersInSession(true);
        $this->setUseAjax(true);
    }
    protected function _prepareCollection(){
        $collection = Mage::getModel('easylife_news/article')->getCollection()
            ->addAttributeToSelect('publish_date')
            ->addAttributeToSelect('status')
            ->addAttributeToSelect('url_key');
        $adminStore = Mage_Core_Model_App::ADMIN_STORE_ID;
        $store = $this->_getStore();
        $collection->joinAttribute('title', 'easylife_news_article/title', 'entity_id', null, 'inner', $adminStore);
        if ($store->getId()) {
            $collection->joinAttribute('easylife_news_article_title', 'easylife_news_article/title', 'entity_id', null, 'inner', $store->getId());
        }

        $this->setCollection($collection);
        return parent::_prepareCollection();
    }
    protected function _prepareColumns(){
        $this->addColumn('entity_id', array(
            'header'    => Mage::helper('easylife_news')->__('Id'),
            'index'        => 'entity_id',
            'type'        => 'number'
        ));
        $this->addColumn('title', array(
            'header'    => Mage::helper('easylife_news')->__('Title'),
            'align'     => 'left',
            'index'     => 'title',
        ));
        if ($this->_getStore()->getId()){
            $this->addColumn('easylife_news_article_title', array(
                'header'    => Mage::helper('easylife_news')->__('Title in %s', $this->_getStore()->getName()),
                'align'     => 'left',
                'index'     => 'easylife_news_article_title',
            ));
        }

        $this->addColumn('status', array(
            'header'    => Mage::helper('easylife_news')->__('Status'),
            'index'        => 'status',
            'type'        => 'options',
            'options'    => array(
                '1' => Mage::helper('easylife_news')->__('Enabled'),
                '0' => Mage::helper('easylife_news')->__('Disabled'),
            )
        ));
        $this->addColumn('publish_date', array(
            'header'=> Mage::helper('easylife_news')->__('Publish Date'),
            'index' => 'publish_date',
            'type'=> 'date',

        ));
        $this->addColumn('url_key', array(
            'header' => Mage::helper('easylife_news')->__('URL key'),
            'index'  => 'url_key',
        ));
        $this->addColumn('created_at', array(
            'header'    => Mage::helper('easylife_news')->__('Created at'),
            'index'     => 'created_at',
            'width'     => '120px',
            'type'      => 'datetime',
        ));
        $this->addColumn('updated_at', array(
            'header'    => Mage::helper('easylife_news')->__('Updated at'),
            'index'     => 'updated_at',
            'width'     => '120px',
            'type'      => 'datetime',
        ));
        $this->addColumn('action',
            array(
                'header'=>  Mage::helper('easylife_news')->__('Action'),
                'width' => '100',
                'type'  => 'action',
                'getter'=> 'getId',
                'actions'   => array(
                    array(
                        'caption'   => Mage::helper('easylife_news')->__('Edit'),
                        'url'   => array('base'=> '*/*/edit'),
                        'field' => 'id'
                    )
                ),
                'filter'=> false,
                'is_system'    => true,
                'sortable'  => false,
        ));
        $this->addExportType('*/*/exportCsv', Mage::helper('easylife_news')->__('CSV'));
        $this->addExportType('*/*/exportExcel', Mage::helper('easylife_news')->__('Excel'));
        $this->addExportType('*/*/exportXml', Mage::helper('easylife_news')->__('XML'));
        return parent::_prepareColumns();
    }

    protected function _getStore(){
        $storeId = (int) $this->getRequest()->getParam('store', 0);
        return Mage::app()->getStore($storeId);
    }

    protected function _prepareMassaction(){
        $this->setMassactionIdField('entity_id');
        $this->getMassactionBlock()->setFormFieldName('article');
        $this->getMassactionBlock()->addItem('delete', array(
            'label'=> Mage::helper('easylife_news')->__('Delete'),
            'url'  => $this->getUrl('*/*/massDelete'),
            'confirm'  => Mage::helper('easylife_news')->__('Are you sure?')
        ));
        $this->getMassactionBlock()->addItem('status', array(
            'label'=> Mage::helper('easylife_news')->__('Change status'),
            'url'  => $this->getUrl('*/*/massStatus', array('_current'=>true)),
            'additional' => array(
                'status' => array(
                        'name' => 'status',
                        'type' => 'select',
                        'class' => 'required-entry',
                        'label' => Mage::helper('easylife_news')->__('Status'),
                        'values' => array(
                                '1' => Mage::helper('easylife_news')->__('Enabled'),
                                '0' => Mage::helper('easylife_news')->__('Disabled'),
                        )
                )
            )
        ));
        return $this;
    }

    public function getRowUrl($row){
        return $this->getUrl('*/*/edit', array('id' => $row->getId()));
    }
    public function getGridUrl(){
        return $this->getUrl('*/*/grid', array('_current'=>true));
    }
}

app/code/local/Easylife/News/Block/Adminhtml/Article/Edit/Tabs.php - the tabs in the add/edit form

<?php
class Easylife_News_Block_Adminhtml_Article_Edit_Tabs
    extends Mage_Adminhtml_Block_Widget_Tabs {
    public function __construct() {
        parent::__construct();
        $this->setId('article_info_tabs');
        $this->setDestElementId('edit_form');
        $this->setTitle(Mage::helper('easylife_news')->__('Article Information'));
    }
    protected function _prepareLayout(){
        $article = $this->getArticle();
        $entity = Mage::getModel('eav/entity_type')->load('easylife_news_article', 'entity_type_code');
        $attributes = Mage::getResourceModel('eav/entity_attribute_collection')
                ->setEntityTypeFilter($entity->getEntityTypeId());
        $attributes->addFieldToFilter('attribute_code', array('nin'=>array('meta_title', 'meta_description', 'meta_keywords')));
        $attributes->getSelect()->order('additional_table.position', 'ASC');

        $this->addTab('info', array(
            'label'     => Mage::helper('easylife_news')->__('Article Information'),
            'content'   => $this->getLayout()->createBlock('easylife_news/adminhtml_article_edit_tab_attributes')
                            ->setAttributes($attributes)
                            ->toHtml(),
        ));
        $seoAttributes = Mage::getResourceModel('eav/entity_attribute_collection')
                ->setEntityTypeFilter($entity->getEntityTypeId())
                ->addFieldToFilter('attribute_code', array('in'=>array('meta_title', 'meta_description', 'meta_keywords')));
        $seoAttributes->getSelect()->order('additional_table.position', 'ASC');

        $this->addTab('meta', array(
            'label'     => Mage::helper('easylife_news')->__('Meta'),
            'title'     => Mage::helper('easylife_news')->__('Meta'),
            'content'   => $this->getLayout()->createBlock('easylife_news/adminhtml_article_edit_tab_attributes')
                            ->setAttributes($seoAttributes)
                            ->toHtml(),
        ));
        return parent::_beforeToHtml();
    }
    public function getArticle(){
        return Mage::registry('current_article');
    }
}

app/code/local/Easylife/News/Block/Adminhtml/Article/Edit/Form.php - the form container

<?php
class Easylife_News_Block_Adminhtml_Article_Edit_Form
    extends Mage_Adminhtml_Block_Widget_Form {
    protected function _prepareForm() {
        $form = new Varien_Data_Form(array(
                        'id'         => 'edit_form',
                        'action'     => $this->getUrl('*/*/save', array('id' => $this->getRequest()->getParam('id'), 'store' => $this->getRequest()->getParam('store'))),
                        'method'     => 'post',
                        'enctype'    => 'multipart/form-data'
                    )
        );
        $form->setUseContainer(true);
        $this->setForm($form);
        return parent::_prepareForm();
    }
}

app/code/local/Easylife/News/Block/Adminhtml/Article/Edit/Tab/Attribtues.php - the attributes contained in a tab.

<?php
class Easylife_News_Block_Adminhtml_Article_Edit_Tab_Attributes
    extends Mage_Adminhtml_Block_Widget_Form {
    protected function _prepareForm() {
        $form = new Varien_Data_Form();
        $form->setDataObject(Mage::registry('current_article'));
        $fieldset = $form->addFieldset('info',
            array(
                'legend'=>Mage::helper('easylife_news')->__('Article Information'),
                 'class'=>'fieldset-wide',
            )
        );
        $attributes = $this->getAttributes();
        foreach ($attributes as $attribute){
            $attribute->setEntity(Mage::getResourceModel('easylife_news/article'));
        }
        $this->_setFieldset($attributes, $fieldset, array());
        $formValues = Mage::registry('current_article')->getData();
        $form->addValues($formValues);
        $form->setFieldNameSuffix('article');
        $this->setForm($form);
    }
    protected function _prepareLayout() {
        Varien_Data_Form::setElementRenderer(
            $this->getLayout()->createBlock('adminhtml/widget_form_renderer_element')
        );
        Varien_Data_Form::setFieldsetRenderer(
            $this->getLayout()->createBlock('adminhtml/widget_form_renderer_fieldset')
        );
        Varien_Data_Form::setFieldsetElementRenderer(
            $this->getLayout()->createBlock('easylife_news/adminhtml_news_renderer_fieldset_element')
        );
    }
    protected function _getAdditionalElementTypes(){
        return array(
            'file'    => Mage::getConfig()->getBlockClassName('easylife_news/adminhtml_article_helper_file'),
            'image' => Mage::getConfig()->getBlockClassName('easylife_news/adminhtml_article_helper_image'),
            'textarea' => Mage::getConfig()->getBlockClassName('adminhtml/catalog_helper_form_wysiwyg')
        );
    }
    public function getArticle() {
        return Mage::registry('current_article');
    }
}

app/code/local/Easylfe/News/Block/Adminhtml/Article/Helper/File.php - block helper for rendering file elements.

<?php
class Easylife_News_Block_Adminhtml_Article_Helper_File
    extends Varien_Data_Form_Element_Abstract {
    public function __construct($data){
        parent::__construct($data);
        $this->setType('file');
    }
    public function getElementHtml(){
        $html = '';
        $this->addClass('input-file');
        $html.= parent::getElementHtml();
        if ($this->getValue()) {
            $url = $this->_getUrl();
            if( !preg_match("/^http\:\/\/|https\:\/\//", $url) ) {
                $url = Mage::helper('easylife_news/article')->getFileBaseUrl() . $url;
            }
            $html .= '<br /><a href="'.$url.'">'.$this->_getUrl().'</a> ';
        }
        $html.= $this->_getDeleteCheckbox();
        return $html;
    }
    protected function _getDeleteCheckbox(){
        $html = '';
        if ($this->getValue()) {
            $label = Mage::helper('easylife_news')->__('Delete File');
            $html .= '<span class="delete-image">';
            $html .= '<input type="checkbox" name="'.parent::getName().'[delete]" value="1" class="checkbox" id="'.$this->getHtmlId().'_delete"'.($this->getDisabled() ? ' disabled="disabled"': '').'/>';
            $html .= '<label for="'.$this->getHtmlId().'_delete"'.($this->getDisabled() ? ' class="disabled"' : '').'> '.$label.'</label>';
            $html .= $this->_getHiddenInput();
            $html .= '</span>';
        }
        return $html;
    }
    protected function _getHiddenInput(){
        return '<input type="hidden" name="'.parent::getName().'[value]" value="'.$this->getValue().'" />';
    }
    protected function _getUrl(){
        return $this->getValue();
    }
    public function getName(){
        return $this->getData('name');
    }
}

app/code/local/Easylfe/News/Block/Adminhtml/Article/Helper/Image.php - block helper for rendering image elements.

<?php
class Easylife_News_Block_Adminhtml_Article_Helper_Image
    extends Varien_Data_Form_Element_Image {
    protected function _getUrl(){
        $url = false;
        if ($this->getValue()) {
            $url = Mage::helper('easylife_news/article_image')->getImageBaseUrl().$this->getValue();
        }
        return $url;
    }
}

app/code/local/Easylife/News/Block/Adminhtml/Attribute.php - main block for managing attributes

<?php
class Easylife_News_Block_Adminhtml_Article_Attribute
    extends Mage_Adminhtml_Block_Widget_Grid_Container {
    public function __construct(){
        $this->_controller = 'adminhtml_article_attribute';
        $this->_blockGroup = 'easylife_news';
        $this->_headerText = Mage::helper('easylife_news')->__('Manage Article Attributes');
        parent::__construct();
        $this->_updateButton('add', 'label', Mage::helper('easylife_news')->__('Add New Article Attribute'));
    }
}

app/code/local/Easylife/News/Block/Adminhtml/Attribute/Grid.php - the attribute grid block

<?php
class Easylife_News_Block_Adminhtml_Article_Attribute_Grid
    extends Mage_Eav_Block_Adminhtml_Attribute_Grid_Abstract {
    protected function _prepareCollection(){
        $collection = Mage::getResourceModel('easylife_news/article_attribute_collection')
            ->addVisibleFilter();
        $this->setCollection($collection);
        return parent::_prepareCollection();
    }
    protected function _prepareColumns() {
        parent::_prepareColumns();
        $this->addColumnAfter('is_global', array(
            'header'=>Mage::helper('easylife_news')->__('Scope'),
            'sortable'=>true,
            'index'=>'is_global',
            'type' => 'options',
            'options' => array(
                Mage_Catalog_Model_Resource_Eav_Attribute::SCOPE_STORE   =>Mage::helper('easylife_news')->__('Store View'),
                Mage_Catalog_Model_Resource_Eav_Attribute::SCOPE_WEBSITE =>Mage::helper('easylife_news')->__('Website'),
                Mage_Catalog_Model_Resource_Eav_Attribute::SCOPE_GLOBAL  =>Mage::helper('easylife_news')->__('Global'),
            ),
            'align' => 'center',
        ), 'is_user_defined');
        return $this;
    }
}

app/code/local/Easylife/News/Block/Adminhtml/Attribute/Edit.php - the attribute edit form.

<?php
class Easylife_News_Block_Adminhtml_Article_Attribute_Edit
    extends Mage_Adminhtml_Block_Widget_Form_Container {
    public function __construct() {
        $this->_objectId = 'attribute_id';
        $this->_controller = 'adminhtml_article_attribute';
        $this->_blockGroup = 'easylife_news';

        parent::__construct();
        $this->_addButton(
            'save_and_edit_button',
            array(
                'label'     => Mage::helper('easylife_news')->__('Save and Continue Edit'),
                'onclick'   => 'saveAndContinueEdit()',
                'class'     => 'save'
            ),
            100
        );
        $this->_updateButton('save', 'label', Mage::helper('easylife_news')->__('Save Article Attribute'));
        $this->_updateButton('save', 'onclick', 'saveAttribute()');

        if (!Mage::registry('entity_attribute')->getIsUserDefined()) {
            $this->_removeButton('delete');
        } else {
            $this->_updateButton('delete', 'label', Mage::helper('easylife_news')->__('Delete Article Attribute'));
        }
    }
    public function getHeaderText(){
        if (Mage::registry('entity_attribute')->getId()) {
            $frontendLabel = Mage::registry('entity_attribute')->getFrontendLabel();
            if (is_array($frontendLabel)) {
                $frontendLabel = $frontendLabel[0];
            }
            return Mage::helper('easylife_news')->__('Edit Article Attribute "%s"', $this->htmlEscape($frontendLabel));
        }
        else {
            return Mage::helper('easylife_news')->__('New Article Attribute');
        }
    }
    public function getValidationUrl(){
        return $this->getUrl('*/*/validate', array('_current'=>true));
    }
    public function getSaveUrl(){
        return $this->getUrl('*/'.$this->_controller.'/save', array('_current'=>true, 'back'=>null));
    }
}

app/code/local/Easylife/News/Block/Adminhtml/Attribute/Edit/Tabs.php - the attribute edit tabs

<?php
class Easylife_News_Block_Adminhtml_Article_Attribute_Edit_Tabs
    extends Mage_Adminhtml_Block_Widget_Tabs {
    public function __construct() {
        parent::__construct();
        $this->setId('article_attribute_tabs');
        $this->setDestElementId('edit_form');
        $this->setTitle(Mage::helper('easylife_news')->__('Attribute Information'));
    }
    protected function _beforeToHtml() {
        $this->addTab('main', array(
            'label'     => Mage::helper('easylife_news')->__('Properties'),
            'title'     => Mage::helper('easylife_news')->__('Properties'),
            'content'   => $this->getLayout()->createBlock('easylife_news/adminhtml_article_attribute_edit_tab_main')->toHtml(),
            'active'    => true
        ));
        $this->addTab('labels', array(
            'label'     => Mage::helper('easylife_news')->__('Manage Label / Options'),
            'title'     => Mage::helper('easylife_news')->__('Manage Label / Options'),
            'content'   => $this->getLayout()->createBlock('easylife_news/adminhtml_article_attribute_edit_tab_options')->toHtml(),
        ));
        return parent::_beforeToHtml();
    }
}

app/code/local/Easylife/News/Block/Adminhtml/Attribute/Edit/Form.php - the attribute edit form container

<?php
class Easylife_News_Block_Adminhtml_Article_Attribute_Edit_Form
    extends Mage_Adminhtml_Block_Widget_Form {
    protected function _prepareForm() {
        $form = new Varien_Data_Form(array('id' => 'edit_form', 'action' => $this->getUrl('adminhtml/news_article_attribute/save'), 'method' => 'post'));
        $form->setUseContainer(true);
        $this->setForm($form);
        return parent::_prepareForm();
    }
}

app/code/local/Easylife/News/Block/Adminhtml/Attribute/Edit/Tab/Main.php - the attribute edit main tab:

<?php
class Easylife_News_Block_Adminhtml_Article_Attribute_Edit_Tab_Main
    extends Mage_Eav_Block_Adminhtml_Attribute_Edit_Main_Abstract {
    protected function _prepareForm(){
        parent::_prepareForm();
        $attributeObject = $this->getAttributeObject();
        $form = $this->getForm();
        $fieldset = $form->getElement('base_fieldset');
        $frontendInputElm = $form->getElement('frontend_input');
        $additionalTypes = array(
            array(
                'value' => 'image',
                'label' => Mage::helper('easylife_news')->__('Image')
            ),
            array(
                'value' => 'file',
                'label' => Mage::helper('easylife_news')->__('File')
            )
        );
        $response = new Varien_Object();
        $response->setTypes(array());
        Mage::dispatchEvent('adminhtml_article_attribute_types', array('response'=>$response));
        $_disabledTypes = array();
        $_hiddenFields = array();
        foreach ($response->getTypes() as $type) {
            $additionalTypes[] = $type;
            if (isset($type['hide_fields'])) {
                $_hiddenFields[$type['value']] = $type['hide_fields'];
            }
            if (isset($type['disabled_types'])) {
                $_disabledTypes[$type['value']] = $type['disabled_types'];
            }
        }
        Mage::register('attribute_type_hidden_fields', $_hiddenFields);
        Mage::register('attribute_type_disabled_types', $_disabledTypes);

        $frontendInputValues = array_merge($frontendInputElm->getValues(), $additionalTypes);
        $frontendInputElm->setValues($frontendInputValues);

        $yesnoSource = Mage::getModel('adminhtml/system_config_source_yesno')->toOptionArray();

        $scopes = array(
            Mage_Catalog_Model_Resource_Eav_Attribute::SCOPE_STORE =>Mage::helper('easylife_news')->__('Store View'),
            Mage_Catalog_Model_Resource_Eav_Attribute::SCOPE_WEBSITE =>Mage::helper('easylife_news')->__('Website'),
            Mage_Catalog_Model_Resource_Eav_Attribute::SCOPE_GLOBAL =>Mage::helper('easylife_news')->__('Global'),
        );

        $fieldset->addField('is_global', 'select', array(
            'name'  => 'is_global',
            'label' => Mage::helper('easylife_news')->__('Scope'),
            'title' => Mage::helper('easylife_news')->__('Scope'),
            'note'  => Mage::helper('easylife_news')->__('Declare attribute value saving scope'),
            'values'=> $scopes
        ), 'attribute_code');
        $fieldset->addField('position', 'text', array(
            'name'  => 'position',
            'label' => Mage::helper('easylife_news')->__('Position'),
            'title' => Mage::helper('easylife_news')->__('Position'),
            'note'  => Mage::helper('easylife_news')->__('Position in the admin form'),
        ), 'is_global');
        $fieldset->addField('note', 'textarea', array(
            'name'  => 'note',
            'label' => Mage::helper('easylife_news')->__('Note'),
            'title' => Mage::helper('easylife_news')->__('Note'),
            'note'  => Mage::helper('easylife_news')->__('Text to appear below the input.'),
        ), 'position');

        $fieldset->removeField('default_value_text');
        $fieldset->removeField('default_value_yesno');
        $fieldset->removeField('default_value_date');
        $fieldset->removeField('default_value_textarea');
        $fieldset->removeField('is_unique');
        // frontend properties fieldset
        $fieldset = $form->addFieldset('front_fieldset', array('legend'=>Mage::helper('easylife_news')->__('Frontend Properties')));


        $fieldset->addField('is_wysiwyg_enabled', 'select', array(
            'name' => 'is_wysiwyg_enabled',
            'label' => Mage::helper('easylife_news')->__('Enable WYSIWYG'),
            'title' => Mage::helper('easylife_news')->__('Enable WYSIWYG'),
            'values' => $yesnoSource,
        ));
        Mage::dispatchEvent('easylife_news_adminhtml_article_attribute_edit_prepare_form', array(
            'form'      => $form,
            'attribute' => $attributeObject
        ));
        return $this;
    }
}

app/code/local/Easylife/News/Block/Adminhtml/Attribute/Edit/Tab/Options.php - tab for rendering attribute options

<?php
class Easylife_News_Block_Adminhtml_Article_Attribute_Edit_Tab_Options
    extends Mage_Eav_Block_Adminhtml_Attribute_Edit_Options_Abstract {

}

app/code/local/Easylife/News/Block/Adminhtml/News/Helper/Form/Wysiwyg/Content.php - helper block for WYSIWYG elements

<?php
class Easylife_News_Block_Adminhtml_News_Helper_Form_Wysiwyg_Content
    extends Mage_Adminhtml_Block_Widget_Form {
    protected function _prepareForm(){
        $form = new Varien_Data_Form(array('id' => 'wysiwyg_edit_form', 'action' => $this->getData('action'), 'method' => 'post'));
        $config['document_base_url']     = $this->getData('store_media_url');
        $config['store_id']              = $this->getData('store_id');
        $config['add_variables']         = false;
        $config['add_widgets']           = false;
        $config['add_directives']        = true;
        $config['use_container']         = true;
        $config['container_class']       = 'hor-scroll';
        $editorConfig = Mage::getSingleton('cms/wysiwyg_config')->getConfig($config);
        $editorConfig->setData('files_browser_window_url', Mage::getSingleton('adminhtml/url')->getUrl('adminhtml/cms_wysiwyg_images/index'));
        $form->addField($this->getData('editor_element_id'), 'editor', array(
            'name'      => 'content',
            'style'     => 'width:725px;height:460px',
            'required'  => true,
            'force_load' => true,
            'config'    => $editorConfig
        ));
        $this->setForm($form);
        return parent::_prepareForm();
    }
}

now look for part 4.

Tags:

Module

Admin

Eav