Wednesday, May 29, 2013

magento Grids and Forms in the Admin Panel example


In my humble opinion, creating new sections in the Admin Panel are a tad bit more complicated than creating new features on the frontend. Hopefully, this post will help get you a few steps closer to being able to understand and create adminhtml grids and forms.
The first thing you need to do is create a menu item to get to your new grid, then you begin the exciting journey into some Adminhtml action.
The Config
How about I just throw the whole stinkin' config.xml file out here right off the bat:
?

































































































<config>
    <modules>
        <Super_Awesome>
            <version>..</version>
        </Super_Awesome>
    </modules>
    <adminhtml>
        <!-- The <layout> updates allow us to define our block layouts in a seperate file so are aren't messin' with the magento layout files.  -->
        <layout>
            <updates>
                <awesome>
                    <file>awesome.xml</file>
                </awesome>
            </updates>
        </layout>
        <!-- The <acl> section is for access control. Here we define the pieces where access can be controlled within a role. -->
        <acl>
            <resources>
                <admin>
                    <children>
                        <awesome>
                            <title>Awesome Menu Item</title>
                            <children>
                                <example translate="title" module="awesome">
                                    <title>Example Menu Item</title>
                                </example>
                            </children>
                        </awesome>
                    </children>
                </admin>
            </resources>
        </acl>
    </adminhtml>
    <admin>
        <!--
            Here we are telling the Magento router to look for the controllers in the Super_Awesome_controllers_Adminhtml before we look in the
            Mage_Adminhtml module for all urls that begin with /admin/controller_name
         -->
        <routers>
            <adminhtml>
                <args>
                    <modules>
                        <awesome before="Mage_Adminhtml">Super_Awesome_Adminhtml</awesome>
                    </modules>
                </args>
            </adminhtml>
        </routers>
    </admin>

    <global>
        <models>
            <awesome>
                <class>Super_Awesome_Model</class>
                <resourceModel>awesome_mysql</resourceModel>
            </awesome>
             <awesome_mysql>
                <class>Super_Awesome_Model_Mysql</class>
                <entities>
                    <example>
                        <table>Super_Awesome_example</table>
                    </example>
                </entities>
            </awesome_mysql>
        </models>

        <resources>
            <awesome_setup>
                <setup>
                    <module>Super_Awesome</module>
                </setup>
                <connection>
                    <use>core_setup</use>
                </connection>
            </awesome_setup>
            <awesome_write>
                <connection>
                    <use>core_write</use>
                </connection>
            </awesome_write>
            <awesome_read>
                <connection>
                    <use>core_read</use>
                </connection>
            </awesome_read>
        </resources>

        <blocks>
            <awesome>
                <class>Super_Awesome_Block</class>
            </awesome>
        </blocks>
        <helpers>
            <awesome>
                <class>Super_Awesome_Helper</class>
            </awesome>
        </helpers>
    </global>
</config>
Some things of note about the config.xml:
  1. There is a layout file defined (awesome.xml)
  2. There is a change to the adminhtml router that tells the route to look in our module before looking into Mage_Adminhtml
  3. Everything else is pretty straight-forward (models, resource models, collections, setup, blocks, helpers...)
The Layout
Before I forget, here is the contents of the layout file (design/adminhtml/default/default/layout/awesome.xml):
?















<?xml version=“1.0”?>

<layout>
    <adminhtml_example_index>
        <reference name="content">
            <block type="awesome/adminhtml_example" name="example" />
        </reference>
    </adminhtml_example_index>

     <adminhtml_example_edit>
        <reference name="content">
            <block type="awesome/adminhtml_example_edit" name="example_edit" />
        </reference>
    </adminhtml_example_edit>

</layout>
We don't need to define much in here. The reason I think that Admin Panel coding is a little more complicated is because there is a lot of things that happen behind the scenes that are not driven by the layout as we normally see it.
Install Script
For testing/example purposes, I also created an install script to create a table and load up some data. In the sql/awesome_setup/
mysql4-install-0.1.0.php file, I have:

























<?php

$installer = $this;

$installer->startSetup();

$installer->run("

-- DROP TABLE IF EXISTS {$this->getTable('super_awesome_example')};
CREATE TABLE {$this->getTable('super_awesome_example')} (
  `id` int() unsigned NOT NULL auto_increment,
  `name` varchar() NOT NULL,
  `description` varchar() NOT NULL,
  `other` varchar() NOT NULL,
  PRIMARY KEY (`id`)
 ) ENGINE=InnoDB DEFAULT CHARSET=latin AUTO_INCREMENT= ;

INSERT INTO {$this->getTable('super_awesome_example')} (name, description, other) values ('Example ', 'Example One Description', 'This first example is reall awesome.');
INSERT INTO {$this->getTable('super_awesome_example')} (name, description, other) values ('Example ', 'Example Two Description', 'This second example is reall awesome.');
INSERT INTO {$this->getTable('super_awesome_example')} (name, description, other) values ('Example ', 'Example Three Description', 'This third example is reall awesome.');
INSERT INTO {$this->getTable('super_awesome_example')} (name, description, other) values ('Example ', 'Example Four Description', 'This fourth example is reall awesome.');

");

$installer->endSetup();
The Models
In this example, I am going to assume you know how to create a model, its resource model, and its collection model. I created the following classes:
Super_Awesome_Model_Example
Super_Awesome_Model_Mysql_Example
Super_Awesome_Model_Mysql_Example_Collection
The Controller
Hopefully, we all know what a controller does, so I won't explain that part of the MVC pattern. The adminhtml controllers generally provide actions to do basic CRUD operations on the model. In ours, you will find the following actions:
  • index - Shows the grid.
  • edit - Shows the edit/new form.
  • save - Saves the form data.
  • delete - Deletes the model.
  • new - Forwards on to the edit action
There really isn't anything crazy going on here, so I would just take a few minutes to read through the code and get an understanding of what each function does (and does not do):
?












































































































<?php

class Super_Awesome_Adminhtml_ExampleController extends Mage_Adminhtml_Controller_Action
{

    public function indexAction()
    {

        $this->loadLayout();
        $this->renderLayout();
    }

    public function newAction()
    {
        $this->_forward('edit');
    }

    public function editAction()
    {
        $id = $this->getRequest()->getParam('id', null);
        $model = Mage::getModel('awesome/example');
        if ($id) {
            $model->load((int) $id);
            if ($model->getId()) {
                $data = Mage::getSingleton('adminhtml/session')->getFormData(true);
                if ($data) {
                    $model->setData($data)->setId($id);
                }
            } else {
                Mage::getSingleton('adminhtml/session')->addError(Mage::helper('awesome')->__('Example does not exist'));
                $this->_redirect('*/*/');
            }
        }
        Mage::register('example_data', $model);

        $this->loadLayout();
        $this->getLayout()->getBlock('head')->setCanLoadExtJs(true);
        $this->renderLayout();
    }

    public function saveAction()
    {
        if ($data = $this->getRequest()->getPost())
        {
            $model = Mage::getModel('awesome/example');
            $id = $this->getRequest()->getParam('id');
            if ($id) {
                $model->load($id);
            }
            $model->setData($data);

            Mage::getSingleton('adminhtml/session')->setFormData($data);
            try {
                if ($id) {
                    $model->setId($id);
                }
                $model->save();

                if (!$model->getId()) {
                    Mage::throwException(Mage::helper('awesome')->__('Error saving example'));
                }

                Mage::getSingleton('adminhtml/session')->addSuccess(
Mage::helper('awesome')->__('Example was successfully saved.'));
                Mage::getSingleton('adminhtml/session')->setFormData(false);

                // The following line decides if it is a "save" or "save and 
 continue"
                if ($this->getRequest()->getParam('back')) {
                    $this->_redirect('*/*/edit', array('id' => $model->getId()));
                } else {
                    $this->_redirect('*/*/');
                }

            } catch (Exception $e) {
                Mage::getSingleton('adminhtml/session')->addError($e->getMessage());
                if ($model && $model->getId()) {
                    $this->_redirect('*/*/edit', array('id' => $model->getId()));
                } else {
                    $this->_redirect('*/*/');
                }
            }

            return;
        }
        Mage::getSingleton('adminhtml/session')->addError(Mage::helper('awesome')->__('No data found to save'));
        $this->_redirect('*/*/');
    }

    public function deleteAction()
    {
        if ($id = $this->getRequest()->getParam('id')) {
            try {
                $model = Mage::getModel('awesome/example');
                $model->setId($id);
                $model->delete();
                Mage::getSingleton('adminhtml/session')->addSuccess(
Mage::helper('awesome')->__('The example has been deleted.'));
                $this->_redirect('*/*/');
                return;
            }
            catch (Exception $e) {
                Mage::getSingleton('adminhtml/session')->addError($e->getMessage());
                $this->_redirect('*/*/edit', array('id' => $this->getRequest()->getParam('id')));
                return;
            }
        }
        Mage::getSingleton('adminhtml/session')->addError(Mage::helper('adminhtml')->__('Unable to find the example to delete.'));
        $this->_redirect('*/*/');
    }

}
The Grid Block
Here is where it starts getting a touch tricky. When you originally click on the menu item to see the grid of examples, you are going to the "indexAction" in the controller and simply loading and rendering the layout. This means that you will probably have something halfway useful to see in the awesome.xml layout file. We see that there is only one block defined for that handle, and that block is: 'awesome/adminhtml_example'. This block extends Mage_Adminhtml_Block_Widget_Grid_Container which which tells us that our block (Super_Awesome_Block_Adminhtml_Example) will be be a container for a grid. What does that mean? This container will provide a few buttons at the top and automagically define the grid as a child block of itself. Below I will show you the entire contents of our container, and the piece that builds the name of the grid block (which is in the parent Mage_Adminhtml_Block_Widget_Grid_Container.
Super_Awesome_Block_Adminhtml_Example:
?













<?php

class Super_Awesome_Block_Adminhtml_Example extends Mage_Adminhtml_Block_Widget_Grid_Container
{
    protected $_addButtonLabel = 'Add New Example';

    public function __construct()
    {
        parent::__construct();
        $this->_controller = 'adminhtml_example';
        $this->_blockGroup = 'awesome';
        $this->_headerText = Mage::helper('awesome')->__('Examples');
    }
}
?






protected function _prepareLayout()
   {
       $this->setChild( 'grid',
           $this->getLayout()->createBlock( $this->_blockGroup.'/' . $this->_controller . '_grid',
           $this->_controller . '.grid')->setSaveParametersInSession(true) );
       return parent::_prepareLayout();
   }
Now that we have the container we need to build our grid (Super_Awesome_Block_Adminhtml_Example_Grid):
?






















































<?php

class Super_Awesome_Block_Adminhtml_Example_Grid extends Mage_Adminhtml_Block_Widget_Grid
{
    public function __construct()
    {
        parent::__construct();
        $this->setId('example_grid');
        $this->setDefaultSort('id');
        $this->setDefaultDir('desc');
        $this->setSaveParametersInSession(true);
    }

    protected function _prepareCollection()
    {
        $collection = Mage::getModel('awesome/example')->getCollection();
        $this->setCollection($collection);
        return parent::_prepareCollection();
    }

    protected function _prepareColumns()
    {
        $this->addColumn('id', array(
            'header'    => Mage::helper('awesome')->__('ID'),
            'align'     =>'right',
            'width'     => 'px',
            'index'     => 'id',
        ));

        $this->addColumn('name', array(
            'header'    => Mage::helper('awesome')->__('Name'),
            'align'     =>'left',
            'index'     => 'name',
        ));

        $this->addColumn('description', array(
            'header'    => Mage::helper('awesome')->__('Description'),
            'align'     =>'left',
            'index'     => 'description',
        ));

        $this->addColumn('other', array(
            'header'    => Mage::helper('awesome')->__('Other'),
            'align'     => 'left',
            'index'     => 'other',
        ));

        return parent::_prepareColumns();
    }

    public function getRowUrl($row)
    {
        return $this->getUrl('*/*/edit', array('id' => $row->getId()));
    }
}
The _prepareCollection() function gets the collection of data that will populate our grid, and the _prepareColumns() function maps that data into the specific columns. Keep in mind that the _prepareCollection() and the _prepareColumns() can be much more detailed/complicated than my example here, so don't be afraid to try crazy things.
If you stop here, you should have a working grid.
The Forms
If you notice, the getRowUrl() on the grid returns back a url that maps to the editAction() in our controller. That is where were start the "form fun". The editAction() handles both the "edit" scenario and the "new" scenario for the model. It makes no difference to us since it is the same form for both.
The edit action maps to a handle in the awesome.xml layout file which simply defines the block: awesome/adminhtml_example_edit. If we take a look at that block we will see the following code:
?













































<?php

class Super_Awesome_Block_Adminhtml_Example_Edit extends Mage_Adminhtml_Block_Widget_Form_Container
{
    public function __construct()
    {
        parent::__construct();

        $this->_objectId = 'id';
        $this->_blockGroup = 'awesome';
        $this->_controller = 'adminhtml_example';
        $this->_mode = 'edit';

        $this->_addButton('save_and_continue', array(
                  'label' => Mage::helper('adminhtml')->__('Save And Continue Edit'),
                  'onclick' => 'saveAndContinueEdit()',
                  'class' => 'save',
        ), -);
        $this->_updateButton('save', 'label', Mage::helper('awesome')->__('Save Example'));

        $this->_formScripts[] = "
            function toggleEditor() {
                if (tinyMCE.getInstanceById('form_content') == null) {
                    tinyMCE.execCommand('mceAddControl', false, 'edit_form');
                } else {
                    tinyMCE.execCommand('mceRemoveControl', false, 'edit_form');
                }
            }

            function saveAndContinueEdit(){
                editForm.submit($('edit_form').action+'back/edit/');
            }
        ";
    }

    public function getHeaderText()
    {
        if (Mage::registry('example_data') && Mage::registry('example_data')->getId())
        {
            return Mage::helper('awesome')->__('Edit Example "%s"', $this->htmlEscape(Mage::registry('example_data')->getName()));
        } else {
            return Mage::helper('awesome')->__('New Example');
        }
    }

}
Just like the grid had a container, so does the form. We are just changing some labels on buttons and creating some javascript to handle the save scenarios. Below is the snippet of code in the parent container that builds the name of the block that will be rendered containing the actual form:
?






protected function _prepareLayout()
    {
        if ($this->_blockGroup && $this->_controller && $this->_mode) {
            $this->setChild('form', $this->getLayout()->createBlock($this->_blockGroup . '/' . $this->_controller . '_' . $this->_mode . '_form'));
        }
        return parent::_prepareLayout();
    }
Next/finally, we look at the actual form class; It's so awesome, you might faint when you see it:
?


























































<?php

class Super_Awesome_Block_Adminhtml_Example_Edit_Form extends Mage_Adminhtml_Block_Widget_Form
{
    protected function _prepareForm()
    {
        if (Mage::getSingleton('adminhtml/session')->getExampleData())
        {
            $data = Mage::getSingleton('adminhtml/session')->getExamplelData();
            Mage::getSingleton('adminhtml/session')->getExampleData(null);
        }
        elseif (Mage::registry('example_data'))
        {
            $data = Mage::registry('example_data')->getData();
        }
        else
        {
            $data = array();
        }

        $form = new Varien_Data_Form(array(
                'id' => 'edit_form',
                'action' => $this->getUrl('*/*/save', array('id' => $this->getRequest()->getParam('id'))),
                'method' => 'post',
                'enctype' => 'multipart/form-data',
        ));

        $form->setUseContainer(true);

        $this->setForm($form);

        $fieldset = $form->addFieldset('example_form', array(
             'legend' =>Mage::helper('awesome')->__('Example Information')
        ));

        $fieldset->addField('name', 'text', array(
             'label'     => Mage::helper('awesome')->__('Name'),
             'class'     => 'required-entry',
             'required'  => true,
             'name'      => 'name',
             'note'     => Mage::helper('awesome')->__('The name of the example.'),
        ));

        $fieldset->addField('description', 'text', array(
             'label'     => Mage::helper('awesome')->__('Description'),
             'class'     => 'required-entry',
             'required'  => true,
             'name'      => 'description',
        ));

        $fieldset->addField('other', 'text', array(
             'label'     => Mage::helper('awesome')->__('Other'),
             'class'     => 'required-entry',
             'required'  => true,
             'name'      => 'other',
        ));

        $form->setValues($data);

        return parent::_prepareForm();
    }
}
Did you faint?
The only thing not straight-forward here is the line: $form->setUseContainer(true);. This line is important because it is the line that actually causes the form renderer to output the surrounding <form> tags.