Plugin development tips and tricks

by sky_l3ppard
This tutorial is for users who are already familiar with plugins. The main objective of it is to show the power of plugins and to make them more independent from an application itself.

I will start from the most common problems then creating an independent plugin:

  • Configuration files are configured to be common
  • There is no callback to a plugin AppController like beforeFilter from a controller

Plugin configuration and callback methods

I think many developers struggle on handling these operations. In this tutorial i will build a Language Controller Plugin as an example so the main functionality will be storing a chosen language in the session and loading it into configuration before any controller action.

It is very simple plugin but it needs some adjustments in the AppController because PluginAppController is accessed only then it`s controller action is requested and a configuration file like /app/config/bootstrap.php to set default language. My purpose is to make none of adjustments in the application. One and only thing is to display a language switcher in the layout

First you will need a PluginHandler Component which will do all this magic

Download PluginHandler component and load with all others in your /app/app_controller.php like:

Controller Class:

<?php 
//File: /app/app_controller.php

class AppController extends Controller {
    var 
$components = array('PluginHandler''Session');
    var 
$helpers = array('Html''Javascript');
}
?>

Lets bake our plugin with cake console

by using command cake bake plugin lcp it will create a lcp plugin in your /app/plugins directory. If you having problems with it read the manual about cake console

Now you should have a plugin like this tree structure:

/app
    /plugins
        /lcp
            /controllers
            /models
            /views
            lcp_app_controller.php
            lcp_app_model.php

Create a folder config in the plugin root like one in app, location would be /app/plugins/lcp/config and create a bootstrap.php file in this new folder. PluginHandler component will automatically include these configuration files from all plugins

Notice: be careful with configuration settings because plugin config files will override matching app settings, it is better to name settings with a plugin prefix

We will override or set a default language in our plugin bootstrap.php configuration file like:

<?php
//File: /app/plugins/lcp/config/bootstrap.php

// in this case our PluginHandler will overwrite default language
// if any was set in one of the app/config/ configuration files
Configure::write('Config.language''en');
?>

Now we will need a simple language controller class

Create a language controller in your /app/plugins/lcp/controllers directory:

Controller Class:

<?php 
//File: /app/plugins/lcp/controllers/lcp_languages_controller.php

class LcpLanguagesController extends LcpAppController {
    var 
$name 'LcpLanguages';
    var 
$uses null;

    function 
change($lang) {
        
$this->Session->write('Config.language'$lang);
        
$this->redirect($this->referer(), nulltrue);
    }
}
?>

Here comes the most important part - setting the language before any controller action

We will create a separate file which will hold callback functions required for our purpose. Create lcp_auto_loader.php file in our plugin root directory:

<?php
//File: /app/plugins/lcp/lcp_auto_loader.php

/**
 * This is a callback class for an app controller to
 * communicate with plugin methods which are required
 * to execute permanently then using this plugin. These
 * callback methods are called from a PluginHandler component
 * 
 * @author Sky_l3ppard
 * @version 1.0
 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
 * @category Plugins
 *
 */
class LcpAutoLoader extends Object {
    
    
/**
     * This callback method is executed right after initialization of 
     * PluginHandler component. Triggered by PluginHandler component's
     * initialize method.
     * 
     * @param Object $controller - reference to the caller
     * @return void
     */
    
function initialize(&$controller) {
        if (
array_key_exists('Session'$controller->Component->_loaded) && $controller->Component->_loaded['Session']->enabled) {
            
$Session = &$controller->Component->_loaded['Session'];
            if(
$Session->check('Config.language')) {
                
Configure::write('Config.language'$Session->read('Config.language'));
            } else {
                
$Session->write('Config.language'Configure::read('Config.language'));
            }
        } else {
            
$this->log('LCP Plugin Loader initialize: Session component required');
        }
    }
    
    
/**
     * This callback method is executed right after AppController's
     * beforeFilter method. Triggered by PluginHandler component's
     * startup method.
     * 
     * @param Object $controller - reference to the caller
     * @return void
     */
    
function beforeFilter(&$controller) {
    }
    
    
/**
     * This callback method is executed right after AppController's
     * beforeRender method. Triggered by PluginHandler component's
     * beforeRender method.
     * 
     * @param Object $controller - reference to the caller
     * @return void
     */
    
function beforeRender(&$controller) {    
        
App::Import('Core''Folder');
        
$folder = new Folder(APP.'locale');
        
$content $folder->read();
        unset(
$folder);
        
        foreach (
$content[0] as $lang) {
            
$record['link'] = Router::url(array(
                
'plugin' => 'lcp',
                
'controller' => 'lcp_languages',
                
'action' => 'change',
                
$lang
            
));
            
$record['title'] = up($lang);
            
$list[] = $record;
        }
        
$controller->set('languages'$list);
    }
}
?>

If we want to have some languages add few translations to your /app/locale folder, for example add ENG and LIT locales:

Locale tree structure:

/app
    /locale
        /eng
            /LC_MESSAGES
                default.po
        /lit
            /LC_MESSAGES
                default.po

eng locale default.po file:

#File: /app/locale/eng/LC_MESSAGES/default.po
msgid "translation"
msgstr "An english language Translation"

lit locale default.po file:

#File: /app/locale/lit/LC_MESSAGES/default.po
msgid "translation"
msgstr "Some high tech alien language Translation"

To finish your application in your layout template add somewhere:

View Template:

<!-- File: /app/views/layouts/default.ctp -->

<h1><?php __('translation')?></h1>
<?php 
    
if (!empty($languages)) {
        foreach (
$languages as $lang) {
            echo 
'&nbsp;&nbsp;';
            echo 
$html->link($lang['title'], $lang['link']);
        }
    }
?>

So what the hell happened then we added LcpAutoLoader class?


Our callback class LcpAutoLoader is called on every PluginHandler component callback(method) like startup(), afterRender(), initialize() and these callbacks triggers LcpAutoLoader`s methods. In this case then initialize method is triggered function checks the session and writes current language to config. And then beforeRender is triggered function checks for locales adds languages variable to the caller's template wars

Notice:LcpAutoLoader class and file name depends on the name of plugin. And PluginHandler`s position in the component array is also important if you want to trigger beforeFilter callback before another component startup method.


A tip on how to use translation files under plugins


Localization for plugins is handled well and you can use it simply by giving a plugin name for your po or mo files. For example your locale folder in the plugin root directory should look like:

/app
    /plugins
        /lcp
            /locale
                /eng
                    /LC_MESSAGES
                        lcp.po
                /lit
                    /LC_MESSAGES
                        lcp.po
                    /LC_MONETARY
                        lcp.po
            /controllers
            ...

And you should use translation function with possibility to specify domain for example:

View Template:

<h1><?php __d('lcp''test')?></h1>

If somehow you are not using mod_rewrite and .htaccess files, you will not be able to load media files for plugins like /plugin/css/cssfile, because htaccess configuration is needed here. This situation can occur then hosting company is not allowing to have htaccess files and mod_rewrite for apache. The most convenient way is to override helper method so lets create a file /app/app_helper.php and copy the following code:

[b]Helper Class:

<?php 
//File: /app/app_helper.php

App::import('Core''Helper');
/**
 * Overrides webroot method for plugin css js img integration
 * 
 */
class AppHelper extends Helper {
    
    
/**
     * Overrides webroot method, which in case of plugin changes
     * css, js or image location. Plugin is identified by /plugin_name/
     * slash is important. You can check the manual
     * 
     * @see cake/libs/view/Helper#webroot($file)
     * @param String file - media file
     * 
     */
    
function webroot($file) {
        foreach (
Configure::listobjects('plugin') as $plugin) {
            
$plugin Inflector::underscore($plugin); 
            if (
strpos($file'/'.$plugin.'/') !== false && strpos($file'/'.$plugin.'/') == 0) {
                
$webPath substr($this->webroot0strpos($this->webroot'webroot'));
                
$webPath .= 'plugins/'.$plugin.'/vendors'.r('/'.$plugin''$file);
                return 
$webPath;
            }
        }
        
        return 
parent::webroot($file);
    }
}
?>

Now our app_helper will automatically override webroot function which in case of identified plugin will return a location in plugin folder. If no plugin was detected it will return usual method implementation.

Here is an example on how to retrieve your plugin media files, for more information read manual.

[b]View Template:

<?php
//File: /app/plugins/my_plugin/views/my_plugin_controller/action.ctp
echo $html->css('/my_plugin/css/main'nullnullfalse);
echo 
$html->css('/my_plugin/css/new');
echo 
$javascript->link('/my_plugin/js/my_js');
?>
<div id="my_css_div"><?php $html->image('/my_plugin/img/my_image.png')?></div>

Your plugin media file tree should look like:

/app
    /plugins
        /my_plugin
            /vendors
                /img
                    my_image.png
                /css
                    main.css
                    new.css
                /js
                    my_js.js
            /...

Any ideas on functionality improvements are very welcome, enjoy

Report

More on Tutorials

Advertising

Comments

  • dmayer77 posted on 08/05/10 02:26:31 PM
    good article.

    thanks
  • lucascaro posted on 04/28/10 09:29:48 PM
    One issue i ran into: the auto loader won't save the loader_instance on the first run (for initialize) so i've changed the loaderExecute method to:

    function loaderExecute($method) {
            foreach (App::objects('plugin') as $plugin) {
                $loader_file = Inflector::underscore($plugin).'_auto_loader';
                $loader_class = Inflector::classify($loader_file);
                $loader_instance = null;

                if (!ClassRegistry::isKeySet($loader_class)) {
                    App::import('Plugin', $loader_class, Inflector::underscore($plugin).DS.$loader_file.'.php');
                    if (class_exists($loader_class)) {
                        ClassRegistry::addObject($loader_class, new $loader_class());
                    }
                }
                $loader_instance =& ClassRegistry::getObject($loader_class);
                if (!empty($loader_instance) && in_array($method, get_class_methods($loader_class))) {
                    $loader_instance->{$method}($this->__controller);
                }
            }
        }
    and now it works.
    thanks for sharing!
  • emnu posted on 04/18/10 08:03:22 PM
    this is howto auto load plugins routes.php


        foreach(App::objects('plugin') as $plugin) {
            App::import('Plugin',Inflector::classify("{$plugin}_routes"),array('file' => Inflector::underscore($plugin) . DS . 'config' . DS . 'routes.php'));
        }
  • emnu posted on 02/10/10 12:35:12 AM
    how about routes.php, how do we load custom plugin route?
  • malamalca posted on 06/21/09 02:36:18 AM
    I'am not using .htaccess...

    check http://code.google.com/p/lilblogs/ . I am using css and image files via html helper from plugin's vendor folder - all without any problem.
    • sky_l3ppard posted on 06/21/09 08:30:18 AM
      I'am not using .htaccess...

      check http://code.google.com/p/lilblogs/ . I am using css and image files via html helper from plugin's vendor folder - all without any problem.

      no it does not work, i copied a fresh cakePHP, removed .htaccess files, uncommented line in core config Configure::write('App.baseUrl', env('SCRIPT_NAME')); and added a test plugin, and it does not work. So in case other people are dealing with same problem it will be helpful. I'll fix this paragraph about app_helper
  • malamalca posted on 06/20/09 11:50:21 AM
    First part confuses me. You're saying that in version 1.2.3.8166 media files are still inaccessible by using /plugin/css/file... I don't get your point.

    I am using vendor files from plugins all the time without any hacks and it works properly as it should - as you described it. So when issuing


    <?php echo $html->css('/plugin/css/main'); ?>

    Cake will serve/output APP/plugins/plugin/vendors/css/main.css without any hacks.
    • sky_l3ppard posted on 06/20/09 03:11:57 PM
      First part confuses me. You're saying that in version 1.2.3.8166 media files are still inaccessible by using /plugin/css/file... I don't get your point.

      I am using vendor files from plugins all the time without any hacks and it works properly as it should - as you described it. So when issuing


      <?php echo $html->css('/plugin/css/main'); ?>

      Cake will serve/output APP/plugins/plugin/vendors/css/main.css without any hacks.
      Maybe you`re using .htaccess files, I will test these cases and fix the article by mentioning these cases
login to post a comment.