The problem
Joomla! has a fairly good component architecture but what it lacks is a way to break up a custom component into smaller modules, plugins and other components. This is essential for the development of complex components. An example is my own mvd-gui, which has multiple views of the same data. I realise that I can supply multiple controllers, models and view files and coordinate them all from a master controller. That's what I did before, but it gets unmanageable after more than a few views. I had eight with more planned.
However, breaking up a Joomla! application into many small components requires the end-user to manually install multiple components, modules and plugins. Since scripting of the installation isn't possible, how can many separately developed extensions be unified into a single package?
The Solution
I have seen two other approaches to this problem. One is a relatively low level customisation of the JInstaller code used in the Joomdle package. Another is this post on "Jeff Channel" which is higher level if also incomplete. It's very simple and I used it as the basis for my own approach. But whereas Jeff adds the components to the manifest file, so they get copied to the installation directory of one of the components to be installed, I decided not to copy any of them and instead created an empty Packager component that would just take care of the installation. The user drops any number of components, plugins and modules into an "extensions" folder within a generic Packager component, zips it up, and installs it. The install script then copies them directly from the tmp directory during installation.
Uninstalling
Likewise, when the user wants to uninstall the package, removal of the Packager component will trigger removal of all sub-extensions automatically. To do this the installer script saves a list of the extensions it originally installed in "extensions.txt" in the administrator directory. It will also need a small uninstall script for each sub-extension to warn (but not prohibit) the user from uninstalling it separately. The master Packager uninstall script can always skip any extensions it can't find so this doesn't have to generate an error.
So far I've got the install part working well, but not uninstall yet. Here is the manifest file of the Packager component. The current state of this package in on Googlecode.
<install type="component" version="1.5.0">
<name>packager</name>
<creationDate>2011-01-21</creationDate>
<author>Desmond Schmidt</author>
<copyright>Desmond Schmidt 2011</copyright>
<license>GPL v3</license>
<version>1.01</version>
<description>Packager to install/uninstall multiple
components/modules/plugins in one go</description>
<installfile>install.packager.php</installfile>
<files>
<filename>index.html</filename>
<filename>packager.php</filename>
</files>
<administration>
<menu>packager</menu>
<files folder="admin">
<filename>index.html</filename>
</files>
</administration>
</install>
Simple eh? Other than the "install.packager.php" file the rest are more or less empty files. Here's that installer script:
<?php
/**
* All-in-one installer/uninstaller component. To install
* a set of components, modules and plugins just drop them
* into the "extensions" directory within this component.
* Then zip up the component and install it via the Joomla
* interface. Similarly when uninstalling the packager
* all previously installed components and modules etc will
* be uninstalled (by the uninstaller script).
* Copyright 2011 Desmond Schmidt
* License: GPLv3.
* @package packager
*/
// No direct access
defined( '_JEXEC' ) or die( 'Restricted access' );
// rename this to something you like
define( 'PACKAGER','Packager');
jimport('joomla.installer.helper');
$installer = new JInstaller();
$installer->_overwrite = true;
$config =& JFactory::getConfig();
$tmp_dir = $config->getValue('config.tmp_path');
$dir_handle = opendir( $tmp_dir );
$jroot = JURI::root( true );
// look for the installation directory of Packager in tmp
if ( $dir_handle )
{
$found_dirs = array();
while ( $file = readdir($dir_handle) )
{
if ( strncmp("install_",$file,8)==0 )
{
if ( file_exists($tmp_dir.DS.$file.DS.PACKAGER) )
$found_dirs[] = $tmp_dir.DS.$file;
}
}
if ( count($found_dirs) > 0 )
{
$best_dir = $found_dirs[0];
$best_ctime = filectime( $found_dirs[0] );
for ($i=1;$i<count($found_dirs);$i++ )
{
if ( filectime($found_dirs[$i])>$best_ctime )
{
$best_dir = $found_dirs[$i];
$best_ctime = filectime( $found_dirs[$i] );
}
}
// so $best_dir is our best candidate directory
$extensions_dir = $best_dir.DS.PACKAGER.DS."extensions";
if ( file_exists($extensions_dir) )
{
// save record of installed extensions
$exts = array();
$types = array();
// look for and install all extensions
$zip_handle = opendir($extensions_dir);
if ( $zip_handle )
{
while ( $zip = readdir($zip_handle) )
{
// ends in ".zip"?
if ( strrpos($zip,".zip")==strlen($zip)-4 )
{
$zip_file = $extensions_dir.DS.$zip;
$package = JInstallerHelper::unpack( $zip_file );
$msgtext = "";
$msgcolor = "";
$pkgname = substr( $zip, 0, strlen($zip)-4 );
$image = $jroot."/administrator/images/tick.png";
if( $installer->install( $package['dir'] ) )
{
$msgcolor = "#E0FFE0";
$msgtext = "$pkgname successfully installed.";
if ( count($installer->_adapters)>0 )
{
$type = $package['type'];
$exts[] = $installer->_adapters[$type]->name;
$types[] = $type;
}
}
else
{
$msgcolor = "#FFD0D0";
$msgtext = "ERROR: Could not install the ".
$pkgname.". Please install manually.";
$image = $jroot
."/administrator/images/publish_x.png";
}
echo "<table bgcolor=\"$msgcolor\" width =\"100%\">";
echo "<tr style=\"height:30px\">";
echo "<td width=\"50px\"><img src=\"$image\" height="
."\"20px\" width=\"20px\"></td>";
echo "<td><font size=\"2\"><b>$msgtext</b></font></td>";
echo "</tr></table>";
JInstallerHelper::cleanupInstall(
$package['packagefile'], $package['extractdir'] );
}
}
closedir( $zip_handle );
}
// save record of installed extensions
if ( count($exts)> 0 )
{
$handle = fopen( $this->parent->_paths[
'extension_administrator'].DS."extensions.txt", "w" );
if ( $handle )
{
for ( $i=0;$i<count($exts)&&$i<count($types);$i++ )
fwrite( $handle, $types[$i]."\t".$exts[$i]."\n" );
fclose( $handle );
}
}
}
else
error_log(PACKAGER.": missing extensions directory!");
}
else
error_log(PACKAGER.": couldn't find a suitable install directory!");
closedir( $dir_handle );
}
else
error_log(PACKAGER.": couldn't open $tmp_dir!");
?>
You also need to create a folder called "extensions" within the Packager component where you can put sub-components and plugins etc. You will also probably want to rename the "Packager" component as something else. I'll post the uninstall script and any modifications to the installer script in my next post.