How To Create An Admin-Manageable Magento Entity For Brands

Advertisement

In this tutorial, we will create a new “brand” entity in Magento that can be managed through the admin panel. Once we are finished, you will be able to create, update and delete brands that can be viewed in the front-end independently, much in the same way that you can interact with existing entities such as “products” and “categories.”

In addition, we will associate our new brand entity with other Magento entities, such as products. The process itself is quite lengthy because I will explain each step in detail, but it really is easy once you know how, and it’s a great example of how powerful the Magento platform can be with minimal effort.

We will cover the following topics:

  • Create a new brand entity in Magento.
  • Browse, filter and sort brands in Magento’s admin panel.
  • Create, update and delete brands in the admin panel.
  • Create a brand list, and view pages in the front-end.
  • Associate brands with products.
  • Display a product-brand association in the front-end.
  • Include images throughout.

This tutorial refers to the Magento Community Edition 1.8.0.0, the latest version available at the time of writing. But the code and principles apply to all recent Magento versions, both Community and Enterprise.

Magento Brand Admin Grid

While I have tried to be as thorough as possible in my explanations, documenting the code is sometimes the best way to explain certain things, so you will find a lot of comments in the code snippets. The code snippet linked to at the end contains more usages and items than are covered in this tutorial, to show you how to extend the things that you will learn about here.

Creating Our Module

Start by creating the following folder and file structure. For the time being, all files will be empty. Normally, these files and folders would be created one at a time as needed, but for the sake of this tutorial, we will put everything in place up front.

As usual, I assume you already know the basics of creating a Magento module1 and are comfortable with this step, with no explanation required.

app
  - code
      - community
          - SmashingMagazine
              - BrandDirectory
                  - Block
                      - Adminhtml
                          - Brand
                              - Edit
                                  - Form.php
                              - Edit.php
                              - Grid.php
                          - Brand.php
                  - Helper
                      - Data.php
                  - Model
                      - Resource
                          - Brand
                              - Collection.php
                          - Brand.php
                      - Brand.php
                  - controllers
                      - Adminhtml
                          - BrandController.php
                  - etc
                      - adminhtml.xml
                      - config.xml
                  - sql
                      - smashingmagazine_branddirectory_setup
                          - install-0.0.1.php
  - etc
      - modules
          - SmashingMagazine_BrandDirectory.xml

Module Setup Scripts

For our module to be of any use, we need somewhere to store our data. Therefore, we need to write a module setup script to create a database table to hold information about our brand entity. Setup scripts are stored in two places within the module, /sql and /data. The former is for physical changes to your Magento instance, such as database schema updates, and the latter is for populating or removing entries from the existing schema.

In our case, we are adding a new table. So, edit the file at sql/smashingmagazine_branddirectory_setup/install-0.0.1.php with the following code:

<?php
$this->startSetup();

/**
 * Note: there are many ways in Magento to achieve the same result of
 * creating a database table. For this tutorial, we have gone with the
 * Varien_Db_Ddl_Table method, but feel free to explore what Magento
 * does in CE 1.8.0.0 and earlier versions.
 */
$table = new Varien_Db_Ddl_Table();

/**
 * This is an alias of the real name of our database table, which is
 * configured in config.xml. By using an alias, we can refer to the same
 * table throughout our code if we wish, and if the table name ever has
 * to change, we can simply update a single location, config.xml
 * - smashingmagazine_branddirectory is the model alias
 * - brand is the table reference
 */
$table->setName($this->getTable('smashingmagazine_branddirectory/brand'));

/**
 * Add the columns we need for now. If you need more later, you can
 * always create a new setup script as an upgrade. We will introduce
 * that later in this tutorial.
 */
$table->addColumn(
    'entity_id',
    Varien_Db_Ddl_Table::TYPE_INTEGER,
    10,
    array(
        'auto_increment' => true,
        'unsigned' => true,
        'nullable'=> false,
        'primary' => true
    )
);
$table->addColumn(
    'created_at',
    Varien_Db_Ddl_Table::TYPE_DATETIME,
    null,
    array(
        'nullable' => false,
    )
);
$table->addColumn(
    'updated_at',
    Varien_Db_Ddl_Table::TYPE_DATETIME,
    null,
    array(
        'nullable' => false,
    )
);
$table->addColumn(
    'name',
    Varien_Db_Ddl_Table::TYPE_VARCHAR,
    255,
    array(
        'nullable' => false,
    )
);
$table->addColumn(
    'url_key',
    Varien_Db_Ddl_Table::TYPE_VARCHAR,
    255,
    array(
        'nullable' => false,
    )
);
$table->addColumn(
    'description',
    Varien_Db_Ddl_Table::TYPE_TEXT,
    null,
    array(
        'nullable' => false,
    )
);
$table->addColumn(
    'visibility',
    Varien_Db_Ddl_Table::TYPE_BOOLEAN,
    null,
    array(
        'nullable' => false,
    )
);

/**
 * These two important lines are often missed.
 */
$table->setOption('type', 'InnoDB');
$table->setOption('charset', 'utf8');

/**
 * Create the table!
 */
$this->getConnection()->createTable($table);

$this->endSetup();

Initializing Our Module

At this point, we still do not have an active Magento module; we just have a number of folders and empty files and an installation setup script that does not do anything yet. This is on purpose. In the next steps, we will populate the app/etc/modules XML file and configure config.xml so that Magento knows where to look for our setup script, after which the next visit to the website will trigger the content of our setup script to run.

If we had performed these tasks the other way round (that is, configured the module first and then populated the setup script), then there is a chance Magento would think that our module was on version 0.0.1 and that our setup script was still empty and, as a result, that the script would effectively have to be skipped. So, to limit face-palm moments for you guys, I’ve tried to keep the order of steps as safe as possible.

Configuring Our Module

Edit the file at app/etc/modules/SmashingMagazine_BrandDirectory.xml, and add the following to enable our module:

<?xml version="1.0"?>
<config>
    <modules>
        <SmashingMagazine_BrandDirectory>
            <active>true</active>
            <codePool>community</codePool>
        </SmashingMagazine_BrandDirectory>
    </modules>
</config>

You will notice we are using the community codepool. This BrandDirectory module will not contain any client-specific code or customizations. Instead, it will contain the building blocks for our new entity, which can be used in other modules, depending on the use case. Therefore, this community module may be dropped into any Magento instance and used as is, without any code needing to be changed. If code changes were required for each use, then this would be more suitable to the local code pool.

Now, we tell Magento that we have a module with a version number, which in fact will determine which setup scripts are run and where to find the setup scripts. Edit etc/config.xml with the following:

<?xml version="1.0"?>
<config>
    <modules>
        <SmashingMagazine_BrandDirectory>
            <!--
            This is the version number that our module is currently at. In
            order for setup scripts to run, their version number must be less
            than or equal to this value.

            As we add upgrade scripts, we increment this value. The next time
            your Magento instance is accessed, Magento will compare values in
            the DB table 'core_resource' against this value. If the DB is
            lower, it will attempt to run any setup scripts for the module
            and then update the database table to match this value.
            -->
            <version>0.0.1</version>
        </SmashingMagazine_BrandDirectory>
    </modules>
    <global>
        <models>

            <!--
            This is the Model alias referred to in install-0.0.1.php.
            -->
            <smashingmagazine_branddirectory>

                <!--
                This tells Magento where to find
                resource materials for this module.
                -->
      <resourceModel>smashingmagazine_branddirectory_resource</resourceModel>

            </smashingmagazine_branddirectory>

            <!--
            This alias must match the above <resourceModel/> value.
            -->
            <smashingmagazine_branddirectory_resource>

                <entities>

                    <!--
                    This is the table alias referred to in install-0.0.1.php.
                    -->
                    <brand>

                        <!--
                        This is the actual name of the database table.
                        -->
                        <table>smashingmagazine_branddirectory_brand</table>

                    </brand>

                </entities>

            </smashingmagazine_branddirectory_resource>

        </models>

        <resources>

            <!--
            This must match our folder name in the module sql folder.
            -->
            <smashingmagazine_branddirectory_setup>

                <setup>

                    <!--
                    This defines which module the setup
                    scripts in this location belong to.
                    -->
                    <module>SmashingMagazine_BrandDirectory</module>

                    <!--
                    In each setup script, this
                    value determines the class of $this.
                    -->
                    <class>Mage_Core_Model_Resource_Setup</class>

                </setup>

                <!--
                This is only relevant if you have
        multiple database connections.
                -->
                <connection>
                    <use>core_setup</use>
                </connection>

            </smashingmagazine_branddirectory_setup>

        </resources>

    </global>
</config>

Is Everything Working So Far?

Go ahead and access any page of your website — the home page will do. Magento will find that it has a new module at version 0.0.1, yet it has no record of this module in the core_resource database table. This missing entry will trigger Magento to look for an installation setup script and execute its contents.

If All Goes Well…

If all goes well, then it will seem like nothing has happened. The Magento page might take a few moments longer to load while the contents of our setup script is run (i.e. while the new database table is created), and then the page will continue to load as normal. You now have two tasks to perform to check that everything has gone as expected:

  1. Ensure that Magento knows about your module and that the module is enabled by going to System → Configuration → Advanced → Advanced.
  2. Access your database via either the terminal or something like PHPMyAdmin to see whether Magento has created a new table, smashingmagazine_branddirectory_brand.

If All Does Not Go Well…

If all does not go well, then it might also seem like nothing has happened, only this time nothing really has happened! The reason for this could be typos in config.xml, badly named folders or files (take care with case-sensitivity) or something else. Go through the previous steps and double-check that everything is as it should be.

On the other hand, you might see an error message — perhaps a “PHP Fatal Error” or a report page, depending on the severity of the error. Use your debugging skills to identify the problem and correct it, again double-checking all previous steps in this tutorial.

“It Went Wrong. How Do I Try Again?”

To try again from scratch, you can perform the following actions — not all may be required, depending on how far Magento got before things went wrong. You will need to access your database directly because this cannot be performed through Magento:

  1. In the core_resource table, delete the single row smashingmagazine_branddirectory_setup.
  2. Delete the smashingmagazine_branddirectory_brand table.

Creating Our Helper

We don’t actually need to define any custom functionality in a helper for this tutorial. But we will be adding menu items to the admin panel that use a helper for translation purposes, so we can simply create one in Helper/Data.php and forget about it.

<?php
class SmashingMagazine_BrandDirectory_Helper_Data
    extends Mage_Core_Helper_Abstract
{

}

Creating Our Models

Next, we need to create models and resource models so that we can persist brand data to the database upon creating and saving, display information about our brands in the Magento admin panel grids, and display our brands to the customer in the front-end.

Brand Model

We need to define a model to enable developers to interact with our brand entities. I won’t go into any more detail than that because people smarter than me have no doubt already explained what a model does, so feel free to search out some articles. For now, I will stick to getting our model to do the job we need in order to continue our tutorial. So, edit Model/Brand.php with this:

<?php
class SmashingMagazine_BrandDirectory_Model_Brand
    extends Mage_Core_Model_Abstract
{
    const VISIBILITY_HIDDEN = '0';
    const VISIBILITY_DIRECTORY = '1';

    protected function _construct()
    {
        /**
         * This tells Magento where the related resource model can be found.
         *
         * For a resource model, Magento will use the standard model alias -
         * in this case 'smashingmagazine_branddirectory' - and look in
         * config.xml for a child node <resourceModel/>. This will be the
         * location that Magento will look for a model when
         * Mage::getResourceModel() is called - in our case,
         * SmashingMagazine_BrandDirectory_Model_Resource.
         */
        $this->_init('smashingmagazine_branddirectory/brand');
    }

    /**
     * This method is used in the grid and form for populating the dropdown.
     */
    public function getAvailableVisibilies()
    {
        return array(
            self::VISIBILITY_HIDDEN
                => Mage::helper('smashingmagazine_branddirectory')
                       ->__('Hidden'),
            self::VISIBILITY_DIRECTORY
                => Mage::helper('smashingmagazine_branddirectory')
                       ->__('Visible in Directory'),
        );
    }

    protected function _beforeSave()
    {
        parent::_beforeSave();

        /**
         * Perform some actions just before a brand is saved.
         */
        $this->_updateTimestamps();
        $this->_prepareUrlKey();

        return $this;
    }

    protected function _updateTimestamps()
    {
        $timestamp = now();

        /**
         * Set the last updated timestamp.
         */
        $this->setUpdatedAt($timestamp);

        /**
         * If we have a brand new object, set the created timestamp.
         */
        if ($this->isObjectNew()) {
            $this->setCreatedAt($timestamp);
        }

        return $this;
    }

    protected function _prepareUrlKey()
    {
        /**
         * In this method, you might consider ensuring
         * that the URL Key entered is unique and
         * contains only alphanumeric characters.
         */

        return $this;
    }
}

Brand Resource Model

As in my introduction to the model above, I won’t go into any more detail than to say that the job of the resource model is to persist and retrieve data from the database. So edit Model/Resource/Brand.php with this:

<?php
class SmashingMagazine_BrandDirectory_Model_Resource_Brand
    extends Mage_Core_Model_Resource_Db_Abstract
{
    protected function _construct()
    {
        /**
         * Tell Magento the database name and primary key field to persist
         * data to. Similar to the _construct() of our model, Magento finds
         * this data from config.xml by finding the <resourceModel/> node
         * and locating children of <entities/>.
         *
         * In this example:
         * - smashingmagazine_branddirectory is the model alias
         * - brand is the entity referenced in config.xml
         * - entity_id is the name of the primary key column
         *
         * As a result, Magento will write data to the table
         * 'smashingmagazine_branddirectory_brand' and any calls
         * to $model->getId() will retrieve the data from the
         * column named 'entity_id'.
         */
        $this->_init('smashingmagazine_branddirectory/brand', 'entity_id');
    }
}

Brand Resource Collection

Finally, we need a resource collection to allow iteration through our brands for things like admin panel grids and front-end listing pages. Edit Model/Resource/Brand/Collection.php with this:

<?php
class SmashingMagazine_BrandDirectory_Model_Resource_Brand_Collection
    extends Mage_Core_Model_Resource_Db_Collection_Abstract
{
    protected function _construct()
    {
        parent::_construct();

        /**
         * Tell Magento the model and resource model to use for
         * this collection. Because both aliases are the same,
         * we can omit the second paramater if we wish.
         */
        $this->_init(
            'smashingmagazine_branddirectory/brand',
            'smashingmagazine_branddirectory/brand'
        );
    }
}

Creating Our Admin Blocks

Most of the grunt work is now done. A database is ready to be populated, and models and resource models are ready to populate them. We just need to create the interface to do so. We’ll start by creating and configuring admin blocks to display our brands as grids in the admin panel and allowing them to be created and updated.

Grid Container Block

The job of the grid container is to house the individual rows of brand entries to be displayed in Magento’s admin panel. The grid container is like a wrapper and includes the buttons in the top right (for example, “Add”). Edit Block/Adminhtml/Brand.php with this:

<?php
class SmashingMagazine_BrandDirectory_Block_Adminhtml_Brand
    extends Mage_Adminhtml_Block_Widget_Grid_Container
{
    protected function _construct()
    {
        parent::_construct();

        /**
         * The $_blockGroup property tells Magento which alias to use to
         * locate the blocks to be displayed in this grid container.
         * In our example, this corresponds to BrandDirectory/Block/Adminhtml.
         */
        $this->_blockGroup = 'smashingmagazine_branddirectory_adminhtml';

        /**
         * $_controller is a slightly confusing name for this property.
         * This value, in fact, refers to the folder containing our
         * Grid.php and Edit.php - in our example,
         * BrandDirectory/Block/Adminhtml/Brand. So, we'll use 'brand'.
         */
        $this->_controller = 'brand';

        /**
         * The title of the page in the admin panel.
         */
        $this->_headerText = Mage::helper('smashingmagazine_branddirectory')
            ->__('Brand Directory');
    }

    public function getCreateUrl()
    {
        /**
         * When the "Add" button is clicked, this is where the user should
         * be redirected to - in our example, the method editAction of
         * BrandController.php in BrandDirectory module.
         */
        return $this->getUrl(
            'smashingmagazine_branddirectory_admin/brand/edit'
        );
    }
}

Grid Block

When rendering the grid, Magento will expect to find a grid block in the _controller location defined in the grid container above, so we will create this now. Here, we can define which fields to retrieve from the database and show in the admin panel grid, and Magento will automatically allow these columns to be searched and filtered. Edit Block/Adminhtml/Brand/Grid.php with this:

<?php
class SmashingMagazine_BrandDirectory_Block_Adminhtml_Brand_Grid
    extends Mage_Adminhtml_Block_Widget_Grid
{
    protected function _prepareCollection()
    {
        /**
         * Tell Magento which collection to use to display in the grid.
         */
        $collection = Mage::getResourceModel(
            'smashingmagazine_branddirectory/brand_collection'
        );
        $this->setCollection($collection);

        return parent::_prepareCollection();
    }

    public function getRowUrl($row)
    {
        /**
         * When a grid row is clicked, this is where the user should
         * be redirected to - in our example, the method editAction of
         * BrandController.php in BrandDirectory module.
         */
        return $this->getUrl(
            'smashingmagazine_branddirectory_admin/brand/edit',
            array(
                'id' => $row->getId()
            )
        );
    }

    protected function _prepareColumns()
    {
        /**
         * Here, we'll define which columns to display in the grid.
         */
        $this->addColumn('entity_id', array(
            'header' => $this->_getHelper()->__('ID'),
            'type' => 'number',
            'index' => 'entity_id',
        ));

        $this->addColumn('created_at', array(
            'header' => $this->_getHelper()->__('Created'),
            'type' => 'datetime',
            'index' => 'created_at',
        ));

        $this->addColumn('updated_at', array(
            'header' => $this->_getHelper()->__('Updated'),
            'type' => 'datetime',
            'index' => 'updated_at',
        ));

        $this->addColumn('name', array(
            'header' => $this->_getHelper()->__('Name'),
            'type' => 'text',
            'index' => 'name',
        ));

        $this->addColumn('lastname', array(
            'header' => $this->_getHelper()->__('Url Key'),
            'type' => 'text',
            'index' => 'url_key',
        ));

        $brandSingleton = Mage::getSingleton(
            'smashingmagazine_branddirectory/brand'
        );
        $this->addColumn('visibility', array(
            'header' => $this->_getHelper()->__('Visibility'),
            'type' => 'options',
            'index' => 'visibility',
            'options' => $brandSingleton->getAvailableVisibilies()
        ));

        /**
         * Finally, we'll add an action column with an edit link.
         */
        $this->addColumn('action', array(
            'header' => $this->_getHelper()->__('Action'),
            'width' => '50px',
            'type' => 'action',
            'actions' => array(
                array(
                    'caption' => $this->_getHelper()->__('Edit'),
                    'url' => array(
                        'base' => 'smashingmagazine_branddirectory_admin'
                                  . '/brand/edit',
                    ),
                    'field' => 'id'
                ),
            ),
            'filter' => false,
            'sortable' => false,
            'index' => 'entity_id',
        ));

        return parent::_prepareColumns();
    }

    protected function _getHelper()
    {
        return Mage::helper('smashingmagazine_branddirectory');
    }
}

Form Container Block

The form container block has a function similar to that of the grid container but is used to create or edit an entity. Edit Block/Adminhtml/Brand/Edit.php with this:

<?php
class SmashingMagazine_BrandDirectory_Block_Adminhtml_Brand_Edit
    extends Mage_Adminhtml_Block_Widget_Form_Container
{
    protected function _construct()
    {
        $this->_blockGroup = 'smashingmagazine_branddirectory_adminhtml';
        $this->_controller = 'brand';

        /**
         * The $_mode property tells Magento which folder to use
         * to locate the related form blocks to be displayed in
         * this form container. In our example, this corresponds
         * to BrandDirectory/Block/Adminhtml/Brand/Edit/.
         */
        $this->_mode = 'edit';

        $newOrEdit = $this->getRequest()->getParam('id')
            ? $this->__('Edit')
            : $this->__('New');
        $this->_headerText =  $newOrEdit . ' ' . $this->__('Brand');
    }
}

Form Block

In the form block, we define which fields may be managed when creating or editing an entity. Edit Block/Adminhtml/Brand/Edit/Form.php with this:

<?php
class SmashingMagazine_BrandDirectory_Block_Adminhtml_Brand_Edit_Form
    extends Mage_Adminhtml_Block_Widget_Form
{
    protected function _prepareForm()
    {
        // Instantiate a new form to display our brand for editing.
        $form = new Varien_Data_Form(array(
            'id' => 'edit_form',
            'action' => $this->getUrl(
                'smashingmagazine_branddirectory_admin/brand/edit',
                array(
                    '_current' => true,
                    'continue' => 0,
                )
            ),
            'method' => 'post',
        ));
        $form->setUseContainer(true);
        $this->setForm($form);

        // Define a new fieldset. We need only one for our simple entity.
        $fieldset = $form->addFieldset(
            'general',
            array(
                'legend' => $this->__('Brand Details')
            )
        );

        $brandSingleton = Mage::getSingleton(
            'smashingmagazine_branddirectory/brand'
        );

        // Add the fields that we want to be editable.
        $this->_addFieldsToFieldset($fieldset, array(
            'name' => array(
                'label' => $this->__('Name'),
                'input' => 'text',
                'required' => true,
            ),
            'url_key' => array(
                'label' => $this->__('URL Key'),
                'input' => 'text',
                'required' => true,
            ),
            'description' => array(
                'label' => $this->__('Description'),
                'input' => 'textarea',
                'required' => true,
            ),
            'visibility' => array(
                'label' => $this->__('Visibility'),
                'input' => 'select',
                'required' => true,
                'options' => $brandSingleton->getAvailableVisibilies(),
            ),

            /**
             * Note: we have not included created_at or updated_at.
             * We will handle those fields ourself in the model
       * before saving.
             */
        ));

        return $this;
    }

    /**
     * This method makes life a little easier for us by pre-populating
     * fields with $_POST data where applicable and wrapping our post data
     * in 'brandData' so that we can easily separate all relevant information
     * in the controller. You could of course omit this method entirely
     * and call the $fieldset->addField() method directly.
     */
    protected function _addFieldsToFieldset(
        Varien_Data_Form_Element_Fieldset $fieldset, $fields)
    {
        $requestData = new Varien_Object($this->getRequest()
            ->getPost('brandData'));

        foreach ($fields as $name => $_data) {
            if ($requestValue = $requestData->getData($name)) {
                $_data['value'] = $requestValue;
            }

            // Wrap all fields with brandData group.
            $_data['name'] = "brandData[$name]";

            // Generally, label and title are always the same.
            $_data['title'] = $_data['label'];

            // If no new value exists, use the existing brand data.
            if (!array_key_exists('value', $_data)) {
                $_data['value'] = $this->_getBrand()->getData($name);
            }

            // Finally, call vanilla functionality to add field.
            $fieldset->addField($name, $_data['input'], $_data);
        }

        return $this;
    }

    /**
     * Retrieve the existing brand for pre-populating the form fields.
     * For a new brand entry, this will return an empty brand object.
     */
    protected function _getBrand()
    {
        if (!$this->hasData('brand')) {
            // This will have been set in the controller.
            $brand = Mage::registry('current_brand');

            // Just in case the controller does not register the brand.
            if (!$brand instanceof
                    SmashingMagazine_BrandDirectory_Model_Brand) {
                $brand = Mage::getModel(
                    'smashingmagazine_branddirectory/brand'
                );
            }

            $this->setData('brand', $brand);
        }

        return $this->getData('brand');
    }
}

Magento Brand Admin Form

Creating Our Admin Controller

Now, we need a controller to accept requests and render the container blocks from above. The controller will also handle POST requests to create, update and delete brands as required. Edit controllers/Adminhtml/BrandController.php with this:

<?php
class SmashingMagazine_BrandDirectory_Adminhtml_BrandController
    extends Mage_Adminhtml_Controller_Action
{
    /**
     * Instantiate our grid container block and add to the page content.
     * When accessing this admin index page, we will see a grid of all
     * brands currently available in our Magento instance, along with
     * a button to add a new one if we wish.
     */
    public function indexAction()
    {
        // instantiate the grid container
        $brandBlock = $this->getLayout()
            ->createBlock('smashingmagazine_branddirectory_adminhtml/brand');

        // Add the grid container as the only item on this page
        $this->loadLayout()
            ->_addContent($brandBlock)
            ->renderLayout();
    }

    /**
     * This action handles both viewing and editing existing brands.
     */
    public function editAction()
    {
        /**
         * Retrieve existing brand data if an ID was specified.
         * If not, we will have an empty brand entity ready to be populated.
         */
        $brand = Mage::getModel('smashingmagazine_branddirectory/brand');
        if ($brandId = $this->getRequest()->getParam('id', false)) {
            $brand->load($brandId);

            if ($brand->getId() _getSession()->addError(
                    $this->__('This brand no longer exists.')
                );
                return $this->_redirect(
                    'smashingmagazine_branddirectory_admin/brand/index'
                );
            }
        }

        // process $_POST data if the form was submitted
        if ($postData = $this->getRequest()->getPost('brandData')) {
            try {
                $brand->addData($postData);
                $brand->save();

                $this->_getSession()->addSuccess(
                    $this->__('The brand has been saved.')
                );

                // redirect to remove $_POST data from the request
                return $this->_redirect(
                    'smashingmagazine_branddirectory_admin/brand/edit',
                    array('id' => $brand->getId())
                );
            } catch (Exception $e) {
                Mage::logException($e);
                $this->_getSession()->addError($e->getMessage());
            }

            /**
             * If we get to here, then something went wrong. Continue to
             * render the page as before, the difference this time being
             * that the submitted $_POST data is available.
             */
        }

        // Make the current brand object available to blocks.
        Mage::register('current_brand', $brand);

        // Instantiate the form container.
        $brandEditBlock = $this->getLayout()->createBlock(
            'smashingmagazine_branddirectory_adminhtml/brand_edit'
        );

        // Add the form container as the only item on this page.
        $this->loadLayout()
            ->_addContent($brandEditBlock)
            ->renderLayout();
    }

    public function deleteAction()
    {
        $brand = Mage::getModel('smashingmagazine_branddirectory/brand');

        if ($brandId = $this->getRequest()->getParam('id', false)) {
            $brand->load($brandId);
        }

        if ($brand->getId() _getSession()->addError(
                $this->__('This brand no longer exists.')
            );
            return $this->_redirect(
                'smashingmagazine_branddirectory_admin/brand/index'
            );
        }

        try {
            $brand->delete();

            $this->_getSession()->addSuccess(
                $this->__('The brand has been deleted.')
            );
        } catch (Exception $e) {
            Mage::logException($e);
            $this->_getSession()->addError($e->getMessage());
        }

        return $this->_redirect(
            'smashingmagazine_branddirectory_admin/brand/index'
        );
    }

    /**
     * Thanks to Ben for pointing out this method was missing. Without
     * this method the ACL rules configured in adminhtml.xml are ignored.
     */
    protected function _isAllowed()
    {
        /**
         * we include this switch to demonstrate that you can add action
         * level restrictions in your ACL rules. The isAllowed() method will
         * use the ACL rule we have configured in our adminhtml.xml file:
         * - acl
         * - - resources
         * - - - admin
         * - - - - children
         * - - - - - smashingmagazine_branddirectory
         * - - - - - - children
         * - - - - - - - brand
         *
         * eg. you could add more rules inside brand for edit and delete.
         */
        $actionName = $this->getRequest()->getActionName();
        switch ($actionName) {
            case 'index':
            case 'edit':
            case 'delete':
                // intentionally no break
            default:
                $adminSession = Mage::getSingleton('admin/session');
                $isAllowed = $adminSession
                    ->isAllowed('smashingmagazine_branddirectory/brand');
                break;
        }

        return $isAllowed;
    }
}

Completing The Configuration

That’s all the code we need for the back end. We just need to make a few changes to config.xml to tell Magento where to find our controller, blocks, models and helper. We also have to add a menu item to the admin panel for easy access to manage our brand entity.

config.xml

We need to add block, helper and model definitions to our module configuration so that Magento knows where to locate these files, as well as an admin router so that Magento knows where to locate our controller for the menu items that we are about to add in the next step. The final version of our etc/config.xml file will look as follows:

<?xml version="1.0"?>
<config>
    <modules>
        <SmashingMagazine_BrandDirectory>
            <!--
            This is the version number that our module is currently at.
            In order for setup scripts to run, their version number must
            be less than or equal to this value.

            As we add upgrade scripts, we increment this value. The next time
            your Magento instance is accessed, Magento will compare values in
            the database table 'core_resource' against this value. If the
            database is lower, it will attempt to run any setup scripts for
            the module and then update the database table to match this value.
            -->
            <version>0.0.1</version>
        </SmashingMagazine_BrandDirectory>
    </modules>
    <global>

        <!--
        add an adminhtml block definition
        -->
        <blocks>
            <smashingmagazine_branddirectory_adminhtml>
               <class>SmashingMagazine_BrandDirectory_Block_Adminhtml</class>
            </smashingmagazine_branddirectory_adminhtml>
        </blocks>

        <!--
        Add a helper definition for use in adminhtml.xml menu translation.
        -->
        <helpers>
            <smashingmagazine_branddirectory>
                <class>SmashingMagazine_BrandDirectory_Helper</class>
            </smashingmagazine_branddirectory>
        </helpers>

        <models>

            <!--
            This is the model alias referred to in install-0.0.1.php.
            -->
            <smashingmagazine_branddirectory>
                <!--
                This tells Magento where to find models for this module.
                -->
                <class>SmashingMagazine_BrandDirectory_Model</class>

                <!--
                This tells Magento where to find resource
                materials for this module.
                -->
      <resourceModel>smashingmagazine_branddirectory_resource</resourceModel>

            </smashingmagazine_branddirectory>

            <!--
            This alias must match the <resourceModel/> value above.
            -->
            <smashingmagazine_branddirectory_resource>
                <!--
                This tells Magento where to find resource
                models for this module.
                -->
                <class>SmashingMagazine_BrandDirectory_Model_Resource</class>

                <entities>

                    <!--
                    This is the table alias referred to in install-0.0.1.php.
                    -->
                    <brand>

                        <!--
                            This is the name of the database table itself.
                        -->
                        <table>smashingmagazine_branddirectory_brand</table>

                    </brand>

                </entities>

            </smashingmagazine_branddirectory_resource>

        </models>

        <resources>

            <!--
            This must match our folder name in the module sql folder.
            -->
            <smashingmagazine_branddirectory_setup>

                <setup>

                    <!--
                    This defines which module the setup
                    scripts in this location belong to.
                    -->
                    <module>SmashingMagazine_BrandDirectory</module>

                    <!--
                    In each setup script, this
                    value determines the class of $this.
                    -->
                    <class>Mage_Core_Model_Resource_Setup</class>

                </setup>

                <!--
                This is relevant only if you have multiple database connections.
                -->
                <connection>
                    <use>core_setup</use>
                </connection>

            </smashingmagazine_branddirectory_setup>

        </resources>

    </global>

    <!-- Add a router for access to our admin panel controller. -->
    <admin>
        <routers>

            <!-- This is the alias for this router. -->
            <smashingmagazine_branddirectory_admin>

                <!--
                This basically informs Magento to use the
                admin scope for requests to this router.
                -->
                <use>admin</use>

                <args>
                    <!--
                    This tells Magento where to find
                    adminhtml controllers for this module.
                    -->
                   <module>SmashingMagazine_BrandDirectory_Adminhtml</module>

                    <!-- This is the term used in the actual URL. -->
                    <frontName>brand-directory-admin</frontName>
                </args>

            </smashingmagazine_branddirectory_admin>

        </routers>
    </admin>

</config>

adminhtml.xml

Adding menu items to Magento’s admin panel is straightforward. We simply have to create an adminhtml.xml file, define which items should appear and where they should lead to when clicked. Edit etc/adminhtml.xml with this:

<?xml version="1.0"?>
<config>
    <!-- We are defining a new menu item for the admin panel. -->
    <menu>

        <!--
        First, create a top-level menu item, which will appear alongside CMS
        -->
        <smashingmagazine_branddirectory translate="title"
                module="smashingmagazine_branddirectory">
            <title>Brand Directory</title>
            <sort_order>75</sort_order>
            <depends>
                <module>SmashingMagazine_BrandDirectory</module>
            </depends>

            <!-- Under this top-level menu, create a child menu item. -->
            <children>
                <brand translate="title"
                        module="smashingmagazine_branddirectory">
                    <title>Manage Brands</title>
                    <sort_order>10</sort_order>

                    <!--
                    When the menu is clicked, take the user here.
                    -->
                 <action>smashingmagazine_branddirectory_admin/brand</action>

                </brand>
            </children>
        </smashingmagazine_branddirectory>
    </menu>

    <!-- Define ACL for access to these menu items. -->
    <acl>
        <resources>
            <admin>
                <children>
                    <smashingmagazine_branddirectory translate="title"
                            module="smashingmagazine_branddirectory">
                        <title>Brand Directory</title>
                        <sort_order>75</sort_order>
                        <children>
                            <brand translate="title"
                                    module="smashingmagazine_branddirectory">
                                <title>Manage Brands</title>
                            </brand>
                        </children>
                    </smashingmagazine_branddirectory>
                </children>
            </admin>
        </resources>
    </acl>
</config>

Displaying Brands In The Front-End

We have reached the end of this tutorial on how to create an entity that is manageable from the admin panel. By now, you should be able to create, update and delete brands and to have those changes reflected in the database. You have a fully functioning brand entity, but it doesn’t actually do anything. The next step is to integrate this new entity in the rest of your Magento code.

Magento Brand Product View

Rather than continue to ramble about how to do this, I have included in the attached source code an additional local module, named “BrandExample,” which contains examples of how to achieve this.

Upon inspecting the attachment, you will notice I have kept this supplemental front-end example as a separate local module, in order for it not to be confused with the admin panel management sections covered above. Once you are more familiar, then by all means, bundle both modules into one if you wish.

I won’t go into much detail with the example usages, but feel free to ask any questions in the comments section below.

Magento Brand List

Magento Brand View

Feel free to view or download the source code2 (Github), which contains the following additional content:

  • the completed tutorial from above;
  • a new product attribute to associate products with brands;
  • inclusion of brands on the product view page, once associated;
  • a brands listing and brands landing page, including related products;
  • data setup scripts to create dummy products and brands.

(al, il)

Footnotes

  1. 1 http://www.smashingmagazine.com/2012/03/01/basics-creating-magento-module/
  2. 2 https://github.com/josephmcdermott/SmashingMagazine-MagentoBrandEntity

↑ Back to topShare on Twitter

Advertising
  1. 1

    Woo!
    We need more tutorials like this one!

    I’m going to try it tomorrow and see how it works.

    Good job!

    0
    • 2

      Joseph McDermott

      March 18, 2014 4:45 am

      Glad to help Daniele, and it would be good to know how you got on with the tutorial! Of course more Magento articles to follow soon :)

      0
  2. 3

    Thanks for the article, but I need some examples on configuring Magento entity products…

    0
  3. 4

    Nice article – should help serve as a reference for new and even experienced developers.

    A couple of notes:
    1) Why use the EAV setup resource when your entity is flat?
    2) You are missing the isAllowed() method in your adminhtml controller, which means ACL restrictions will be ignored.
    3) Why use a custom route & frontName for your adminhtml controller rather than just adding yours under Mage_Adminhtml’s route & frontName?

    Again, good job – might want to make the additional content in the download even more prominent.

    0
    • 5

      What’s the upside of creating a new entity vs creating a brand based category?

      Could you accomplish the same goals with a ‘brand’ attribute?

      0
      • 6

        That’s a good question. I was asking me the same.

        0
        • 7

          Joseph McDermott

          March 18, 2014 4:16 am

          Hi Darren and Javier,

          If you see the last comment from Ben above, I recommend you download the additional content and have a look at the local module I have provided. You will see that creating an entity in this way provides a lot more than just a product attribute. As a product attribute you would just have a brand name to display on your product page, however if you create an entity you get this same functionality in addition to a page you can link through to for a brand landing page where you can display an image, description, related products, etc. whatever you desire. The brand entity is a sibling of a product entity, having its own attributes, rather than being a single attribute of a product.

          I hope that answers your question and the additional content demonstrates this.

          0
    • 8

      Joseph McDermott

      March 18, 2014 4:13 am

      Hi Ben, thank you for the feedback and excellent questions.

      1. Good point there is no need to use the EAV resource here. I have amended to use Mage_Core_Model_Resource_Setup instead. For those of you interested, the reason this worked previously was that Mage_Eav_Model_Entity_Setup extends Mage_Core_Model_Resource_Setup so inherits the required functionality anyway, but as Ben has pointed out the EAV class contains additional functionality that was not required here, since we are using a flat table structure for our brand.

      2. Again good spot, I left this out as a test which you passed ;) I’ve amended the article to reflect this.

      3. Just to show that you can really, and to illustrate the similarity between this and a frontend router. For those of you interested, as Ben suggests you could use something like this in your config.xml and amend the target paths in adminhtml.xml, making your admin URLs begin /admin/ instead of /brand-directory-admin/:

      <admin>
      <routers>
      <adminhtml>
      <args>
      <modules>
      <SmashingMagazine_BrandDirectory before="Mage_Adminhtml">SmashingMagazine_BrandDirectory_Adminhtml</SmashingMagazine_BrandDirectory>
      </modules>
      </args>
      </adminhtml>
      </routers>
      </admin>

      As for making the additional content in the download more prominent, I didn’t want to overwhelm people as there is already a lot to digest, but thank you and let me take the opportunity here to recommend that all readers do download the additional content at the end of the article. Look in particular at the local module included which demonstrates how powerful Magento can be with custom entities.

      Thanks again for your feedback, Ben.

      0
  4. 9

    My first try to comment was marked as s pa m by Smashing Mag so here a small second try.
    Thank you for this Joseph! Very kind to put so much effort in this to teach others.
    I will look at the code because it seems what I need for Book Author listings in Magento.
    I quess this is as much as I can comment before I get blocked again.

    0
  5. 10

    Hi there this is very useful module for me because i am new to magento i followed all steps as it is still i am getting following error will you please tell me solution to fix this error. thanks.

    Error:
    Fatal error: Class ‘SmashingMagazine_BrandDirectory_Helper_Data’ not found in C:xampphtdocsmagentoappMage.php on line 548
    Call Stack
    # Time Memory Function Location
    1 0.0210 142656 {main}( ) ..index.php:0
    2 0.1140 10268168 Mage::run( ) ..index.php:87
    3 0.1180 10303184 Mage_Core_Model_App->run( ) ..Mage.php:685
    4 6.0363 12251424 Mage_Core_Controller_Varien_Front->dispatch( ) ..__default.php:20465
    5 6.0553 12324824 Mage_Core_Controller_Varien_Router_Standard->match( ) ..__default.php:17865
    6 6.0953 12493528 Mage_Core_Controller_Varien_Action->dispatch( ) ..__default.php:18331
    7 6.6184 13785536 Mage_Adminhtml_System_ConfigController->editAction( ) ..__default.php:13969
    8 14.9889 17102280 Mage_Core_Controller_Varien_Action->renderLayout( ) ..ConfigController.php:119
    9 14.9909 17103568 Mage_Core_Model_Layout->getOutput( ) ..__default.php:13941
    10 14.9909 17103648 Mage_Core_Block_Abstract->toHtml( ) ..__default.php:27804
    11 14.9929 17104400 Mage_Adminhtml_Block_Template->_toHtml( ) ..__default.php:2592
    12 14.9929 17104632 Mage_Core_Block_Template->_toHtml( ) ..Mage_Adminhtml_Block_Template.php:81
    13 14.9929 17104672 Mage_Core_Block_Template->renderView( ) ..__default.php:3432
    14 14.9969 17105040 Mage_Core_Block_Template->fetchView( ) ..__default.php:3418
    15 14.9999 17136808 include( ‘C:xampphtdocsmagentoappdesignadminhtmldefaultdefaulttemplatepage.phtml’ ) ..__default.php:3387
    16 15.4369 18942896 Mage_Core_Block_Abstract->getChildHtml( ) ..page.phtml:53
    17 15.4369 18942960 Mage_Core_Block_Abstract->_getChildHtml( ) ..__default.php:2254
    18 15.4369 18942960 Mage_Core_Block_Abstract->toHtml( ) ..__default.php:2310
    19 15.4379 18943608 Mage_Adminhtml_Block_Template->_toHtml( ) ..__default.php:2592
    20 15.4379 18943632 Mage_Core_Block_Template->_toHtml( ) ..Mage_Adminhtml_Block_Template.php:81
    21 15.4379 18943648 Mage_Core_Block_Template->renderView( ) ..__default.php:3432
    22 15.4419 18943880 Mage_Core_Block_Template->fetchView( ) ..__default.php:3418
    23 15.4439 18963888 include( ‘C:xampphtdocsmagentoappdesignadminhtmldefaultdefaulttemplatepagemenu.phtml’ ) ..__default.php:3387
    24 15.4439 18963936 Mage_Adminhtml_Block_Page_Menu->getMenuArray( ) ..menu.phtml:30
    25 15.4439 18963952 Mage_Adminhtml_Block_Page_Menu->_buildMenuArray( ) ..Mage_Adminhtml_Block_Page_Menu.php:98
    26 17.7560 19459288 Mage_Adminhtml_Block_Page_Menu->_isEnabledModuleOutput( ) ..Mage_Adminhtml_Block_Page_Menu.php:144
    27 17.7560 19459592 Mage::helper( ) ..Mage_Adminhtml_Block_Page_Menu.php:325

    0
  6. 11

    Hey great tutorials Joseph, just pointing out error in the BrandController.php code.

    Under the editAction function you are missing some code (it’s on your github)
    Here it is
    if ($brand->getId() _getSession()->addError(
    $this->__(‘This brand no longer exists.’)
    );
    return $this->_redirect(
    ‘smashingmagazine_branddirectory_admin/brand/index’
    );
    }

    Where it should be
    if ($brand->getId() _getSession()->addError(
    $this->__(‘This brand no longer exists.’)
    );
    return $this->_redirect(
    ‘smashingmagazine_branddirectory_admin/brand/index’
    );
    }

    Not a big deal, just might help some people who rush through these things.

    0
    • 12

      It’s also the same thing for deleteAction() too!

      0
    • 13

      You posted the exact same code snippet, no difference.

      What was the error?

      0
    • 14

      i see no difference between 2 snaps

      0
    • 17

      i dont know what those fuck*ng lines do but i commented them and seems to be working fine, you have to change it also on delete action

      public function editAction()
      {
      /**
      * Retrieve existing brand data if an ID was specified.
      * If not, we will have an empty brand entity ready to be populated.
      */
      $brand = Mage::getModel(‘smashingmagazine_branddirectory/brand’);
      if ($brandId = $this->getRequest()->getParam(‘id’, false)) {
      $brand->load($brandId);

      //if ($brand->getId() _getSession()->addError(
      // $this->__(‘This brand no longer exists.’)
      // );
      return $this->_redirect(
      ‘smashingmagazine_branddirectory_admin/brand/index’
      );
      }
      //}

      // process $_POST data if the form was submitted
      if ($postData = $this->getRequest()->getPost(‘brandData’)) {
      try {
      $brand->addData($postData);
      $brand->save();

      $this->_getSession()->addSuccess(
      $this->__(‘The brand has been saved.’)
      );

      // redirect to remove $_POST data from the request
      return $this->_redirect(
      ‘smashingmagazine_branddirectory_admin/brand/edit’,
      array(‘id’ => $brand->getId())
      );
      } catch (Exception $e) {
      Mage::logException($e);
      $this->_getSession()->addError($e->getMessage());
      }

      /**
      * If we get to here, then something went wrong. Continue to
      * render the page as before, the difference this time being
      * that the submitted $_POST data is available.
      */
      }

      // Make the current brand object available to blocks.
      Mage::register(‘current_brand’, $brand);

      // Instantiate the form container.
      $brandEditBlock = $this->getLayout()->createBlock(
      ‘smashingmagazine_branddirectory_adminhtml/brand_edit’
      );

      // Add the form container as the only item on this page.
      $this->loadLayout()
      ->_addContent($brandEditBlock)
      ->renderLayout();
      }

      0
  7. 18

    thank you for the great tutorial sir. :)

    there is one error in my case otherwise seems like all are fine.
    the error is ,
    there is no content shows in the
    Brand Directory->Manage Brands
    in the admin panel

    thanks in advanced :)

    1
    • 19

      Kala,

      I get the exact same result when using the GitHub code provided. I am glad I am not the only one.

      1. I have no ‘smashingmagazine_branddirectory_brand’ entry in my core-resource table in phpmyadmin

      2. “There has been an error processing your request” is all i get when I go into SmashingMagazine -> Brand

      And this is using the GitHub code as-is. Please advise.

      0
      • 20

        Correction:

        1. I do not have a “smashingmagazine_branddirectory_brand” table that should have been created.

        I do get a “There has been an error processing your request” where no such table (“smashingmagazine_branddirectory_brand”) can be found.

        So the script is not creating a table. I do have a “smashingmagazine_branddirectory_setup” in my core_resources table.

        Is there something that we are missing from your GitHub code? If it is a typo-error, then it is not on our end.

        0
  8. 22

    sir there is a error while i am trying to add or edit products.
    it says
    Source model “smashingmagazine_brandexample/source_brand” not found for attribute “brand_id”

    how to solve this issue sir

    thank you :)

    0
  9. 23

    Hey Joseph,
    Thanks for the awesome tutorial. I thought this would be of value to our blog readers, so I included it in our roundup of March’s best Magento, ExpressionEngine, and WordPress content. http://blog.nexcess.net/2014/04/08/marchs-roundup-of-the-best-expressionengine-wordpress-and-magento-content/ Thanks again for the informative and helpful post.

    Ben

    0
  10. 24

    Hey Joseph,

    Thanks for the awesome tutorial.
    It works, but I have question:
    How to display brands in browser? There isn’t specified how to do this.

    0
  11. 25

    Your site is very nice. I understand the structure of magento file. I have no words to express. Thank’s you very much.

    0
  12. 26

    Every admin can manage their products from their backend provided in this post?

    0

Leave a Comment

Yay! You've decided to leave a comment. That's fantastic! Please keep in mind that comments are moderated and rel="nofollow" is in use. So, please do not use a spammy keyword or a domain as your name, or else it will be deleted. Let's have a personal and meaningful conversation instead. Thanks for dropping by!

↑ Back to top