The XML configuration necessary for rewriting Blocks & Models is skipped here. Search around if you don't know how to do that. Also there is additional attributes in this instructions that won't apply to your project. You'll have to dissect that on your own.
1. Rewrite the Product model.
<?php
class Your_Extension_Model_Rewrite_Catalog_Product extends Mage_Catalog_Model_Product
{
/**
* Stores the URLs of configurable products for use by simple
* products so that during product listings we don't have to
* load the same configurable product numerous times for
* multiple simple products.
*
* @var array
*/
protected $_configurableUrls = array();
/**
* Retrieve Product URL
*
* @param bool $useSid
* @return string
*/
public function getProductUrl($useSid = null)
{
if ($this->getData('type_id') == 'simple') {
$resource = Mage::getSingleton('core/resource');
$connection = $resource->getConnection('core_read');
$tblRelations = $resource->getTableName('catalog_product_super_link');
$sql = "SELECT `parent_id` FROM `{$tblRelations}` WHERE `product_id` = :product_id LIMIT 1";
$bind = array('product_id' => $this->getId());
if (false != ($productId = $connection->fetchOne($sql, $bind)) ) {
$configurable = Mage::getResourceModel('catalog/product_collection')
->addAttributeToFilter('entity_id', $productId)
->addAttributeToFilter('is_sliced_by_color', '1')
->getFirstItem();
if ($configurable->getId()) {
return Mage::getSingleton('catalog/product')->getConfigurableProductUrl($configurable->getId());
}
}
}
return $this->getUrlModel()->getProductUrl($this, $useSid);
}
/**
* Return the URL for a configurable product.
*
* @access public
* @param int $configurableId
* @return string
*/
public function getConfigurableProductUrl($configurableId)
{
if (!isset($this->_urlReadyConfigurables[$configurableId])) {
$configurable = Mage::getModel('catalog/product')->load($configurableId);
$this->_configurableUrls[$configurableId] = $configurable->getProductUrl();
}
return $this->_configurableUrls[$configurableId];
}
}
2. Rewrite the Category Resource Model
$resource = Mage::getSingleton('core/resource');
$productTable = $resource->getTableName('catalog/category_product');
$tblVarchar = $resource->getTableName('catalog_product_entity_varchar');
$attributeId = Mage::getResourceModel('eav/entity_attribute')->getIdByCode('catalog_product', 'style_number');
$select = $this->getReadConnection()->select()
->from(
array('main_table' => $productTable),
array(new Zend_Db_Expr('COUNT(main_table.product_id)'))
)
->where('main_table.category_id = :category_id')
->joinLeft(
array('client_group_by' => $tblVarchar),
"client_group_by.attribute_id = '{$attributeId}' AND client_group_by.entity_id = main_table.product_id",
array('IF(`client_group_by`.`value` IS NOT NULL, CAST(`client_group_by`.`value` AS CHAR(255)), CAST(`main_table`.`product_id` AS CHAR(255))) AS client_group_by')
)
->group('client_group_by');
$bind = array('category_id' => (int)$category->getId());
$counts = count($this->getReadConnection()->fetchAll($select, $bind));
return intval($counts);
}
}
3. Rewrite the Product Collection Resource
<?php
class Your_Extension_Model_Rewrite_Catalog_Resource_Product_Collection extends Mage_Catalog_Model_Resource_Product_Collection
{
/**
* On Category pages (actually, any page that has a toolbar, but for this client's purposes that's category pages)
* we need to properly count the number of products. Because of our modifications
* to the way collections display Magento was returning all products of all sizes. However this is not accurate
* since we're only showing one size per color.
*
* @return int
*/
public function getSize()
{
// @see Your_Extension_Block_Rewrite_Catalog_Product_List_Toolbar::setCollection()
if (!Mage::registry('catalog_list_query_modified')) {
return parent::getSize();
}
if (is_null($this->_totalRecords)) {
$sql = $this->getSelectCountSql();
// Group the simple products by their color (style_number)
$tblVarchar = Mage::getSingleton('core/resource')->getTableName('catalog_product_entity_varchar');
$attributeId = Mage::getResourceModel('eav/entity_attribute')->getIdByCode('catalog_product', 'style_number');
$sql->joinLeft(
array('client_group_by' => $tblVarchar),
"client_group_by.attribute_id = '{$attributeId}' AND client_group_by.entity_id = e.entity_id",
array('IF(`client_group_by`.`value` IS NOT NULL, CAST(`client_group_by`.`value` AS CHAR(255)), CAST(`e`.`entity_id` AS CHAR(255))) AS client_group_by')
);
$sql->group('client_group_by');
$this->_totalRecords = count($this->getConnection()->fetchAll($sql, $this->_bindParams));
}
return intval($this->_totalRecords);
}
/**
* Adding product count to categories collection
*
* @param Mage_Eav_Model_Entity_Collection_Abstract $categoryCollection
* @return Mage_Catalog_Model_Resource_Product_Collection
*/
public function addCountToCategories($categoryCollection)
{
if (!Mage::registry('catalog_list_query_modified')) {
return parent::addCountToCategories($categoryCollection);
}
$isAnchor = array();
$isNotAnchor = array();
foreach ($categoryCollection as $category) {
if ($category->getIsAnchor()) {
$isAnchor[] = $category->getId();
} else {
$isNotAnchor[] = $category->getId();
}
}
$productCounts = array();
if ($isAnchor || $isNotAnchor) {
$select = $this->getProductCountSelect();
Mage::dispatchEvent(
'catalog_product_collection_before_add_count_to_categories',
array('collection' => $this)
);
if ($isAnchor) {
$anchorStmt = clone $select;
$anchorStmt->limit(); //reset limits
$anchorStmt->where('count_table.category_id IN (?)', $isAnchor);
$anchorStmt = $this->modifyForSliced($anchorStmt, true);
$counts = $this->getConnection()->fetchAll($anchorStmt);
$actualCounts = array();
foreach ($counts as $count) {
$categoryId = $count['category_id'];
if (!isset($actualCounts[$categoryId])) {
$actualCounts[$categoryId] = 0;
}
$actualCounts[$categoryId]++;
}
$productCounts += $actualCounts;
$anchorStmt = null;
}
if ($isNotAnchor) {
$notAnchorStmt = clone $select;
$notAnchorStmt->limit(); //reset limits
$notAnchorStmt->where('count_table.category_id IN (?)', $isNotAnchor);
$notAnchorStmt->where('count_table.is_parent = 1');
$notAnchorStmt = $this->modifyForSliced($notAnchorStmt, true);
$counts = $this->getConnection()->fetchAll($notAnchorStmt);
$actualCounts = array();
foreach ($counts as $count) {
$categoryId = $count['category_id'];
if (!isset($actualCounts[$categoryId])) {
$actualCounts[$categoryId] = 0;
}
$actualCounts[$categoryId]++;
}
$productCounts += $actualCounts;
$notAnchorStmt = null;
}
$select = null;
$this->unsProductCountSelect();
}
foreach ($categoryCollection as $category) {
$_count = 0;
if (isset($productCounts[$category->getId()])) {
$_count = $productCounts[$category->getId()];
}
$category->setProductCount($_count);
}
return $this;
}
private function modifyForSliced($selectStmt, $includeIsSliced = false)
{
if (false === strpos($selectStmt->__toString(), 'client_group_by')) {
$tblVarchar = Mage::getSingleton('core/resource')->getTableName('catalog_product_entity_varchar');
$attributeId = Mage::getResourceModel('eav/entity_attribute')->getIdByCode('catalog_product', 'style_number');
$selectStmt->joinLeft(
array('client_group_by' => $tblVarchar),
"client_group_by.attribute_id = '{$attributeId}' AND client_group_by.entity_id = e.entity_id",
array('IF(`client_group_by`.`value` IS NOT NULL, CAST(`client_group_by`.`value` AS CHAR(255)), CAST(`e`.`entity_id` AS CHAR(255))) AS client_group_by')
);
$selectStmt->group('client_group_by');
}
if ($includeIsSliced && false === strpos($selectStmt->__toString(), 'client_is_sliced')) {
// Hide Configurable Products which are sliced by their color
$tblInt = Mage::getSingleton('core/resource')->getTableName('catalog_product_entity_int');
$attributeId = Mage::getResourceModel('eav/entity_attribute')->getIdByCode('catalog_product', 'is_sliced_by_color');
$selectStmt->joinLeft(
array('client_is_sliced' => $tblInt),
"client_is_sliced.attribute_id = '{$attributeId}' AND client_is_sliced.entity_id = e.entity_id",
array('client_is_sliced' => 'value')
);
$selectStmt->where('client_is_sliced.value != 1 OR client_is_sliced.value IS NULL');
}
return $selectStmt;
}
}
4. Rewrite the Price filter model
<?php
class Your_Extension_Model_Rewrite_Catalog_Resource_Layer_Filter_Price extends Mage_Catalog_Model_Resource_Layer_Filter_Price
{
/**
* Retrieve clean select with joined price index table.
*
* @version 2015.11.23 - Line added to reset grouping on the Zend_Db_Select
* object. Our extension's grouping performed in Your_Extension_Block_Product_List_Toolbar
* was causing this portion to break without it.
*
* @param Mage_Catalog_Model_Layer_Filter_Price $filter
* @return Varien_Db_Select
*/
protected function _getSelect($filter)
{
// @see Your_Extension_Block_Rewrite_Catalog_Product_List_Toolbar::setCollection()
if (!Mage::registry('catalog_list_query_modified')) {
return parent::_getSelect();
}
$collection = $filter->getLayer()->getProductCollection();
$collection->addPriceData($filter->getCustomerGroupId(), $filter->getWebsiteId());
if (!is_null($collection->getCatalogPreparedSelect())) {
$select = clone $collection->getCatalogPreparedSelect();
} else {
$select = clone $collection->getSelect();
}
// reset columns, order and limitation conditions
$select->reset(Zend_Db_Select::COLUMNS);
$select->reset(Zend_Db_Select::ORDER);
$select->reset(Zend_Db_Select::LIMIT_COUNT);
$select->reset(Zend_Db_Select::LIMIT_OFFSET);
$select->reset(Zend_Db_Select::GROUP); // <- OUR NEW LINE
// remove join with main table
$fromPart = $select->getPart(Zend_Db_Select::FROM);
if (!isset($fromPart[Mage_Catalog_Model_Resource_Product_Collection::INDEX_TABLE_ALIAS])
|| !isset($fromPart[Mage_Catalog_Model_Resource_Product_Collection::MAIN_TABLE_ALIAS])
) {
return $select;
}
// processing FROM part
$priceIndexJoinPart = $fromPart[Mage_Catalog_Model_Resource_Product_Collection::INDEX_TABLE_ALIAS];
$priceIndexJoinConditions = explode('AND', $priceIndexJoinPart['joinCondition']);
$priceIndexJoinPart['joinType'] = Zend_Db_Select::FROM;
$priceIndexJoinPart['joinCondition'] = null;
$fromPart[Mage_Catalog_Model_Resource_Product_Collection::MAIN_TABLE_ALIAS] = $priceIndexJoinPart;
unset($fromPart[Mage_Catalog_Model_Resource_Product_Collection::INDEX_TABLE_ALIAS]);
$select->setPart(Zend_Db_Select::FROM, $fromPart);
foreach ($fromPart as $key => $fromJoinItem) {
$fromPart[$key]['joinCondition'] = $this->_replaceTableAlias($fromJoinItem['joinCondition']);
}
$select->setPart(Zend_Db_Select::FROM, $fromPart);
// processing WHERE part
$wherePart = $select->getPart(Zend_Db_Select::WHERE);
foreach ($wherePart as $key => $wherePartItem) {
$wherePart[$key] = $this->_replaceTableAlias($wherePartItem);
}
$select->setPart(Zend_Db_Select::WHERE, $wherePart);
$excludeJoinPart = Mage_Catalog_Model_Resource_Product_Collection::MAIN_TABLE_ALIAS . '.entity_id';
foreach ($priceIndexJoinConditions as $condition) {
if (strpos($condition, $excludeJoinPart) !== false) {
continue;
}
$select->where($this->_replaceTableAlias($condition));
}
$select->where($this->_getPriceExpression($filter, $select) . ' IS NOT NULL');
return $select;
}
}
5. Rewrite the Attribute Filter model
<?php
class Your_Extension_Model_Rewrite_Catalog_Resource_Layer_Filter_Attribute extends Mage_Catalog_Model_Resource_Layer_Filter_Attribute
{
/**
* Retrieve array with products counts per attribute option
*
* @version 2015.11.23 - Line added to reset grouping on the Zend_Db_Select
* object. Our extension's grouping performed in Your_Extension_Block_Product_List_Toolbar
* was causing this portion to break without it.
*
* @param Mage_Catalog_Model_Layer_Filter_Attribute $filter
* @return array
*/
public function getCount($filter)
{
// @see Your_Extension_Block_Rewrite_Catalog_Product_List_Toolbar::setCollection()
if (!Mage::registry('catalog_list_query_modified')) {
return parent::getCount();
}
// clone select from collection with filters
$select = clone $filter->getLayer()->getProductCollection()->getSelect();
// reset columns, order and limitation conditions
$select->reset(Zend_Db_Select::COLUMNS);
$select->reset(Zend_Db_Select::ORDER);
$select->reset(Zend_Db_Select::LIMIT_COUNT);
$select->reset(Zend_Db_Select::LIMIT_OFFSET);
$select->reset(Zend_Db_Select::GROUP); // <- OUR NEW LINE
$connection = $this->_getReadAdapter();
$attribute = $filter->getAttributeModel();
$tableAlias = sprintf('%s_idx', $attribute->getAttributeCode());
$conditions = array(
"{$tableAlias}.entity_id = e.entity_id",
$connection->quoteInto("{$tableAlias}.attribute_id = ?", $attribute->getAttributeId()),
$connection->quoteInto("{$tableAlias}.store_id = ?", $filter->getStoreId()),
);
$select
->join(
array($tableAlias => $this->getMainTable()),
join(' AND ', $conditions),
array('value', 'count' => new Zend_Db_Expr("COUNT({$tableAlias}.entity_id)")))
->group("{$tableAlias}.value");
return $connection->fetchPairs($select);
}
}
6. Rewrite the Product List Block
<?php
/**
* Extended Magento's Toolbar to add additional sorting criteria per the client's request
*
*/
class Your_Extension_Block_Rewrite_Catalog_Product_List extends Mage_Catalog_Block_Product_List
{
public function getPriceHtml($product, $displayMinimalPrice = false, $idSuffix = '')
{
// for sliced products, use the parent configurable when showing pricing
if ($product->getData('type_id') == 'simple') {
$configurable = Mage::getSingleton('your_extension/catalog_product_type_simple')->getParent($product->getId());
$priceProduct = $configurable ? $configurable : $product;
} else {
$priceProduct = $product;
}
return parent::getPriceHtml($priceProduct, $displayMinimalPrice, $idSuffix);
}
}
7. Rewrite the Product List Toolbar Block
<?php
/**
* Extended Magento's Toolbar to add additional sorting criteria per the client's request
*/
class Your_Extension_Block_Rewrite_Catalog_Product_List_Toolbar extends Mage_Catalog_Block_Product_List_Toolbar
{
/**
* @var Mage_Catalog_Model_Resource_Product_Collection
*/
protected $_collection;
/**
* Sort order client desires (anything selected from the drop down such as "Name" is allowed to be first)
*
* Priority Attribute ID Direction
* 1 Category Position Ascending
* 2 Age Code Ascending
* 3 Gender Code Ascending
* 4 Product Division Code Ascending
* 5 Department Code Ascending
* 6 Sport Code Ascending
* 7 Subcategory ID Ascending
* 8 Class ID Ascending
* 9 Price Descending
*
* @param Mage_Catalog_Model_Resource_Product_Collection $collection
* @return Mage_Catalog_Block_Product_List_Toolbar
*/
public function setCollection($collection)
{
// If this toolbar is being used for anything other than a product collection, fallback to the original logic
if (! $collection instanceOf Mage_Catalog_Model_Resource_Product_Collection) {
return parent::setCollection($collection);
}
Mage::unregister('catalog_list_query_modified'); // Just a safe check to avoid error if this block type is ever instantiated twice in one HTTP request
Mage::register('catalog_list_query_modified', true);
$this->_collection = $collection;
$this->_collection->setCurPage($this->getCurrentPage());
// we need to set pagination only if passed value integer and more that 0
$limit = (int)$this->getLimit();
if ($limit) {
$this->_collection->setPageSize($limit);
}
if ($this->getCurrentOrder()) {
$this->_collection->addAttributeToSort($this->getCurrentOrder(), $this->getCurrentDirection());
}
if ( ($this->getCurrentOrder() && 'position' != $this->getCurrentOrder() && Mage::registry('current_category'))
|| (!$this->getCurrentOrder() && Mage::registery('current_category'))
) {
$this->_collection->addAttributeToSort('position', Varien_Data_Collection::SORT_ORDER_ASC);
}
$this->_collection->addAttributeToSort('age_code', Varien_Data_Collection::SORT_ORDER_ASC);
$this->_collection->addAttributeToSort('gender', Varien_Data_Collection::SORT_ORDER_ASC);
$this->_collection->addAttributeToSort('product_div_code', Varien_Data_Collection::SORT_ORDER_ASC);
$this->_collection->addAttributeToSort('dept_code', Varien_Data_Collection::SORT_ORDER_ASC);
$this->_collection->addAttributeToSort('sport_code', Varien_Data_Collection::SORT_ORDER_ASC);
$this->_collection->addAttributeToSort('subcat_id', Varien_Data_Collection::SORT_ORDER_ASC);
$this->_collection->addAttributeToSort('class_id', Varien_Data_Collection::SORT_ORDER_ASC);
if ( ($this->getCurrentOrder() && 'price' != $this->getCurrentOrder())
|| !$this->getCurrentOrder()
) {
$this->_collection->addAttributeToSort('price', Varien_Data_Collection::SORT_ORDER_DESC);
}
/* @var $select Varien_Db_Select */
$select = $this->_collection->getSelect();
// Group the simple products by their color (style_number)
$tblVarchar = Mage::getSingleton('core/resource')->getTableName('catalog_product_entity_varchar');
$attributeId = Mage::getResourceModel('eav/entity_attribute')->getIdByCode('catalog_product', 'style_number');
$select->joinLeft(
array('client_group_by' => $tblVarchar),
"client_group_by.attribute_id = '{$attributeId}' AND client_group_by.entity_id = e.entity_id",
array('IF(`client_group_by`.`value` IS NOT NULL, CAST(`client_group_by`.`value` AS CHAR(255)), CAST(`e`.`entity_id` AS CHAR(255))) AS client_group_by')
);
$select->group('client_group_by');
// Hide Configurable Products which are sliced by their color
$tblInt = Mage::getSingleton('core/resource')->getTableName('catalog_product_entity_int');
$attributeId = Mage::getResourceModel('eav/entity_attribute')->getIdByCode('catalog_product', 'is_sliced_by_color');
$select->joinLeft(
array('client_is_sliced' => $tblInt),
"client_is_sliced.attribute_id = '{$attributeId}' AND client_is_sliced.entity_id = e.entity_id",
array('client_is_sliced' => 'value')
);
$select->where('client_is_sliced.value != 1 OR client_is_sliced.value IS NULL');
return $this;
}
}
8. Add this class
Note: Class name may indicate that it is rewriting Mage_Catalog_Model_Product_Type_Simple but it is not, just adds a method for us that is required in one of the classes above)
<?php
class Your_Extension_Model_Catalog_Product_Type_Simple
{
protected $_loadedConfigurables = array();
/**
* Return the parent configurable product if there is one
*
* @param int $simpleId
*/
public function getParent($simpleId)
{
/* @var $resource Mage_Core_Model_Resource */
$resource = Mage::getSingleton('core/resource');
$query_link = "SELECT `parent_id` FROM `{$resource->getTableName('catalog_product_super_link')}` WHERE `product_id` = ?";
$productId = $resource->getConnection('core_read')->fetchOne($query_link, $simpleId);
if (!$productId) {
return false;
}
if (!isset($this->_loadedConfigurables[$productId])) {
$product = Mage::getModel('catalog/product')
->setStoreId(Mage::app()->getStore()->getId())
->load($productId);
$this->_loadedConfigurables[$productId] = $product;
}
return $this->_loadedConfigurables[$productId];
}
}
Some final notes:
- Only "sliced" configurable products are actually setup to shown multiple colors.
- Add a "yes/no" attribute to your configurable products to indicate if it is sliced or not. In my code above I've given it an attribute_code "client_is_sliced".
- Similar to what Amit Bera mentioned, simple products are set to "Visible" in the catalog. However a slight difference is that ALL simple products are allowed to be visible. Any decent sized catalog would be unmanageable if the admin user's had to meticulously ensure only of each color is set to visible. Sure it could be scripted to do this, but the above solution allows for easier client management of their catalog by having all simple products visible.
- Honestly I don't recommend any of this at all. It was a pain in the ass to setup.