PluginHandler to load configuration and callbacks for plugins

By Gediminas Morkevicius (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:

Download code <?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:

Download code <?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:

Download code /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 1087

CakePHP Team Comments Author Comments
 

Comment

1 A nice job!

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.
Posted Sep 26, 2009 by Marco Pegoraro
 

Bug

2 Does Not Work With Router Files

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 Oct 11, 2009 by Cody Lundquist
 

Comment

3 foreach

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 Feb 1, 2010 by Mark
 

Comment

4 A quick and simple way to include routes.php

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 Feb 10, 2010 by Gediminas Morkevicius
 

Comment

5 Hi Mark, answer to foreach/for/while question

foreach loads the array only once, it looks alike in the begining, you'll get used to it :)
Posted Feb 10, 2010 by Gediminas Morkevicius
 

Comment

6 I believe this still won't work

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 Feb 10, 2010 by Cody Lundquist
 

Comment

7 Hi Cody, i'll give you step by step:

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 Feb 10, 2010 by Gediminas Morkevicius
 

Comment

8 Isn't that what I said??

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 Feb 14, 2010 by Cody Lundquist
 

Bug

9 Very good!

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 Apr 28, 2010 by Lucas S Caro
 

Comment

10 Thanks for the information.

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 Jul 13, 2010 by William
 

Comment

11 thanks

It's a a COM Component that creates and validates image and sound CAPTCHAs, which can be simply added to your ASP forms using our ASP wrapper CAPTCHA module. bad credit loans
Posted Jul 14, 2010 by Jim
 

Comment

12 ed

When the majority of people will see the realistic way to solve that over time that it starts to blend suddenly. auto transport Yes, the will be read again without any doubt.
Posted Jul 22, 2010 by dan kaylee
 

Comment

13 soap

im fed up of this word soap!!! my mum and sis are crazy fans of these dumb familial soap operas . . .


tower defense
Posted Jul 25, 2010 by bad anooj
 

Comment

14 plugins

be careful when adding plugins, always crosscheck or it can cost you a lot of cheques . . .


tower defense
Posted Jul 25, 2010 by bad anooj
 

Comment

15 Callback

Hey,
I understand how the callback works, but I don't see what the advantage this callback gives. I mean, the plugin still works without it.
- Will
Virus Protection
Posted Jul 27, 2010 by William Clayton
 

Comment

16 handling

proper handling is the first step towards care . . .


Dropship
Posted Jul 30, 2010 by bad anooj