Building an Integrated Magento Blog: Part 1
by David Joly | Tuesday, December 14th, 2010If you’re reading this, chances are you are a relative newbie when it comes to developing with Magento. Before you continue, I must advise that this tutorial is for Magento developers that already have a basic idea of how Magento works. If you have not already done so, I highly recommend reading Alan Storm’s series of tutorials before getting started with this tutorial series.
Getting Started
Today we are going to begin a journey through the bowels of Magento’s code base. To be prepared you shoud have:
- Locally Installed Magento Community Edition 1.4.x
- Know the basic layout of a Magento module
- Have a basic understanding of how Magento theme layout.xml, template files, and block classes work.
Defining Our Blog’s Requirements
As all good developers, we will begin by defining a set of requirements. While Magento already comes with a built-in CMS, it is quite limited. For this reason, many developers have integrated Wordpress with Magento. So already we have a basis from which to build our requirements list. Generally speaking, if Wordpress can do it, so should our blog. So let’s list them out shall we:
- The Blog MUST allow administrators to manage an ACL (access control list), specifying user groups and their privileges to blog resources.
- The Blog MUST be compatible with multi-store setups.
- The Blog MUST make use of categories and tags.
- The Blog MUST allow for the posting, modification, and removal of articles and blog entries.
- The Blog MUST have a draft feature so work in progress is not displayed to the public and may be previewed before publishing. Similarly, published content may be unpublished.
- The Blog MUST allow customer and admins to post comments to published blog content. Administrators MUST be able to turn on or off comments on a per-post basis or store-wide.
- The Blog MUST have a comment filtering and moderating feature.
- The Blog MUST have a media upload feature, allowing the upload of media content.
- The Blog SHOULD have an RSS subscription feature.
Leveraging Current Magento Features to Speed Up our Work
If you haven’t already noticed, Magento is a very large, sprawling piece of software. Chances are good that some of our requirements can be met, at least in part, by Magento core features. If these don’t suffice, Magento includes the very good Zend Framework library on the include path, making all of it available to us.
The Access Control System
Lucky for us, Magento ships with a customizable access-control system. We can leverage this system to our benefit.
To get a preview of what we will be dealing with here, in the Admin Panel, go to System-> Permissions -> Roles. By default, Magento includes an “Administrators” role. Click on the role as if you were going to edit it. Click on the “Role Resources” tab. In the resource access drop-down menu, select “Custom.” Doing so reveals a large list of resources represented by checkboxes.
Scroll down that list of resources. Notice something? Each resource represents a top-level admin menu-item, a controller, or an action. Let’s look at the first resource in the list – Sales. The Sales resource checkbox, if checked, will allow a user access to the Sales menu.
For giggles, open up your Magento project in your favorite IDE and browse to app/code/core/Mage/Adminhtml/controllers/Sales/OrdersController.php. Take a look at the private method _isAllowed(). Now open ../Sales/Orders/Billing/AgreementController.php and take a look at the _isAllowed() method. You will notice that the _isAllowed() methods are calling the isAllowed() method of the singleton model class Mage_Admin_Model_Session.
Let’s take a quick look at the Admin module to see what is happening. Go to app/code/core/Mage/Admin/Model. You will see a number or classes dealing with roles and access control, which those of you astute enough will have noticed extend Zend_Acl classes. Open Session.php and take a look at the isAllowed() method. The method is querying a Zend_Acl object containing all the resources you were just seeing in the Admin Panel , in the format admin/controller_class/action, which will return a boolean value if the current user’s role permits them access to the desired resource. Clear as mud? Good!
(Note: For more information regarding Zend_Acl, please read the official documentation.)
CMS WYSISWYG Editor
Magento comes with TinyMCE, a popular WYSIWYG editor, which is used by the CMS. We will be using this editor for our blog also.
Media Upload
Magento also comes with a number of media upload tools. We will be taking a closer look at these when the time comes.
Defining Our Core Models
Starting out, we know we need at least two models – one to represent blog posts and another user comments. What is less clear is how we will handle users/authors. For blog posts we can simply map authors to Magento’s existing admin_user table. That’s a cinch. Comments, however, we will need to treat a little differently, because comments will be being posted by either admin users or customers. So what’s the problem? To answer that question, we will need to explore Magento’s database. Using your favorite MySQL database browsing tool, find the table customer_entity. Viewing its columns, we find an entity_id, but no customer_id. What gives? It turns out that customer data is stored in Magento’s EAV tables. So our comment authors will need to be either mapped to the user_id field of the admin_user table or the entity_id field of the customer_entity table.
We also need category and tag models, this is a blog after all. Unfortunately, while Magento does have its own category and tag features, they are intended to be used with products. So we will need to add our own category and tag models.
So to start, our data model will look like this:
Most of the fields in the above tables should make sense. For the post and comment tables I included a status field to track status, which will work well with defined constant status codes in our model classes. I also added a comments field which will act as a boolean (1 = on, 0 = off) for determining whether comments can be posted to a particular post.
Building Out the Skeleton of Our Blog Module
Our first task will be to build out our blog’s directories and model files. This step is fairly straight forward and is generally a copy-and-paste affair. For blocks and controllers, it’s good practice to clearly separate admin from frontend. Good organization will be necessary, especially when we start building our Admin Panel interface.
Don’t forget to add our blog module to the app/etc/modules directory.
(Note: Be sure to capitalize controller subfolders. The ubiquitous Linux host OS is case-sensitive, so Magento will be unable to locate your controllers. Also, avoid CamelCasing your class names and directories to avoid case-sensitivity issues.)
Configuration Galore
Hopefully you’re good with XML, because we have a ton of it to write. Starting with config.xml, which should be somewhat familiar to you:
<?xml version="1.0" encoding="UTF-8"?> <!-- @author David Joly <david@zeletron.com> or <djoly@gorillachicago.com> --> <config> <modules> <Joly_Blog> <version>0.1.0</version> </Joly_Blog> </modules> <global> <models> <blog> <class>Joly_Blog_Model</class> <resourceModel>blog_mysql4</resourceModel> </blog> <blog_mysql4> <class>Joly_Blog_Model_Mysql4</class> <entities> <post> <table>joly_blog_post</table> </post> <comment> <table>joly_blog_comment</table> </comment> <tag> <table>joly_blog_tag</table> </tag> <category> <table>joly_blog_category</table> </category> </entities> </blog_mysql4> </models> <resources> <blog_write> <connection> <use>core_write</use> </connection> </blog_write> <blog_read> <connection> <use>core_read</use> </connection> </blog_read> <blog_setup> <setup> <module>Joly_Blog</module> <class>Joly_Blog_Model_Resource_Mysql4_Setup</class> </setup> <connection> <use>core_setup</use> </connection> </blog_setup> </resources> <helpers> <blog> <class>Joly_Blog_Helper</class> </blog> </helpers> <blocks> <blog> <class>Joly_Blog_Block</class> </blog> </blocks> </global> <frontend> <routers> <blog> <use>standard</use> <args> <module>Joly_Blog</module> <frontName>blog</frontName> </args> </blog> </routers> <layout> <updates> <blog> <file>blog.xml</file> </blog> </updates> </layout> </frontend> <admin> <routers> <blog> <use>admin</use> <args> <module>Joly_Blog</module> <frontName>blog</frontName> </args> </blog> </routers> </admin> </config>
We will be avoiding layout updates to the admin theme, so we have no need to define a layout update file. We can always add one if we need it later.
Adding our own System Configuration Tab
We will be using Magento’s built-in system configuration utility to provide administrators with configurable options. But before we start pounding out more XML, take a gander at System-> Configuration. Navigate to any one of the configuration sections via the tab links. Taking a quick look at the address bar, we can see that the system configuration page is controlled by the edit action of the system config controller.
Each tab is represented by a unique section parameter, which is passed to the system config controller. Let’s take a quick look at this contoller’s edit action. Open up Mage/Adminhtml/controllers/System/ConfigController.php.
The first thing you should notice is the instantiation of the Mage_Adminhtml_Model_Config singleton.
$current = $this->getRequest()->getParam('section'); $website = $this->getRequest()->getParam('website'); $store = $this->getRequest()->getParam('store'); $configFields = Mage::getSingleton('adminhtml/config');
Scroll down a few more lines (line 76 in my version) :
if ($this->_isSectionAllowed($this->getRequest()->getParam('section'))) { $this->_addContent($this->getLayout()->createBlock('adminhtml/system_config_edit')->initForm());
Two things to note: First, the conditional ultimately results in a call to Zend_Acl (discussed previously) to determine whether or not the current user is allow to see the requested section. Second, the creation of a Mage_Adminhtml_Block_System_Config_Edit block object. Let’s take a look shall we? In the constructor, we find the following:
$sectionCode = $this->getRequest()->getParam('section'); $sections = Mage::getSingleton('adminhtml/config')->getSections(); $this->_section = $sections->$sectionCode;
The call to getSections() on the Mage_Adminhtml_Model_Config singleton returns a configuration object of all section nodes defined in system.xml config files.
The config data for these sections is then reduced to the section of the specified section parameter. Next, let’s take a look at the initForm method:
$blockName = (string)$this->_section->frontend_model; if (empty($blockName)) { $blockName = self::DEFAULT_SECTION_BLOCK; } $this->setChild('form', $this->getLayout()->createBlock($blockName) ->initForm() );
The initForm method builds a form based on configuration settings in the system.xml files. Without further exploration, lets make our own!
<?xml version="1.0" encoding="UTF-8"?> <config> <!-- The tab section is used to define a tab to be displayed on the tab menu on the system config page. --> <tabs> <blog translate="label" module="blog"> <label>Blog</label> <sort_order>250</sort_order> </blog> </tabs> <sections> <blog translate="label" module="blog"> <class>separator-top</class> <label>Blog</label> <!-- The tab directive instructes Magento to render the link to this section under the specified tab. --> <tab>blog</tab> <!-- The frontend_type instructs Magento to create a block of the specified type. --> <frontend_type>text</frontend_type> <sort_order>10</sort_order> <show_in_default>1</show_in_default> <show_in_website>1</show_in_website> <show_in_store>1</show_in_store> <!-- Groups show up as accordions in the section's config form. --> <groups> <comment translate="label" module="blog"> <label>Comments</label> <sort_order>10</sort_order> <show_in_default>1</show_in_default> <show_in_website>1</show_in_website> <show_in_store>1</show_in_store> <!-- Form fields, their types, and many other options can be set here. --> <fields> <enable_comments translate="label" module="blog"> <label>Enable Comments</label> <frontend_type>select</frontend_type> <source_model>adminhtml/system_config_source_enabledisable</source_model> <sort_order>100</sort_order> <show_in_default>1</show_in_default> <show_in_website>1</show_in_website> <show_in_store>1</show_in_store> </enable_comments> </fields> </comment> </groups> </blog> </sections> </config>
Let’s give this baby a test drive. Refresh (or delete) your Magento cache and reload the system config page. You should now see a tab titled Blog with a link also titled Blog under it. Click on it. You are probably wondering why you are seeing a big, fat access denied message. Remember our little talks about the ACL? We will need to create this before we can see a bright, shiny, new configuration form.
Defining our Blog’s Admin ACL and Menu
To define our ACL, we need to create our adminhtml.xml file. We will also define some admin menu items to point to our admin controllers, which we will create in a later tutorial.
<?xml version="1.0" encoding="UTF-8"?> <config> <!-- Magento looks for menu nodes in adminhtml files when building its admin menu. --> <menu> <!-- Here we define a top level blog menu item. --> <blog> <title>Blog</title> <sort_order>50</sort_order> <!-- Children nodes represent dropdown menu items. --> <children> <post> <title>Manage Posts</title> <sort_order>1</sort_order> <!-- The action node tells Magento to render a link to point to the the following controller. If no action is specified, in typical MVC fashon, index is the default. --> <action>blog/adminhtml_post</action> </post> <comment> <title>Manage Comments</title> <sort_order>2</sort_order> <action>blog/adminhtml_comment</action> </comment> </children> </blog> </menu> <acl> <resources> <admin> <children> <!-- Here we define a new blog acl resource --> <blog translate="title" module="blog"> <!-- The title is displayed in the acl resource list. --> <title>Blog</title> <sort_order>90</sort_order> <children> <!-- Define a blog post sub-resource. --> <post translate="title"> <title>Posts</title> <sort_order>1</sort_order> <children> <actions> <title>Actions</title> <children> <!-- These represent actions that may be taken on our blog post sub-resource. --> <create translate="title"><title>Create</title></create> <view translate="title"><title>View</title></view> <edit translate="title"><title>Edit</title></edit> <delete translate="title"><title>Delete</title></delete> <publish translate="title"><title>Publish</title></publish> <unpublish translate="title"><title>Unpublish</title></unpublish> </children> </actions> </children> </post> <comment translate="title"> <title>Comments</title> <sort_order>2</sort_order> <children> <actions> <title>Actions</title> <children> <flag translate="title"><title>Flag</title></flag> <spam translate="title"><title>Spam</title></spam> <trash translate="title"><title>Trash</title></trash> <edit translate="title"><title>Edit</title></edit> </children> </actions> </children> </comment> </children> </blog> <!-- We add our blog to the system config acl resource to before we can view the configuration form. --> <system> <children> <config> <children> <blog translate="title" module="catalog"> <title>Blog Section</title> </blog> </children> </config> </children> </system> </children> </admin> </resources> </acl> </config>
With our ACL configuration done, clear your Magento cache and log out of the Admin Panel. When you log back in, the new ACL data will be active. If you set everything up correctly, you should now be seeing a blog menu item on the nav bar, have access to the blog section in the system configuration page, and see the ACL resources in the resource list while creating custom ACLs for user roles.
Models and Collections
Our blog’s models will be extending Magento’s “simple” model classes. First let’s make our base model classes:
<?php //Post model class Joly_Blog_Model_Post extends Mage_Core_Model_Abstract { protected function _construct() { $this->_init('blog/post'); } } //Comment model <?php class Joly_Blog_Model_Comment extends Mage_Core_Model_Abstract { protected function _construct() { $this->_init('blog/comment'); } } //Category model <?php class Joly_Blog_Model_Category extends Mage_Core_Model_Abstract { protected function _construct() { $this->_init('blog/category'); } } //Tag model <?php class Joly_Blog_Model_Tag extends Mage_Core_Model_Abstract { protected function _construct() { $this->_init('blog/tag'); } }
Next up, our mysql4 entity classes. These should go in the Model/Mysql4 directory.
//Post entity <?php class Joly_Blog_Model_Mysql4_Post extends Mage_Core_Model_Mysql4_Abstract { public function _construct() { $this->_init('blog/post', 'post_id'); } } //Comment entity <?php class Joly_Blog_Model_Mysql4_Comment extends Mage_Core_Model_Mysql4_Abstract { public function _construct() { $this->_init('blog/comment', 'comment_id'); } } //Category entity <?php class Joly_Blog_Model_Mysql4_Category extends Mage_Core_Model_Mysql4_Abstract { public function _construct() { $this->_init('blog/category', 'category_id'); } } //Tag entity <?php class Joly_Blog_Model_Mysql4_Tag extends Mage_Core_Model_Mysql4_Abstract { public function _construct() { $this->_init('blog/tag', 'tag_id'); } }
Now we need our initialize or collections. For each of our entities, Magento will look for Model/Mysql4/
<?php
class Joly_Blog_Model_Resource_Mysql4_Setup extends Mage_Core_Model_Resource_Setup
{}
And don’t forget the actual install file,
<?php /** * @var Joly_Blog_Model_Resource_Mysql4_Setup */ $installer = $this; $date = date('Y:m:d H:i:s'); $sql = " /******************************************************************************* Start Category Table SQL *******************************************************************************/ CREATE TABLE {$installer->getTable('blog/category')} ( `category_id` INT NOT NULL AUTO_INCREMENT , `parent_id` INT NULL , `name` VARCHAR(45) NOT NULL , `store_id` SMALLINT UNSIGNED NOT NULL , PRIMARY KEY (`category_id`) )ENGINE = InnoDB DEFAULT CHARSET=utf8; INSERT INTO {$installer->getTable('blog/category')} SET `name` = 'samplecategory', `store_id` = 1; /******************************************************************************* End Category Table SQL *******************************************************************************/ /******************************************************************************* Start Tag Table SQL *******************************************************************************/ CREATE TABLE {$installer->getTable('blog/tag')} ( `tag_id` INT NOT NULL AUTO_INCREMENT , `name` VARCHAR(45) NULL , `count` INT NOT NULL DEFAULT 0 , `store_id` SMALLINT UNSIGNED NOT NULL , PRIMARY KEY (`tag_id`) ) ENGINE = InnoDB DEFAULT CHARSET=utf8; INSERT INTO {$installer->getTable('blog/tag')} SET `name` = 'sampletag', `store_id` = 1, `count` = 5; /******************************************************************************* End Tag Table SQL *******************************************************************************/ /******************************************************************************* Start Post Table SQL *******************************************************************************/ CREATE TABLE {$installer->getTable('blog/post')} ( `post_id` INT NOT NULL AUTO_INCREMENT , `user_id` MEDIUMINT(9) UNSIGNED NOT NULL , `store_id` SMALLINT UNSIGNED NOT NULL , `category_id` INT NOT NULL , `tags` TINYTEXT NOT NULL DEFAULT '', `title` VARCHAR(100) NOT NULL , `content` TEXT NOT NULL , `post_date` DATETIME NOT NULL , `publish_date` DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00' , `edit_date` DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00' , `status` VARCHAR(20) NULL DEFAULT 'draft' , `comments` int NOT NULL DEFAULT 1, PRIMARY KEY (`post_id`) )ENGINE = InnoDB DEFAULT CHARSET=utf8; INSERT INTO {$installer->getTable('blog/post')} SET `user_id` = 1, `title` = 'Welcome to your Magento Blog', `content` = 'This is a sample post, edit or delete it', `post_date` = '{$date}', `publish_date` = '{$date}', `status` = 'live', `store_id` = 1, `category_id` = 1; /******************************************************************************* End Post Table SQL *******************************************************************************/ /******************************************************************************* Start Comment Table SQL *******************************************************************************/ CREATE TABLE {$installer->getTable('blog/comment')} ( `comment_id` INT NOT NULL AUTO_INCREMENT , `post_id` INT NOT NULL, `parent_id` INT NULL , `user_type` VARCHAR(20) NOT NULL , `user_id` INT NULL , `title` VARCHAR(100) NOT NULL , `content` TEXT NOT NULL , `post_date` DATETIME NOT NULL , `edit_date` DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00' , `status` VARCHAR(20) NOT NULL DEFAULT 'live' , `store_id` SMALLINT UNSIGNED NOT NULL , PRIMARY KEY (`comment_id`) )ENGINE = InnoDB DEFAULT CHARSET=utf8; INSERT INTO {$installer->getTable('blog/comment')} SET `user_type` = 'admin', `post_id` = 1, `user_id` = 1, `title` = 'Sample Comment', `content` = 'This is a sample comment', `post_date` = '{$date}', `store_id` = 1; /******************************************************************************* End Comment Table SQL *******************************************************************************/ "; $installer->startSetup(); $installer->run($sql) ->endSetup();
Clear your cache folder and refresh your browser. The setup script will run automatically if everything has been setup correctly. Browse you data tables and make sure they were created. If you copied my config file, they will be namespaces with “joly_”.
A Good Time to Stop
We have a skeleton of our module complete. We plowed through a ton of configuration XML, our tables are setup, and our core models are ready for work.
Next time we will start working on our Adminhtml controller and block classes.
