Making single installation packages for Joomla! 1.5 and 2.5 series

From Joomla! Documentation

Revision as of 05:57, 18 July 2011 by Radek (talk | contribs) (closing brackest)
Copyedit.png
This Article Needs Your Help

This article is tagged because it NEEDS REVIEW. You can help the Joomla! Documentation Wiki by contributing to it.
More pages that need help similar to this one are here. NOTE-If you feel the need is satistified, please remove this notice.


It is possible to create a single installation package which can install a component in Joomla! 1.5, 1.6 and 1.7 with minor changes. Below you will find some of the most common tricks required to create an extension which is compatible with all current versions of the Joomla! CMS.

Dealing with API changes[edit]

You will regularly bump into cases where an API has changed between Joomla! 1.5, 1.6 and 1.7. In order to cater for them, you need to run slightly different code based on the Joomla! version. Right now it's possible to do that by writing conditional statements like this:

if(version_compare(JVERSION,'1.7.0','ge')) {
// Joomla! 1.7 code here
} elseif(version_compare(JVERSION,'1.6.0','ge')) {
// Joomla! 1.6 code here
} else {
// Joomla! 1.5 code here
}

If you do not have Joomla! 1.7-specific code -i.e it is the same as the Joomla! 1.6-specific code- please use the following code:


if(version_compare(JVERSION,'1.6.0','ge')) {
// Joomla! 1.6 code here
} else {
// Joomla! 1.5 code here
}

While this conditional statement looks like an overkill, it has two major advantages:

  • Compatibility with all Joomla! versions using the same package, therefore using the same codebase, ergo less maintenance overhead
  • Once, let's say, Joomla! 1.8 is released and you decide to drop support for earlier Joomla! releases, we can just search your code for "if(version_compare(JVERSION," and remove all of the legacy code.

One XML configuration file, multiple Joomla! versions[edit]

Various XML files in Joomla! accept settings, or parameters. For example your extension manifest file, the config.xml file and the view XML files all accept such settings. You may have noticed that whereas in Joomla! 1.5 these were found under the <params> tags, in Joomla! 1.6 and later they are found inside <filedsets>. One hidden secret of Joomla! is that Joomla! 1.5 will ignore Joomla! 1.6 syntax, whereas Joomla! 1.6/1.7 will ignore Joomla! 1.5 syntax. This allows you to have a single XML file which caters for all Joomla! releases. For example, here's a sample config.xml file, to be placed in the component's backend directory:

<?xml version="1.0" encoding="utf-8"?>
<config>
	<params>
		<!-- Joomla! 1.5 uses params -->
		<param name="foobar" type="text" default="" size="30"
			label="COM_FOOBAR_OPTION_FOOBAR_LBL"
			description ="COM_FOOBAR_OPTION_FOOBAR_DESC" />
	</params>

	<fieldset>
		<!-- Joomla! 1.6 uses fieldset -->
		name="basic"
		label="COM_FOOBAR_OPTIONS_SECTION_BASIC_LBL"
		description="COM_FOOBAR_OPTIONS_SECTION_BASIC_DESC"
		>
		<field name="foobar" type="text" default="" size="30"
			label="COM_FOOBAR_OPTION_FOOBAR_LBL"
			description ="COM_FOOBAR_OPTION_FOOBAR_DESC" />
	</fieldset>
</config>

That's how easy it is!

Dealing with languages[edit]

How to load component-local translation files in Joomla! 1.5[edit]

Joomla! 1.6 introduced a new feature where a user can place language files inside the component's directory, e.g. administrator/com_foobar/language/en-GB/en-GB.com_foobar.ini, in order to provide language overrides. But Joomla! 1.5 doesn't support this feature. Or does it? Even though it can not do that automatically, you can do so in your extension's dispatcher file, e.g. administrator/com_foobar/foobar.php. Here's the magic code to do that in the backend:

$jlang =& JFactory::getLanguage();
$jlang->load('com_foobar', JPATH_ADMINISTRATOR, null, true);
$jlang->load('com_foobar', JPATH_COMPONENT_ADMINISTRATOR, null, true);

and the frontend:

$jlang =& JFactory::getLanguage();
$jlang->load('com_foobar', JPATH_SITE, null, true);
$jlang->load('com_foobar', JPATH_COMPONENT, null, true);

Tips and tricks for language strings[edit]

All your translation keys should consist of uppercase characters without spaces (that is A-Z, 0-9 and underscores). All translation values should be included in double quotation marks. This allows the translation files to be parsed by Joomla! 1.5/1.6/1.7 alike. You are also suggested to prefix your translation keys with the extension's name, albeit this is not required. For example, this is a good translation string: COM_FOOBAR_CPANEL_TITLE="Foobar Control Panel" whereas this is a bad translation string: CONTROL PANEL=Foobar Control Panel The former works with all current versions of Joomla!, the latter doesn't.

If you have double quotation marks in your translation values, please replace them with single quotation marks. While Joomla! 1.5 allowed escaping the double quotation marks with \", Joomla! 1.6 under newer versions of PHP don't and require you to escape them as "__QQ__" which throws off Joomla! 1.5's parser. The workaround is to take advantage of a little known fact about HTML, that attributes can be wrapped in single quotation marks. For instance, change this: COM_FOOBAR_SOME_KEY=<a href="http://www.example.com">Just a test</a> to this: COM_FOOBAR_SOME_KEY="<a href='http://www.example.com'>Just a test</a>" It is valid HTML, it works on Joomla! 1.5/1.6/1.7 but it will invalidates XHTML. You can't always win, sorry.

Making untranslated strings look more beautiful[edit]

The problem with using tight translation keys as we described is that untranslated strings now look something like COM_FOOBAR_VIEWNAME_SOMEKEY. If you have volunteer translators you can bet your head that most translations will not be in sync with your component and you will have untranslated strings. In Joomla! 1.5, using "natural language" keys, you got to show your users the default (English) text if a translation key didn't exist in their language. This is impossible in Joomla! 1.6... or maybe not?

We can use the same little trick in our component's dispatcher as with our component-local translation loading string above. Here's how to load the English translation file in the backend of your component, then overwrite only the keys which exist with the user's selected language, essentially allowing untranslated keys to show in English:

$jlang =& JFactory::getLanguage();
$jlang->load('com_foobar', JPATH_ADMINISTRATOR, 'en-GB', true);
$jlang->load('com_foobar', JPATH_ADMINISTRATOR, null, true);

The same thing in the front-end:

$jlang =& JFactory::getLanguage();
$jlang->load('com_foobar', JPATH_SITE, 'en-GB', true);
$jlang->load('com_foobar', JPATH_SITE, null, true);

JElement vs JFormField[edit]

Oh, the struggle! You need a custom widget in the configuration section of your component. In Joomla! 1.5 you could just create a new JElement. In Joomla! 1.6 you could just create a new JFormField. But both? In a single file? You can employ a simple trick. In the following example I am going to create a rather lame element, called SQL2, which displays a multi-selection box out of the results of a SQL statement. This is made as part of a fictitious module called mod_foobar

First, let your configuration know where to load the custom widget files from, by putting this in your module's XML manifest file:

	<params addpath="/modules/mod_foobar/elements">
		<param name="ids" type="sql2" default=""
			label="MOD_FOOBAR_LEVELS_TITLE"
			description="MOD_FOOBAR_LEVELS_DESC"
			query="SELECT `foobar_level_id`, `title` FROM `#__foobar_levels`"
			key_field="foobar_level_id"
			value_field="title" />
	</params>
	
	<config addfieldpath="/modules/mod_foobar/elements">
		<fields name="params">
			<fieldset name="basic">
				<field name="ids" type="sql2" default=""
					label="MOD_FOOBAR_LEVELS_TITLE"
					description="MOD_FOOBAR_LEVELS_DESC"
					query="SELECT `foobar_level_id`, `title` FROM `#__foobar_levels`"
					key_field="foobar_level_id"
					value_field="title" />
			</fieldset>
		</fields>
	</config>

As you see, we instruct both Joomla! 1.5 and 1.6/1.7 to look into the same directory, for the same-named file (sql2.php). Here are the contents of the modules/mod_foobar/elements/sql2.php file:

defined('_JEXEC') or die('Restricted Access');

/*
 * This trick allows us to extend the correct class, based on whether it's Joomla! 1.5 or 1.6
 */
if(!class_exists('JFakeElementBase')) {
        if(version_compare(JVERSION,'1.6.0','ge')) {
                class JFakeElementBase extends JFormField {
                        // This line is required to keep Joomla! 1.6/1.7 from complaining
                        public function getInput() {}
                }               
        } else {
                class JFakeElementBase extends JElement {}
        }
}

/**
 * Our main element class, creating a multi-select list out of an SQL statement
 */
class JFakeElementSQL2 extends JFakeElementBase
{
	var	$_name = 'SQL2';

	// Joomla! 1.5
	function fetchElement($name, $value, &$node, $control_name)
	{
		$db			= & JFactory::getDBO();
		$db->setQuery($node->attributes('query'));
		$key = ($node->attributes('key_field') ? $node->attributes('key_field') : 'value');
		$val = ($node->attributes('value_field') ? $node->attributes('value_field') : $name);
		return JHTML::_('select.genericlist',  $db->loadObjectList(), ''.$control_name.'['.$name.'][]', 'class="inputbox" multiple="multiple" size="5"', $key, $val, $value, $control_name.$name);
	}
	
	// Joomla! 1.6
	function getInput()
	{
		$db			= & JFactory::getDBO();
		$db->setQuery($this->element['query']);
		$key = ($this->element['key_field'] ? $this->element['key_field'] : 'value');
		$val = ($this->element['value_field'] ? $this->element['value_field'] : $this->name);
		return JHTML::_('select.genericlist',  $db->loadObjectList(), $this->name, 'class="inputbox" multiple="multiple" size="5"', $key, $val, $this->value, $this->id);
	}
}

/*
 * Part two of our trick; we define the proper element name, depending on whether it's Joomla! 1.5 or 1.6
 */
if(version_compare(JVERSION,'1.6.0','ge')) {
        class JFormFieldSQL2 extends JFakeElementSQL2 {}
} else {
        class JElementSQL2 extends JFakeElementSQL2 {}                
}

Considering that most custom widgets have a lot more code than this, you can create two different initialisation sections for Joomla! 1.5 and 1.6/1.7 (fetchElement and getInput), populate class variables, then call a big, common method to render the bulk of the widget (just like we did with JHTML's genericlist in the example above). This will eliminate the need to rewrite the same code over again just to cater for a new Joomla! version.

Content plugins work slightly different[edit]

The content plugins in Joomla! 1.5 were using the onPrepareContent method, whereas the content plugins in Joomla! 1.6/1.7 use the onContentPrepare method. The arguments have also changed. You can, however, create a single content plugin which runs on both Joomla! versions without rewriting code. Here's how:

defined('_JEXEC') or die();

jimport('joomla.plugin.plugin');

class plgContentFoobar extends JPlugin
{
	public function onPrepareContent( &$article, &$params, $limitstart = 0 )
	{
		$article->text = $this->doSomethingWith($article->text);
	}
	
	public function onContentPrepare($context, &$row, &$params, $page = 0)
	{
		// Danger, Will Robinson! $row in Joomla! 1.6/1.7 may be a string, not an article object!
		if(is_object($row)) {
			return $this->onPrepareContent($row, $params, $page);
		} else {
			$row = $this->doSomethingWith($row);
		}
		
		return true;
	}

	private function doSomethingWith($text)
	{
		// Apparently, you have to do something here ;)
		return $text;
	}
}

Updating doesn't work as you'd expect[edit]

There is a small but very disturbing bug in Joomla! 1.6. Updating a component whose manifest XML states version="1.5.0" doesn't allow you to run the SQL statements. Namely, the SQL files you specify under the "<install><sql>" tags won't run. You can neither use the new "<update>" tag. You are stuck with no way to run SQL commands on component update!

The ugly workaround is to create a file named script.foobar.php with the following contents:

<?php
define('_JEXEC') or die();

class Com_FoobarInstallerScript {
	
	function update($parent) {
		$db = JFactory::getDBO();
		// Obviously you may have to change the path and name if your installation SQL file ;)
		if(method_exists($parent, 'extension_root')) {
			$sqlfile = $parent->getPath('extension_root').DS.'install.sql';
		} else {
			$sqlfile = $parent->getParent()->getPath('extension_root').DS.'install.sql';
		}
		// Don't modify below this line
		$buffer = file_get_contents($sqlfile);
		if ($buffer !== false) {
			jimport('joomla.installer.helper');
			$queries = JInstallerHelper::splitSql($buffer);
			if (count($queries) != 0) {
				foreach ($queries as $query)
				{
					$query = trim($query);
					if ($query != '' && $query{0} != '#') {
						$db->setQuery($query);
						if (!$db->query()) {
							JError::raiseWarning(1, JText::sprintf('JLIB_INSTALLER_ERROR_SQL_ERROR', $db->stderr(true)));
							return false;
						}
					}
				}
			}
		}
	}

Reference that file in the end of your manifest XML file, just above the closing install tag, with:

<scriptfile>script.foobar.php</scriptfile>

This will force Joomla! 1.6 to run your extension's installation SQL file during the component update, just like Joomla! 1.5 used to do. Enjoy!

More tricks?[edit]

I am sure that by writing this page I have left out a number of tricks which I am already using in my software. If you get stuck somewhere, feel free to take a look at how I've implemented things in Admin Tools Core, Akeeba Backup Core and Akeeba Release System, my Joomla! 1.5/1.6/1.7 compatible extensions. If you find a better way to implement something, or found a trick not listed here, feel free to edit this wiki page. Sharing the knowledge is caring!

Peace, love and friendship,

Nicholas K. Dionysopoulos, AkeebaBackup.com Lead Developer