Magento Model Basics : The Complete Series

Creating and manipulating models is a fundamental part of Magento development, so here’s my all-in-one mini-series on the basics of setting up a new custom model for your Magento store. In this example I’m going to create a photograph model because I want to store photographs in my Magento admin. For the sake of simplicity I’m only going to use a text field for image paths rather than an upload function.

Namespace: Creare
Module: Photo

Part 1 – Creating Your Model

These are the initial steps that you need to follow when creating a model, and this is what I’ll cover in this post:

  1. Create Model constructor class
  2. Create Resource Model class
  3. Create Model Collection class
  4. Create Database table for model

etc/config.xml

To start with, within your <config> node we need to declare the module version so that later we can create install/upgrade scripts.

<modules>
        <Creare_Photo>
            <version>0.1.0</version>
        </Creare_Photo>
    </modules>

Next we declare our model name within the node, which in this case be ‘photo’. Inside this we put our model class name which is simply the directory path to your model folder. We also specify our resource model name. After this we use our newly defined resource model name inside a self-titled node where we declare its class (again the file path), and database table name (which we’ll look at later).

<models>
            <photo>
                <class>Creare_Photo_Model</class>
                <resourceModel>photo_resource</resourceModel>
            </photo>
            <photo_resource>
                <class>Creare_Photo_Model_Resource</class>
                <entities>
                    <photograph>
                        <table>photo_photograph</table>
                    </photograph>
                </entities>
            </photo_resource>
</models>

The final bit of configuration we need to do is again in our <global> node. When working with models, everything is pretty much scoped globally. We just need to make Magento aware if our install/upgrade scripts.

<resources>
            <photo_setup>
                <setup>
                    <module>Creare_Photo</module>
                    <class>Creare_Photo_Model_Resource_Setup</class>
                </setup>
            </photo_setup>
</resources>

Using the resources node, you can then call the inside node anything you like – just remember that you’ll need to use this name in your setup path later on (inside the sql directory).

Model/Photograph.php

This model initialisation class is as simple as it looks. From this point onwards we have a model type called ‘photo/photograph’. Always extends the Mage_Core_Model_Abstract class.

<?php

class Creare_Photo_Model_Photograph extends Mage_Core_Model_Abstract
{
    protected function _construct()
    {
        $this->_init('photo/photograph');
    }
}

Model/Resource/Photograph.php

We have now initialised our resource model, allowing our model to interact with the database. We simply pass in the model name and the database table primary key (which we haven’t created yet).

<?php

class Creare_Photo_Model_Resource_Photograph extends Mage_Core_Model_Resource_Db_Abstract
{
    protected function _construct()
    {
        $this->_init('photo/photograph', 'entity_id');
    }
}

Model/Resource/Photograph/Collection.php

We can now use our resource model to create the ability to grab a collection of our models, which we can do by simply initialising our collection class.

<?php

class Creare_Photo_Model_Resource_Photograph_Collection extends Mage_Core_Model_Resource_Db_Collection_Abstract
{
    protected function _construct() {
       $this->_init('photo/photograph');
    }
}

Almsot there! We now have our model setup and configuration pretty much done, but with one problem. Where’s the data? Well, remember the photo_setup config that we set earlier? We’re going to give it what it’s asking for by creating our install file, which will create the database table ‘creare_photo’.

Model/Resource/Setup.php

We start by creating a setup class so we can create an instance of the install object. We don’t need to change anything in the original core setup class so you can leave it blank.

<?php

class Creare_Photo_Model_Resource_Setup extends Mage_Core_Model_Resource_Setup
{
}

sql/photo_setup/install-0.1.0.php

Remember it’s good Magento practice to alias $this object to $installer. I’m gonna create a pretty basic table to demonstrate how it works.

<?php

$installer = $this;
$installer->startSetup();

$table = $installer->getConnection()->newTable($installer->getTable('photo/photograph'))
    ->addColumn('entity_id', Varien_Db_Ddl_Table::TYPE_INTEGER, null, array(
        'unsigned' => true,
        'nullable' => false,
        'primary' => true,
        'identity' => true,
        ), 'ID')
    ->addColumn('title', Varien_Db_Ddl_Table::TYPE_TEXT, null, array(
        'nullable' => false,
        ), 'Title')
	->addColumn('img_path', Varien_Db_Ddl_Table::TYPE_TEXT, null, array(
        'nullable' => true,
        ), 'Image Path')
    ->addColumn('timestamp', Varien_Db_Ddl_Table::TYPE_TIMESTAMP, null, array(
        'nullable' => false,
        ), 'Timestamp');
		
$installer->getConnection()->createTable($table);
$installer->endSetup();

Load a page in your admin or frontend to run the install script. Check your database for the table to ensure that it has worked. If not check the ‘core_resource’ table for your ‘photo_setup’ record. If it’s not there, check your versions match. Your install script needs to be equal or higher than your Magento module version to run.

We now have a model, but no data to make instances of this model. In part 2, I’ll be going through how to make this model editable in your Magento admin so that you can add new data to the collection. You’ll then be able to actually get some data when you instantiate your model in the following ways:

// Load model
Mage::getModel('photo/photograph');
// Load resource model
Mage::getResourceModel('photo/photograph');
// Load resource model collection
Mage::getResourceModel('photo/photograph_collection');

Part 2 – Preparing The Admin

Here’s part 2 which goes through the basics of making your model editable in the Magento administration. It involves the following steps:

  1. Updating our config to declare our new blocks, helpers, controllers
  2. Adding the controller path to your admin menu and setting ACL permissions
  3. Creating a helper class
  4. Creating an admin controller
  5. Specifying layout XML updates to show the grid on your controller action

etc/config.xml

We’re going to add a few lines to our config file within our config node. The first is to declare our controller path, which is using the adminhtml module and must be loaded before the adminhtml module.

<admin>
    <routers>
      <adminhtml>
        <args>
          <modules>
            <photo before="Mage_Adminhtml">Creare_Photo_Adminhtml</photo>
          </modules>
        </args>
      </adminhtml>
    </routers>
  </admin>

We need to declare a path to our layout updates XML file for adminhtml (which we haven’t created yet).

<adminhtml>
        <layout>
          <updates>
            <photo>
              <file>photo.xml</file>
            </photo>
          </updates>
        </layout>
    </adminhtml>

We also need to make our module aware that we’ll be using helpers and blocks from this point onwards.

<blocks>
	<photo>
		<class>Creare_Photo_Block</class>
	</photo>
</blocks>
<helpers>
	<photo>
		<class>Creare_Photo_Helper</class>
	</photo>
</helpers>

etc/adminhtml.xml

As with a standard adminhtml.xml file we’re just adding our menu item and then specifying ACL permissions so that admin users can see it, and other users must be specified.

<?xml version="1.0" encoding="UTF-8"?>
<config>
<menu>
  <photo translate="title" module="photo">
    <title>Photography</title>
    <sort_order>10</sort_order>
    <children>
      <photograph module="photo">
        <title>My Photographs</title>
        <action>adminhtml/photograph</action>
      </photograph>
    </children>
  </photo>
</menu>
<acl>
  <resources>
    <all>
      <title>Allow Everything</title>
    </all>
    <admin>
      <children>
        <photo>
          <children>
            <photograph>
              <title>My Photographs</title>
              <sort_order>10</sort_order>
            </photograph>
          </children>
        </photo>
      </children>
    </admin>
  </resources>
</acl>
</config>

Helper/Data.php

We create an empty helper class which inherits the core helper class allowing us to translate our text, if we decide to do so at a future date.

<?php

class Creare_Photo_Helper_Data extends Mage_Core_Helper_Abstract
{

}

controllers/Adminhtml/PhotographController.php

As it’s going in our Adminhtml module it goes into an Adminhtml directory within your controllers directory. The files itself is broken down into five actions: index, edit, new, save, delete. I’ll explain what’s roughly happening.

indexAction

Loads the contents of the index action which is a grid we’ll specify later.

newAction

Redirects to the edit action to be handled there.

editAction

Takes the passed ID parameter and tries to load an entity of your model, and pre-fill the fields with the existing data. If it fails it will treat the request as a new entity creation and the fields will be blank.

saveAction

Form data is posted here, an instance of your model is loaded based on the ID parameter. Form data is set on the model and then it is saved. If anything goes wrong in this process a series of exceptions may be thrown.

deleteAction

As you’d expect – model loaded by ID parameter, if found – deleted forever.

<?php

class Creare_Photo_Adminhtml_PhotographController extends Mage_Adminhtml_Controller_Action
{
    public function indexAction()
    {
        $this->_title($this->__('Photo'))->_title($this->__('Photographs'));
        $this->loadLayout();
	$this->_setActiveMenu('photo/photograph');
	$this->renderLayout();
    }

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

    public function editAction()
    {
        $id = $this->getRequest()->getParam('id', null);
        $model = Mage::getModel('photo/photograph');
        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('photo')->__('Photograph does not exist'));
                $this->_redirect('*/*/');
            }
        }
        Mage::register('photograph_data', $model);

 	$this->_title($this->__('Photo'))->_title($this->__('Edit Photograph'));
        $this->loadLayout();
        $this->getLayout()->getBlock('head')->setCanLoadExtJs(true);
        $this->renderLayout();
    }

    public function saveAction()
    {
        if ($data = $this->getRequest()->getPost())
        {
            $model = Mage::getModel('photo/photograph');
            $id = $this->getRequest()->getParam('id');

            foreach ($data as $key => $value)
            {
                if (is_array($value))
                {
                        $data[$key] = implode(',',$this->getRequest()->getParam($key));
                }
            }

            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('photo')->__('Error saving photograph'));
                }

                Mage::getSingleton('adminhtml/session')->addSuccess(Mage::helper('photo')->__('Photograph 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('photo')->__('No data found to save'));
        $this->_redirect('*/*/');
    }

    public function deleteAction()
    {
        if ($id = $this->getRequest()->getParam('id')) {
            try {
                $model = Mage::getModel('photo/photograph');
                $model->setId($id);
                $model->delete();
                Mage::getSingleton('adminhtml/session')->addSuccess(Mage::helper('photo')->__('The photograph 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 photograph to delete.'));
        $this->_redirect('*/*/');
    }
}

app/design/adminhtml/default/default/layout/photo.xml

We set two layout handles based on our controller/action path and set the content for those to either be the edit form or the admin grid container. We haven’t created these blocks yet.

<?xml version="1.0"?>
<layout>
   <adminhtml_photograph_index>
        <reference name="content">
            <block type="photo/adminhtml_photograph" name="photograph" />
        </reference>
    </adminhtml_photograph_index>
     <adminhtml_photograph_edit>
        <reference name="content">
            <block type="photo/adminhtml_photograph_edit" name="photograph_edit" />
        </reference>
    </adminhtml_photograph_edit>
</layout>

In part 3, I’ll show you how to build the blocks needed to create your grid and your edit forms so that you can actually start editing your model.

Part 3 – Building Your Grid

The final chapter in my ‘model basics’ series is all about making your model editable from the Magento admin. So far we’ve created our actual model, set up a model collection, created a database table for the model to use and we’ve created an admin controller to eventually access our grid from.

I’ll go through how we put our grid together. It involves 3 steps:

  1. Make a grid container to display on our controller
  2. Building the actual grid columns and content
  3. Creating a form container
  4. Creating an edit form for making new, and editing existing instances of your model

Block/Adminhtml/Photograph.php

The first block we create is the grid container – we know this because it extends the admin grid container. You may remember that we referenced this block as the content for our main controller in photo.xml in our last tutorial.

<?php

class Creare_Photo_Block_Adminhtml_Photograph extends Mage_Adminhtml_Block_Widget_Grid_Container
{
    public function __construct()
    {
        $this->_blockGroup = 'photo';
        $this->_controller = 'adminhtml_photograph';
        $this->_headerText = Mage::helper('photo')->__('Photographs');
        parent::__construct();
    }
}

The block group and controller are absolutely essential that are entered correctly, otherwise your entire grid will never show up. Despite the naming convention, $this->_controller does not refer to your controller path. It is actually the block path to your Grid.php file which we’ll make in a minute. $this->blockGroup  refers to your block group as set in config.xml.

If you check out the parent class, you’ll see how the two items are used to define a path:

$this->_blockGroup.'/' . $this->_controller . '_grid'

Which becomes: photo/adminhtml_photograph_grid

Sounds awfully familiar to a block type declaration in your layout XML eh?

Block/Adminhtml/Photograph/Grid.php

The actual grid lies in the exact path that we specify in our container class. It consists of just four methods. The initial construct builds some basic info to be used by the parent grid class. Then we create our collection to be used in the grid – how about we use our photograph model collection?

<?php

class Creare_Photo_Block_Adminhtml_Photograph_Grid extends Mage_Adminhtml_Block_Widget_Grid
{
    public function __construct()
    {
        parent::__construct();
        $this->setId('photograph_grid');
        $this->setDefaultSort('entity_id');
        $this->setDefaultDir('asc');
        $this->setSaveParametersInSession(true);
    }

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

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

        $this->addColumn('title', array(
            'header'    => Mage::helper('photo')->__('Title'),
            'align'     =>'left',
            'index'     => 'title',
        ));

        $this->addColumn('img_path', array(
            'header'    => Mage::helper('photo')->__('Image Path'),
            'align'     =>'left',
            'index'     => 'img_path',
        ));

        return parent::_prepareColumns();
    }

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

We then use the _prepareColumns() method to populate our grid with our model collection data. There are tons of tutorials out there which specify different column data types. Finally we create a link path for getRowUrl() should a row be clicked upon. We know that we want to be able to edit our model information so we’ll direct the user to our editAction on the current controller.

Block/Adminhtml/Photograph/Edit.php

Now we create a container for our edit form. You may remember that we specified this block in our photo.xml layout updates as the content for the edit action. As with the grid container, we need to specify the path to our content by setting a blockGroup, controller and mode at in our _construct method of this class. If you want you can add some JavaScript which allows the user to ‘Save And Continue Edit’.

<?php

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

        $this->_objectId = 'id';
        $this->_blockGroup = 'photo';
        $this->_controller = 'adminhtml_photograph';
        $this->_mode = 'edit';

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

        $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('photograph_data') && Mage::registry('photograph_data')->getId())
        {
            return Mage::helper('photo')->__('Edit Photograph "%s"', $this->htmlEscape(Mage::registry('photograph_data')->getTitle()));
        } else {
            return Mage::helper('photo')->__('New Photograph');
        }
    }
}

In the parent container class, as with the grid the block is created as a child of this container using the following code in _prepareLayout():

$this->_blockGroup . '/' . $this->_controller . '_' . $this->_mode . '_form'

Which becomes: photo/adminhtml_photograph_edit_form

The getHeaderText() method at the bottom of the class checks for an instance of our model. If none exists in the registry it will treat it as a new model, otherwise it will treat it as an existing one being edited. Simple eh?

Block/Adminhtml/Photograph/Edit/Form.php

It is possible to divide your form into tabs which makes your form layout a bit more user-friendly. I’ll go into that in a future tutorial, but in this case it’s all gonna be on the same page.

<?php

class Creare_Photo_Block_Adminhtml_Photograph_Edit_Form extends Mage_Adminhtml_Block_Widget_Form
{
    protected function _prepareForm()
    {
        if (Mage::registry('photograph_data'))
        {
            $data = Mage::registry('photograph_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('photograph_form', array(
             'legend' =>Mage::helper('photo')->__('Photograph Information')
        ));

        $fieldset->addField('title', 'text', array(
             'label'     => Mage::helper('photo')->__('Photograph Name'),
             'class'     => 'required-entry',
             'required'  => true,
             'name'      => 'title'
        ));

        $fieldset->addField('img_path', 'text', array(
             'label'     => Mage::helper('photo')->__('Image Path'),
             'class'     => 'required-entry',
             'required'  => true,
             'name'      => 'img_path'
        ));

        $form->setValues($data);

        return parent::_prepareForm();
    }
}

As before I have checked for a copy of my model in the registry – if one exists then the form is populated and it’s treated as an edit. Otherwise it’s treated as a new model. I’ve created one field-set and added a couple of fields to it.

You should now know the basics for creating a custom model. There’s so much more that you can do with models and different data types so have fun exploring the possibilities.