Difference between revisions of "Making single installation packages for Joomla! 1.5 and 2.5 series"

From Joomla! Documentation

Line 41: Line 41:
 
While this conditional statement looks like an overkill, it has two major advantages:
 
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
 
* 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.
+
* Once, let's say, Joomla! 3.0 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 ==
 
== One XML configuration file, multiple Joomla! versions ==

Revision as of 16:57, 1 February 2013

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, 1.7 and 2.5 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, 1.7 and 2.5. 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,'2.5.0','ge')) {
// Joomla! 2.5 code here
} elseif(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(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! 3.0 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"?>
 
	<!-- Joomla! 1.5 uses params -->

<params>
	<param name="foobar" type="text" default="" size="30"
		label="COM_FOOBAR_OPTION_FOOBAR_LBL"
		description ="COM_FOOBAR_OPTION_FOOBAR_DESC" />
</params>

 
	<!-- Joomla! 1.6 uses fieldset -->
<config>
	<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.

Obsolete code[edit]

Joomla 1.6 / 1.7 have been left any code obsolete, we should review code for do compatible with all versions.

Global Mainframe[edit]

Bad:

global $mainframe;

Good:

$mainframe = &JFactory::getApplication();

Global Option[edit]

Bad:

global $option;

Good:

$option = JRequest::getCmd('option');

I will add more changes like it.

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!

Menu item creation tricks[edit]

For Joomla! 1.5[edit]

You have to add a line similar to the following in your component's manifest XML file, inside the <administration> tag.

<menu view="yourview" img="../media/com_example/logo-16.png">COM_EXAMPLE</menu>

or

<menu view="yourview" img="../media/com_example/logo-16.png">Example Component</menu>

In both cases, you need to create a file named en-GB.com_example.menu.ini in administrator/languages/en-GB (you can use the manifest's <languages> tag to copy it during installation). In the first case (using a translation key), its contents should be:

COM_EXAMPLE=Example Component

In the second case (using natural language strings) its contents should be

Example Component=Example Component

NB: Joomla! 1.5 sorts the Component menu items based on the key you supply in your XML manifest. If you use the COM_EXAMPLE approach, your component will be sorted with all the components beginning with "C", e.g. just before Contacts on a fresh Joomla! 1.5 installation, no matter what the translation is! This leads to funny results, especially with non-English languages where the translated component names do not follow the same sorting order as their English names. If you are bilingual or multilingual, I think you know what I mean…

For Joomla! 1.6 and later versions[edit]

You have to add a line similar to the following in your component's manifest XML file, inside the <administration> tag.

<menu view="yourview" img="../media/com_example/logo-16.png">COM_EXAMPLE</menu>

You can not use a natural language string. Doing so will result in your component name appearing as example-component in the menu and you will be unable to provide a translation.

You need to create a file named en-GB.com_example.sys.ini in administrator/languages/en-GB (you can use the manifest's <languages> tag to copy it during installation) or in administrator/components/com_example/language/en-GB. In the latter case, you must not include the translation file in the <languages> tag. As long as you have placed the language directory in your <files> tag, it will be copied along when the component is being installed.

The contents of that file should be:

COM_EXAMPLE="Example Component"

Please note that the language string must be enclosed in double quotes, as per Joomla!'s translation standards.

NB: Joomla! 1.6 and later sorts the Component menu items based on the actual translation of the key you supply in your XML manifest. This means that the sorting order is correct no matter what you call your translation key and no matter which language the site is being displayed in. Essentially, Joomla! 1.6 fixed the wrong sorting of the Components menu for the majority (non-English speaking!) of Joomla! users.

How to have a cross-version (Joomla! 1.5, 1.6 and later) component[edit]

If your component is supposed to be installed under both Joomla! 1.5 and Joomla! 1.6/1.7/2.5, you can do a little trick. You have to add a line similar to the following in your component's manifest XML file, inside the <administration> tag.

<menu view="yourview" img="../media/com_example/logo-16.png">COM_EXAMPLE</menu>

Now, you need to create two files, one for Joomla! 1.5 and one for Joomla! 1.6. For Joomla! 1.5 you need to create a file named en-GB.com_example.menu.ini in administrator/languages/en-GB (you can use the manifest's <languages> tag to copy it during installation). Its contents should be:

COM_EXAMPLE=Example Component

For Joomla! 1.6 you need to create a file named en-GB.com_example.sys.ini in administrator/languages/en-GB (you can use the manifest's <languages> tag to copy it during installation) or in administrator/components/com_example/language/en-GB. In the latter case, you must not include the translation file in the <languages> tag. As long as you have placed the language directory in your <files> tag, it will be copied along when the component is being installed.

The contents of that file should be:

COM_EXAMPLE="Example Component"

The only drawback is that your component will appear as weirdly sorted in the Components menu of Joomla! 1.5. Given the very near expiration date of Joomla! 1.5 (April 2012) this is not a major drawback and experience shows that users don't really care that much.

What if you do that and nothing happens?[edit]

Joomla! is supposed to create the components menu entries afresh every time you re-install the component. If this doesn't happen in your case, try uninstalling and re-installing your component. If you're not sure if Joomla! has the correct Components menu item translation key, you can always check the menu items database table. This will prevent you from unnecessary frustration when your translation files don't work simply because Joomla! has the wrong translation key in the table.

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