Using Content History in your Component

From Joomla! Documentation

In version 3.2, Joomla added the ability to track content history (also called versions) for all core components. This allows you to view and restore from prior versions of an article, banner, category, and other content types. This feature can easily be added to third-party components. This tutorial shows you how to do this. We will illustrate this by adding content history to an example component.

Important Note: This tutorial assumes that the component uses a JTable sub-class for its "CRUD" (create/update/delete) operations. If the component does not use JTable then the simplest thing to do is to re-work it to use JTable's store() and delete() methods. If you do that, you will be able to use the methods described in this tutorial.

Set up Working Environment[edit]

  1. Install a new instance of Joomla version 3.2 on your local workstation.
  2. Install the example component using the file com_joomprosubs_3.2.0.zip in the Extension Manager: Install screen. You can get the ZIP archive here: https://github.com/joomla/jdoc-examples/raw/master/zip_archives/com_joomprosubs-3.2.0.zip.

At this point, you should be able to see the Joompro Subscriptions component in the Components menu in the back end of Joomla. This is the component before we have added content history to it.

Add Rows to Content Types Table[edit]

The first thing we need to do is to add two new rows into the #__content_types table. This table stores information about the different tables for each content type. As of Joomla version 3.2, this table is used by the com_tags and com_contenthistory components to get information about each content type for which history is stored. The columns for #__content_types are as follows:

  • type_id: auto-increment id number.
  • type_title: descriptive title for this table.
  • type_alias: <component name>.<type name>. For example: "com_content.article" or "com_content.category".
  • table: JSON string that contains the name of the JTable class and other information about the table.
  • rules: Not used as of Joomla version 3.2.
  • field_mappings: Used by the com_tags component to map database columns from the component table to the ucm_content table.
  • router: Optional location of the component's router, if any.
  • content_history_options: JSON string used to store information for rendering the pop-up windows in the content history component. We will discuss this in detail later in this tutorial.

Our example component uses a table called #__joompro_subscriptions to store each subscription. In addition, it uses the standard Joomla categories. So we will add a row in #__content_types for the category table and one for the component table.

The row for the categories can be copied from the row for "Weblinks Category" (or any other category row). The only columns that need to be changed are type_title and type_alias. The other columns are the same as for com_weblinks.category.

The SQL statement for adding this row is as follows:

INSERT INTO `#__content_types` (`type_id`, `type_title`, `type_alias`, `table`, `rules`, `field_mappings`, `router`, `content_history_options`) 
VALUES
(null, 
'Subscription Category', 
'com_joomprosubs.category', 
'{"special":{"dbtable":"#__categories","key":"id","type":"Category","prefix":"JTable","config":"array()"},"common":   {"dbtable":"#__ucm_content","key":"ucm_id","type":"Corecontent","prefix":"JTable","config":"array()"}}', 
'', 
'{"common":{"core_content_item_id":"id","core_title":"title","core_state":"published","core_alias":"alias","core_created_time":"created_time","core_modified_time":"modified_time","core_body":"description", "core_hits":"hits","core_publish_up":"null","core_publish_down":"null","core_access":"access", "core_params":"params", "core_featured":"null", "core_metadata":"metadata", "core_language":"language", "core_images":"null", "core_urls":"null", "core_version":"version", "core_ordering":"null", "core_metakey":"metakey", "core_metadesc":"metadesc", "core_catid":"parent_id", "core_xreference":"null", "asset_id":"asset_id"}, "special":{"parent_id":"parent_id","lft":"lft","rgt":"rgt","level":"level","path":"path","extension":"extension","note":"note"}}', 
'WeblinksHelperRoute::getCategoryRoute', 
'{"formFile":"administrator\\/components\\/com_categories\\/models\\/forms\\/category.xml", "hideFields":["asset_id","checked_out","checked_out_time","version","lft","rgt","level","path","extension"], "ignoreChanges":["modified_user_id", "modified_time", "checked_out", "checked_out_time", "version", "hits", "path"],"convertToInt":["publish_up", "publish_down"], "displayLookup":[{"sourceColumn":"created_user_id","targetTable":"#__users","targetColumn":"id","displayColumn":"name"},{"sourceColumn":"access","targetTable":"#__viewlevels","targetColumn":"id","displayColumn":"title"},{"sourceColumn":"modified_user_id","targetTable":"#__users","targetColumn":"id","displayColumn":"name"},{"sourceColumn":"parent_id","targetTable":"#__categories","targetColumn":"id","displayColumn":"title"}]}');

For the actual subscriptions, we only need the following information in the content types table:

  • type_title: Subscriptions
  • type_alias: com_joomprosubs.subscription
  • table:
{"special":{"dbtable":"#__joompro_subscriptions","key":"id","type":"Subscription","prefix":"JoomprosubsTable"}}
This creates a JSON object as follows:
special:
dbtable = #__joompro_subscriptions
key = id
type = Subscription
prefix = JoomprosubsTable

The type_alias column is used by the com_contenthistory component to find the row for each component in the #__content_types table. The table column gives the com_contenthistory component the information it needs to work with the JTable class for each component. The None of the other columns need to be set for now. Note that we will come back to the content_history_options column later in the tutorial to improve the appearance of the pop-up window.

The SQL for adding this second row to the #__content_types table is as follows:

INSERT INTO `#__content_types` (`type_id`, `type_title`, `type_alias`, `table`, `rules`, `field_mappings`, `router`, `content_history_options`) 
VALUES
(null, 
'Subscriptions', 
'com_joomprosubs.subscription', 
'{"special":{"dbtable":"#__joompro_subscriptions","key":"id","type":"Subscription","prefix":"JoomprosubsTable"}}', 
'', '', '', '');

These lines should be added to the file administrator/components/com_joomprosubs/sql/install.mysql.utf8.sql so that these rows will be created when the component is installed. You should also run these commands on your database to add the two rows to your #__content_types table before going to the next section. Remember to change the placeholder prefix "#__" to the actual prefix for your database before running the SQL commands on your local database.

Tip: When creating the values for the JSON strings, one trick is to copy from an existing row and edit the strings.

Add Component Level Options[edit]

The content history component uses two options as follows.

  • save_history: If Yes, history is saved. Otherwise, no history is saved.
  • history_limit: If > 0, limits the number of different history versions saved in the database. For example, if this is set to 10, then when the 11th history version is saved, the oldest is deleted automatically.

If our example component, we will add the following lines to the file administrator/components/com_joomprosubs/config.xml.

<fieldset name="component">
	<field
		name="save_history"
		type="radio"
		class="btn-group btn-group-yesno"
		default="1"
		label="JGLOBAL_SAVE_HISTORY_OPTIONS_LABEL"
		description="JGLOBAL_SAVE_HISTORY_OPTIONS_DESC"
		>
		<option
			value="0">JNO</option>
		<option
			value="1">JYES</option>
	</field>
		
	<field
		name="history_limit"
		type="text"
		filter="integer"
		label="JGLOBAL_HISTORY_LIMIT_OPTIONS_LABEL"
		description="JGLOBAL_HISTORY_LIMIT_OPTIONS_DESC"
		default="5"
	/>
</fieldset>

With this code, we will now be able to add and save these options. Go into the Options for the component and set save_history to Yes and history_limit to 10.

Update Content History During Table Save and Delete[edit]

Next we need to tell our component to call the content history methods during the save and delete operations. We can do this by adding this line of code to the __construct() method of the JoomprosubsTableSubscription class.

JObserverMapper::addObserverClassToClass('JTableObserverContenthistory', 'JoomprosubsTableSubscription', array('typeAlias' => 'com_joomprosubs.subscription'));

This code registers the content history table as an observer class for our component table. When a row is saved or deleted from our table, the appropriate methods are called automatically for the content history table.

At this point, we should be able to see content history working for the Joomprosubs categories. Test this by adding a new category for the component and then clicking on the Versions button in the toolbar. It should work exactly the same as categories for the core Joomla components.

Add Versions Button to Tool Bar[edit]

At this point, categories are working completely and we are saving history versions in the #__ucm_history table for our component. However, we need to be able to access these changes on our edit screen. To do this, we need to add the Versions button to the edit screen's toolbar. We do this by adding this code to the file administrator/components/com_joomprosubs/views/subscription/view.html.php, just before the code that adds the "cancel" button, as shown here.

else {
  if ($this->state->params->get('save_history', 1) && $user->authorise('core.edit')) {
    JToolbarHelper::versions('com_joomprosubs.subscription', $this->item->id);
  }
  JToolBarHelper::cancel('subscription.cancel', 'JTOOLBAR_CLOSE');
}

This code checks that we have the save_history option set and that we have edit permission for this component. If so, we show the Versions button using the JToolbarHelper::versions() method. This method has two arguments: the type_alias and the primary key of the component item row in the database.

With this code added, we can now go into the Joomprosubs component, add and save a new subscription, and now see the Versions button. If we click on it, we will see the Item Version History modal window open with our saved version.

There are still two things missing, however. One is that we don't have a way to add the Version Note to each version. The second is that we don't currently have meaningful labels for the versions when we use the Preview or Compare features. We will address these in the next two sections of this tutorial.

Add Version Note Field to Form[edit]

The content history feature adds a new field called "version_note" to the component edit form. This optional field is not saved in the component table. Instead, it is used to label the version in the content history table.

To add this field to our component edit screen, we do changes to two files. First, we add the field to the XML form file administrator/components/com_joomprosubs/models/forms/subscription.xml as follows:

<field name="version_note"
	type="text"
	label="JGLOBAL_FIELD_VERSION_NOTE_LABEL"
	description="JGLOBAL_FIELD_VERSION_NOTE_DESC"
	class="inputbox" size="45"
	labelclass="control-label"
/>

We can add it anywhere inside the fieldset element. Because we will place the field "created_by_alias" on the screen, we add it in the XML file after this element.

Second, we add this code to the file administrator/components/com_joomprosubs/views/subscription/tmpl/edit.php, just after the control group for "created_by_alias", as follows:

<div class="control-group">
   <div class="control-label"><?php echo $this->form->getLabel('version_note'); ?></div>
  <div class="controls"><?php echo $this->form->getInput('version_note'); ?></div>
</div>

Now we can add in a version note and see this in the Item Version History modal window.

Add Labels to Pop-Up Windows[edit]

At this point, everything works. However, when we use the pop-up Preview or Compare windows in the Item Version History modal, we see the database column names instead of the translated label from the form. Also, we see the category and user id numbers instead of the category name and user name.

We can improve the readability of windows by entering in some additional about our component's table. This information is entered as a JSON-formatted string in the content_history_options column of the #__content_types table. The following information can be entered in this column.

  • formfile: This is the path to the XML JForm file for this form. If you add this, the Preview and Compare views will look up the labels from this XML file. This way the user will see translated labels instead of the database column name.
  • hideFields: Some database columns are not meaningful for the user when viewing the item. For example, asset_id or check_out_time are not things that appear in the form and are not helpful to the user when figuring out the contents of an item. This is entered as an array of column names.
  • ignoreChanges: The content history component uses a "hash" calculation (Sha1) to determine whether an item has changed. This allows you to see which version in history matches the current version. It also prevents duplicate versions from being saved in the content history table (for example, if you press the "save" button without making any changes). For this to work properly, we need to exclude some columns from the hash calculate. The "ignoreChanges" lets you exclude some database columns from the hash so that changes to these columns will not be considered real changes to the item. For example, columns such as "hits" or "modified_time" will change with each save, even if no meaningful data was changed in the item. This is an array of database column names.
  • convertToInt: When the hash value is created, it uses a JSON array of the database column values. In some cases, such as start and stop publishing dates, the value might be blank when a row is first created and then a different value after the item is saved. To get a consistent hash value for the first and subsequent saves, these values can be converted to integers before the hash is created. That way, we don't think a value has changed when it really hasn't. This is an array of database column names.
  • displayLookup: Here we can define how to display more meaningful data, for example, displaying a category title or user name instead of the ID. This is stored as an array of PHP standard class objects. Each object has the following fields.
  • sourceColumn: The column in the form to replace. For example, the "created_user_id" or "catid".
  • targetTable: The database table to get the title or name. For example, "#__users" or "#__categories".
  • targetColumn: The column in the target table to use in the SQL query JOIN statement. For example, "id".
  • displayColumn: The column in the target table to display in the Preview or Compare pop-up window. For example, "name" or "title".

In our example component, we need the following values.

  • formfile: administrator/components/com_joomprosubs/models/forms/subscription.xml.
  • hideFields: checked_out, checked_out_time, params, langauge. The last two columns are not used.
  • ignoreChanges: modified_by, modified, checked_out, checked_out_time.
  • convertToInt: publish_up, publish_down.
  • displayLookup: We look up five fields: catid, group_id, created_by, access, and modified_by. The values for each are as follows.
  • sourceColumn: catid
  • targetTable: #__categories
  • targetColumn: id
  • displayColumn: title
  • sourceColumn: group_id
  • targetTable: #__usergroups
  • targetColumn: id
  • displayColumn: title
  • sourceColumn: created_by
  • targetTable: #__users
  • targetColumn: id
  • displayColumn: name
  • sourceColumn: access
  • targetTable: #__viewlevels
  • targetColumn: id
  • displayColumn: title
  • sourceColumn: modified_by
  • targetTable: #__users
  • targetColumn: id
  • displayColumn: name

The entire JSON string for this column is as follows.

{"formFile":"administrator\/components\/com_joomprosubs\/models\/forms\/subscription.xml", 
"hideFields":["checked_out","checked_out_time","params","language"], 
"ignoreChanges":["modified_by", "modified", "checked_out", "checked_out_time"], 
"convertToInt":["publish_up", "publish_down"], 
"displayLookup":[
{"sourceColumn":"catid","targetTable":"#__categories","targetColumn":"id","displayColumn":"title"},
{"sourceColumn":"group_id","targetTable":"#__usergroups","targetColumn":"id","displayColumn":"title"},
{"sourceColumn":"created_by","targetTable":"#__users","targetColumn":"id","displayColumn":"name"},
{"sourceColumn":"access","targetTable":"#__viewlevels","targetColumn":"id","displayColumn":"title"},
{"sourceColumn":"modified_by","targetTable":"#__users","targetColumn":"id","displayColumn":"name"} ]}

Note that the slashes are escaped inside the JSON string. In order to get this column correctly into the database when our component is installed, we need to modify the file administrator/components/com_joomprosubs/sql/install.mysql.utf8.sql where we create the #__content_types for for the component as follows:

INSERT INTO `#__content_types` (`type_id`, `type_title`, `type_alias`, `table`, `rules`, `field_mappings`, `router`, `content_history_options`) VALUES
(null, 'Subscriptions', 'com_joomprosubs.subscription', '{"special":{"dbtable":"#__joompro_subscriptions","key":"id","type":"Subscription","prefix":"JoomprosubsTable"}}', '', '', '', '{"formFile":"administrator\\/components\\/com_joomprosubs\\/models\\/forms\\/subscription.xml", "hideFields":["checked_out","checked_out_time","params","language"], "ignoreChanges":["modified_by", "modified", "checked_out", "checked_out_time"], "convertToInt":["publish_up", "publish_down"], "displayLookup":[{"sourceColumn":"catid","targetTable":"#__categories","targetColumn":"id","displayColumn":"title"},{"sourceColumn":"group_id","targetTable":"#__usergroups","targetColumn":"id","displayColumn":"title"},{"sourceColumn":"created_by","targetTable":"#__users","targetColumn":"id","displayColumn":"name"},{"sourceColumn":"access","targetTable":"#__viewlevels","targetColumn":"id","displayColumn":"title"},{"sourceColumn":"modified_by","targetTable":"#__users","targetColumn":"id","displayColumn":"name"} ]}');

Note that the "/" characters are now preceeded by two escape "\" characters. The first one is the SQL escape character for the second "\" character. The second "\" is the JSON escape character for the "/" character.

With this change to the SQL install file, the changes for content history are complete.

Update Extension SQL Files[edit]

To tidy things up, should should edit the uninstall SQL file to remove the rows from the #__content_types table. Also, if your component uses the one-click update feature, you should create a SQL file to create these rows when the component is updated.

Hopefully, this tutorial will help you as you update your component to take advantage of the content history feature in Joomla. You can download the com_joomprosubs component with the code for content history added here: https://github.com/joomla/jdoc-examples/raw/master/zip_archives/com_joomprosubs-3.2.1.zip.