PluginHandler to load configuration and callbacks for plugins

This article is also available in the following languages:
By sky_l3ppard
Purpose of this component is to make plugins more powerful by adding a callbacks from any controller to trigger before or after any application action for every plugin used in the application.
Another feature is to load plugin configuration files automatically.

Main features of this component

  • Load all plugin configuration files automaticaly
  • Trigger a plugin callback method before any controller action

Changes

1.4
  • Removed and explained Routes configuration from autoloading
  • Fixed a bug related to object storing in ClassRegistry
  • Removed a method which was used to make unique setting keys, related to bug

Component Class:

<?php 
//File: /app/controllers/components/plugin_handler.php

/**
 * PluginHandler component adds a basic functionality
 * required for the plugin development. Main features
 * are plugin configuration autoloading and callbacks
 * from the controller. 
 * 
 * @author Sky_l3ppard
 * @version 1.4
 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
 * @category Components
 */
class PluginHandlerComponent extends Object {
    
/**
     * Reference to the controller
     * 
     * @var object
     * @access private
     */
    
var $__controller null;
    
    
/**
     * Plugin Settings, available options:
     *         autoload - array of configuration files to be loaded
     *         permanently - true to load configuration files before any action,
     *             false - loaded only for a plugin's controller actions
     * Notice: bootstrap is loaded then the component initialize method is
     * fired and for the same reason routes will not work. If you want to include
     * then from the plugin. Use the app bootstrap to scan plugins for routes
     * 
     * @var array
     * @access private
     */
    
var $__settings = array(
        
'autoload' => array(
            
'bootstrap',
            
'core',
            
'inflections'
        
),
        
'permanently' => true
    
);
    
    
/**
     * Initializes component by loading all configuration files from 
     * all plugins found in application. Configuration files should be
     * placed in \app\plugins\your_plugin\config\ directory. Be careful,
     * it will overwrite all settings loaded from \app\config if the 
     * setting name matches.
     * At the end it will execute an 'initialize' callback method loaded
     * from \plugins\your_plugin\{your_plugin}_auto_loader.php file
     * 
     * @param object $controller - reference to the controller
     * @param array $settings - component settings, list of autoload files
     * @return void
     * @access public
     */
    
function initialize(&$controller$settings = array()) {
        
$this->__controller $controller;
        
$this->__settings array_merge_recursive($this->__settings, (array)$settings);

        foreach (
App::objects('plugin') as $plugin) {
            
$is_parent_class strpos(get_parent_class($controller), Inflector::classify($plugin)) !== false;
            if (
$this->__settings['permanently'] || (!$this->__settings['permanently'] && $is_parent_class)) {
                foreach (
$this->__settings['autoload'] as $type) {
                    
App::import(
                        
'Plugin'
                        
Inflector::classify("{$plugin}_{$type}"), 
                        array(
'file' => Inflector::underscore($plugin).DS.'config'.DS.$type.'.php')
                    );
                }
            }
        }
        
        
$this->loaderExecute('initialize');
    }
    
    
/**
     * Executes a 'beforeFilter' callback method loaded
     * from \plugins\your_plugin\{your_plugin}_auto_loader.php file
     * 
     * @param object $controller - reference to the controller
     * @return void
     * @access public
     */
    
function startup(&$controller) {
        
$this->loaderExecute('beforeFilter');
    }
    
    
/**
     * Executes a 'beforeRender' callback method loaded
     * from \plugins\your_plugin\{your_plugin}_auto_loader.php file
     * 
     * @param object $controller - reference to the controller
     * @return void
     * @access public
     */
    
function beforeRender(&$controller) {
        
$this->loaderExecute('beforeRender');
    }
    
    
/**
     * Initializes \plugins\your_plugin\{your_plugin}_auto_loader.php file
     * and executes specified callback $method from AutoLoader class for
     * all plugins found in application. 
     * 
     * @param string $method - name of the method to execute
     * @return void
     * @access public
     */
    
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_classInflector::underscore($plugin).DS.$loader_file.'.php');
                if (
class_exists($loader_class)) {
                    
ClassRegistry::addObject($loader_class, new $loader_class());
                }
            } else {
                
$loader_instance =& ClassRegistry::getObject($loader_class);
            }
            
            if (!empty(
$loader_instance) && in_array($methodget_class_methods($loader_class))) {
                
$loader_instance->{$method}($this->__controller);
            }
        }
    }
}
?>

Here is a tutorial on how to use this component


Using PluginHandler component settings


There are cases then you need some additional options like plugin priority, additional configuration file or to set this component to execute after another one. Here is the usage example:

<?php
var $components = array(
    
'PluginHandler' => array(
        
'autoload' => array('conf_file''another'),
        
'priority' => array('MyPlugin''AnotherPlugin''Third'),
        
'primary' => true,
        
'permanently' => true
    
)
);
?>

autoload is the list of configuration files to be scanned then initializing this component. Default are: bootstrap, core, inflections. These plugin configuration files must be located in /app/plugins/your_plugin/config directory and in all cases they are executed after app config files so be careful, you can easily override default setting values


Notice: routes cannot be loaded from this component, because they must be invoked before Dispatcher is called. And these configurations are loaded on component initialize method


A tip on how you can include your routes from plugins

To do that you should scan all plugins in your main application bootstrap.php file and import them as usual.



priority is the list of plugins which will setup the execution order for these plugins, ones what were not included automatically will be added at the end of the list. This is advantage if some plugin callbacks must be executed after or before another, same as configuration files


primary if this option is set to true the first time this component is called it will set it`s priority to be executed before all other (e.g.: Auth, Session) components


permanently if this setting is set to true PluginHandler component will load configuration settings before any controller action no matter if it belongs to this plugin or not. In the other case, it will load configuration files only for the plugin which action is currently called.


Here is a directory tree for the example used:

/app
    /plugins
        /my_plugin
            /config
                bootstrap.php
                conf_file.php
                another.php
            /controllers
            /models
            ...
            my_plugin_auto_loader.php
            my_plugin_app_controller.php
            ...
        /another_plugin
            /config
                conf_file.php
            ...
        /third
            ...

Any improvements and ideas are very welcome, enjoy.

Comments

  • Posted 07/13/10 07:56:23 AM
    This information is very helpful. I would suggest that you make a video tutorial that would be user friendly compared to the text version.
  • Posted 04/28/10 09:32:16 PM
    For some reason the bakery login redirected me to other article and I've posted this on the wrong page, so here I post it again:
    ---

    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:

    <?php
    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_classInflector::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($methodget_class_methods($loader_class))) {
                    
    $loader_instance->{$method}($this->__controller);
                }
            }
        }
    ?>
    and now it works.
    thanks for sharing!
  • Posted 02/10/10 02:27:57 PM
    I've already tried loading Routes from a bootstrap.php file and it doesn't work. The Router class is not initialised yet and it gives an error. The only way to load more routes than what's in the routes.php file under app/config is to put a include() or require() or use App::import().
    Also, in CakePHP 1.3, things get a bit flipped around and the config/routes get loaded before the app/config/bootstrap.php so this definitely would not work.
    The dispatcher loads the routes and checks for a match all at one time so there's really no point of insertion the anyone can make rather than putting what I suggested in the routes.php file.
    • Posted 02/10/10 04:03:19 PM
      app/config/core.php loaded first of all
      1. Place the PluginConfigure class in the app/config/bootstrap.php
      2. Here on the same app/config/bootstrap.php you can load PluginConfigure::Load('bootstrap');
      3. Then in your app/config/routes.php add PluginConfigure::Load('routes');

      Wasn`t that simple enough?

      I've already tried loading Routes from a bootstrap.php file and it doesn't work. The Router class is not initialised yet and it gives an error. The only way to load more routes than what's in the routes.php file under app/config is to put a include() or require() or use App::import().
      Also, in CakePHP 1.3, things get a bit flipped around and the config/routes get loaded before the app/config/bootstrap.php so this definitely would not work.
      The dispatcher loads the routes and checks for a match all at one time so there's really no point of insertion the anyone can make rather than putting what I suggested in the routes.php file.
      • Posted 02/14/10 06:45:18 PM
        I'm pretty sure that's exactly what I said needed to be done.... The only way to load routes is to do it in the routes.php file.... Which is what you outlined below....and I already said that....
        *shrugs*

        app/config/core.php loaded first of all
        1. Place the PluginConfigure class in the app/config/bootstrap.php
        2. Here on the same app/config/bootstrap.php you can load PluginConfigure::Load('bootstrap');
        3. Then in your app/config/routes.php add PluginConfigure::Load('routes');

        Wasn`t that simple enough?

        I've already tried loading Routes from a bootstrap.php file and it doesn't work. The Router class is not initialised yet and it gives an error. The only way to load more routes than what's in the routes.php file under app/config is to put a include() or require() or use App::import().
        Also, in CakePHP 1.3, things get a bit flipped around and the config/routes get loaded before the app/config/bootstrap.php so this definitely would not work.
        The dispatcher loads the routes and checks for a match all at one time so there's really no point of insertion the anyone can make rather than putting what I suggested in the routes.php file.
  • Posted 02/10/10 01:38:25 PM
    If you add this class to your app/config/bootsrap.php and then use it`s method to load the routes from app/all_plugins/config/routes.php it should work fine. But I haven`t placed it on the article because of the mess. But anyway in some cases it will be useful, I hope someone will come up with a better idea..

    Sorry guys, I'm very busy recently, get_class_methods function is really very slow and a big brake. It should be and will be cached on next update. If you planning to use it on production version, better cache it..

    I'll place this component on the github also, you'll have link on next update

    <?php
    class PluginConfigure {
        
    /**
         * Load config files for all plugins
         * example in the config/bootstrap.php include this class and 
         * do PluginConfigure::Load('routes.php', 'core.php', 'bootstrap.php');
         * searching in config directory 
         * 
         * @return Void
         * @access Public
         */
        
    static function load() {
            
    $args func_get_args();
            if (empty(
    $args)) {
                return;
            }
            
            
    $pluginsAvailable App::objects('plugin');
            foreach (
    $args as $configType) {
                if (empty(
    $configType) || !is_string($configType)) {
                    continue;
                }
                
                foreach (
    App::objects('plugin') as $plugin) {
                    
    App::import(
                        
    'Plugin'
                        
    Inflector::classify("{$plugin}_{$configType}"), 
                        array(
    'file' => Inflector::underscore($plugin).DS.'config'.DS.Inflector::underscore($configType).'.php')
                    );
                }
            }
        }    

    ?>
  • Posted 02/01/10 07:36:23 AM
    doesnt foreach (App::objects('plugin') as $plugin) {}
    invoke the App::objects method on each and every run?
    dont know how slow this function is, but
    wouldnt it be better (and faster) to use

    $plugins = App::objects('plugin');
    foreach ($plugins as $plugin) {}

    ? But I guess I am wrong - and just got mixed up with for ($i...) loops
    • Posted 02/10/10 01:50:09 PM
      foreach loads the array only once, it looks alike in the begining, you'll get used to it :)
  • Posted 10/11/09 06:05:56 PM
    Greetings,

    Great piece of code. Just letting you know that this doesn't work for router files. It runs after the dispatcher code so it doesn't take any effect. The only place I've seen where you can fool around with routes is include a file in your app's bootstrap file and then do:
    App::import('Core', 'Router');
    Then just add your routes under that.
  • Posted 09/26/09 04:35:04 AM
    Hi, very compliment for you component. It is amazing.

    I'm planning a CakePHP extension named CakePOWER (http://cakepower.org) and I'm looking for a similar solution.

    In my mind plugins expose their configuration settings BEFORE the application. Application can overwrite every plugins settings...

    I'm doing this via bootstrap and routes custom inclusion. I use a "config" folder into plugin's folder exactly like your plugin...

    Your solution is better than mine because you don't have to touch any application configuration to work... just include the component into AppController.

    Very nice job!

    ... after a while ....

    I'm trying to use your plugin. It work well for bootstrap and other configurations but it does not work with routes rules.
    Did you try to set customized routing for plugins? It seems not to work.

Comments are closed for articles over a year old