How to integrate CakePHP to Joomla 1.6 as a component

Have you ever dreamed about integrating Joomla and CakePHP together such a way, that the best sides of both the systems would be utilized most efficiently? We’ll tell you now, how to do that!

Like we know, CakePHP is an excellent tool (for example) creating CRUD-interfaces from existing databases in a quick and easy fashion. However, it often takes too much of work when you have to code some features again and again (like image galleries, user registration system and many other things). On the other hand, Joomla is a well known content management system, that has a lot of features and plugins that are helpful when creating functional, dynamic and good-looking websites. So is there a way to make Cake and Joomla to work together?

Some years ago there was published an article about integrating Joomla and CakePHP together (this integration was called ”JAKE”). As many people (including we) found this article very useful, and since both of the systems have evolved during last years, we thought it is now a right time to update article’s technical content to cover Joomla 1.6 and CakePHP 1.3. Like in the article mentioned above, also this article is meant to inform you how to include CakePHP to Joomla as a component.

1. Start by installing CakePHP 1.3 in ’\components\com_cake’ of Joomla 1.6 directory. Of course, you need to create the subdirectory ’com_cake’ first. When CakePHP is installed, the directories app, cake, plugins and vendors should be found directly under ’\com_cake’ (like com_cake\app etc.).

2. After installation you have to create the so-called triggers for the component. Without them, Joomla can’t use the component. So open your favourite text-editor and create files ”cake.php” and ”cake.html.php” to ’components\com_cake’ -directory. Put following content to the files:

cake.php:

<?php
defined( '_JEXEC' ) or die( 'Restricted access' );

require_once( JApplicationHelper::getPath('front_html') );
jimport('joomla.application.component.controller');
jimport('joomla.application.component.helper');

$document = JFactory::getDocument();
$document->setTitle("com_cake: Ultimate Joomla Component");
$joomla_path = dirname(dirname(dirname(__FILE__)));

// As this component (cakephp) will need database access, lets include Joomla's config file
require_once($joomla_path.'/configuration.php');

// Constants to be used later in com_cake
$config = new JConfig();

define(JOOMLA_PATH,JURI::base());
define(DB_SERVER,$config->host);
define(DB_USER,$config->user);
define(DB_PASSWORD,$config->password);
define(DB_NAME,$config->db);

$controller = JArrayHelper::getValue($_REQUEST ,'module'); //option passed is treated as a controller in cake
$action = JArrayHelper::getValue($_REQUEST ,'task'); //task passed is treated as a controller in cake
$param = JArrayHelper::getValue($_REQUEST ,'id');
HTML_cake::requestCakePHP('/'.$controller.'/'.$action.'/'.$param);

cake.html.php:

<?php
defined( '_JEXEC' ) or die( 'Restricted access' );
class HTML_cake {
    function requestCakePHP($url) {
        $_GET['url']=$url;
        require_once 'app/webroot/index.php';
    }
}

Edit also Cake’s database.php following way:

class DATABASE_CONFIG
{
    var $default = array(
	'driver' => 'mysql',
	'connect' => 'mysql_connect',
	'host' => DB_SERVER,
	'login' => DB_USER,
	'password' => DB_PASSWORD,
	'database' => DB_NAME
    );
}

3. For next, let’s configure rewrite-rules. Joomla will now take over the URL Rewriting for CakePHP. Disable all CakePHP .htaccess files by renaming them for example as ”htaccess” (without the leading dot):

* components/com_cake/.htaccess
* components/com_cake/app/.htaccess
* components/com_cake/app/webroot/.htaccess

Configure then App.baseURL from ’components/com_cake/app/config/core.php’, by uncommenting and editing the following row:

Configure::write('App.baseUrl', 'components/com_cake/app');

4. After that, we need to modify Cake’s helpers that are used to output links, images and other things. This must be done in order to make the helpers to output URLs in Joomla’s format:

index.php?option=com_cake&module=names&task=edit&id=5

So open firstly the file ’app/config/bootstrap.php’ and add the following function to end of the file. It’s possible that you have to modify it afterwards (according to your application), but the function’s idea is to find individual parameters given by the user in URL (like ”task=edit” etc.) and output the URL in Joomla’s format:

function reform_url($url) {
    // Explode url to parts according to the / -character
    $temp=explode('/',$url);
    $controller = false;
    $action = false;
    $param = false;
    if (count($temp) > 3) $controller = "&module=".$temp[3];
    if (count($temp) > 4) $action = '&task='.$temp[4];
    if (count($temp) > 5) $param = '&id='.$temp[5];
    $url=JOOMLA_PATH.'index.php?option=com_cake'.$controller.$action.$param;
    return $url;
}

5. Then integrate the function ”reform_url” with all the helper functions that process URLs, to make them output URLs in Joomla’s format. Copy file ’cake/libs/view/helpers/html.php’ and ’form.php’ to directory ’app/views/helpers’ and file ’cake/libs/controller/controller.php’ to ’app/controllers’. After that, edit the following functions:

html.php:

function link($title, $url = null, $options = array(), $confirmMessage = false) {
	$escapeTitle = true;
	if ($url !== null) {
           $url = reform_url($this->url($url));
	} else {
		$url = reform_url($this->url($title));
		$title = $url;
		$escapeTitle = false;
	}
	if (isset($options['escape'])) {
		$escapeTitle = $options['escape'];
	}
	if ($escapeTitle === true) {
		$title = h($title);
	} elseif (is_string($escapeTitle)) {
		$title = htmlentities($title, ENT_QUOTES, $escapeTitle);
	}
	if (!empty($options['confirm'])) {
		$confirmMessage = $options['confirm'];
		unset($options['confirm']);
	}
	if ($confirmMessage) {
		$confirmMessage = str_replace("'", "\'", $confirmMessage);
		$confirmMessage = str_replace('"', '\"', $confirmMessage);
		$options['onclick'] = "return confirm('{$confirmMessage}');";
	} elseif (isset($options['default']) && $options['default'] == false) {
		if (isset($options['onclick'])) {
			$options['onclick'] .= ' event.returnValue = false; return false;';
		} else {
			$options['onclick'] = 'event.returnValue = false; return false;';
		}
		unset($options['default']);
	}
	return sprintf($this->tags['link'], $url, $this->_parseAttributes($options), $title);
}
function image($path, $options = array()) {
	if (is_array($path)) {
		$path = reform_url($this->url($path));
	} elseif (strpos($path, '://') === false) {
		if ($path[0] !== '/') {
			$path = IMAGES_URL . $path;
		}
		$path = $this->assetTimestamp($this->webroot($path));
	}
	if (!isset($options['alt'])) {
		$options['alt'] = '';
	}
	$url = false;
	if (!empty($options['url'])) {
		$url = $options['url'];
		unset($options['url']);
	}
	$image = sprintf($this->tags['image'], $path, $this->_parseAttributes($options, null, '', ' '));
	if ($url) {
		return sprintf($this->tags['link'], reform_url($this->url($url)), null, $image);
	}
	return $image;
}

form.php:

function create($model = null, $options = array()) {
        $created = $id = false;
        $append = '';
        $view =& ClassRegistry::getObject('view');
        if (is_array($model) && empty($options)) {
            $options = $model;
            $model = null;
        }
        if (empty($model) && $model !== false && !empty($this->params['models'])) {
            $model = $this->params['models'][0];
            $this->defaultModel = $this->params['models'][0];
        } elseif (empty($model) && empty($this->params['models'])) {
            $model = false;
        }
        $models = ClassRegistry::keys();
        foreach ($models as $currentModel) {
            if (ClassRegistry::isKeySet($currentModel)) {
                $currentObject =& ClassRegistry::getObject($currentModel);
                if (is_a($currentObject, 'Model') && !empty($currentObject->validationErrors)) {
                    $this->validationErrors[Inflector::camelize($currentModel)] =& $currentObject->validationErrors;
                }
            }
        }
        $object = $this->_introspectModel($model);
        $this->setEntity($model . '.', true);
        $modelEntity = $this->model();
        if (isset($this->fieldset[$modelEntity]['key'])) {
            $data = $this->fieldset[$modelEntity];
            $recordExists = (
                isset($this->data[$model]) &&
                !empty($this->data[$model][$data['key']]) &&
                !is_array($this->data[$model][$data['key']])
            );
            if ($recordExists) {
                $created = true;
                $id = $this->data[$model][$data['key']];
            }
        }
        $options = array_merge(array(
            'type' => ($created && empty($options['action'])) ? 'put' : 'post',
            'action' => null,
            'url' => null,
            'default' => true,
            'encoding' => strtolower(Configure::read('App.encoding')),
            'inputDefaults' => array()),
        $options);
        $this->_inputDefaults = $options['inputDefaults'];
        unset($options['inputDefaults']);
        if (empty($options['url']) || is_array($options['url'])) {
            if (empty($options['url']['controller'])) {
                if (!empty($model) && $model != $this->defaultModel) {
                    $options['url']['controller'] = Inflector::underscore(Inflector::pluralize($model));
                } elseif (!empty($this->params['controller'])) {
                    $options['url']['controller'] = Inflector::underscore($this->params['controller']);
                }
            }
            if (empty($options['action'])) {
                $options['action'] = $this->params['action'];
            }

            $actionDefaults = array(
                'plugin' => $this->plugin,
                'controller' => $view->viewPath,
                'action' => $options['action']
            );
            if (!empty($options['action']) && !isset($options['id'])) {
                $options['id'] = $this->domId($options['action'] . 'Form');
            }
            $options['action'] = array_merge($actionDefaults, (array)$options['url']);
            if (empty($options['action'][0])) {
                $options['action'][0] = $id;
            }
        } elseif (is_string($options['url'])) {
            $options['action'] = $options['url'];
        }
        unset($options['url']);

        switch (strtolower($options['type'])) {
            case 'get':
                $htmlAttributes['method'] = 'get';
            break;
            case 'file':
                $htmlAttributes['enctype'] = 'multipart/form-data';
                $options['type'] = ($created) ? 'put' : 'post';
            case 'post':
            case 'put':
            case 'delete':
                $append .= $this->hidden('_method', array(
                    'name' => '_method', 'value' => strtoupper($options['type']), 'id' => null
                ));
            default:
                $htmlAttributes['method'] = 'post';
            break;
        }
        $this->requestType = strtolower($options['type']);

        $htmlAttributes['action'] = reform_url($this->url($options['action']));
        unset($options['type'], $options['action']);

        if ($options['default'] == false) {
            if (isset($htmlAttributes['onSubmit']) || isset($htmlAttributes['onsubmit'])) {
                $htmlAttributes['onsubmit'] .= ' event.returnValue = false; return false;';
            } else {
                $htmlAttributes['onsubmit'] = 'event.returnValue = false; return false;';
            }
        }

        if (!empty($options['encoding'])) {
            $htmlAttributes['accept-charset'] = $options['encoding'];
            unset($options['encoding']);
        }

        unset($options['default']);
        $htmlAttributes = array_merge($options, $htmlAttributes);

        $this->fields = array();
        if (isset($this->params['_Token']) && !empty($this->params['_Token'])) {
            $append .= $this->hidden('_Token.key', array(
                'value' => $this->params['_Token']['key'], 'id' => 'Token' . mt_rand())
            );
        }

        if (!empty($append)) {
            $append = sprintf($this->Html->tags['block'], ' style="display:none;"', $append);
        }

        $this->setEntity($model . '.', true);
        $attributes = $this->_parseAttributes($htmlAttributes, null, '');
        return sprintf($this->Html->tags['form'], $attributes) . $append;
}

controller.php:

function flash($message, $url, $pause = 1, $layout = 'flash') {

    $this->autoRender = false;
    $this->set('url', reform_url(Router::url($url)));
    $this->set('message', $message);
    $this->set('pause', $pause);
    $this->set('page_title', $message);
    $this->render(false, $layout);
}

Finally, edit the file ’cake/libs/router.php’:

function url($url = null, $full = false) {

        $self =& Router::getInstance();
        $defaults = $params = array('plugin' => null, 'controller' => null, 'action' => 'index');

        if (is_bool($full)) {
            $escape = false;
        } else {
            extract($full + array('escape' => false, 'full' => false));
        }

        if (!empty($self->__params)) {
            if (isset($this) && !isset($this->params['requested'])) {
                $params = $self->__params[0];
            } else {
                $params = end($self->__params);
            }
        }
        $path = array('base' => null);

        if (!empty($self->__paths)) {
            if (isset($this) && !isset($this->params['requested'])) {
                $path = $self->__paths[0];
            } else {
                $path = end($self->__paths);
            }
        }
        $base = $path['base'];
        $extension = $output = $mapped = $q = $frag = null;

        if (is_array($url)) {
            if (isset($url['base']) && $url['base'] === false) {
                $base = null;
                unset($url['base']);
            }
            if (isset($url['full_base']) && $url['full_base'] === true) {
                $full = true;
                unset($url['full_base']);
            }
            if (isset($url['?'])) {
                $q = $url['?'];
                unset($url['?']);
            }
            if (isset($url['#'])) {
                $frag = '#' . urlencode($url['#']);
                unset($url['#']);
            }
            if (empty($url['action'])) {
                if (empty($url['controller']) || $params['controller'] === $url['controller']) {
                    $url['action'] = $params['action'];
                } else {
                    $url['action'] = 'index';
                }
            }

            $prefixExists = (array_intersect_key($url, array_flip($self->__prefixes)));
            foreach ($self->__prefixes as $prefix) {
                if (!empty($params[$prefix]) && !$prefixExists) {
                    $url[$prefix] = true;
                } elseif (isset($url[$prefix]) && !$url[$prefix]) {
                    unset($url[$prefix]);
                }
                if (isset($url[$prefix]) && strpos($url['action'], $prefix . '_') === 0) {
                    $url['action'] = substr($url['action'], strlen($prefix) + 1);
                }
            }

            $url += array('controller' => $params['controller'], 'plugin' => $params['plugin']);

            if (isset($url['ext'])) {
                $extension = '.' . $url['ext'];
                unset($url['ext']);
            }
            $match = false;

            for ($i = 0, $len = count($self->routes); $i < $len; $i++) {                 $originalUrl = $url;                 if (isset($self->routes[$i]->options['persist'], $params)) {
                    $url = $self->routes[$i]->persistParams($url, $params);
                }

                if ($match = $self->routes[$i]->match($url)) {
                    $output = trim($match, '/');
                    break;
                }
                $url = $originalUrl;
            }

            $output = str_replace("/sort:","/sort=",$output);
            $output = str_replace("/direction:","/direction=",$output);

            if ($match === false) {
                $output = $self->_handleNoRoute($url);
            }

            $output = str_replace('//', '/', $base . '/' . $output);
            } else {
            $url = reform_url($url);
            if (((strpos($url, '://')) || (strpos($url, 'javascript:') === 0) || (strpos($url, 'mailto:') === 0)) || (!strncmp($url, '#', 1))) {
                return $url;
            }
            if (empty($url)) {
                if (!isset($path['here'])) {
                    $path['here'] = '/';
                }
                $output = $path['here'];
            } elseif (substr($url, 0, 1) === '/') {
                $output = $base . $url;
            } else {
                $output = $base . '/';
                foreach ($self->__prefixes as $prefix) {
                    if (isset($params[$prefix])) {
                        $output .= $prefix . '/';
                        break;
                    }
                }
                if (!empty($params['plugin']) && $params['plugin'] !== $params['controller']) {
                    $output .= Inflector::underscore($params['plugin']) . '/';
                }
                $output .= Inflector::underscore($params['controller']) . '/' . $url;
            }
            $output = str_replace('//', '/', $output);
        }
        if ($full && defined('FULL_BASE_URL')) {
            $output = FULL_BASE_URL . $output;
        }
        if (!empty($extension) && substr($output, -1) === '/') {
            $output = substr($output, 0, -1);
        }

        return $output . $extension . $self->queryString($q, array(), $escape) . $frag;
}

6. Your new CakePHP -component is now ready to be tested, so open it by writing an URL like:

http://localhost/joomla/index.php?option=com_cake

If everything went right, you’ll see Cake’s homepage in Joomla layout. Make sure you have edited your default.html file so that CSS and HTML tags do not mess up.

7. Maybe you want to distribute your new CakePHP -component for your friends or other people? Even if you don’t, it is recommended that all the components used by Joomla are installed correctly to CMS.

It’s quite easy task to to create an install package for your CakePHP -component. Just create an empty folder to somewhere on your disk (like ’temp/com_cake’) and make there two subdirectories: ’site’ and ’admin’. Then copy all the content from the folder in where you just created your component (like ’joomla/components/com_cake’) to the new sub-directory ’temp/com_cake/site’.

As we aren’t going to make an admin-interface for the CakePHP-component (at least, yet), just create the following two files to the folder ’temp/com_cake/admin’:

cake.php:

<html>
<body>
    CakePHP component v1.0
</body>
</html>

index.html:

<html>
<body>
</body>
</html>

Finally, create an install-file for the component. Open your favorite text-editor and create file ’install.xml’ to the root folder ’temp/com_cake’. Add the following content to the file:

<?xml version="1.0" encoding="utf-8"?>
<install type="component" version="1.6.2">

 <name>Cake</name>
 <!-- The following elements are optional and free of formatting constraints -->
 <creationDate>2011-08-08</creationDate>
 <author>Your name</author>
 <authorEmail>Your e-mail address</authorEmail>
 <authorUrl>Your web-site</authorUrl>
 <copyright>Copyright Info</copyright>
 <license>License Info</license>
 <!--  The version string is recorded in the components table -->
 <version>1.00</version>
 <!-- The description is optional and defaults to the name -->
 <description>CakePHP inside Joomla</description>

 <!-- Site Main File Copy Section -->
 <!-- Note the folder attribute: This attribute describes the folder
      to copy FROM in the package to install therefore files copied
      in this section are copied from /site/ in the package -->
 <files folder="site">
     <filename>cake.php</filename>
     <filename>cake.html.php</filename>
     <filename>index.php</filename>
     <filename>README</filename>
     <folder>app</folder>
     <folder>cake</folder>
     <folder>vendors</folder>
     <folder>plugins</folder>
 </files>

 <administration>
  <!-- Administration Menu Section -->
  <menu>Cake</menu>

  <!-- Administration Main File Copy Section -->
  <files folder="admin">
      <filename>cake.php</filename>
      <filename>index.html</filename>
  </files>

 </administration>
</install>

8. When you have saved install.xml -file, just pack it with the subdirectories ”site” and ”admin” to the zip-package ’com_cake.zip’. This file can then be distributed and installed to Joomla simply with using Joomla’s Extension manager!

Article written by
Jussi Laine