Overview
After scratching my head, i finally built a module.
Models
Created a model that extends the Category model to avoid having a custom entity.
Observers
addCategoryEssentialBlock
This observer adds our new products tab and serialized grid in the category management page and listens to the adminhtml_catalog_category_tabs
event.
saveCategoryEssentialData
This observer saves the category-essentials product relation on category save and listens to the
catalog_category_save_after
event.
Code
Block
app/code/local/Lg/Essentials/Block/Adminhtml/Catalog/Category/Tab/Essential.php
class Lg_Essentials_Block_Adminhtml_Catalog_Category_Tab_Essential extends Mage_Adminhtml_Block_Widget_Grid
{
/**
* Set grid params
*/
public function __construct()
{
parent::__construct();
$this->setId('catalog_category_essential');
$this->setDefaultSort('position');
$this->setDefaultDir('ASC');
$this->setUseAjax(true);
}
/**
* Get current category
* @return Mage_Catalog_Model_Category
*/
public function getCategory()
{
return Mage::registry('current_category');
}
/**
* Add filter
* @param $column
* @return $this
*/
protected function _addColumnFilterToCollection($column)
{
if ($column->getId() == 'in_essentials') {
$essentialIds = $this->_getSelectedEssentials();
if (empty($essentialIds)) {
$essentialIds = 0;
}
if ($column->getFilter()->getValue()) {
$this->getCollection()->addFieldToFilter('entity_id', array('in'=>$essentialIds));
} else {
if ($essentialIds) {
$this->getCollection()->addFieldToFilter('entity_id', array('nin'=>$essentialIds));
}
}
} else {
parent::_addColumnFilterToCollection($column);
}
return $this;
}
/**
* Prepare the collection
* @return Mage_Adminhtml_Block_Widget_Grid
* @throws Exception
*/
protected function _prepareCollection()
{
if ($this->getCategory()->getId()) {
$this->setDefaultFilter(array('in_essentials'=>1));
}
$collection = Mage::getModel('catalog/product')->getCollection()
->addAttributeToSelect('name')
->addAttributeToSelect('sku')
->addAttributeToSelect('price')
->addStoreFilter($this->getRequest()->getParam('store'))
->joinField(
'position',
'lg_essentials/category_essential',
'position',
'product_id=entity_id',
'category_id='.(int) $this->getRequest()->getParam('id', 0),
'left'
);
$this->setCollection($collection);
if ($this->getCategory()->getProductsReadonly()) {
$essentialIds = $this->_getSelectedEssentials();
if (empty($essentialIds)) {
$essentialIds = 0;
}
$this->getCollection()->addFieldToFilter('entity_id', array('in'=>$essentialIds));
}
return parent::_prepareCollection();
}
/**
* Prepare the columns
* @return $this
* @throws Exception
*/
protected function _prepareColumns()
{
if (!$this->getCategory()->getProductsReadonly()) {
$this->addColumn(
'in_essentials',
array(
'header_css_class' => 'a-center',
'type' => 'checkbox',
'name' => 'in_essentials',
'values' => $this->_getSelectedEssentials(),
'align' => 'center',
'index' => 'entity_id'
));
}
$this->addColumn(
'entity_id',
array(
'header' => Mage::helper('lg_essentials')->__('ID'),
'sortable' => true,
'width' => '60',
'index' => 'entity_id'
)
);
$this->addColumn('name', array(
'header' => Mage::helper('lg_essentials')->__('Name'),
'index' => 'name'
));
$this->addColumn('sku', array(
'header' => Mage::helper('lg_essentials')->__('SKU'),
'width' => '80',
'index' => 'sku'
));
$this->addColumn('price', array(
'header' => Mage::helper('lg_essentials')->__('Price'),
'type' => 'currency',
'width' => '1',
'currency_code' => (string) Mage::getStoreConfig(Mage_Directory_Model_Currency::XML_PATH_CURRENCY_BASE),
'index' => 'price'
));
$this->addColumn(
'position',
array(
'header' => Mage::helper('lg_essentials')->__('Position'),
'width' => '1',
'type' => 'number',
'index' => 'position',
'editable' => !$this->getCategory()->getProductsReadonly()
)
);
return parent::_prepareColumns();
}
/**
* Get grid url
* @return string
*/
public function getGridUrl()
{
return $this->getUrl(
'adminhtml/essentials_essential_catalog_category/essentialsgrid',
array(
'id'=>$this->getCategory()->getId()
)
);
}
/**
* Get list of selected Essential Ids
* @return array
*/
protected function _getSelectedEssentials()
{
$essential = Mage::getModel('lg_essentials/category');
$essentials = $this->getCategoryEssentials();
if (!is_array($essentials)) {
$essentials = $essential->getEssentialsPosition($this->getCategory());
return array_keys($essentials);
}
return $essentials;
}
/**
* Get list of selected Essential Id & positions
* @return array
*/
public function getSelectedEssentials()
{
$essentials = array();
$selected = Mage::getModel('lg_essentials/category')->getEssentialsPosition(Mage::registry('current_category'));
if (!is_array($selected)) {
$selected = array();
}
foreach ($selected as $essentialId => $position) {
$essentials[$essentialId] = array('position' => $position);
}
return $essentials;
}
}
app/code/local/Lg/Essentials/Block/Catalog/Category/List/Essential.php
class Lg_Essentials_Block_Catalog_Category_List_Essential extends Mage_Core_Block_Template
{
/**
* Get the list of Essentials
* @return Mage_Catalog_Model_Resource_Product_Collection
*/
public function getEssentialProductsCollection()
{
$category = Mage::registry('current_category');
$collection = Mage::getModel('catalog/product')->getCollection()
->addAttributeToSelect('name')
->addAttributeToSelect('sku')
->addAttributeToSelect('price')
->joinField(
'position',
'lg_essentials/category_essential',
'position',
'product_id=entity_id',
'at_position.category_id='.(int) $category->getId(),
'right'
);
$collection->addOrder('position', 'asc');
return $collection;
}
}
controllers
app/code/local/Lg/Essentials/controllers/Adminhtml/Essentials/Essential/Catalog/CategoryController.php
require_once ("Mage/Adminhtml/controllers/Catalog/CategoryController.php");
class Lg_Essentials_Adminhtml_Essentials_Essential_Catalog_CategoryController
extends Mage_Adminhtml_Catalog_CategoryController
{
/**
* construct
*/
protected function _construct()
{
// Define module dependent translate
$this->setUsedModuleName('Lg_Essentials');
}
/**
* Essentials grid in category page
*/
public function essentialsgridAction()
{
$this->_initCategory();
$this->loadLayout();
$this->getLayout()->getBlock('category.edit.tab.essential')
->setCategoryEssentials($this->getRequest()->getPost('category_essentials', null));
$this->renderLayout();
}
}
etc
app/code/local/Lg/Essentials/etc/config.xml
<?xml version="1.0"?>
<config>
<modules>
<Lg_Essentials>
<version>0.1.0</version>
</Lg_Essentials>
</modules>
<global>
<resources>
<lg_essentials_setup>
<setup>
<module>Lg_Essentials</module>
<class>Lg_Essentials_Model_Resource_Setup</class>
</setup>
</lg_essentials_setup>
</resources>
<blocks>
<lg_essentials>
<class>Lg_Essentials_Block</class>
</lg_essentials>
</blocks>
<helpers>
<lg_essentials>
<class>Lg_Essentials_Helper</class>
</lg_essentials>
</helpers>
<models>
<lg_essentials>
<class>Lg_Essentials_Model</class>
<resourceModel>lg_essentials_resource</resourceModel>
</lg_essentials>
<lg_essentials_resource>
<class>Lg_Essentials_Model_Resource</class>
<entities>
<category_essential>
<table>lg_essentials_category_essential</table>
</category_essential>
</entities>
</lg_essentials_resource>
</models>
</global>
<adminhtml>
<layout>
<updates>
<lg_essentials>
<file>lg/essentials.xml</file>
</lg_essentials>
</updates>
</layout>
<events>
<adminhtml_catalog_category_tabs>
<observers>
<lg_essentials_essential_category>
<type>singleton</type>
<class>lg_essentials/adminhtml_observer</class>
<method>addCategoryEssentialBlock</method>
</lg_essentials_essential_category>
</observers>
</adminhtml_catalog_category_tabs>
<catalog_category_save_after>
<observers>
<lg_essentials_essential_category>
<type>singleton</type>
<class>lg_essentials/adminhtml_observer</class>
<method>saveCategoryEssentialData</method>
</lg_essentials_essential_category>
</observers>
</catalog_category_save_after>
</events>
</adminhtml>
<admin>
<routers>
<adminhtml>
<args>
<modules>
<Lg_Essentials before="Mage_Adminhtml">Lg_Essentials_Adminhtml</Lg_Essentials>
</modules>
</args>
</adminhtml>
</routers>
</admin>
<frontend>
<layout>
<updates>
<lg_essentials>
<file>lg/essentials.xml</file>
</lg_essentials>
</updates>
</layout>
</frontend>
</config>
Helper
app/code/local/Lg/Essentials/Helper/Data.php
class Lg_Essentials_Helper_Data extends Mage_Core_Helper_Abstract
{
}
Model
app/code/local/Lg/Essentials/Model/Adminhtml/Observer.php
class Lg_Essentials_Model_Adminhtml_Observer
{
/**
* Add the Essential tab to categories
* @param Varien_Event_Observer $observer
* @return Lg_Essentials_Model_Adminhtml_Observer $this
*/
public function addCategoryEssentialBlock($observer)
{
$tabs = $observer->getEvent()->getTabs();
$content = $tabs->getLayout()->createBlock(
'lg_essentials/adminhtml_catalog_category_tab_essential',
'category.essential.grid'
)->toHtml();
$serializer = $tabs->getLayout()->createBlock(
'adminhtml/widget_grid_serializer',
'category.essential.grid.serializer'
);
$serializer->initSerializerBlock(
'category.essential.grid',
'getSelectedEssentials',
'essentials',
'category_essentials'
);
$serializer->addColumnInputName('position');
$content .= $serializer->toHtml();
$tabs->addTab(
'essential',
array(
'label' => Mage::helper('lg_essentials')->__('Essentials'),
'content' => $content,
)
);
return $this;
}
/**
* Save category - essential relation
* @param Varien_Event_Observer $observer
* @return Lg_Essentials_Model_Adminhtml_Observer $this
*/
public function saveCategoryEssentialData($observer)
{
$post = Mage::app()->getRequest()->getPost('essentials', -1);
if ($post != '-1') {
$post = Mage::helper('adminhtml/js')->decodeGridSerializedInput($post);
$category = Mage::registry('category');
$category->setPostedEssentials($post);
$essentialResource = Mage::getResourceModel('lg_essentials/category');
$essentialResource->saveCategoryEssentials($category);
}
return $this;
}
}
app/code/local/Lg/Essentials/Model/Resource/Category.php
class Lg_Essentials_Model_Resource_Category extends Mage_Catalog_Model_Resource_Category
{
/**
* Save category essentials relation
* @param Mage_Catalog_Model_Category $category
* @return Mage_Catalog_Model_Resource_Category
*/
public function saveCategoryEssentials($category)
{
$categoryEssentialTable = $this->getTable('lg_essentials/category_essential');
$category->setIsChangedEssentialList(false);
$id = $category->getId();
/**
* new category-essential relationships
*/
$products = $category->getPostedEssentials();
/**
* Example re-save category
*/
if ($products === null) {
return $this;
}
/**
* old category-essential relationships
*/
$oldProducts = Mage::getModel('lg_essentials/category')->getEssentialsPosition($category);
$insert = array_diff_key($products, $oldProducts);
$delete = array_diff_key($oldProducts, $products);
/**
* Find product ids which are presented in both arrays
* and saved before (check $oldProducts array)
*/
$update = array_intersect_key($products, $oldProducts);
if ($update) {
$update = $this->cleanPositions($update);
}
$update = array_diff_assoc($update, $oldProducts);
$adapter = $this->_getWriteAdapter();
/**
* Delete essentials from category
*/
if (!empty($delete)) {
$cond = array(
'product_id IN(?)' => array_keys($delete),
'category_id=?' => $id
);
$adapter->delete($categoryEssentialTable, $cond);
}
/**
* Add essentials to category
*/
if (!empty($insert)) {
$data = array();
foreach ($insert as $productId => $position) {
$data[] = array(
'category_id' => (int)$id,
'product_id' => (int)$productId,
'position' => (int)$position['position']
);
}
$adapter->insertMultiple($categoryEssentialTable, $data);
}
/**
* Update essential positions in category
*/
if (!empty($update)) {
foreach ($update as $productId => $position) {
$where = array(
'category_id = ?'=> (int)$id,
'product_id = ?' => (int)$productId
);
$bind = array('position' => (int)$position);
$adapter->update($categoryEssentialTable, $bind, $where);
}
}
if (!empty($insert) || !empty($delete)) {
$productIds = array_unique(array_merge(array_keys($insert), array_keys($delete)));
Mage::dispatchEvent('catalog_category_change_essentials', array(
'category' => $category,
'product_ids' => $productIds
));
}
if (!empty($insert) || !empty($update) || !empty($delete)) {
$category->setIsChangedEssentialList(true);
/**
* Setting affected essentials to category for third party engine index refresh
*/
$productIds = array_keys($insert + $delete + $update);
$category->setAffectedEssentialIds($productIds);
}
return $this;
}
/**
* Get positions of associated to category essentials
* @param Mage_Catalog_Model_Category $category
* @return array
*/
public function getEssentialsPosition($category)
{
$categoryEssentialTable = $this->getTable('lg_essentials/category_essential');
$select = $this->_getWriteAdapter()->select()
->from($categoryEssentialTable, array('product_id', 'position'))
->where('category_id = :category_id');
$bind = array('category_id' => (int)$category->getId());
return $this->_getWriteAdapter()->fetchPairs($select, $bind);
}
/**
* Get Essential count in category
* @param Mage_Catalog_Model_Category $category
* @return int
*/
public function getEssentialCount($category)
{
$categoryEssentialTable = Mage::getSingleton('core/resource')->getTableName('lg_essentials/category_essential');
$select = $this->getReadConnection()->select()
->from(
array('main_table' => $categoryEssentialTable),
array(new Zend_Db_Expr('COUNT(main_table.product_id)'))
)
->where('main_table.category_id = :category_id');
$bind = array('category_id' => (int)$category->getId());
$counts = $this->getReadConnection()->fetchOne($select, $bind);
return intval($counts);
}
/**
* Clean Essentials positions
* @param $products
* @return array
*/
protected function cleanPositions($products)
{
$cleanPositions = array();
foreach ($products as $productid => $info) {
$cleanPositions[$productid] = $info['position'];
}
return $cleanPositions;
}
}
app/code/local/Lg/Essentials/Model/Resource/Setup.php
class Lg_Essentials_Model_Resource_Setup extends Mage_Catalog_Model_Resource_Setup
{
}
app/code/local/Lg/Essentials/Model/Category.php
class Lg_Essentials_Model_Category extends Mage_Catalog_Model_Category
{
/**
* Retrieve array of essential id's for category
* array($productId => $position)
* @param $category
* @return array
*/
public function getEssentialsPosition($category)
{
if (!$category->getId()) {
return array();
}
$array = $category->getData('essentials_position');
if (is_null($array)) {
$array = Mage::getResourceModel('lg_essentials/category')->getEssentialsPosition($category);
$category->setData('essentials_position', $array);
}
return $array;
}
/**
* Retrieve count essentials of category
* @param $category
* @return int
*/
public function getEssentialCount($category)
{
if (!$this->hasEssentialCount()) {
$count = Mage::getResourceModel('lg_essentials/category')->getEssentialCount($category);
$this->setData('essential_count', $count);
}
return $this->getData('essential_count');
}
}
sql
app/code/local/Lg/Essentials/sql/lg_essentials_setup/install-0.1.0.php
/* @var $installer Mage_Catalog_Model_Resource_Setup */
$installer = $this;
$installer->startSetup();
/**
* Create table 'lg_essentials/category_essential'
*/
$table = $installer->getConnection()
->newTable($installer->getTable('lg_essentials/category_essential'))
->addColumn(
'category_id',
Varien_Db_Ddl_Table::TYPE_INTEGER,
null,
array(
'unsigned' => true,
'nullable' => false,
'primary' => true,
'default' => '0',
),
'Category ID'
)
->addColumn(
'product_id',
Varien_Db_Ddl_Table::TYPE_INTEGER,
null,
array(
'unsigned' => true,
'nullable' => false,
'primary' => true,
'default' => '0',
),
'Product ID'
)
->addColumn(
'position',
Varien_Db_Ddl_Table::TYPE_INTEGER,
null,
array(
'nullable' => false,
'default' => '0',
),
'Position'
)
->addIndex(
$installer->getIdxName(
'lg_essentials/category_essential',
array('product_id')
),
array('product_id')
)
->addForeignKey(
$installer->getFkName(
'lg_essentials/category_essential',
'category_id',
'catalog/category',
'entity_id'
),
'category_id',
$installer->getTable('catalog/category'),
'entity_id',
Varien_Db_Ddl_Table::ACTION_CASCADE,
Varien_Db_Ddl_Table::ACTION_CASCADE
)
->addForeignKey(
$installer->getFkName(
'lg_essentials/category_essential',
'product_id',
'catalog/product',
'entity_id'
),
'product_id',
$installer->getTable('catalog/product'),
'entity_id',
Varien_Db_Ddl_Table::ACTION_CASCADE,
Varien_Db_Ddl_Table::ACTION_CASCADE
)
->addIndex(
$this->getIdxName(
'lg_essentials/category_essential',
array('product_id', 'category_id'),
Varien_Db_Adapter_Interface::INDEX_TYPE_UNIQUE
),
array('product_id', 'category_id'),
array('type' => Varien_Db_Adapter_Interface::INDEX_TYPE_UNIQUE)
)
->setComment('Essential To Category Linkage Table');
$installer->getConnection()->createTable($table);
$installer->endSetup();
adminthml
app/design/adminhtml/default/default/layout/lg/essentials.xml
<?xml version="1.0"?>
<layout>
<adminhtml_essentials_essential_catalog_category_essentialsgrid>
<block type="core/text_list" name="root" output="toHtml">
<block type="lg_essentials/adminhtml_catalog_category_tab_essential" name="category.edit.tab.essential"/>
</block>
</adminhtml_essentials_essential_catalog_category_essentialsgrid>
</layout>
frontend
app/design/frontend/base/default/layout/lg/essentials.xml
<?xml version="1.0"?>
<layout>
<lg_essentials_category>
<reference name="category.products">
<block type="lg_essentials/catalog_category_list_essential" name="category.info.essentials"
as="category_essentials" template="lg/essentials/catalog/category/list/essential.phtml"/>
</reference>
</lg_essentials_category>
<catalog_category_default>
<update handle="lg_essentials_category" />
</catalog_category_default>
<catalog_category_layered>
<update handle="lg_essentials_category" />
</catalog_category_layered>
</layout>
app/design/frontend/base/default/template/lg/essentials/catalog/category/list/essential.phtml
<?php $essentials = $this->getEssentialProductsCollection();?>
<?php if ($essentials && $essentials->count() > 0) :?>
<div class="box-collateral box-essentials box-up-sell">
<h2>Essentials</h2>
<?php foreach ($essentials as $_essential) : ?>
<div class="item">
<?php echo $_essential->getName();?>
<br />
</div>
<?php endforeach; ?>
</div>
<?php endif;?>
app/etc/modules/Lg_Essentials.xml
<?xml version="1.0"?>
<config>
<modules>
<Lg_Essentials>
<active>true</active>
<codePool>local</codePool>
<depends>
<Mage_Catalog />
</depends>
</Lg_Essentials>
</modules>
</config>