10. Components Part IV: Example Component Front End

In the previous chapter, we created the back end of our example component. In this chapter, we create the front end of the component and create an installable zip archive file. Then we will add a new report to the component to demonstrate how the model-view-controller (MVC) design pattern makes it easy to add to the component’s functionality.

Files Overview

Table 10.1 shows the front-end files for our component, excluding index.html files. The file names are relative to the components/com_joomprosubs folder.

Table 10.1. Front-End Subscriptions Component Folders

Image

Installation XML File

In the previous chapter, we created the file administrator/components/com_joomprosubs/joomprosubs.xml with the information needed for the back-end files. We left out the front-end files so we could install and test just the administrative back end of the component.

In this chapter, we complete the front end of the component. So let’s begin by adding the front-end (or site) files to the XML file, as follows:

   . . .
  </uninstall>
  <files folder="site">
    <folder>controllers</folder>
    <folder>helpers</folder>
    <folder>language</folder>
    <folder>models</folder>
    <folder>views</folder>
    <filename>controller.php</filename>
    <filename>index.html</filename>
    <filename>joomprosubs.php</filename>
  </files>
  <administration>
    . . .

The highlighted lines are the new lines in the file. This adds a files element between the uninstall and administration elements. As before, this lists top-level files and folders only. The folder attribute of “site” tells the installer to copy these folders in the front-end components folder.

Component Entry Point

The first thing we need to do is create our front-end component folder, components/com_joomprosubs. Because all the front-end files are in this folder, we will refer to the front-end files relative to this location.

The entry point for the front end of our component is the file joomprosubs.php. This has the expected code as follows:

defined('_JEXEC') or die;

jimport('joomla.application.component.controller'),

$controller = JController::getInstance('Joomprosubs'),
$controller->execute(JRequest::getCmd('task'));
$controller->redirect();

Default Controller

Our default controller, which handles the display task, is a class called JoomproSubsController (controller.php). The code for this class is as follows:

defined('_JEXEC') or die;

jimport('joomla.application.component.controller'),

/**
 * Joomprosubs Component Controller
 *
 */
class JoomproSubsController extends JController
{
   /**
    * Method to display a view.
    *
    * @param   boolean    If true, the view output will be cached
    * @param   array      An array of safe url parameters and their variable types, for valid values see {@link JFilterInput::clean()}.
    *
    * @return  JController   This object to support chaining.
    */
   public function display($cachable = false, $urlparams = false)
   {
      // Initialise variables.
      $cachable = true;
      $user = JFactory::getUser();

      // Set the default view name and format from the Request.
      // Note we are using sub_id to avoid collisions with the router and the return page.
      $id = JRequest::getInt('sub_id'),
      $vName = JRequest::getCmd('view', 'category'),
      JRequest::setVar('view', $vName);
      if ($user->get('id')) {
          $cachable = false;
      }

      $safeurlparams = array(
          'id'                => 'INT',
          'limit'             => 'INT',
          'limitstart'        => 'INT',
          'filter_order'      => 'CMD',
          'filter_order_Dir'  => 'CMD',
          'lang'              => 'CMD'
      );

      // Check for edit form.
      if ($vName == 'form' && !$this->checkEditId( 'com_joomprosubs.edit.subscription', $id)) {
          // Somehow the person just went to the form - we don't allow that.
          return JError::raiseError(403, JText::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id));
      }

      return parent::display($cachable,$safeurlparams);
   }
}

This class is almost the same as the Weblinks front-end default controller. We need to determine whether or not the view will be cacheable—whether we can save a previous copy of the view in a quick-loading cache file. If the user is logged in, we won’t try to use a cache file. In that case, we need to check the database each time to see what permissions the user might have for subscribing to an item, so caching won’t work.

The other thing we do here is get the subscription id and the view name. We set the default view to category in case we don’t have one in the request. In our example, we only have one view, called category.

We also create an array called $safeurlparams. This contains all the valid parameters for the URL. Any other parameters in the URL are invalid, so this allows us to remove any invalid code from the URL.

Finally, as we have seen elsewhere, we return the result of the parent’s (JController) display() method. That method returns the variable $this. As discussed previously, returning $this allows us to do method chaining.

Subscription-Category View

Our component only has one front-end view, called category. This is similar to core category list views. It shows all the subscriptions in a single category. Let’s first look at how we create a menu item for this view.

Menu Item XML File

Each menu item provides a set of options (also called parameters) that control the display of the menu item. As with other options, these are specified in an XML file. The XML file is located in the tmpl subfolder of the view and is named the same as the layout file for that menu item.

In our example, this file is called views/category/tmpl/default.xml. Its contents are as follows:

<?xml version="1.0" encoding="utf-8"?>
<metadata>
  <layout title="COM_JOOMPROSUBS_CATEGORY_VIEW_DEFAULT_TITLE"
    option="COM_JOOMPROSUBS_CATEGORY_VIEW_DEFAULT_OPTION">
    <help url="COM_JOOMPROSUBS_CATEGORY_LIST_HELP_LINK"
    />
    <message>
      <![CDATA[COM_JOOMPROSUBS_CATEGORY_VIEW_DEFAULT_DESC]]>
    </message>
  </layout>

  <!-- Add fields to the request variables for the layout. -->
  <fields name="request">
    <fieldset name="request">
      <field name="id" type="category"
        default="0"
        description="COM_JOOMPROSUBS_FIELD_SELECT_CATEGORY_DESC"
        extension="com_joomprosubs"
        label="COM_JOOMPROSUBS_FIELD_SELECT_CATEGORY_LABEL"
        required="true"
      />
    </fieldset>
  </fields>

  <!-- Add fields to the parameters object for the layout. -->
  <fields name="params">
    <fieldset name="basic" label="JGLOBAL_CATEGORY_OPTIONS">
      <field name="show_description" type="list"
        description="JGLOBAL_SHOW_CATEGORY_DESCRIPTION_DESC"
        label="JGLOBAL_SHOW_CATEGORY_DESCRIPTION_LABEL"
      >
        <option value="0">JHIDE</option>
        <option value="1">JSHOW</option>
      </field>
    </fieldset>
  </fields>
</metadata>

The layout element specifies the title and description that will show when the list of available menu items is displayed. Here we are again using a URL for the help file. When we are adding or editing a subscription-category list menu item and click the Help icon in the toolbar, the URL specified in our language file will be loaded in a new browser window. This allows us to have language-specific help sites.

We then have two fields elements. The first one is called request and has a fieldset element called request. This element will be added to the request variable when this menu item is loaded. Typically, we have one request field in a menu item, such as category id or article id. In this case, we specify the type as category and the extension as com_joomprosubs. This uses the core JFormFieldCategory class (in the core file libraries/joomla/form/fields/category.php) to present the user with a list box of all categories for this component.

The folder libraries/joomla/form/fields contains class files that support the predefined types for JForm fields. In Chapter 6 we created a custom JFormRule class that worked when we set the validate attribute in a JForm field element. In a similar fashion we can, if needed, create custom JFormField classes. To do this, we include an attribute called “addfieldpath” in the JForm fieldset element that points to the folder of the custom class. The type attribute of a field matches the file name of the custom field class. This technique is used in a few places in Joomla!—for example, in administrator/components/com_content/config.xml.

Back in our menu item’s XML file, we have a second fields element called params. We have one fieldset, called basic, and one field, called show_description. This is exactly the same as other parameter fields we have seen previously. Here we give the user the option to display the category description.

Figure 10.1 shows screenshots of these two options when you create a new subscription-category list menu item.

Image

Figure 10.1. Menu item options (both shown expanded)

Category View

Next let’s look at the view class, JoomproSubsViewCategory. This is the file views/category/view.html.php. The first part of the code in this file is as follows:

defined('_JEXEC') or die;

jimport('joomla.application.component.view'),

/**
 * HTML View class for the JoomproSubs component
 *
 */
class JoomprosubsViewCategory extends JView
{
   protected $state;
   protected $items;
   protected $category;
   protected $children;
   protected $pagination;

   function display($tpl = null)
   {
     $app = JFactory::getApplication();
     $params = $app->getParams();

     // Get some data from the models
     $state = $this->get('State'),
     $items = $this->get('Items'),
     $category = $this->get('Category'),
     $pagination = $this->get('Pagination'),

     // Check for errors.
     if (count($errors = $this->get('Errors'))) {
       JError::raiseError(500, implode(" ", $errors));
       return false;
     }

     if ($category == false) {
       return JError::raiseWarning(404, JText::_('JGLOBAL_CATEGORY_NOT_FOUND'));
     }

This code is similar to the category view for Weblinks. We have some fields for the class and a method called display(). Recall that this method is called from the display() method of the controller. We get the state, list of items, category, and pagination from the model. Then we check for errors.

The next section of the display() method is as follows:

   // Check whether category access level allows access.
   $user = JFactory::getUser();
   $groups = $user->getAuthorisedViewLevels();
   if (!in_array($category->access, $groups)) {
     return JError::raiseError(403, JText::_('JERROR_ALERTNOAUTHOR'));
   }

   // Prepare the data.
   // Compute the joomprosub slug & link url.
   for ($i = 0, $n = count($items); $i < $n; $i++)
   {
     $item = &$items[$i];
     $item->slug  = $item->alias ? ($item->id.':'.$item->alias) : $item->id;
   }

   // Setup the category parameters.
   $cparams = $category->getParams();
   $category->params = clone($params);
   $category->params->merge($cparams);

   $this->state = $state;
   $this->items = $items;
   $this->category = $category;
   $this->params = $params;
   $this->pagination = $pagination;

   //Escape strings for HTML output
   $this->pageclass_sfx = htmlspecialchars( $params->get('pageclass_sfx'));

First, we check that the user is authorized to view this category. We use the getAuthorisedViewLevels() method of the JUser class to get an array of the view levels and then check that the view level for this category is in that array. Otherwise, we show a 403 error.

Next we loop through each item and calculate its “slug” value. This is item id followed by a colon and the alias. Then we merge the category parameters with the menu item parameters. In our example, we don’t have any special category parameters. However, we might want to allow the site administrator to specify parameters at the category and component levels and then merge these with the parameters for this menu item.

It is important to understand how the JRegistry merge() method works. This method is the key to understanding how hierarchical parameters are implemented in Joomla. In this example, we have this code:

$cparams = $category->getParams();
$category->params = clone($params);
$category->params->merge($cparams);

The first line puts the category-level parameters in $cparams. The second line uses the PHP clone() function to make a new copy of the current component-level parameters and stores this in the params field of the JCategory object. We use the clone() function so that we don’t change the original $params object. The last line does the merge.

When we call the merge method for a JRegistry object, we have the calling object (in this case $category->params) and the object being merged (in this case $cparams). The logic for merging the two objects is as follows:

• If a value is set in one object and not set (or undefined) in the other, the set value is used.

• If a value is set in both objects, the calling object’s value takes priority over the argument object’s value.

In core components, we use this to inherit global settings at lower levels in the hierarchy. The value “Use Global” equates to a blank or unset value. For example, if we had menu item parameters, they would take priority over component-level parameters. When the JRegistry objects are merged, the JRegistry object for menu item parameters is the calling object and the component level is the merged object. If the menu item has a blank value (equating to Use Global), the component-level value will be applied to the merged object. If both objects have a value set, the menu item value is used (because it is the calling object).

The next block sets the class fields for state, items, category, params, and pagination. This allows them to be available when we are inside the layout scripts. Then we add the page class suffix, using the htmlspecialchars() function to escape it to protect against any embedded code.

The last part of the display() method is as follows:

    // Check for layout override only if this is not the active menu item
   // If it is the active menu item, then the view and category id will match
   $active = $app->getMenu()->getActive();
   if ((!$active) || (strpos($active->link, 'view=category') === false)) {
     if ($layout = $category->params->get('category_layout')) {
     $this->setLayout($layout);
     }
   }
   elseif (isset($active->query['layout'])) {
     // We need to set the layout in case this is an alternative menu item (with an alternative layout)
     $this->setLayout($active->query['layout']);
   }

   $this->_prepareDocument();

   parent::display($tpl);
}

This checks for alternative layout files. In our example, this logic is not strictly needed. However, it is important to understand. It comes into play when (a) there is more than one type of layout possible for a category and (b) you link to a category layout that doesn’t have an associated menu item.

In this case, we need to know which layout to use for the category. This can be set at the component or the category level. For example, if you look at the Article Manager → Options → Category, there is an option called Choose Layout that lets you choose whether to use a Blog or List layout as the default.

The logic in the code is as follows. If we are in the active menu item, we know we have a menu item. If we are not processing a category view, we don’t have to worry about it. If we (a) are not in the active menu item and (b) are processing a category view, then we get the alternative layout for the category and use that. Otherwise, we check if there is a layout specified in the URL query and use that.

After we have processed the alternative layout logic, we call the _prepareDocument() method and then the parent’s display() method. The _prepareDocument() method is similar to the corresponding method in the Weblinks component. The first part of this method is as follows:

protected function _prepareDocument()
{
  $app = JFactory::getApplication();
  $menu = $app->getMenu()->getActive();
  $pathway = $app->getPathway();
  $title = null;

  if ($menu) {
     $this->params->def('page_heading', $this->params->get ('page_title', $menu->title));
  }
  else {
     $this->params->def('page_heading', JText::_('COM_JOOMPROSUBS_DEFAULT_PAGE_TITLE'));
  }

  $title = $this->params->get('page_title', ''),

  if (empty($title)) {
     $title = $app->getCfg('sitename'),
  }
  elseif ($app->getCfg('sitename_pagetitles', 0)) {
     $title = JText::sprintf('JPAGETITLE', $app->getCfg('sitename'), $title);
  }
  elseif ($app->getCfg('sitename_pagetitles', 0) == 2) {
     $title = JText::sprintf('JPAGETITLE', $title, $app->getCfg('sitename'));
  }

  $this->document->setTitle($title);

This sets the page title based on a hierarchy, starting with the menu title, then the component default, then the site name, and then some global configuration. The idea here is to have the most specific page title that is defined, but also to make sure you have something in the page title.

The remainder of this method and this class is as follows:

     if ($this->category->metadesc) {
         $this->document->setDescription($this->category->metadesc);
     }
     elseif (!$this->category->metadesc && $this->params->get( 'menu-meta_description')) {
         $this->document->setDescription($this->params->get( 'menu-meta_description'));
     }

     if ($this->category->metakey) {
         $this->document->setMetadata('keywords', $this->category->metakey);
     }
     elseif (!$this->category->metakey && $this->params->get( 'menu-meta_keywords')) {
         $this->document->setMetadata('keywords', $this->params->get('menu-meta_keywords'));
     }

     if ($this->params->get('robots')) {
         $this->document->setMetadata('robots', $this->params->get('robots'));
     }

     if ($app->getCfg('MetaTitle') == '1') {
         $this->document->setMetaData('title', $this->category->getMetadata()->get('page_title'));
     }

     if ($app->getCfg('MetaAuthor') == '1') {
         $this->document->setMetaData('author', $this->category->getMetadata()->get('author'));
     }

     $mdata = $this->category->getMetadata()->toArray();

     foreach ($mdata as $k => $v)
     {
         if ($v) {
           $this->document->setMetadata($k, $v);
         }
     }
  }
} // end of class

This adds the metadata to the document from the category or the menu item. Again, the idea is to use the most specific metadata that is available.

Model

In the previous section, we called the methods getState(), getItems(), getCategory(), and getPagination() from the model. Our model is JoomproSubsModelCategory (models/category.php). Let’s review its code.

The first section is as follows:

defined('_JEXEC') or die;

jimport('joomla.application.component.modellist'),
jimport('joomla.application.categories'),

/**
 * Joomprosubs Component Joomprosub Model
 *
 * @package        Joomla.Site
 * @subpackage com_joomprosubs
 */
class JoomprosubsModelCategory extends JModelList
{
   /**
    * Category items data
    *
    * @var array
    */
   protected $_item = null;

   /**
    * Constructor.
    *
    * @param   array   An optional associative array of configuration settings.
    * @see     JController
    */
   public function __construct($config = array())
   {
        if (empty($config['filter_fields'])) {
           $config['filter_fields'] = array(
               'id', 'a.id',
               'title', 'a.title',
               'g.title', 'group_title',
               'duration', 'a.duration'
           );
        }

        parent::__construct($config);
   }

Our model extends JModelList, which in turn extends JModel. Note that we inherit the getState() method from JModel and the getPagination() method from JModelList. Note also that we import the JCategories class, which we will use in the getCategory() method. We have one field called $_item. We will use this to save the category object.

The constructor creates an array of filter fields that we use to check that the ordering column is valid. Then we call the parent’s constructor.

The next method in the class is getListQuery(). The first part of this method is as follows:

   protected function getListQuery()
   {
      $user = JFactory::getUser();
      $groups = implode(',', $user->getAuthorisedViewLevels());

      // Create a new query object.
      $db      = $this->getDbo();
      $query   = $db->getQuery(true);

      // Select required fields from the categories.
      $query->select($this->getState('list.select', 'a.*'));
      $query->select('g.title as group_title'),
      $query->from($db->quoteName('#__joompro_subscriptions').' AS a'),

      // Join on groups to get title of group
      $query->join('LEFT', $db->quoteName('#__usergroups').' AS g ON
a.group_id = g.id'),
      $query->where('a.access IN ('.$groups.')'),
      // Filter by category.
      if ($categoryId = $this->getState('category.id')) {
         $query->where('a.catid = '.(int) $categoryId);
         $query->join('LEFT', $db->quoteName('#__categories').' AS c ON
c.id = a.catid'),
         $query->where('c.access IN ('.$groups.')'),

         //Filter by published category
         $cpublished = $this->getState('filter.c.published'),
         if (is_numeric($cpublished)) {
            $query->where('c.published = '.(int) $cpublished);
         }
      }

In this method, we build the query for the list layout. We include all columns from the #__joompro_subscriptions table and the title from the user groups table. We only include subscriptions in an access level group to which the current user is allowed access. Then we filter on category id and the category published state.

The last part of the method is as follows:

       // Filter by state
      $state = $this->getState('filter.state'),
      if (is_numeric($state)) {
         $query->where('a.published = '.(int) $state);
      }

      // Filter by search
      if ($this->getState('list.filter') != '') {
         $filter = JString::strtolower($this->getState('list.filter'));
         $filter = $db->quote('%'.$filter.'%', true);
         $query->where('a.title LIKE ' . $filter);
      }

      // Filter by start and end dates.
      $nullDate = $db->quote($db->getNullDate());
      $nowDate = $db->quote(JFactory::getDate()->toSQL());

      if ($this->getState('filter.publish_date')){
         $query->where('(a.publish_up = ' . $nullDate . ' OR a.publish_up <= ' . $nowDate . ')'),
         $query->where('(a.publish_down = ' . $nullDate . ' OR a.publish_down >= ' . $nowDate . ')'),
      }

      // Add the list ordering clause.
      $query->order($db->getEscaped($this->getState('list.ordering', 'a.title')).
         ' '.$db->getEscaped($this->getState('list.direction', 'ASC')));
      return $query;
   }

Here we add WHERE clauses to our query to filter on the published state of the category, any filtering text the user typed in, and the start and stop publishing dates for the subscription. Finally, we order the list based on the column clicked by the user and return the query object.

Image

The list filter is text the user types in to filter the list. For example, they might type in “Chevy” to only show subscriptions with “Chevy” in the title. This text gets incorporated into our SQL query, so we have to be careful to filter out malicious SQL code. We do this using the quote() method of the database object. By setting the second argument to true, we tell the method to run its escape() method on the first argument. This escapes any special characters that could be used to inject malicious SQL code into our query.

Next is the populateState() method, which is as follows:

     protected function populateState($ordering = null, $direction = null)
    {
        // Initialise variables.
        $app   = JFactory::getApplication();
        $params       = JComponentHelper::getParams('com_joomprosubs'),

        // List state information
        $limit = $app->getUserStateFromRequest('global.list.limit', 'limit', $app->getCfg('list_limit'));
        $this->setState('list.limit', $limit);

        $limitstart = JRequest::getVar('limitstart', 0, '', 'int'),
        $this->setState('list.start', $limitstart);

        $orderCol = JRequest::getCmd('filter_order', 'title'),
        if (!in_array($orderCol, $this->filter_fields)) {
           $orderCol = 'ordering';
        }
        $this->setState('list.ordering', $orderCol);

        $listOrder = Request::getCmd('filter_order_Dir', 'ASC'),
        if (!in_array(strtoupper($listOrder), array('ASC', 'DESC', ''))) {
           $listOrder = 'ASC';
        }
        $this->setState('list.direction', $listOrder);

        $this->setState('list.filter', JRequest::getString( 'filter-search'));
        $$id = JRequest::getInt('id', 0);
        $this->setState('category.id', $id);

        $user = JFactory::getUser();
        if ((!$user->authorise('core.edit.state', 'com_joomprosubs')) && (!$user->authorise('core.edit', 'com_joomprosubs'))){
           // limit to published for people who can't edit or edit.state.
           $this->setState('filter.state',   1);

           // Filter by start and end dates.
           $this->setState('filter.publish_date', true);
        }
    }

Here, we set the state object’s fields based on the request variable. We use the getUserStateFromRequest() method to get the setting for the list limit (the number of items to display on the page). This reads this value from the session object. Using this method allows us to save the value in the session and “remember” what the user has specified for this screen.

Next we get the limit start. This tells the database where to start when we are paging through a long list. Note that we use the getInt() method of JRequest to guarantee that we have an integer for this.

Then we get the ordering column and direction. Note that we use the field called filter_fields, which we created in the constructor to validate the ordering column. We also validate the direction to be either ASC or DESC.

Then we set the list.filter field of the state based on input the user typed in to filter the list. This is a point of potential vulnerability in our application. The user can type any string in this field, and the getString() method only checks that we have a valid string and does not do any filtering of malicious content. That is why we used the $db->quote() method when we built the query.

Next we get the category id from the request and set it in the state. Finally, if the user does not have permission to edit state, we only allow them to see published items.

The last method in the file is getCategory(), as follows:

     public function getCategory()
     {
        if(!is_object($this->_item))
        {
           $categories = JCategories::getInstance('Joomprosubs'),
           $this->_item = $categories->get($this->getState('category.id', 'root'));
        }

        return $this->_item;
     }
} // end of class

Here we simply get the category object based on the category id. Note that we only do this once and save it in the field $_item. If we need it again, we just get it from the field.

Category Helper File

Note that when we call the getInstance() method of JCategories, we in turn call the __construct() method of the JoomproSubsCategories class (helpers/category.php). This class has the following code:

defined('_JEXEC') or die;

// Component Helper
jimport('joomla.application.component.helper'),
jimport('joomla.application.categories'),

/**
 * Joomprosubs Component Category Tree
 *
 * @static
 * @package    Joomla.Site
 * @subpackage com_joomprosubs
 */
class JoomproSubsCategories extends JCategories
{
    public function __construct($options = array())
    {
       $options['table'] = '#__joompro_subscriptions';
       $options['extension'] = 'com_joomprosubs';
       $options['statefield'] = 'published';
       parent::__construct($options);
    }
} // end of class

This file provides the specific information needed for categories for this extension, including the table name, the extension name, and the fact that we have a column called published where we store our state information.

Category Layout Files

Two layout files are used to render the category list. The entry point is the file views/category/tmpl/default.php, whose code is as follows:

<?php
/**
 * @package    Joomla.Site
 * @subpackage com_joomprosubs
 * @copyright  Copyright (C) 2012 Mark Dexter and Louis Landry. All rights reserved.
 * @license    GNU General Public License version 2 or later; see LICENSE.txt
 */
// no direct access
defined('_JEXEC') or die;
JHtml::addIncludePath(JPATH_COMPONENT.'/helpers'), ?>
<div class="joomprosub-category<?php echo $this->pageclass_sfx;?>">
<?php if ($this->params->def('show_page_heading', 1)) : ?>
<h1>
   <?php echo $this->escape($this->params->get('page_heading')); ?>
</h1>
<?php endif; ?>
<?php if($this->params->get('show_category_title', 1)) : ?>
<h2>
   <?php echo JHtml::_('content.prepare', $this->category->title); ?>
</h2>
<?php endif; ?>
<?php if ($this->params->get('show_description', 1) || $this->params->def('show_description_image', 1)) : ?>
   <div class="category-desc">
   <?php if ($this->params->get('show_description_image') && $this->category->getParams()->get('image')) : ?>
      <img src="<?php echo $this->category->getParams()->get('image'), ?>"/>
   <?php endif; ?>
   <?php if ($this->params->get('show_description') && $this->category->description) : ?>
      <?php echo JHtml::_('content.prepare', $this->category->description); ?>
   <?php endif; ?>
   <div class="clr"></div>
   </div>
<?php endif; ?>
<?php echo $this->loadTemplate('items'), ?>
</div>

Again, this is similar to the corresponding file for Weblinks or the other category list layouts. We display the category description and image. Then we use the loadTemplate() method to load the default_items.php layout file.

The first part of the default_items.php file is as follows:

<?php
/**
 * @subpackage  com_joomprosubs
 * @copyright   Copyright (C) 2012 Mark Dexter and Louis Landry. All rights reserved.
 * @license     GNU General Public License version 2 or later; see LICENSE.txt
 */

// no direct access
defined('_JEXEC') or die;
// Code to support edit links for joomaprosubs
// Create a shortcut for params.

JHtml::addIncludePath(JPATH_COMPONENT.'/helpers/html'),
JHtml::_('behavior.tooltip'),
JHtml::core();

// Get the user object.
$user = JFactory::getUser();
// Check if user is allowed to add/edit based on joomprosubs permissions.
$canEdit = $user->authorise('core.edit', 'com_joomprosubs.category.' . $this->category->id);

$listOrder = $this->escape($this->state->get('list.ordering'));
$listDirn = $this->escape($this->state->get('list.direction'));
$listFilter = $this->state->get('list.filter'),
?>

<?php if (empty($this->items)) : ?>
  <p> <?php echo JText::_('COM_JOOMPROSUBS_NO_JOOMPROSUBS'), ?></p>
<?php else : ?>

<form action="<?php echo htmlspecialchars(JFactory::getURI()-> toString()); ?>"
  method="post" name="adminForm" id="adminForm">
  <fieldset class="filters">
  <legend class="hidelabeltxt"><?php echo JText::_('JGLOBAL_FILTER_LABEL'), ?></legend>
  <div class="filter-search">
     <label class="filter-search-lbl" for="filter-search">
     <?php echo JText::_('COM_JOOMPROSUBS_FILTER_LABEL').' '; ?></label>
     <input type="text" name="filter-search" id="filter-search"
        value="<?php echo $this->escape($this->state->get('list.filter')); ?>"
        class="inputbox" onchange="document.adminForm.submit();"
        title="<?php echo JText::_('COM_CONTENT_FILTER_SEARCH_DESC'), ?>" />
  </div>
<div class="display-limit">
  <?php echo JText::_('JGLOBAL_DISPLAY_NUM'), ?>
  <?php echo $this->pagination->getLimitBox(); ?>
</div>
</fieldset>

We start with some housekeeping to load the helper files, the JavaScript tooltip behavior, and the core JavaScript behavior. Then we check if the user can edit this category. If so, we will show the category as a link to subscribe. Otherwise, we will just show the category as text.

Next we get the sort ordering field and direction and then the filter text, if any. Recall that the user can sort the list by clicking on the column headings.

Next we check to see if we have any items to process. If not, we output a message saying there are no subscriptions in this category. Otherwise, we proceed with the layout for the items.

Next we create the form element. The action is the URL back to this same page. We need a form to process the sorting by column and filtering based on user input. Then we render the filter text box and the display limit list box.

The next code block is as follows:

<table class="category">
  <thead><tr>
    <th class="title">
         <?php echo JHtml::_('grid.sort',  'COM_JOOMPROSUBS_GRID_TITLE',
         'a.title', $listDirn, $listOrder); ?>
    </th>
    <th class="group">
      <?php echo JHtml::_('grid.sort', 'COM_JOOMPROSUBS_GRID_GROUP',
         'g.title', $listDirn, $listOrder); ?>
    </th>
    <th class="duration">
      <?php echo JHtml::_('grid.sort', 'COM_JOOMPROSUBS_GRID_DURATION',
         'a.duration', $listDirn, $listOrder); ?>
    </th>
</tr></thead>

This renders the sortable column headings.

The next block is as follows:

  <tbody>
  <?php foreach ($this->items as $i => $item) : ?>
     <tr class="cat-list-row<?php echo $i % 2; ?>" >
     <td class="title">
       <?php if ($canEdit) : ?>
         <a href="<?php echo JRoute::_( 'index.php?option=com_joomprosubs&task=subscription.edit &sub_id='. $item->id.'&catid='.$item->catid); ?>">
         <?php echo $item->title; ?></a>
       <?php else: ?>
         <?php echo $item->title;?>
       <?php endif; ?>
       <?php if ($this->params->get('show_description')) : ?>
         <?php echo nl2br($item->description); ?>
       <?php endif; ?>
     </td>
     <td class="item-group">
       <?php echo $item->group_title; ?>
     </td>
     <td class="item-duration">
       <?php echo $item->duration; ?>
     </td>
     </tr>
  <?php endforeach; ?>
</tbody>
</table>

This loops through the array of items and renders each of the rows of the table. This includes the title, the description (if the parameter is set to show description), the subscription title, and the duration.

The last section of the file is as follows:

<div class="pagination">
  <p class="counter">
  <?php echo $this->pagination->getPagesCounter(); ?>
  </p>
  <?php echo $this->pagination->getPagesLinks(); ?>
</div>
<div>
  <input type="hidden" name="filter_order" value="<?php echo $listOrder; ?>" />
  <input type="hidden" name="filter_order_Dir" value="<?php echo $listDirn; ?>" />
</div>
</form>
<?php endif; ?>

This renders the pagination controls, closes the form, and ends the if-else block. An example of this layout is shown in Figure 10.2.

Image

Figure 10.2. Example of subscription layout

Subscription View

Next let’s look at the subscription view. This is where a user can subscribe to one of our subscriptions. If a user who has permission to edit this subscription category is logged in to the site, the subscription titles display as links to the subscribe form. For example, a subscription with an id of 3 shows the following link:

index.php/subscriptions?task=subscription.edit&sub_id=3

Clicking this link causes the screen in Figure 10.3 to appear.

Image

Figure 10.3. Subscribe form

Subscription Edit Controller Methods

We know from earlier work that the task “susbscription.edit” will cause the edit() method of the JoomproSubsControllerSubscription (controllers/subscription.php) class to be run. So clicking on this link will initiate this method. Let’s follow the logic for this view, starting with the controller.

The first part of the code is as follows:

<?php
/**
 * @copyright   Copyright (C) 2012 Mark Dexter and Louis Landry. All rights reserved.
 * @license     GNU General Public License version 2 or later; see LICENSE.txt
 */

// no direct access
defined('_JEXEC') or die;

jimport('joomla.application.component.controllerform'),
jimport('joomla.user.helper'),

class JoomproSubsControllerSubscription extends JControllerForm
{

   protected $view_item = 'form';

This imports the classes we will need and creates a field, $view_item, that we will use later in the class.

The next block of code defines the edit() method that is called when the subscription link is clicked. Here is the code:

public function edit($key = null, $urlVar = 'sub_id')
{
   $result = false;
   $itemid = JRequest::getInt('Itemid'),
   $catid = JRequest::getInt('catid'),
   if (($catid) && ($this->allowEdit($catid))) {
      $result = parent::edit($key, $urlVar);

      // Check in the subscription, since it was checked out in the edit method
      $this->getModel()->checkIn(JRequest::getInt($urlVar));
   }
   return $result;
}

This method gets the current menu item id and category id from the request. Then, if we have a category id and if the allowEdit() method for this category id returns a boolean true, we call the edit() method of the parent class. Because this method checks out the item, we immediately check it back in using our model. (Notice that we use method chaining to call the checkIn() method of the model.) We don’t need to hold the row for editing because we do not change the row for this subscription. As we will see, instead we add a row to the mapping table. Finally, we return the result, which will be true if the method was successful and otherwise false.

The next method in the controller that we use at this point in the process is getRedirectToItemAppend(). This method is called from the parent’s (JControllerForm) edit() method and is used to append the current Itemid to the redirect URL. Because we override this method in our class (JoomproSubsControllerSubscription), the override code in our class gets executed. The code is as follows:

protected function getRedirectToItemAppend($recordId = null, $urlVar = null)
{
   $append = parent::getRedirectToItemAppend($recordId, $urlVar);
   $itemId = JRequest::getInt('Itemid'),
   if ($itemId) {
      $append .= '&Itemid='.$itemId;
   }
   return $append;
}

This calls the parent’s method first. Then it appends the current Itemid and returns the result. This means that we keep the same Itemid value in our redirect so the modules and template that are assigned to this menu item will still show when we are redirected to the form.

The allowEdit() method of the controller is as follows:

protected function allowEdit($catid)
{
   return JFactory::getUser()->authorise('core.edit', $this->option.'.category.'.$catid);
}

This just checks that the current user has permissions in the access control list (ACL) to edit this category of subscriptions.

The getModel() method of the controller is as follows:

public function getModel($name = 'form', $prefix = '', $config = array('ignore_request' => true))
{
   $model = parent::getModel($name, $prefix, $config);
   return $model;
}

This just specifies that our model’s name is “form”, meaning that the full model name will be JoomproSubsModelForm.

Edit View and Form

The next step in the process is to execute the display() method of the view Joompro SubsViewForm (views/form/view.html.php). The first part of this class is as follows:

<?php
/**
 * @package    Joomla.Site
 * @subpackage com_joomprosubs
 * @copyright  Copyright (C) 2012 Mark Dexter and Louis Landry. All rights reserved.
 * @license    GNU General Public License version 2 or later; see LICENSE.txt
 */

// no direct accessdefined('_JEXEC') or die;
jimport('joomla.application.component.view'),

/**
 * HTML View class for the JoomproSubs component
 *
 */
class JoomprosubsViewForm extends JView{
   protected $state;
   protected $item;
   protected $category;

   function display($tpl = null)
   {
      $app = JFactory::getApplication();
      $params = $app->getParams();
      $user = JFactory::getUser();


      // Get some data from the model
      $item = $this->get('Item'),
      $this->form = $this->get('Form'),

Here we declare the class fields and start the display() method. We do some standard housekeeping and then get the item and form from the model.

Our model, JoomproSubsModelForm, extends the back-end model JoomproSubs ModelSubscription. The back-end model has a getForm() method and inherits getState() and getItem(). When called from our front-end class, the getForm() method loads the subscription.xml file from the folder components/com_joomprosubs/models/forms (not from the administrator/components folder). This is because we use the constant JPATH_COMPONENT, which has a different value when we are in the front or back end of the site.

The form’s XML file, models/forms/subscription.xml, is as follows:

<?xml version="1.0" encoding="UTF-8"?>
<form>
   <fields>
   <fieldset>
      <field name="subscription_terms" type="checkbox"
          default="0" filter="bool"
          label="COM_JOOMPROSUBS_FORM_TERMS_LABEL"
          desc="COM_JOOMPROSUBS_FORM_TERMS_DESC"
          required="true"
          value="1"
      />
   </fieldset>
   </fields>
</form>

This file defines one form field, which is a check box called subscription_terms.

Back in the view JoomproSubsViewForm (views/form/view.html.php), the rest of the display() method is as follows:

   $authorised = $user->authorise('core.edit', 'com_joomprosubs.category.' . $item->catid);

   if ($authorised !== true) {
      JError::raiseError(403, JText::_('JERROR_ALERTNOAUTHOR'));
      return false;
   }

   $this->form->bind($item);

   // Check for errors.
   if (count($errors = $this->get('Errors'))) {
      JError::raiseWarning(500, implode(" ", $errors));
      return false;
   }

   //Escape strings for HTML output
   $this->pageclass_sfx = htmlspecialchars($params->get('pageclass_sfx'));

   $this->params = $params;
   $this->user = $user;
   $this->item = $item;

   $this->_prepareDocument();
   parent::display($tpl);
}

Here we make sure the user has edit permissions for this category. Note that this is not strictly needed in this case. We have already checked this permission in the controller’s edit() method. However, it is good practice to put this check in the view as well. That way, if we use this method in a different context, it will ensure that the user has the correct permissions.

Then we call the bind() method and check for errors. Then we escape the page class suffix to eliminate any malicious code. Next we save the params, user, and item as fields so we can use them in the layout files. Finally, we call the _prepareDocument() method and the parent’s display method to invoke the layout files.

The _prepareDocument() method is also in this class. It has the following code:

     protected function _prepareDocument()
     {
        $app = JFactory::getApplication();
        $menus = $app->getMenu();
        $pathway = $app->getPathway();
        $title = null;

        // Because the application sets a default page title,
        // we need to get it from the menu item itself
        $menu = $menus->getActive();

        $head = JText::_('COM_JOOMPROSUBS_FORM_SUBMIT_SUB'),

        if ($menu) {
           $this->params->def('page_heading', $this->params->get('page_title', $menu->title));
        } else {
           $this->params->def('page_heading', $head);
        }

        $title = $this->params->def('page_title', $head);
        if ($app->getCfg('sitename_pagetitles', 0)) {
           $title = JText::sprintf('JPAGETITLE', $app->getCfg('sitename'), $title);
        }
        $this->document->setTitle($title);

        if ($this->params->get('menu-meta_description'))
        {
           $this->document->setDescription($this->params->get( 'menu-meta_description'));
        }

        if ($this->params->get('menu-meta_keywords'))
        {
           $this->document->setMetadata('keywords', $this->params->get('menu-meta_keywords'));
        }

        if ($this->params->get('robots'))
        {
           $this->document->setMetadata('robots', $this->params->get('robots'));
        }
     }
} // end of class

This method sets the document page heading, page title, description, keywords, and robots. It is almost exactly the same as the method in the other views. Note that this is a strong indication that we could eliminate some duplicate code if we created a method in the parent class and called that method.

Edit Layout

The subscribe layout is the file edit.php in the folder views/tmpl. Its code is as follows:

<?php

 /**
 * @copyright  Copyright (C) 2012 Mark Dexter and Louis Landry. All rights reserved.
 * @license    GNU General Public License version 2 or later; see LICENSE.txt
 */
defined('_JEXEC') or die;
JHtml::_('behavior.keepalive'),
JHtml::_('behavior.formvalidation'),
JHtml::_('behavior.tooltip'),
$itemid = JRequest::getInt('Itemid'),
?>

<div class="edit joomprosubs-edit<?php echo $this->pageclass_sfx; ?>">
  <form action="<?php echo JRoute::_('index.php'), ?>"
      id="adminForm" name="adminForm" method="post" class="form-validate">
    <fieldset>
    <legend><?php echo JText::_('COM_JOOMPROSUBS_FORM_LABEL'), ?></legend>
      <dl>
        <dt><?php echo JText::_('COM_JOOMPROSUBS_GRID_TITLE'), ?></dt>
        <dd><?php echo $this->escape($this->item->get('title')); ?></dd>
        <dt><?php echo JText::_('COM_JOOMPROSUBS_GRID_DESC'), ?></dt>
        <dd><?php echo $this->escape(strip_tags($this-> item->get('description'))); ?></dd>
        <dt><?php echo $this->form->getLabel('subscription_terms'), ?></dt>
        <dd><?php echo $this->form->getInput('subscription_terms'), ?></dd>
      </dl>
    </fieldset>
    <fieldset>
      <button class="button validate" type="submit"><?php echo JText::_('COM_JOOMPRPOSUBS_FORM_SUBMIT'), ?></button>
      <a href="<?php echo JRoute::_('index.php?option=com_joomprosubs&Itemid='. $itemid); ?>">
        <?php echo JText::_('COM_JOOMPRPOSUBS_FORM_CANCEL'), ?></a>
      <input type="hidden" name="option" value="com_joomprosubs" />
      <input type="hidden" name="task" value="subscription.subscribe" />
      <input type="hidden" name="sub_id" value="<?php echo $this->item->id; ?>" />
      <?php echo JHtml::_( 'form.token'), ?>
    </fieldset>
  </form>
</div>

This creates a form and then renders the one form field using the getLabel() and getInput() methods. It then creates a submit button that sets the task equal to “subscription.subscribe”. Recall in our subscription.xml file we set the required attribute for the check box to true. If the user clicks the submit button without checking this box, this will give an error message “Field required: I agree to the terms of use.” Also, note that we have provided a cancel link that takes us back to the original menu item.

Subscribe Task

If a user clicks the submit button in the edit form, we execute the subscribe() method in the controller. The code for this is in the JoomproSubsControllerSubscription class (controllers/subscription.php). The first part of this method is as follows:

   /**
    * Subscribe to a subscription.
    *
    * @param  string   $key     The name of the primary key of the URL variable.
    * @param  string   $urlVar  The name of the URL variable if different from the primary key
    *
    * @return   string   The return URL.
    */
public function subscribe ($key = null, $urlVar = 'sub_id')
{
   // Check that user is authorized
   $user = JFactory::getUser();
   if (!$user->authorise('core.edit', 'com_joomprosubs.category.' . $this->category->id)) {
      JError::raiseError(403, JText::_('JERROR_ALERTNOAUTHOR'));
      return false;
   }
   // Check that form data is valid
   if(!$this->validate()) {
      return false;
   }

   // Add user to group if not already a member
   $model = $this->getModel();
   $id = JRequest::getInt('sub_id'),
   $subscription = $model->getItem($id);

   // Set redirect without id in case of an error
   $this->setRedirect(JRoute::_( 'index.php?option=com_joomprosubs&view=form&layout=thankyou', false));

Once again, we check that the user has permissions. Recall that a hacker could simply alter the form to enter in the task and id, so we need to check that this is a legitimate user. Then we execute the validate() method to make sure the data is valid. We discuss this method a bit later. Then we use the model to get the subscription item.

Finally, we set the redirect link omitting the id number. As we will see a bit later, this indicates to the thank-you layout that we were not successful, so we can show the user a message to that effect.

Note that we call JRoute::_() with a second argument of boolean false. This argument defaults to true, in which case the htmlspecialchars command is run on the URL to escape special characters. Recall that this changes “&” to “&amp;”. Normally, when we use JRoute::_() to build a URL, we omit the second argument and take the default value. However, when we are using JRoute::_() to build a URL for a redirect, we need to specify a false value so that the URL does not get escaped. Otherwise, when the redirect is processed, the URL query variables will be wrong (for example, in the $_REQUEST array, instead of a key equal to “Itemid” you will have “&amp;Itemid”).

The rest of the subscribe() method is as follows:

   if (!in_array($subscription->group_id, $user->groups)) {
      if (!JUserHelper::addUserToGroup($user->id, $subscription->group_id)) {
         $this->setMessage($model->getError(), 'error'),
         return false;
      }
   }

   // Add or update row to mapping table
   if (!$result = $model->updateSubscriptionMapping($subscription, $user)) {
      $this->setMessage($model->getError(), 'error'),
      return false;
   }
   // At this point, we have succeeded
   // Trigger the onAfterSubscribe event
   JDispatcher::getInstance()->trigger('onAfterSubscribe', array(&$subscription));

   // Include id in redirect for success message
   $this->setRedirect(JRoute::_( 'index.php?option=com_joomprosubs&view=form&layout=thankyou &sub_id='.$id, false));
   return true;
}

Now we actually change the database to reflect that the user has subscribed. Recall that there are two actions we need to take to create the subscription. First, we add the user to the ACL group for this subscription. Second, we add a row to the mapping table to record the subscription details.

We start by checking whether the user is already in this group. If not, we use the JUserHelper::addUserToGroup() method to add the user to the group. If that is not successful, we set the error message and return false.

The next line calls the updateSubscriptionMapping() method of our model and sets the error if it returns false. We discuss this a bit later.

Next we trigger an event for our component, called onAfterSubscribe. This will look for any plugins with a method of this name and invoke them. This event is not essential for the component to work. It just creates an event that we can use if we like.

Finally, we set the redirect route to the full value, including the id. The layout will use this value to check that our task was successful. Note that again we set the second argument of JRoute::_() to false.

Controller Validate Method

In the subscribe() method we called the controller’s validate() method to check that the data was valid. The code for this method is as follows:

   /**
    * Validate the data
    *
    * @return   boolean   true if data is valid, false otherwise
    */
   protected function validate()
   {
      $app = JFactory::getApplication();
      $model = $this->getModel();
      $data = JRequest::getVar('jform', array(), 'post', 'array'),
      $form = $model->getForm($data, false);
      $validData = $model->validate($form, $data);
      $recordId = JRequest::getInt('sub_id'),

      // Check for validation errors.
      if ($validData === false) {
         // Get the validation messages.
         $errors   = $model->getErrors();

         // Push up to three validation messages out to the user.
         for ($i = 0, $n = count($errors); $i < $n && $i < 3; $i++)
         {
            if (JError::isError($errors[$i])) {
               $app->enqueueMessage($errors[$i]->getMessage(), 'warning'),
            }
            else {
               $app->enqueueMessage($errors[$i], 'warning'),
            }
         }

         // Save the data in the session.
         if (isset($data[0])) {
            $app->setUserState($context.'.data', $data);
         }

         // Redirect back to the edit screen.
         $this->setRedirect(JRoute::_('index.php?option='. $this->option.'&view='.$this->view_item. $this->getRedirectToItemAppend($recordId, 'sub_id'), false));
         return false;
      }
      return true;
   }

} // end of class

Here we get the form data from the request and then call the model’s validate() method. Recall from earlier that this executes the filter() and validate() methods of the form object. If either of these methods finds an error, the model’s validate() method will return false. In that case, the controller validate() method displays up to three different error messages and then sets the redirect back to the form.

If everything is OK, we return true.

Form Model

Recall from our basic discussion of the MVC design pattern that the model is typically where we interact with the database. In this case, we call the updateSubscriptionMapping() method of the model to handle the database changes to the mapping table.

The model class is JoomproSubsModelForm in the file models/form.php. The first part of this model class is as follows:

<?php
/**
 * @copyright Copyright (C) 2012 Mark Dexter and Louis Landry. All rights reserved.
 * @license   GNU General Public License version 2 or later; see LICENSE.txt
 */

// No direct access.
defined('_JEXEC') or die;
JLoader::register('JoomproSubsModelSubscription',
   JPATH_COMPONENT_ADMINISTRATOR.'/models/subscription.php'),

/**
 * Joomprosubs model.
 *
 * @package     Joomla.Site
 * @subpackage  com_joomprosubs
 */
class JoomproSubsModelForm extends JoomproSubsModelSubscription
{

Note that we extend the back-end model class. This gives us the methods we need for getting subscription items. Note also that we use the preferred JLoader::register() method instead of require_once.

The only public method in the model is updateSubscriptionMapping(). Its code is as follows:

   /**
    *
    * Method to add or update the subscription mapping table
    * If the row already exists, update the start and end date.
    * If the row doesn't exist, add a new row.
    *
    * @param JObject  $subscription   Subscription object
    * @param JUser    $user User object
    * @return  Boolean  true on success, false on failure
    */
   public function updateSubscriptionMapping($subscription, $user)
   {
      // Check that we have valid inputs
      if (((int) $subscription->id) && ((int) $subscription->duration)
            && ((int) $user->id)) {
         $today = JFactory::getDate()->toSQL();
         $endDate = JFactory::getDate('+ ' . (int) $subscription->duration . ' days')->toSQL();

         // Check whether the row exists
         $mapRow = $this->getMapRow($subscription->id, $user->id);
         if ($mapRow === false) {
            // We have a database error
            return false;
         } else if ($mapRow) {
            // The row already exists, so update it
            if (!$this->updateMapRow($subscription->id, $user->id, $today, $endDate)) {
               return false;
            }
         } else {
            // The row doesn't exist, so add a new map row
            if (!$this->addMapRow($subscription->id, $user->id, $today, $endDate)) {
               return false;
            }
         }

         // At this point, we have successfully updated the database
         return true;
      }
   }

First, we check that we have valid nonzero integers for the subscription and user ids and the duration. Then we calculate the subscription ending date using the JFactory getDate() method. This method accepts a time interval relative to the current time, such as “+ 10 minutes” or “+ 30 days.” In this case, we are passing “+ xx days” where xx is the duration of our subscription.

Next we call getMapRow() to see if this user already has a row in the table. If so, we call updateMapRow() to update the fields. Otherwise, we call addMapRow() to add a new row.

The getMapRow() method is as follows:

protected function getMapRow($subID, $userID)
{
    $db = $this->getDbo();
    $query = $db->getQuery(true);
    $query->select('subscription_id, user_id, start_date, end_date'),
    $query->from($db->nameQuote('#__joompro_sub_mapping'));
    $query->where('subscription_id = ' . (int) $subID);
    $query->where('user_id = ' . (int) $userID);
    $db->setQuery($query);
    $data = $db->loadObject();
    if ($db->getErrorNum()) {
        $this->setError(JText::_('COM_JOOMPROSUBS_GET_MAP_ROW_FAIL'));
        return false;
    } else {
        return $data;
    }
}

Here we do a select query to get the row using the subscription and user ids and return it as a standard object. Recall that this table has these values as the primary key. That means that there can only be one row for each combination of subscription id and user id.

The updateMapRow() method is as follows:

protected function updateMapRow($subID, $userID, $startDate, $endDate)
{
    $db = $this->getDbo();
    $query = $db->getQuery(true);
    $query->update($db->nameQuote('#__joompro_sub_mapping'));
    $query->set('start_date = ' . $db->quote($startDate));
    $query->set('end_date = ' . $db->quote($endDate));
    $query->where('subscription_id = ' . (int) $subID);
    $query->where('user_id = ' . (int) $userID);
    $db->setQuery($query);
    if ($db->query()) {
        return true;
    } else {
        $this->setError( JText::_('COM_JOOMPROSUBS_UPDATE_MAP_ROW_FAIL'));
        return false;
    }
}

Here we create an update query to update the start and end dates for the row. Again, by specifying the subscription id and user id columns, we know we have the correct database row.

The addMapRow() method is as follows:

protected function addMapRow ($subID, $userID, $startDate, $endDate)
{
    $db = $this->getDbo();
    $query = $db->getQuery(true);
    $query->insert($db->nameQuote('#__joompro_sub_mapping'));
    $query->set('subscription_id = ' . (int) $subID);
    $query->set('user_id = ' . (int) $userID);
    $query->set('start_date = ' . $db->quote($startDate));
    $query->set('end_date = ' . $db->quote($endDate));
    $db->setQuery($query);
    if ($db->query()) {
        return true;
    } else {
        $this->setError(JText::_('COM_JOOMPROSUBS_ADD_MAP_ROW_FAIL'));
        return false;
    }
}

Here we create an insert query and set the columns for the subscription and user ids as well as the start and end dates. Then we set the error message if the query fails. Note that we use the JDatabase quote() method for the dates. In our case, the dates are in the format “yyyy-mm-dd<space>hh:mm:ss” (for example, “2012-08-10 22:40:45”). We need to put this in quotes because it contains a space character.

The last method in the model is the populateState() method. This is called when we call the get('Item') method in the view. The populateState() method is as follows:

protected function populateState()
{
   $app = JFactory::getApplication();

   // Load state from the request.
   $pk = JRequest::getInt('sub_id'),
   $this->setState('joomprosub.sub_id', $pk);
   // Add compatibility variable for default naming conventions.
   $this->setState('form.id', $pk);

   $return = JRequest::getVar('return', null, 'default', 'base64'),

   if (!JUri::isInternal(base64_decode($return))) {
       $return = null;
   }

   $this->setState('return_page', base64_decode($return));

   // Load the parameters.
   $params = $app->getParams();
   $this->setState('params', $params);
   $this->setState('layout', JRequest::getCmd('layout'));
}

Here we set state variables for the sub_id, the form id, return page, params, and layout. Note that we don’t currently use the params object. This is included in case we wish to add parameters at a later time.

Thank-You Layout

The last step in the subscribe task is to communicate to the user that the task has been successful. This is done by displaying the thank-you layout. In the controller’s subscribe() method, recall that we set the redirect to the URL to

$this->setRedirect(JRoute::_('index.php?option=com_joomprosubs
&view=form&layout=thankyou&sub_id='.$id, false));

If the subscribe task was not successful, we omitted the sub_id number.

When this redirect is loaded, it starts a new execution cycle to load the view with the layout file views/form/tmpl/thankyou.php. The listing of this file is as follows:

<?php
/**
 * @package     Joomla.Site
 * @subpackage  com_joomprosubs
 * @copyright   Copyright (C) 2012 Mark Dexter and Louis Landry. All rights reserved.
 * @license     GNU General Public License version 2 or later; see LICENSE.txt
 */

// no direct access
defined('_JEXEC') or die;
$id = (int) $this->item->id;
$name = $this->escape(JFactory::getUser()->get('name'));
$title = $this->escape($this->item->title);
$duration = (int) $this->item->duration;
$itemid = JRequest::getInt('Itemid'),
?>
<?php if ($id) :?>
    <h1><?php echo JText::sprintf('COM_JOOMPROSUBS_THANK_YOU_NAME', $name)?></h1>
    <p><?php echo JText::sprintf('COM_JOOMPROSUBS_THANK_YOU_TITLE', $title)?></p>
    <p><?php echo JText::sprintf('COM_JOOMPROSUBS_THANK_YOU_DURATION', $duration)?></p>
<?php else : ?>
    <p><?php echo JText::sprintf('COM_JOOMPROSUBS_THANK_YOU_ERROR')?></p>
<?php endif; ?>
<br/>
<a href="<?php echo JRoute::_('index.php?option=com_joomprosubs&Itemid=' . $itemid); ?>" >
<?php echo JText::_('COM_JOOMPROSUBS_RETURN_TO_LIST')?></a>

Here we echo the user’s name and see a message indicating the success or failure of the subscribe task. Figure 10.4 shows the screen after a successful subscribe task.

Image

Figure 10.4. Successful subscribe thank-you screen

Figure 10.5 shows the screen if the subscribe task is not successful. Note that you won’t be able to see these screens fully translated until you have finished coding the component, including the language file.

Image

Figure 10.5. Unsuccessful subscribe thank-you screen

Normally, the task should always be successful. So how do we test the unsuccessful condition? There are several ways we can do this. One simple way is to temporarily change the code so we know the method will fail. For example, in the subscribe() method of the controller we have this block of code:

// Set redirect without id in case of an error
   $this->setRedirect(JRoute::_( 'index.php?option=com_joomprosubs&view=form&layout=thankyou', false));
   if (!in_array($subscription->group_id, $user->groups)) {
      if (!JUserHelper::addUserToGroup($user->id, $subscription->group_id)) {
         $this->setMessage($model->getError(), 'error'),
         return false;
      }
   }

If we insert the line

return false;

after the setRedirect(), the method will always return false and we should always go to the thank-you layout with no id. (Remember to remove the line when you are done testing!)

Language File

In the front end, we have only one language file per language. The default English language file is called language/en-GB/en-GB.com_joomprosubs.ini. We have again chosen to keep the language file in the component folder instead of the common languages folder.

The language file listing is as follows:

COM_JOOMPROSUBS_ADD_MAP_ROW_FAIL="Error trying to add row to mapping table."
COM_JOOMPROSUBS_DEFAULT_PAGE_TITLE="Subscriptions"
COM_JOOMPROSUBS_EDIT="Edit Subscription"
COM_JOOMPROSUBS_ERR_TABLES_NAME="There is already a Subscription with that name in this category. Please try again."
COM_JOOMPROSUBS_ERR_TABLES_PROVIDE_URL="Please provide a valid URL"
COM_JOOMPROSUBS_ERR_TABLES_TITLE="Your Joomprosub must contain a title."
COM_JOOMPROSUBS_ERROR_CATEGORY_NOT_FOUND="Subscription category not found"
COM_JOOMPROSUBS_ERROR_JOOMPROSUB_NOT_FOUND="Subscription not found"
COM_JOOMPROSUBS_ERROR_JOOMPROSUB_URL_INVALID="Invalid Subscription URL"
COM_JOOMPROSUBS_ERROR_UNIQUE_ALIAS="Another Joomprosub from this category has the same alias"
COM_JOOMPROSUBS_FIELD_ALIAS_DESC="The alias is for internal use only. Leave this blank and Joomla! will fill in a default value from the title. It has to be unique for each subscription in the same category."
COM_JOOMPROSUBS_FIELD_CATEGORY_DESC="You must select a Category."
COM_JOOMPROSUBS_FIELD_DESCRIPTION_DESC="You may enter here a description for your Subscription"
COM_JOOMPROSUBS_FIELD_TITLE_DESC="Your Subscription must have a Title."
COM_JOOMPROSUBS_FILTER_LABEL="Title Filter"
COM_JOOMPROSUBS_FORM_DURATION_DESC="Number of days the subscription will be active."
COM_JOOMPROSUBS_FORM_DURATION_LABEL="Duration (Days)"
COM_JOOMPROSUBS_FORM_LABEL="Subscribe Form"
COM_JOOMPROSUBS_FORM_SUBMIT_SUB="Subscribe Form"
COM_JOOMPROSUBS_FORM_TERMS_DESC="Terms for the subscription."
COM_JOOMPROSUBS_FORM_TERMS_LABEL="I agree to the terms of use."
COM_JOOMPROSUBS_FORM_TITLE_DESC="Form to subscribe to a group."
COM_JOOMPROSUBS_FORM_TITLE_LABEL="Subscribe To The Group"
COM_JOOMPROSUBS_GET_MAP_ROW_FAIL="Error trying to retrieve a row from mapping table."
COM_JOOMPROSUBS_GRID_DESC="Description"
COM_JOOMPROSUBS_GRID_DURATION="# Days"
COM_JOOMPROSUBS_GRID_GROUP="User Group"
COM_JOOMPROSUBS_GRID_TITLE="Title"
COM_JOOMPROSUBS_LINK="Subscription"
COM_JOOMPROSUBS_NAME="Name"
COM_JOOMPROSUBS_NO_JOOMPROSUBS="There are no Subscriptions in this category"
COM_JOOMPROSUBS_NUM="# of subscriptions:"
COM_JOOMPROSUBS_RETURN_TO_LIST="Return to Subscription List"
COM_JOOMPROSUBS_THANK_YOU_DURATION="Your subscription will expire in %s days."
COM_JOOMPROSUBS_THANK_YOU_ERROR="We are unable to complete your request at this time."
COM_JOOMPROSUBS_THANK_YOU_NAME="Thank you, %s."
COM_JOOMPROSUBS_THANK_YOU_TITLE="You are now subscribed to %s."
COM_JOOMPROSUBS_UPDATE_MAP_ROW_FAIL="Error trying to update a row in the mapping table."
COM_JOOMPRPOSUBS_FORM_CANCEL="Cancel"
COM_JOOMPRPOSUBS_FORM_SUBMIT="Submit"

Note that a few of the tags, such as COM_JOOMPROSUBS_THANK_YOU_NAME, have the characters %s in them. This means that a variable will be added to the tag when it is translated. In the thank-you layout, we saw this code:

<h1><?php echo JText::sprintf('COM_JOOMPROSUBS_THANK_YOU_NAME', $name)?></h1>

Here we use the sprintf() method of JText (instead of the _() method). The second argument of the method is the variable to add to the text—in this case, the user’s name.

This type of construct is more flexible than hard-coding the word order in the layout. For example, we could have used something like this:

<h1><?php echo JText::_('COM_JOOMPROSUBS_THANK_YOU_NAME') . ' ' . $name)?></h1>

However, that would assume that the name would always come after the thank-you message. In some languages, it might be better to have the name before the text or in the middle of the text. Using the sprintf() method allows the translator greater flexibility when translating phrases with embedded data.

Packaging the Component

At this point, our component is complete. A next logical step is to package the component so that it can easily be installed in any Joomla site running a compatible version (for example, versions 2.5.0 or higher). To do this, we need to make an archive file like we did for the example module and plugin extensions. Here are the steps:

• Create a working folder for your archive file (for example, temp) and then create two subfolders, called admin and site.

• From the Joomla site where the component is installed, copy the files and folders from administrator/components/com_joomprosubs to temp/admin.

• Copy the files and folders from components/com_joomprosubs to temp/site.

• Move the file temp/admin/joomprosubs.xml to temp/joomprosubs.xml. At this point, the temp folder should have the two subfolders plus the XML file.

• Create a zip archive of the entire temp folder (for example, called com_joomprosubs_1.0.0.zip).

The zip file should be now installable from the Extension Manager.

New Functionality: Back-End Subscriber Report

At this point, our basic component is complete. In the real world, however, most software is never actually complete. The most common tasks in software development are fixing bugs and adding or changing functionality. One of the primary goals of design patterns such as MVC is to make it easy to expand functionality while minimizing the risk of unintended side effects.

Let’s look at a real example of adding some new functionality to our component. In this case, we will add a new report showing all the subscribers for each subscription. The report will be in a comma-separated values (CSV) formatted file that is downloaded to the browser. It will show a list of our subscriptions and the subscribers for each one. It will be an option from the Subscription Manager screen in the back end.

To make the report more useful, we will use the Subscription Manager filters to prepare the report. In other words, if we have filters set in the manager, the report will use them to filter the report results.

Here are the changes we need to make to implement this new functionality. The file paths are relative to administrator/components/com_joomprosubs (because we are in the back end):

• In JoomproSubsViewSubmanager (views/submanager/view.html.php), add a new toolbar button in the toolbar to download the report file.

• In JoomproSubsControllerSubManager (controllers/submanager.php), add a new method to the controller for the new task.

• Add a new model, JoomproSubsModelCSVReport (models/csvreport.php), to prepare the data for the report.

• Add a new method to the JoomproSubsControllerSubManager, called exportReport(), to render the report (used instead of a layout).

Note that we don’t need a new layout. Instead, the exportReport() method in the controller will create the CSV file and send it to the browser.

New Toolbar Button

Recall that the toolbar for the Subscription Manager screen is created in the addToolBar() method of the view JoomproSubsViewSubmanager (views/submanager/view.html.php). To add the new toolbar, use the following code:

// Add export toolbar
$bar = JToolBar::getInstance('toolbar'),
$bar->appendButton('Link', 'export', 'COM_JOOMPROSUBS_TOOLBAR_CSVREPORT',
    'index.php?option=com_joomprosubs&task=submanager.csvreport'),

We can add this anywhere in the toolbar—for example, between the Edit and Publish icons.

Note that we use different commands to add this button. For the standard buttons, we have helper methods in JToolBarHelper, such as editList() and custom(). These let us add a toolbar with a single line of code.

In this case, we have decided to add the button as a link. This will create a new request cycle to run the report. That way, the state of our manager screen is not changed by running the CSV report. To do this, we specify the arguments for the appendButton() method as follows:

• button type = 'Link'—creates a JButtonLink button

• task = 'export'—sets the button to toolbar-export (used to get the button image)

• text = 'COM_JOOMPROSUBS_TOOLBAR_CSVREPORT'—used for the button text

• URL = 'index.php?option=com_joomprosubs&task=submanager.csvreport'—the URL to link to (which initiates the csvreport task)

You can see an example of how JButtonLink works by reviewing the back() method of JToolBarHelper class.

Controller Method for New Task

When our new button is pressed, it starts a new request cycle with the URL shown earlier. The task in the URL tells Joomla to execute a method called csvReport() in the controller class JoomproSubsControllerSubManager (controllers/submanager.php). The code for this new method is as follows:

public function csvReport()
{
    $model = $this->getModel('CSVReport', 'JoomproSubsModel',
       array('ignore_request' => true));
    $model->setModelState();
    $data = $model->getItems();
    $this->exportReport($data);
}

This code is short and simple. We get our new model class. Then we call the setModelState() method. This method sets the model’s state field based on the current state of the manager screen. We use the state values when creating the query. Next we get the data from the model. Finally, we pass that data to our new exportReport() method.

New Model Class

We need to create a new class called JoomproSubsModelCSVReport (models/csvreport.php). The first part of the code for this class is as follows:

<?php
/**
 * @copyright   Copyright (C) 2012 Mark Dexter and Louis Landry. All rights reserved.
 * @license     GNU General Public License version 2 or later; see LICENSE.txt
 */

defined('_JEXEC') or die;

jimport('joomla.application.component.modellist'),
JLoader::register('JoomproSubsModelSubManager',
   JPATH_COMPONENT.'/models/submanager.php'),

/**
 * Methods supporting a list of joomprosub records.
 *
 */
class JoomproSubsModelCSVReport extends JoomproSubsModelSubManager
{
   /**
    * Method to set the state using the values from the submanager view
    */
   public function setModelState()
   {
      $this->context = 'com_joomprosubs.submanager';
      parent::populateState();
   }

Note that we extend JoomproSubsModelSubManager, the model for the manager screen. That way, we can use the constructor and the populateState() methods we already wrote for that class. Recall that the constructor includes a list of valid columns for sorting the data. This was done to protect against hacking.

The first method is setModelState(). Recall that we want to filter and order our CSV report using the values that have been set in the manager screen. To do this, we need to call populateState() with the manager screen’s context value. However, we have a slight problem. Recall that populateState() is a protected method, so we can’t call it directly from the controller class. Also, we can’t override this method (create a method with the same name) and change its access modifier to public. When you override a parent class’s method in a subclass, it has to have the same access modifier as the method in the parent class.

However, we can call populateState() from our current model, because our model is a subclass of JoomproSubsModelSubManager. So, the solution to our problem is to make a new public method (not an override) in our class that in turn calls the protected method of the parent class. We create a public method called setModelState() in JoomproSubsModelCSVReport and use that to call the protected populateState() method of the parent class. Then we will be able to call the public method setModelState() from the controller.

The other method in our model is getListQuery(). This method overrides the same method in the parent class. For this reason, it must have the same access modifier as the method in the parent class—in this case, protected. The first part of this method is as follows:

   /**
    * Build a SQL query to load the list data.
    *
    * @return   JDatabaseQuery
    */
   protected function getListQuery()
   {
      // Create a new query object.
      $db = $this->getDbo();
      $query = $db->getQuery(true);

      // Select the required fields from the table.
      $query->select('a.id AS subsciption_id,
         a.title AS subscription_title,
         g.title AS group_title, c.title AS category_title,
         a.alias AS subscription_alias,
         a.description AS subscription_description, a.duration,
         a.published AS subscription_published,
         a.access AS subscription_access,
         uc.name AS subscriber_name'),
      $query->from($db->quoteName('#__joompro_subscriptions').' AS a'),

Here we create a new JDatabaseQuery object and add our selected columns to the select field of the query. We list all the columns we want in our report, in the order that we want them. Then we add the primary table into the from field of the query.

The next part of the code is as follows:

   // Join over the mapping table to get subscribers
   $query->select('m.user_id as subscriber_id, m.start_date, m.end_date'),
   $query->join('LEFT', $db->quoteName('#__joompro_sub_mapping').' AS m ON m.subscription_id = a.id'),

   // Join over the users for the subscribed user.
   $query->join('LEFT', $db->quoteName('#__users').' AS uc ON uc.id = m.user_id'),

   // Join over the user groups to get the group name
   $query->join('LEFT', $db->quoteName('#__usergroups').' AS g ON a.group_id = g.id'),

   // Join over the categories.
   $query->join('LEFT', $db->quoteName('#__categories').' AS c ON c.id = a.catid'),

Here we add the user id, start date, and end date columns from the mapping table. Then we add left joins for the mapping user, user group, and categories tables.

The next part of the code is as follows:

// Filter by access level.
if ($access = $this->getState('filter.access')) {
    $query->where('a.access = '.(int) $access);
}

// Filter by published state
$published = $this->getState('filter.state'),
if (is_numeric($published)) {
    $query->where('a.published = '.(int) $published);
} else if ($published === '') {
    $query->where('(a.published IN (0, 1))'),
}

// Filter by category.
$categoryId = $this->getState('filter.category_id'),
if (is_numeric($categoryId)) {
    $query->where('a.catid = '.(int) $categoryId);
}

// Filter by search in title
$search = $this->getState('filter.search'),
if (!empty($search)) {
    if (stripos($search, 'id:') === 0) {
        $query->where('a.id = '.(int) substr($search, 3));
    } else {
        $search = $db->Quote('%'. $db->getEscaped($search, true).'%'),
        $query->where('(a.title LIKE '.$search.' OR a.alias LIKE '.$search.')'),
    }
}

Here we use the filter fields from the manager screen’s state field to filter the report results. We add WHERE clauses to filter by access, published state, category id, and search text. Note that there is some tricky processing in the search text filter. If the first three characters of the text are “id:”, then we search for an integer id. For example, if you put “id:6” in the search text, it will only find rows where the subscription id is equal to “6”. Otherwise, we use a SQL wild-card search to match characters in the title column.

The last part of the method is as follows:

      $orderCol = $this->state->get('list.ordering', 'a.title'),
      $orderDirn = $this->state->get('list.direction', 'ASC'),
      $query->order($db->getEscaped($orderCol.' '.$orderDirn));

      return $query;
   }
} // end of class

Here we set the order field for the query, defaulting to ordering by subscription title.

Controller Method to Export File

At this point, we have the data for our export file. The last step is to create the file and send it to the browser. We do this in the exportReport() method in the controller class JoomproSubsControllerSubManager (controllers/submanager.php). The code for this method is as follows:

    protected function exportReport($data)
   {
       // Set headers
       header('Content-Type: text/csv'),
       header( 'Content-Disposition: attachment;filename='.'subscriptions.csv'),

       if ($fp = fopen('php://output', 'w')) {

           // Output the first row with column headings
           if ($data[0]) {
               fputcsv($fp, array_keys(JArrayHelper::fromObject($data[0])));
           }

           // Output the rows
           foreach ($data as $row) {
               fputcsv($fp, JArrayHelper::fromObject($row));
           }

           // Close the file
           fclose($fp);
       }
       JFactory::getApplication()->close();
    }
} // end of class

The first two lines use the PHP header() function to send a raw HTTP header to the browser. The format for these lines must be exactly as shown. The first line tells the browser that we are sending a CSV text file. The second line tells the browser that we will be sending a file named “subscriptions.csv” as an attachment. Depending on your browser settings, this will give you the option to open or save the CSV file.

Then we use the PHP command fopen() to create a file object you can write to. We have two arguments. The first is the file name. Here we use a special name called php://output. This allows us to write to the output buffer. We use this because we already created the file name in the http header. The second argument is the mode. Here we use “w” to indicate that this is a write-only stream.

We put this inside an if statement because we don’t want to do any file operations if for some reason the fopen() is not successful. Inside the if block, we check the first array element to see whether or not we have any data. If so, we use the PHP command fputcsv() to write out the array keys as column headers for our CSV file. The fputcsv() command does the work of creating the CSV-formatted text, and it uses the $fp object we created with the fopen() command.

Notice that we use the JArrayHelper fromObject() method to convert the object to an array. Then we use the PHP function array_keys() to get the keys of the array. This way, we get a list of all the column names to put in the first row of our file.

Once we have the first row, we do a foreach loop through all the array elements and write a row to our file for each element. Again we use the fromObject() method to convert the object to an array before passing it to the fputcsv() function. And again we use the $fp object as the first argument.

Finally, we use the fclose() method to close the file buffer. Then we call the close() method for the Joomla application object. This closes this request cycle and sends the file to the browser.

Report in Action

When you click the Report button, you see something similar to what’s shown in Figure 10.6. If you save the file and then open it (for example, as a spreadsheet), you should see your subscriptions listed in a format similar to that shown in Figure 10.7 and Figure 10.8.

Image

Figure 10.6. New report button and file download dialog

Image

Figure 10.7. Example CSV file part I

Image

Figure 10.8. Example CSV file part II

This exercise illustrates part of the value of the MVC design pattern. It was relatively easy to add this new functionality to the existing component. Moreover, we didn’t have to change any existing code to do this. That minimizes the chance that something we added might break some existing functionality.

Real-World Considerations

This realistic example illustrates most of the aspects of building a real-life component. However, to reduce the amount of code, some functions that you would want to have in a real component have been left out, such as the following:

• A way for users to unsubscribe from a subscription in the front end

• A way for managers to add or remove users from a subscription in the back end

• A way for users to see what subscriptions they have and when they are due to expire

A simple way to implement the first of these would be to alter the category list layout to indicate whether a user was already subscribed and, if so, when their subscription expired. This would require altering the model to include the mapping table and adding some fields to the layout. A new button could be added to unsubscribe, which would require a new task.

The second feature could be implemented by adding a new mapping table manager screen, similar to the subscription screen. One way to implement the third feature would be with a simple module that shows any subscriptions the user has, along with their expiration dates.

Any or all of these would make great exercises for you to do on your own, applying the knowledge you have gained from the past four chapters.

Summary

In this chapter, we finished the example component by building the front end. We followed the MVC pattern we learned from examining the Weblinks component. Finally, we modified our component to add a new report. This demonstrated how the MVC pattern makes it relatively easy to add and modify existing components.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.129.26.22