This is the fourth and final part of a MVC tutorial for Joomla that I relied heavily on whilst working with J00mla! and it went off line so I saved it for prosperity and personal gain…
Source : http://goo.gl/T334aH
Previous posts in the series:
Developing a Model-View-Controller Component – Part 4 – Creating an Administrator Interface
Introduction
In the first three tutorials, we have developed a MVC component that retrieves its data from a table in the database. Currently, there is no way to add data to the database except to do it manually using another tool. In this tutorial, we will develop an administrator section for our component which will make it possible to manage the entries in the database.
Creating the Basic Framework
The basic framework of the administrator panel is very similar to the site portion. The main entry point for the administrator section of the component is admin.hello.php. This file is identical to the hello.php file that was used in the site portion except the name of the controller it loads will be changed to HellosController. The default controller is also called controller.php and this file is identical to the default controller in the site portion, with the exception that the controller is named HellosController instead of HelloController. This difference is so that JController will by default load the hellos view, which will display a list of our greetings.
Here is the listing for admin.hello.php:
<?php
/**
* @package Joomla.Tutorials
* @subpackage Components
* @link http://dev.joomla.org/component/option,com_jd-wiki/Itemid,31/id,tutorials:components/
* @license GNU/GPL
*/
// no direct access
defined( '_JEXEC' ) or die( 'Restricted access' );
// Require the base controller
require_once( JPATH_COMPONENT.DS.'controller.php' );
// Require specific controller if requested
if($controller = JRequest::getWord('controller')) {
$path = JPATH_COMPONENT.DS.'controllers'.DS.$controller.'.php';
if (file_exists($path)) {
require_once $path;
} else {
$controller = '';
}
}
// Create the controller
$classname = 'HellosController'.$controller;
$controller = new $classname( );
// Perform the Request task
$controller->execute( JRequest::getVar( 'task' ) );
// Redirect if set by the controller
$controller->redirect();
?>
The view and model that we will start with is the hellos view and the hellos model. We will start with the model.
The Hellos Model
The Hellos Model will be very simple. The only operation that we currently need is the ability to retrieve the list of hellos from the database. This operation will be implemented in a method called getData().
The JModel class has a built in protected method called _getList(). This method can be used to simplify the task of retrieving a list of records from the database. We simply need to pass it the query and it will return the list of records.
At a later point in time, we might want to use our query from within another method. Therefore, we will create a private method called _buildQuery() which will return the query that will be passed to _getList(). This makes it easier to change the query as well since it is localised in one place.
Therefore we need two methods in our class: getData() and _buildQuery().
_buildQuery() simply returns the query. It looks like:
/**
* Returns the query
* @return string The query to be used to retrieve the rows from the database
*/
function _buildQuery()
{
$query = ' SELECT * '. ' FROM #__hello ';
return $query;
}
getData() will obtain the query and retrieve the records from the database. Now it might happen that we need to retrieve this list of data twice in one page load. It would be a waste to have to query the database twice. Therefore, we will have this method store the data in a protected property so that on subsequent requests it can simply return the data it has already retrieved. This property will be called _data.
Here is the getData() method:
/**
* Retrieves the hello data
* @return array Array of objects containing the data from the database
*/
function getData()
{
// Lets load the data if it doesn't already exist
if (empty( $this->_data ))
{
$query = $this->_buildQuery();
$this->_data = $this->_getList( $query );
}
return $this->_data;
}
The completed model looks like:
/**
* Hellos Model for Hello World Component
*
* @package Joomla.Tutorials
* @subpackage Components
* @link http://dev.joomla.org/component/option,com_jd-wiki/Itemid,31/id,tutorials:components/
* @license GNU/GPL
*/
// Check to ensure this file is included in Joomla!
defined('_JEXEC') or die();
jimport( 'joomla.application.component.model' );
/**
* Hello Model
*
* @package Joomla.Tutorials
* @subpackage Components
*/
class HellosModelHellos extends JModel
{
/**
* Hellos data array
*
* @var array
*/
var $_data;
/**
* Returns the query
* @return string The query to be used to retrieve the rows from the database
*/
function _buildQuery()
{
$query = ' SELECT * ' . ' FROM #__hello ';
return $query;
}
/**
* Retrieves the hello data
* @return array Array of objects containing the data from the database
*/
function getData()
{
// Lets load the data if it doesn't already exist
if (empty( $this->_data ))
{
$query = $this->_buildQuery();
$this->_data = $this->_getList( $query );
}
return $this->_data;
}
}
This file is saved as models/hellos.php.
The Hellos View
Now that we have a model to retrieve our data, we need to display it. This view will be fairly similar to the view from the site section as well.
Just as our model was automatically instantiated in the site, so it is in the administrator. Methods that start with get in the model can be accessed using the get() method of the JView class. So our view has three lines: one to retrieve the data from the model, one to push the data into the template, and one to invoke the display method to display the output. Thus we have:
/**
* Hellos View for Hello World Component
*
* @package Joomla.Tutorials
* @subpackage Components
* @link http://dev.joomla.org/component/option,com_jd-wiki/Itemid,31/id,tutorials:components/
* @license GNU/GPL
*/
// Check to ensure this file is included in Joomla!
defined('_JEXEC') or die();
jimport( 'joomla.application.component.view' );
/**
* Hellos View
*
* @package Joomla.Tutorials
* @subpackage Components
*/
class HellosViewHellos extends JView
{
/**
* Hellos view display method
* @return void
**/
function display($tpl = null)
{
JToolBarHelper::title( JText::_( 'Hello Manager' ), 'generic.png' );
JToolBarHelper::deleteList();
JToolBarHelper::editListX();
JToolBarHelper::addNewX();
// Get data from the model
$items =& $this->get( 'Data');
$this->assignRef( 'items', $items );
parent::display($tpl);
}
}
This file is saved as views/hellos/view.html.php.
The Hellos Template
The template will take the data pushed into it from the view and produce the output. We will display our output in a simple table. While the frontend template was very simple, in the administrator we will need a minimal amount of extra logic to handle looping through the data.
Here is our template:
<?php defined('_JEXEC') or die('Restricted access'); ?>
<form action="index.php" method="post" name="adminForm">
<div id="editcell">
<table class="adminlist">
<thead>
<tr>
<th width="5">
<?php echo JText::_( 'ID' ); ?>
</th>
<th>
<?php echo JText::_( 'Greeting' ); ?>
</th>
</tr>
</thead>
<?php
$k = 0;
for ($i=0, $n=count( $this->items ); $i < $n; $i++)
{
$row =& $this->items[$i];
?>
<tr class="<?php echo "row$k"; ?>">
<td>
<?php echo $row->id; ?>
</td>
<td>
<?php echo $row->greeting; ?>
</td>
</tr>
<?php
$k = 1 - $k;
}
?>
</table>
</div>
<input type="hidden" name="option" value="com_hello" />
<input type="hidden" name="task" value="" />
<input type="hidden" name="boxchecked" value="0" />
<input type="hidden" name="controller" value="hello" />
</form>
This template is saved as views/hellos/tmpl/default.php. You will notice that our output is enclosed in a form. Though this is not necessary now, it will be soon. We have now completed the basic part of the first view. We have added five files to the admin section of our component:
- admin.hello.php
- controller.php
- models/hellos.php
- views/hellos/view.html.php
- views/hellos/tmpl/default.php
You can now add these files to the XML install file and give it a try!
Adding Functionality
So far our administrator section is pretty useless. It doesn’t really do anything – all it does is display the entries that we have in our database. In order to make it useful, we need to add some buttons and links.
The Toolbar
You may have noticed the toolbar that appears at the top of other Joomla! component administrator panels. Our component needs one as well. Joomla! makes this very easy to do. We will add buttons Delete records, Edit records, and create New records. We will also add a title that will be displayed on our toolbar. This is done by adding code to the view. To add the buttons, we use static methods from the Joomla! JToolBarHelper class. The code looks like:
JToolBarHelper::title( JText::_( 'Hello Manager' ), 'generic.png' );
JToolBarHelper::deleteList();
JToolBarHelper::editListX();
JToolBarHelper::addNewX();
These three methods will create the appropriate buttons. The deleteList() method can optionally take up to three parameters – the first parameter is a string to display to the user to confirm that they want to delete the records. The second is the task that should be sent with the query (the default is ‘remove’), and the third is the text that should be displayed below the button.
The editListX() and addNewX() methods can each take two optional parameters. The first is the task (which are by default edit and add, respectively), and the second is the text that should be displayed below the button.
You may have noticed the use of the JText::_ method in the template before and here as well. This is a handy function that makes component translation much easier. The JText::_ method will look up the string in your component language file and return the translated string. If not translation is found, it will return the string that you passed it. If you want to translate your component into another language, all you have to do is create a language file that will map the strings within the quotes to the translated version of the string.
Checkboxes and Links
We now have buttons. Two of those buttons operate on existing records. But how do we know which records to operate on? We have to let the user tell us. To do this, we need to add checkboxes to our table so that the user can select certain records. This is done in our template.
In order to the add the checkboxes, we need to add an extra column into our table. We will add the column in between the two that we already have.
In the header of the column, we will add a checkbox which can be used to toggle all the boxes below it on or off:
<th width="20">
<input type="checkbox" name="toggle" value="" onclick="checkAll(<?php echo count( $this->items ); ?>);" />
</th>
The Javascript checkAll function is a function that is built into the Joomla! base Javascript package that provides the functionality that we want here.
Now we need to add the checkboxes into the individual rows. Joomla!’s JHTML class has a method, JHTML::_(), which will generate our checkbox for us. We will add the following line to our loop:
$checked = JHTML::_( 'grid.id', $i, $row->id );
after the line:
$row =& $this->items[$i];
Then we will add a cell in between the two that we already have:
<td>
<?php echo $checked; ?>
</td>
It can be cumbersome to have to check the box that we want to edit and then move up and click the edit button. Therefore, we will add a link that it will go straight to the greeting’s edit form. We will add the following line after the call to the JHTML::_() method to generate the link HTML:
$link = JRoute::_( 'index.php?option=com_hello>controller=hello>task=edit>cid[]='. $row->id );
And we include the link in the cell showing the greeting text:
<td>
<a href="<?php echo $link; ?>"><?php echo $row->greeting; ?></a>
</td>
You will notice that this link points to the hello controller. This controller will handle the data manipulation of our greetings.
If you recall from above, we had four hidden input fields at the bottom of our form. The first input field was named ‘option’. This field is necessary so that we stay in our component. The second input field was task. This form property gets set when one of the buttons in the toolbar is clicked. A Javascript error will result and the buttons will not work if this input field is omitted. The third input field is the boxchecked field. This field keeps track of the number of boxes that are checked. The edit and delete buttons will check to ensure that this is greater than zero and will not allow the form to be submitted if it is not. The fourth input field is the controller field. This is used to specify that tasks fired from this form will be handled by the hello controller.
Here is the code for the completed default.php file:
<?php defined('_JEXEC') or die('Restricted access'); ?>
<form action="index.php" method="post" name="adminForm">
<div id="editcell">
<table class="adminlist">
<thead>
<tr>
<th width="5">
<?php echo JText::_( 'ID' ); ?>
</th>
<th>
<?php echo JText::_( 'Greeting' ); ?>
</th>
</tr>
</thead>
<?php
$k = 0;
for ($i=0, $n=count( $this->items ); $i < $n; $i++)
{
$row =& $this->items[$i];
?>
<tr class="<?php echo "row$k"; ?>">
<td>
<?php echo $row->id; ?>
</td>
<td>
<?php echo $row->greeting; ?>
</td>
</tr>
<?php
$k = 1 - $k;
}
?>
</table>
</div>
<input type="hidden" name="option" value="com_hello" />
<input type="hidden" name="task" value="" />
<input type="hidden" name="boxchecked" value="0" />
<input type="hidden" name="controller" value="hello" />
</form>
Our hellos view is now complete. You can try out the component now to see the results.
The component can be downloaded here:
http://dev.joomla.org/components/com_jd-wiki/data/media/components/com_hello4a.zip
Getting Down and Dirty: Doing the Real Work
Now that the Hellos view is done, it is time to move to the Hello view and model. This is where the real work will get done.
The Hello Controller
Our default controller just isn’t going to cut it when it comes to doing work – all it is capable of doing is displaying views.
We need to be able to handle the tasks that we are launching from the Hellos view: add, edit and remove.
Add and edit are essentially the same task: they both display a form to the user that allows a greeting to be edited. The only difference is that new displays a blank form, and edit displays a form with data already in it. Since they are similar, we will map the add task onto the edit task handler. This is specified in our constructor:
/**
* constructor (registers additional tasks to methods)
* @return void
*/
function __construct()
{
parent::__construct();
// Register Extra tasks
$this->registerTask( 'add', 'edit' );
}
The first parameter of JController::registerTask is the task to map, and the second is the method to map it to.
We will start with handling the edit task. The controller’s job is fairly simple for the edit task. All it has to do is specify the view and layout to load (the hello view and the form layout). We will also tell Joomla! to disable the mainmenu while we are editing our greeting. This prevents users from leaving unsaved records open.
Our edit task handler looks like:
/**
* display the edit form
* @return void
*/
function edit()
{
JRequest::setVar( 'view', 'hello' );
JRequest::setVar( 'layout', 'form' );
JRequest::setVar( 'hidemainmenu', 1 );
parent::display();
}
The Hello View
The Hello view will display a form which will allow the user to edit a greeting. The display method if the hello view has to do a few simple tasks:
- retrieve the data from the model
- create the toolbar
- pass the data into the template
- invoke the display() method to render the template
This becomes a bit more complicated because the one view handles both the edit and add tasks. In our toolbar we want the user to know what whether they are adding or editing, so we have to determine which task was fired.
Since we are already retrieving the record that we want to display from the model, we can use this data to determine what task was fired. If the task was edit, then the id field of our record will have been set. If the task was new, then it will not have been set. This can be used to determine if we have a new record or an existing record.
We will add two buttons to the toolbar: save and cancel. Though the functionality will be the same, we want to display different buttons depending on whether it is a new or existing record. If it is a new record, we will display cancel. If it already exists, we will display close.
Thus our display method looks like this:
/**
* display method of Hello view
* @return void
**/
function display($tpl = null)
{
//get the hello
$hello =& $this->get('Data');
$isNew = ($hello->id < 1);
$text = $isNew ? JText::_( 'New' ) : JText::_( 'Edit' );
JToolBarHelper::title( JText::_( 'Hello' ).': <small><small>[ ' . $text.' ]</small></small>' );
JToolBarHelper::save();
if ($isNew) {
JToolBarHelper::cancel();
} else {
// for existing items the button is renamed `close`
JToolBarHelper::cancel( 'cancel', 'Close' );
}
$this->assignRef('hello', $hello);
parent::display($tpl);
}
The Hello Model
Our view needs data. Therefore, we need to create a model to model a hello.
Our model will have two properties: _id and _data. _id will hold the id of the greeting and data will hold the data.
We will start with a constructor, which will attempt to retrieve the id from the query:
/** * Constructor that retrieves the ID from the request * * @access public * @return void */ function __construct() { parent::__construct(); $array = JRequest::getVar('cid', 0, '', 'array'); $this->setId((int)$array[0]); }
The JRequest::getVar() method is used to retrieve data from the request. The first parameter is the name of the form variable. The second parameter is the default value to assign if there is no value found. The third parameter is the name of the hash to retrieve the value from (get, post, etc), and the last value is the data type that should be forced on the value.
Our constructor will take the first value from the cid array and assign it to the id.
Our setId() method can be used to set our id. Changing the id that our model points to will mean the id points to the wrong data. Therefore, when we set the id, we will clear the data property:
/** * Method to set the hello identifier * * @access public * @param int Hello identifier * @return void */ function setId($id) { // Set id and wipe data $this->_id = $id; $this->_data = null; }
Finally, we need a method to retrieve our data: getData()
getData will check if the _data property has already been set. If it has, it will simply return it. Otherwise, it will load the data from the database.
/** * Method to get a hello * @return object with data */ function &getData() { // Load the data if (empty( $this->_data )) { $query = ' SELECT * FROM #__hello '. ' WHERE id = '.$this->_id; $this->_db->setQuery( $query ); $this->_data = $this->_db->loadObject(); } if (!$this->_data) { $this->_data = new stdClass(); $this->_data->id = 0; $this->_data->greeting = null; } return $this->_data; }
The Form
Now all that is left is to create the form that the data will go into. Since we specified our layout as form, the form will go in a file in the tmpl directory of the hello view called form.php:
<?php defined('_JEXEC') or die('Restricted access'); ?> <form action="index.php" method="post" name="adminForm" id="adminForm"> <div class="col100"> <fieldset class="adminform"> <legend><?php echo JText::_( 'Details' ); ?></legend> <table class="admintable"> <tr> <td width="100" align="right" class="key"> <label for="greeting"> <?php echo JText::_( 'Greeting' ); ?>: </label> </td> <td> <input class="text_area" type="text" name="greeting" id="greeting" size="32" maxlength="250" value="<?php echo $this->hello->greeting;?>" /> </td> </tr> </table> </fieldset> </div> <div class="clr"></div> <input type="hidden" name="option" value="com_hello" /> <input type="hidden" name="id" value="<?php echo $this->hello->id; ?>" /> <input type="hidden" name="task" value="" /> <input type="hidden" name="controller" value="hello" /> </form>
Notice that in addition to the input field, there is a hidden field for the id. The user doesn’t need to edit the id (and shouldn’t), so we silently pass it along in the form.
Implementing the Functionality
So far, our controller only handles two tasks: edit and new. However, we also have buttons to save, delete and cancel records. We need to write code to handle and perform these tasks.
Saving a Record
The logical next step is to implement the functionality to save a record. Normally, this would require some switches and logic to handle various cases, such as the difference between creating a new record (an INSERT query), and updating an existing query (an UPDATE query). Also, there are complexities involved in getting the data from the form and putting it into the query.
The Joomla! framework takes care of a lot of this for you. The JTable class makes it easy to manipulate records in the database without having to worry about writing the SQL code that lies behind these updates. It also makes it easy to transfer data from an HTML form into the database.
Creating the Table Class
The JTable class is an abstract class from which you can derive child classes to work with specific tables. To use it, you simply create a class that extends the JTable class, add your database fields as properties, and override the constructor to specify the name of the table and the primary key.
Here is our JTable class:
<?php /** * Hello World table class * * @package Joomla.Tutorials * @subpackage Components * @link http://dev.joomla.org/component/option,com_jd-wiki/Itemid,31/id,tutorials:components/ * @license GNU/GPL */ // no direct access defined('_JEXEC') or die('Restricted access'); /** * Hello Table class * * @package Joomla.Tutorials * @subpackage Components */ class TableHello extends JTable { /** * Primary Key * * @var int */ var $id = null; /** * @var string */ var $greeting = null; /** * Constructor * * @param object Database connector object */ function TableHello( &$db ) { parent::__construct('#__hello', 'id', $db); } } ?>
You will see here that we have defined our two fields: the id field and the greeting field. Then we have defined a constructor, which will call the constructor of the parent class and pass it the name of the table (hello), the name of the field which is the primary key (id), and the database connector object.
This file should be called hello.php and it will go in a directory called tables in the administrator section of our component.
Implementing the Function in our Model
We are now ready to add the method to the model which will save our record. We will call this method store. Our store() method will do three things: bind the data from the form to the TableHello object, check to ensure that the record is properly formed, and store the record in the database.
Our method looks like:
/** * Method to store a record * * @access public * @return boolean True on success */ function store() { $row =& $this->getTable(); $data = JRequest::get( 'post' ); // Bind the form fields to the hello table if (!$row->bind($data)) { $this->setError($this->_db->getErrorMsg()); return false; } // Make sure the hello record is valid if (!$row->check()) { $this->setError($this->_db->getErrorMsg()); return false; } // Store the web link table to the database if (!$row->store()) { $this->setError($this->_db->getErrorMsg()); return false; } return true; }
This method gets added to the hello model.
The method takes one parameter, which is an associative array of data that we want to store in the database. This can easily be retrieved from the request as will be seen later.
You will see that the first line retrieves a reference to our JTable object. If we name our table properly, we don’t have to specify its name – the JModel class knows where to find it. You may recall that we called our table class TableHello and put it in a file called hello.php in the tables directory. If you follow this convention, the JModel class can create your object automatically.
The second line will retrieve the data from the form. The JRequest class makes this very easy. In this case, we are retrieving all of the variables that were submitted using the ‘POST’ method. These will be returned as an associative array.
The rest is easy – we bind, check and store. The bind() method will copy values from the array into the corresponding property of the table object. In this case, it will take the values of id and greeting and copy them to our TableHello object.
The check() method will perform data verification. In the JTable() class, this method simply returns true. While this doesn’t provide any value for us currently, by calling this method we make it possible to do data checking using our TableHello class in the future. This method can be overridden in our TableHello class with a method that performs the appropriate checks.
The store() method will take the data that is in the object and store it in the database. If the id is 0, it will create a new record (INSERT), otherwise, it will update the existing record (UPDATE).
Adding the Task to the Controller
We are now ready to add our task to the controller. Since the task that we are firing is called ‘save’, we must call our method ‘save’. This is simple:
/** * save a record (and redirect to main page) * @return void */ function save() { $model = $this->getModel('hello'); if ($model->store()) { $msg = JText::_( 'Greeting Saved!' ); } else { $msg = JText::_( 'Error Saving Greeting' ); } // Check the table in so it can be edited.... we are done with it anyway $link = 'index.php?option=com_hello'; $this->setRedirect($link, $msg); }
All we do is get our model and invoke the store() method. Then we use the setRedirect() method to redirect back to our list of greetings. We also pass a message along, which will be displayed at the top of the page.
Deleting a Record
Implementing the Function in the Model
In the model, we will retrieve the list of IDs to delete and call the JTable class to delete them. Here it is:
/** * Method to delete record(s) * * @access public * @return boolean True on success */ function delete() { $cids = JRequest::getVar( 'cid', array(0), 'post', 'array' ); $row =& $this->getTable(); foreach($cids as $cid) { if (!$row->delete( $cid )) { $this->setError( $row->getErrorMsg() ); return false; } } return true; }
We invoke the JRequest::getVar() method to get the data from the request, then we invoke the $row->delete() method to delete each row. By storing errors in the model we make it possible to retrieve them later if we so choose.
Handling the Remove Task in the Controller
This is similar to the save() method which handled the save task:
/** * remove record(s) * @return void */ function remove() { $model = $this->getModel('hello'); if(!$model->delete()) { $msg = JText::_( 'Error: One or More Greetings Could not be Deleted' ); } else { $msg = JText::_( 'Greeting(s) Deleted' ); } $this->setRedirect( 'index.php?option=com_hello', $msg ); }
Cancelling the Edit Operation
To cancel the edit operation, all we have to do is redirect back to the main view:
/** * cancel editing a record * @return void */ function cancel() { $msg = JText::_( 'Operation Cancelled' ); $this->setRedirect( 'index.php?option=com_hello', $msg ); }
Conclusion
We have now implemented a basic backend to our component. We are now able to edit the entries that are viewed in the frontend. We have demonstrated the interaction between models, views and controllers. We have shown how the JTable class can be extended to provide easy access to tables in the database. It can also be seen how the JToolBarHelper class can be used to create button bars in components to present a standardised look between components.
Contributors
- staalanden
Download
The component can be downloaded here: com_hello4.zip
http://dev.joomla.org/components/com_jd-wiki/data/media/components/com_hello4.zip
http://dev.joomla.org/component/option,com_jd-wiki/Itemid,/id,components:hello_world_mvc4/