Actions

Difference between revisions of "Adding custom fields to the article component"

From Joomla! Documentation

m (removing category manual)
(onContentPrepareForm)
Line 212: Line 212:
  
 
<source lang="php">
 
<source lang="php">
   public public function onContentPrepareForm($form, $data)
+
   public function onContentPrepareForm($form, $data)
 
   {
 
   {
 
       if (!($form instanceof JForm))
 
       if (!($form instanceof JForm))

Revision as of 06:53, 2 December 2013

Contents

Intro

This plugin demonstrates how a component developer can use the features in Joomla 2.5 to add custom fields to the article component (com_content), without the need to change core files. Capabilities that are implemented in this demo package include:

  1. Adding extra fields to the back-end article manager.
  2. Adding extra fields to the front-end article editor.
  3. Storing the values of the custom fields into a database table.
  4. Converting the field values into a HTML table which is injected into the displayed article.

A capability which is present but not demonstrated, is integrating the values of the custom fields into other components and views, such as article lists or category blogs.

This example

This example adds food ratings to articles. These ratings consist of texture, temperature and taste. When present, they will be displayed as a simple table. This table is positioned before the article content.

example of rendered article
An example of how the plugin places the table at the beginning of an article

Setting the values of these fields can be done in both the backend and frontend.

edit article frontend
Accessing the additional fields through the frontend
edit article backend
... and the backend


Framework

Within Joomla, the article manager is basically a form driven component. Both in the backend as the frontend. When handling a form, the framework performs the following actions.

  1. Loads the form description. This is a XML structure.
  2. Loads the data from the user state if present, and if not from the database.
  3. Injects the data into the form
  4. Renders/displays the form for the user to work on.

When the user is finished and issues a SAVE, the framework will:

  1. Captures the submitted data
  2. Loads the form description.
  3. Validates the data against the form description.
  4. If validation fails, stores the data in the user state and starts from the beginning.
  5. Updates the database accordingly

The framework has placeholders for all these moments. This example plugin will hook into these placeholders, expanding the form and data with additional custom fields.

The hooks are:

  • onContentPrepareForm()
    Called after loading the form description. It will add an extra form group containing the additional fields
  • onContentPrepareData()
    Called after loading the article from the database. It will inject the additional fields and their value into the article.
  • onContentAfterSave()
    Called after an article has been successfully stored in database. It will store the additional fields in their designated database table.
  • onContentAfterDelete()
    Called after an article has been deleted (emptying of trash). It will delete the additional fields.
  • onContentPrepare()
    Called before the article will be rendered/displayed. It will inject the rating values as a table at the beginning of the article, if values have been entered.

Database

The contents of the extra custom fields need to be stored in the database. Ideally would be if they have their own dedicated database table. This however, requires considerable extension overhead. With Joomla 2.5 is is possible to extend the user profile. The extra fields are stored in the table #__user_profiles. That table is constructed and used in such a manner that it can also hold the extra article fields. [I would like to spark the discussion to evolve #__user_profiles into a generic table for expanding core tables.]

This plugin example will use table #__user_profiles to store the extra fields. It will use the tables user_id as article_id

Extension files

The example plugin contains the following files. It has an extra directory rating. The primary reason is that it contains a XML file that is not a manifest. If it were located in the plugin top-level directory, the installer would find it and mistakenly identify it as an orphaned plugin. This would result in the form showing up when 'Discovering' lost extensions.

── plugins/content/rating
   ├── language
   │   └── en-GB
   │       ├── en-GB.plg_content_rating.ini      [language file]
   │       └── en-GB.plg_content_rating.sys.ini  [language file]
   ├── rating
   │   ├── rating.css  [CSS for the rendered rating table]
   │   └── rating.xml  [Form description]
   ├── rating.php   [Plugin hooks]
   └── rating.xml   [Manifest]

Manifest

A regular plugin manifest which takes an extra configuration argument, a CSS name that is tagged onto the rendered rating table.

<?xml version="1.0" encoding="utf-8"?>
   <extension version="2.5" type="plugin" group="content" method="upgrade">
      <name>plg_content_rating</name>
      <author>Joomla! Project</author>
      <creationDate>June 2012</creationDate>
      <copyright>(C) 2005 - 2012 Open Source Matters. All rights reserved.</copyright>
      <license>GNU General Public License version 2 or later; see LICENSE.txt</license>
      <authorEmail>admin@joomla.org</authorEmail>
      <authorUrl>www.joomla.org</authorUrl>
      <version>2.5.0</version>
      <description>PLG_CONTENT_RATING_XML_DESCRIPTION</description>
 
      <files>
         <folder>language</folder>
         <folder>rating</folder>
         <filename plugin="rating">rating.php</filename>
         <filename>index.html</filename>
      </files>
 
      <config>
         <fields name="params">
            <fieldset name="basic">
               <field
                  name="ratingclass_sfx"
                  type="text"
                  label="PLG_CONTENT_RATING_ITEM_FIELD_RATING_CLASS_LABEL"
                  description="PLG_CONTENT_RATING_ITEM_FIELD_RATING_CLASS_DESC"
               />
            </fieldset>
         </fields>
      </config>
   </extension>

language files

; Joomla! Project
; Copyright (C) 2005 - 2012 Open Source Matters. All rights reserved.
; License GNU General Public License version 2 or later; see LICENSE.txt, see LICENSE.php
 
PLG_CONTENT_RATING="Content - Rating"
PLG_CONTENT_RATING_XML_DESCRIPTION="[plg_content_rating] Example plugin on how to add custom fields (texture/temperature/taste) to articles (com_content). Adds ratings to an article, and display them as a table before the article content."
PLG_CONTENT_RATING_SLIDER_LABEL="Rating Options"
PLG_CONTENT_RATING_FIELD_TEXTURE_LABEL="Texture"
PLG_CONTENT_RATING_FIELD_TEXTURE_DESC="What does the sample feel like"
PLG_CONTENT_RATING_FIELD_TEMPERATURE_LABEL="Temperature"
PLG_CONTENT_RATING_FIELD_TEMPERATURE_DESC="What is the temperature of the sample"
PLG_CONTENT_RATING_FIELD_TASTE_LABEL="Taste"
PLG_CONTENT_RATING_FIELD_TASTE_DESC="How does the sample taste"
PLG_CONTENT_RATING_ITEM_FIELD_RATING_CLASS_LABEL="Rating Class"
PLG_CONTENT_RATING_ITEM_FIELD_RATING_CLASS_DESC="Optional CSS class to add to the rating. This allows CSS styling specific to the page."

CSS

The CSS file for the rendered table.

div.rating {
    display: block;
    float: left;
    padding-right: 10px;
}
div.rating table tr td {
    margin: 1px !important;
    padding: 2px !important;
}
div.rating table tr.row0  {
    background-color:#efefef;
}
div.rating table tr.row1 {
    background-color:#ffffff;
}

Form description

The form containing the custom fields.

   <?xml version="1.0" encoding="utf-8"?>
   <form>
      <fields name="rating">
         <fieldset name="rating" label="PLG_CONTENT_RATING_SLIDER_LABEL">
            <field
               name="texture"
               type="text"
               id="texture"
               description="PLG_CONTENT_RATING_FIELD_TEXTURE_DESC"
               label="PLG_CONTENT_RATING_FIELD_TEXTURE_LABEL"
               message="PLG_CONTENT_RATING_FIELD_TEXTURE_MESSAGE"
               size="30"
            />
            <field
               name="temperature"
               type="text"
               id="temperature"
               description="PLG_CONTENT_RATING_FIELD_TEMPERATURE_DESC"
               label="PLG_CONTENT_RATING_FIELD_TEMPERATURE_LABEL"
               message="PLG_CONTENT_RATING_FIELD_TEMPERATURE_MESSAGE"
               size="30"
            />
            <field
               name="taste"
               type="text"
               id="taste"
               description="PLG_CONTENT_RATING_FIELD_TASTE_DESC"
               label="PLG_CONTENT_RATING_FIELD_TASTE_LABEL"
               message="PLG_CONTENT_RATING_FIELD_TASTE_MESSAGE"
               size="30"
            />
         </fieldset>
      </fields>
   </form>

Plugin hooks

The main file 'rating.php' contains the hooks which are called by the Joomla framework.

For all calls:

  • The $context parameter is a string identifying the calling placeholder.
  • The $data parameter is an object containing the form values
  • The $article parameter is an object (JTableContent) representing the article.

__construct

The constructor. It loads the language file.

   public function __construct(& $subject, $config)
   {
      parent::__construct($subject, $config);
      $this->loadLanguage();
   }

onContentPrepareForm

onContentPrepareForm extends the article manager form in both the front-end as the back-end. It loads the form fragment as described in rating/rating.xml and merges that into the framework form.

   public function onContentPrepareForm($form, $data)
   {
      if (!($form instanceof JForm))
      {
         $this->_subject->setError('JERROR_NOT_A_FORM');
         return false;
      }
 
      // Add the extra fields to the form.
      JForm::addFormPath(dirname(__FILE__) . '/rating');
      $form->loadFile('rating', false);
      return true;
   }

onContentPrepareData

onContentPrepareData() is executed after an article is loaded from the #__content table. It will gather and merge the values of the custom fields, which are located in the table #__user_profiles.

The field articleId in $data contains the article record number. That number is used to retrieve the values of the custom fields. If no record number is present, or it is zero, then a new record is being create. When this occurs, $data gets populated with the default values from the form description xml.

It is important that when onContentPrepareData() returns, $data contains the custom fields. This is due to how the framework uses them as place holders. When a user saves a newly created article, and an error situation occurs (like some values are missing) the form data is temporarily stored in the user state. If the place holders are not present, their values will not be preserved. The user will notice this because the custom fields will get erased.

   public function onContentPrepareData($context, $data)
   {
      if (is_object($data))
      {
         $articleId = isset($data->id) ? $data->id : 0;
         if ($articleId > 0)
         {
            // Load the data from the database.
            $db = JFactory::getDbo();
            $query = $db->getQuery(true);
            $query->select('profile_key, profile_value');
            $query->from('#__user_profiles');
            $query->where('user_id = ' . $db->Quote($articleId));
            $query->where('profile_key LIKE ' . $db->Quote('rating.%'));
            $query->order('ordering');
            $db->setQuery($query);
            $results = $db->loadRowList();
 
            // Check for a database error.
            if ($db->getErrorNum())
            {
               $this->_subject->setError($db->getErrorMsg());
               return false;
            }
 
            // Merge the data.
            $data->rating = array();
 
            foreach ($results as $v)
            {
               $k = str_replace('rating.', '', $v[0]);
               $data->rating[$k] = json_decode($v[1], true);
               if ($data->rating[$k] === null)
               {
                  $data->rating[$k] = $v[1];
               }
            }
         } else {
            // load the form
            JForm::addFormPath(dirname(__FILE__) . '/rating');
            $form = new JForm('com_content.article');
            $form->loadFile('rating', false);
 
            // Merge the default values
            $data->rating = array();
            foreach ($form->getFieldset('rating') as $field) {
               $data->rating[] = array($field->fieldname, $field->value);
            }
         }
      }
 
      return true;
   }

onContentAfterSave

onContentAfterSave() is called after the article is stored into the database #__content table. It will extract the values of the custom fields and store them into a separate table.

   public function onContentAfterSave($context, &$article, $isNew)
   {
      $articleId = $article->id;
      if ($articleId && isset($article->rating) && (count($article->rating)))
      {
         try
         {
            $db = JFactory::getDbo();
 
            $query = $db->getQuery(true);
            $query->delete('#__user_profiles');
            $query->where('user_id = ' . $db->Quote($articleId));
            $query->where('profile_key LIKE ' . $db->Quote('rating.%'));
            $db->setQuery($query);
            if (!$db->query()) {
               throw new Exception($db->getErrorMsg());
            }
 
            $query->clear();
            $query->insert('#__user_profiles');
            $order = 1;
            foreach ($article->rating as $k => $v)
            {
               $query->values($articleId.', '.$db->quote('rating.'.$k).', '.$db->quote(json_encode($v)).', '.$order++);
            }
            $db->setQuery($query);
 
            if (!$db->query()) {
               throw new Exception($db->getErrorMsg());
            }
         }
         catch (JException $e)
         {
            $this->_subject->setError($e->getMessage());
            return false;
         }
      }
 
      return true;
   }

onContentAfterDelete

onContentAfterDelete() is called after the article is deleted from the database #__content table. It will delete the linked custom fields which are stored in a separate table.

   public function onContentAfterDelete($context, $article)
   {
      $articleId = $article->id;
      if ($articleId)
      {
         try
         {
            $db = JFactory::getDbo();
 
            $query = $db->getQuery(true);
            $query->delete();
            $query->from('#__user_profiles');
            $query->where('user_id = ' . $db->Quote($articleId));
            $query->where('profile_key LIKE ' . $db->Quote('rating.%'));
            $db->setQuery($query);
 
            if (!$db->query())
            {
               throw new Exception($db->getErrorMsg());
            }
         }
         catch (JException $e)
         {
            $this->_subject->setError($e->getMessage());
            return false;
         }
      }
 
      return true;
   }

onContentPrepare

onContentPrepare() is called when the article is being prepared for display. This is the moment where HTML can be injected into what will be displayed.

$params are the article params, $this->params are the plugin configuration parameters as described in the manifest.

   public function onContentPrepare($context, &$article, &$params, $page = 0)
   {
      if (!isset($article->rating) || !count($article->rating))
         return;
 
      // add extra css for table
      $doc = JFactory::getDocument();
      $doc->addStyleSheet(JURI::base(true).'/plugins/content/rating/rating/rating.css');
 
      // construct a result table on the fly   
      jimport('joomla.html.grid');
      $table = new JGrid();
 
      // Create columns
      $table->addColumn('attr')
         ->addColumn('value');   
 
      // populate
      $rownr = 0;
      foreach ($article->rating as $attr => $value) {
         $table->addRow(array('class' => 'row'.($rownr % 2)));
         $table->setRowCell('attr', $attr);
         $table->setRowCell('value', $value);
         $rownr++;
      }
 
      // wrap table in a classed <div>
      $suffix = $this->params->get('ratingclass_sfx', 'rating');
      $html = '<div class="'.$suffix.'">'.(string)$table.'</div>';
 
      $article->text = $html.$article->text;
   }

Prerequisites

This tutorial requires at least Joomla 2.5.x [to be determined] as the framework for previous versions misses a number of plugin hook place holders.

For Joomla 2.5 a patch file and for Joomla 2.5.6 a patch extension is available.

This patch has been submitted as Joomla patch #28871.

Extension Files

The install zip files for the four versions can be individually downloaded from JoomlaCode.org.

The tracker page for the patch. #28771. Patch #28770 is also related.

Contributors