Magento 1.9 – How to Add Product Grid from PHTML File

adminhtmlcategory-productscategory-treegridmagento-1.9

I have a category with more than 50K products therefore whenever I click on it in admin "Manage Categories" it gives me memory exhaust error, now I want to only load products when I click on "Category Product" tab or a button in phtml file which loads product in the grid.

Can someone guide through steps to load grid from phtml with working filters and selection?

Best Answer

I'm not sure that what you're asking for will solve the problem described. As I read the symptom, my first diagnosis pointed to the following roots:

  1. Most admin content tabs will preload everything at runtime by default.
  2. The collection process for assigned products under a category uses an inefficient query.

Supporting the Diagnosis:

Mage_Adminhtml_Block_Catalog_Category_Tabs::_prepareLayout ~lines 148-154:

...
$this->addTab('products', array(
    'label'     => Mage::helper('catalog')->__('Category Products'),
    'content'   => $this->getLayout()->createBlock(
        'adminhtml/catalog_category_tab_product',
        'category.product.grid'
    )->toHtml(),
));
...
  • The block is rendered instantly upon layout preparation.
  • IMO this is the laziest of available content load mechanisms available.
  • Magento offers grid serialization components and deferred (AJAX) load tooling (more on this later).

Mage_Adminhtml_Block_Catalog_Category_Tab_Product::_prepareCollection ~lines 71-98:

...
protected function _prepareCollection()
{
    if ($this->getCategory()->getId()) {
        $this->setDefaultFilter(array('in_category'=>1));
    }
    $collection = Mage::getModel('catalog/product')->getCollection()
        ->addAttributeToSelect('name')
        ->addAttributeToSelect('sku')
        ->addAttributeToSelect('price')
        ->addStoreFilter($this->getRequest()->getParam('store'))
        ->joinField('position',
            'catalog/category_product',
            'position',
            'product_id=entity_id',
            'category_id='.(int) $this->getRequest()->getParam('id', 0),
            'left');
    $this->setCollection($collection);

    if ($this->getCategory()->getProductsReadonly()) {
        $productIds = $this->_getSelectedProducts();
        if (empty($productIds)) {
            $productIds = 0;
        }
        $this->getCollection()->addFieldToFilter('entity_id', array('in'=>$productIds));
    }

    return parent::_prepareCollection();
}
...
  • The call to $this->_getSelectedProducts() appends 50K IDs to an IN clause in your case.
  • The call to parent::_prepareCollection() instantly loads the collection, but not before applying a limit to the query (see commentary below).

Deeper Commentary:

Knowing that the parent widget-grid block [see parent::_prepareCollection()] applies a limit to the results returned through the assembled query, the most loaded at once should be no greater than 200. I say this because 200 is the highest default limit given by the toolbar class. So unless you have a modification to your code that would increase the limit beyond 200, I am not certain that the memory exhaustion is caused by 50K assigned products.

Therefore, it seems to me that deferred loading won't solve your problem. Ask yourself some questions that might seem unrelated, like:

  • Do you have any observers registered on events like catalog_product_collection_load_before or catalog_product_collection_load_after?
  • Do you have extensions that add additional content tabs to the area?
  • Are you adding additional columns to the Category Products grid that would make the select query more complex?

I would bet that something else is getting in the way. You could try some brute force guesses by stripping away customizations until the problem goes away. Or else, this is a worthwhile exercise in using a profiler like XDebug to pinpoint when the memory exhaustion occurs.

Deferred Tab Content Loading

If you've followed along so far, you learned that the content is loaded at runtime (or more precisely, layout generation time) for the Category Products tab, and also that Magento offers some unique features for deferring the loading of contents. The easiest solution would be to do the following:

Register a rewrite on block adminhtml/catalog_category_tabs:

<blocks>
    ...
    <adminhtml>
        <rewrite>
            <catalog_category_tabs>Vendor_Module_Block_Adminhtml_Catalog_Category_Tabs</catalog_category_tabs>
        </rewrite>
    </adminhtml>
    ...

In this rewrite, copy the original _prepareLayout method, modifying only the section of code that adds the Category Products tab, so that it reads like this:

$this->addTab('products', array(
    'label'     => Mage::helper('catalog')->__('Category Products'),
    'url'       => $this->getUrl('*/*/grid', array('_current' => true)),
    'class'     => 'ajax',
));
  • Notice that we exchanged the contents property for a custom url and class property, effectively changing the behavior to load contents from an external source when the tab is selected.
  • Interestingly, there is already an endpoint for doing exactly what you need, at Mage_Adminhtml_Catalog_CategoryController::gridAction. I haven't researched this, but it may have been an abandoned implementation by the original developer that was intended to load the grid exactly as you are looking for today.
  • Sadly, we have to replace the entire method because there is no other way to avoid preloading of the product grid which, presumably, you believe to be causing your memory exhaustion problems.

Give it some thought, a try, and feedback. I'm sure it can be resolved by branching off from one of these suggestions above.

Related Topic