debuggable

 
Contact Us
 

Welcome to the Dark Side of Plugins in CakePHP

Posted on 24/6/06 by Felix Geisendörfer

Deprecated post

The authors of this post have marked it as deprecated. This means the information displayed is most likely outdated, inaccurate, boring or a combination of all three.

Policy: We never delete deprecated posts, but they are not listed in our categories or show up in the search anymore.

Comments: You can continue to leave comments on this post, but please consult Google or our search first if you want to get an answer ; ).

Important: This is no official way to use plugins and also no complete step by step tutorial for the things I do with plugins. This post is aimed at advanced CakePHP users trying to get more out of plugins.

Working with plugins in CakePHP is tons of fun and I had good success with it so far. However, there were two things I struggled with: Inter-Plugin Communication as well as filter callbacks.

I want to begin to talk about filter callbacks. In SpliceIt!, I want plugins to be independent pieces of useful functionality that are very simple to integrate (just drop the folder into app/plugins). Out of the box, CakePHP plugins seem to be capeable of providing this structure, but at one point I hit a difficulty:

Plugin Callbacks / Hooks

Imagine you want to have a Statistics plugin, that logs every hit on your website and provides a nice interface for viewing those statistics. Doing the interface is easy in CakePHP, but for logging each hit, your plugin would need to be called every time an action is requested. Now, you can do this by using $this->requestAction(..) in your AppController's beforeFilter, but if you start to have lots of plugins that can be around 10-20 dispatching actions for every hit and performance might suffer. Another drawback to this strategy is, that you will always have to change code in your AppController to integrate a new plugin, which doesn't seem like a very RAD approach to me.

So in order to streamline such plugin callbacks, I created a function inside SpliceIt!, that allows plugins to hook into any AppController event, such as beforeFilter, afterFilter, beforeRender, etc. in order to make their own changes to the controller. So a Themes plugin can easily change the Controller::view and a Statistics plugin can make calls to a Model.

Here is the function I use for triggering those event's, if you want to see the complete implementation, I suggest you to checkout the splice_it.php from the current trunk of SpliceIt!.

/**
 * This function calls a specific hook out of any plugin's hooks.php that matches $pluginFilter
 * The list of hooks.php files get's cached for a certain time depending on the value of DEBUG.
 * The 3rd argument &$caller has to be a reference to the caller/variable that get's affected by
 * the Hook.
 *
 * @param string $hook
 * @param string $pluginFilter
 * @param mixed $caller
 */

function callHooks($hook, $pluginFilter = '.+', &$caller)
{
    // pluginHooks contains an array of plugins that provide a hook File
    static $hookPlugins = array();
   
    if (empty($pluginFilter))
        $pluginFilter = '.+';
       
    $params = func_get_args();
   
    // Get rid of $hook, $pluginFilter and &$caller in our $params array
    array_shift($params);
    array_shift($params);
    array_shift($params);
       

    if (empty($hookPlugins))
    {
        $cachePath = 'hook_files';
           
        if (DEBUG==3)
            $cacheExpires = '+5 seconds';
        elseif (DEBUG==1 || DEBUG==2)
            $cacheExpires = '+60 seconds';
        else
            $cacheExpires = '+24 hours';
           
        $hookFiles = cache($cachePath, null, $cacheExpires);
       
        if (empty($hookFiles))
        {
            uses('Folder');        
            $Folder =& new Folder(APP.'plugins');
            $hookFiles = $Folder->findRecursive('hooks.php');
           
            cache($cachePath, serialize($hookFiles));
        }        
        else
            $hookFiles = unserialize($hookFiles);
                   
       
        foreach ($hookFiles as $hookFile)
        {
            list($plugin) = explode(DS, substr($hookFile, strlen(APP.'plugins'.DS)));                
            require($hookFile);
           
            $hookPlugins[] = $plugin;
           
            if (preg_match('/'.$pluginFilter.'/iUs', $plugin))
            {
                $hookFunction = $plugin.$hook.'Hook';
                if (function_exists($hookFunction))
                {
                    call_user_func_array($hookFunction, array_merge(array(&$caller), $params));
                }
            }
        }        
    }
    else
    {
        foreach ($hookPlugins as $plugin)
        {
            if (preg_match('/'.$pluginFilter.'/iUs', $plugin))
            {
                $hookFunction = $plugin.$hook.'Hook';                    
                if (function_exists($hookFunction))
                {
                    call_user_func_array($hookFunction, array_merge(array(&$caller), $params));
                }
            }                  
        }
    }
}

So now the only modification that needs to be made to the AppController, is to call this function for each filter. Here is an example for the beforeFilter:

class AppController extends Controller
{
    function beforeFilter()
    {
        callHooks('beforeFilter', null, $this);
    }
}

So if you now want to make a Themes plugin you can simply create a file called hooks.php inside app/plugins/themes/ and make it look like this:


function themesBeforeFilterHook(&$controller)
{    
    if (file_exists(VIEWS.'theme.php'))
    {
        if ($controller->view=='View');
            $controller->view = 'Theme';
       
        if (empty($controller->theme))
            $controller->theme = 'default';
    }
    else
    {
        trigger_error('Themes Plugin present, but no theme.php file found in app/views/ ');
    }
}    

I currently use those hooks for UrlRewrite (via $from_url in routes.php), AppController::beforeFilter(), AppController::__construct() and some other important points in my application. However, you can also make plugins trigger their own event's, like blogPostBeforeCreate and such.

Anyway, you remember how I told you, that one could avoid using requestAction for plugin communication? Here is what my current approach for SpliceIt! looks like:

Inter-Plugin communication

Generally spoken Controller::requestAction() isn't a bad way to exchange data between controllers. It's a clean interface and you don't have to plan in advance what data should be exchangeable and what data should not. However, there are a couple problems with it. The first and most obvious problem is, that every time you use requestAction(), the entire dispatching process is executed again, which is almost like having a second hit on your site (well not quite as bad, but still). In a normal application this isn't that big of a problem, since there won't be more then 1-3 requestAction's executed per page which doesn't hurt performance that bad. But if you have a system of plugins where you can't share Models,Views and Components as easily as you can in a regular app, you might need up to 20++ requestAction's per page to make things work. And at this point it really get's inefficiant. Because creating instances of Controllers, Models, and Components over and over again takes a lot of cpu cycles.

Another drawback to requestAction() is, that when a Controller/Action or View is missing, CakePHP will render an error page and execute exit; leaving no way of error handling to you. You could create your own AppError handler and change this behavior, but I didn't like this approach that much.

So what I figured was, that the best way of exchanging data between plugins, would be to have special ApiControllers, that do nothing but manage the exchange of data between plugins. They would be normal controllers hidden from the public and only one instance of them would be created when needed, and then shared amongst all other (plugin) controllers. Those ApiControllers normally wouldn't have any Views coupled to them, and therefor only be MVC pieces in your app.

So far I have a working implementation of this ApiController pattern of mine in SpliceIt! and it works like a charm. Performance made a significant jump (3-4x faster) compared to requestAction, and the code looks a lot prettier. I'll try to share the most significant parts of it now, but you should definitly checkout the SpliceIt! trunk for getting a deeper inside into the entire process.

First of all, I'll show you the SpliceItApiController, that I use as the base class for all my ApiController's. Since the ApiController's are sort-of Singletons I added a getInstance() function to them:

class SpliceItApiController extends SpliceItAppController
{
    var $autoRender = false;
   
    function __construct($plugin)
    {
        $this->plugin = $plugin;
   
        parent::__construct();
    }
   
  function &getInstance() {
        return SpliceIt::getApiInstance($this->name);
  }
}

You don't have to know about the SpliceItAppController for now, just imagine it to be your normal AppController.

Now here is how one of this ApiController's could look like:

class UsersApi extends SpliceItApiController
{
    var $name = 'Users';
    var $uses = array('User');

    function addUser($user)
    {
        if ($this->User->save($user))
        {
            return $this->User->id;
        }
        else
            return false;
    }    

    function removeUser($id)
    {                
        if ($this->User->delete($id))
            return $id;
        else
            return false;
    }
   
    // ... More functions
}

Now when you want to add a User using the UsersApi in one of your controllers, you can simply do it like this:

class FooController extends SpliceItAppController
{
    var $name = 'Foo';
    var $uses = array();
    var $apis = array('Users');

    function bar()
    {
        $user = array('name' => 'Jim');
        $this->UsersApi->addUser($user);
    }
}

Now the thing that is still missing, is the way how $this->UsersApi actually get's loaded. I use my own AppController called SpliceItAppController and it contains a function like this:

class SpliceItAppController extends AppController
{
    var $apis = array();

    function constructClasses()
    {
        // Load all Apis used in this controller
        if (!empty($this->apis))
        {
            if (is_array($this->apis))
            {
                foreach ($this->apis as $api)
                {
                    list($api) = SpliceIt::extractApiAndPlugin($api);
                   
                    $apiClass = $api.'Api';
                    $this->$apiClass =& SpliceIt::getApiInstance($api);
                }
            }
            else
            {
                list($api) = SpliceIt::extractApiAndPlugin($this->apis);
               
                $apiClass = $api.'Api';
                $this->$apiClass =& SpliceIt::getApiInstance($api);
            }
        }        

        parent::constructClasses();
    }
}

And here is how SpliceIt::getApiInstance() looks like:

function &getApiInstance($api)
{
    SpliceIt::loadApi($api);
   
    list($api, $plugin) = SpliceIt::extractApiAndPlugin($api);          

    $apiClass = $api.'Api';
   
    uses('class_registry');
   
    $classKey = 'SpliceIt[Apis]::'.$apiClass;
    if (!ClassRegistry::isKeySet($classKey))
    {
        $apiInstance = &new $apiClass($plugin);
        $apiInstance->constructClasses();
       
    foreach($apiInstance->components as $c)
    {
      if (isset($apiInstance->{$c}) && is_object($apiInstance->{$c}) && is_callable(array($apiInstance->{$c}, 'startup')))
      {
        $apiInstance->{$c}->startup($apiInstance);
      }
    }
       
        $apiInstance->beforeFilter();
           
        ClassRegistry::addObject($classKey, $apiInstance);

        return $apiInstance;              
    }
    else
    {
        $apiInstance = &ClassRegistry::getObject($classKey);
       
        return $apiInstance;
    }
 
}

Now you see that all of this isn't that easy to do, and if your project isn't aimed at becoming all that big and using tons of plugins you can easily go with requestAction(). But if you are trying to make a heavily modularized application like I do with SpliceIt! you might find yourself in need of using similiar strategies as the ones presented above. If you have any questions concerning the code above, or SpliceIt! in general, feel free to ask I'll try to answer as good as possible ; ).

--Felix Geisendörfer aka the_undefined

 
&nsbp;

You can skip to the end and add a comment.

Felix Geisendörfer said on Jun 24, 2006:

Ok, if you've read this and are still wondering about the way Api's fit into the entire plugin structure (like some people I talked to in IRC) wait a couple hours and I'll update this article with some more information. Meanwhile you can have a look at the SpliceIt! trunk (link is in the article above) to get a deeper inside.

[...] Welcome to the Dark Side of Plugins in CakePHP: "Important: This is no official way to use plugins and also no complete step by step tutorial for the things I do with plugins. This post is aimed at advanced CakePHP users trying to get more out of plugins. [...]

PHPDeveloper.org said on Jun 26, 2006:

Felix Geisendörfer's Blog: Welcome to the Dark Side of Plugins in CakePHP...

...

DingoNV  said on Jun 26, 2006:

wow. i feel enlightened :)
thanks Felix!!!

Felix Geisendörfer said on Jun 26, 2006:

Hey DingoNV: I'm glad you liked it, I still need to put some more information about the ApiController's up, just didn't get around to do it yet.

Oh and for all the other's, the PHPDeveloper.org pingback (see above) has a really good summary of this article which is worth checking out. Reminds me of: who from the cakephp community is activly posting there? I think that's the 2nd article from my blog that got on there, and I saw rossoft and dhofstet there too. Anyway, thanks for the extra traffic ; ).

[...] ThinkingPHP » Welcome to the Dark Side of Plugins in CakePHP This is a bit over my head atm, but ThinkingPHP shows a couple of methods for getting the most out of your plugins (tags: cakephp plugin) [...]

Tarique Sani said on Jul 07, 2006:

Another very informative article - thanks for all the hard work you are putting in.

A question if I may, what would be the best way to bundle some javascript libs with what is likely to be a stand alone plugin... I would ideally prefer someway which allows the js to reside inside the plugin's own folder

Felix Geisendörfer said on Jul 07, 2006:

Hi Tarique,

I'm glad you liked the article ; ). Your question is interesting and I already asked myself how I'm going to accomplish this as well, but I'm still a bit unsure.

One idea was to make a plugin managment system that would execute an install script when you upload a new plugin. I'll eventually do something for the CMS I'll build upon SpliceIt! but I think there should be a more elegant solution for SpliceIt! itself.

Another idea I actually just had after you asked me, is to create a new folder inside app/webroot and call it plugins. This folder would contain an index.php file, as well as an .htaccess that would redirect all plugin/* calls to index.php?url=$1. The index.php in turn would deliver files from the plugin's weboot's folders. Let me make an example:

A request to: http://www.my-domain.com/plugins/users/edit_user.png

would invoke /app/webroot/plugins/index.php?url=users/edit_user.png

The index.php would look if a file called: /app/plugins/users/webroot/edit_user.png exists, and if yes, return this file. If no, it would invoke:

/app/webroot/index.php?url=plugins/users/edit_user.png

and essentially make CakePHP itself handle the request.

Sounds pretty workable to me, what do you think? I know that returning the files by php is a bit of a performance issue, but I think it's a rather minor one looking at the convenience that would be provided.

Any other ideas?

Tarique Sani said on Jul 07, 2006:

Hi Felix,
I was also thinking along the lines of having a webroot folder in the plugins folder but was not able to get how to make the plugin get that particular file. Your idea of having a plugins folder in webroot and letting cakePHP handle the request is currently very appealing but I dont really like the performance hit.

What if the .htaccess in webroot/plugins just rewrote http://www.my-domain.com/plugins/users/edit_user.png to the point to the actual plugins folder webroot

Will have to see how that can be made possible

Thanks for the response.

Felix Geisendörfer said on Jul 07, 2006:

Hi Tarique,

I think you misunderstood me. I wasn't saying that CakePHP should handle the request. I said that /app/webroot/plugins/index.php should, which would be a php script of 3-5 lines of code. So the performance hit is pretty small I'd think.

Anyway, having the .htaccess directly rewriting things sounds even better, let me know if you are able to accomblish it. I just know that debugging mod_rewrite things can be a bit tricky ocassionally ; ).

Tarique Sani said on Jul 08, 2006:

Yes may be I misunderstood you... I will try to make .htaccess work later today however we will need that index.php for cases where .htaccess does not work.

I have no experience of any other web server than Apache and lightHTTPd

Tarique Sani said on Jul 08, 2006:

Looks like it is more complicated than anitcipated :D

I got to the point where http://tarique.sanisoft.com/cheesecake/plugins/cms/index.html in the browser goes to /cheesecake/webroot/plugins/cms/webroot/index.html However it should really be going to /cheesecake/plugins/cms/webroot/index.html

How do you propose to code the index.php which is in the webroot/plugins/ - I think it would be a bit more involved than a few lines of code

Tarique Sani said on Jul 08, 2006:

Sorry to SPAM your blog like this BUT I have a solution!! We were looking at wrong things...

In the .htaccess of your app folder you need to add the following line
RewriteRule ^plugins/$ - [S=3]

This tells the server to skip the next 3 rules if the URL has 'plugins/' in it

Then in the folder of your plugin's folder you place the following .htaccess

RewriteEngine on
RewriteRule ^$ webroot/ [L]

RewriteRule (.*) webroot/$1 [L]

So if your plugin is called cms this .htaccess goes in /app/plugins/cms/ and finally in the webroot of your plugin place this .htaccess

RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-d

RewriteCond %{REQUEST_FILENAME} !-f

RewriteRule ^(.*)$ - [L]

That is this goes into /app/plugins/cms/webroot/

Phew! !

Now http://yourdomain.com/yourapp/plugins/cms/js/myjs.js points to the correct JS (and other files like CSS, img work as well)

The advantage is that only 1 extra rewrite rule is executed in case of plugins having webroot and there is no PHP involved and you need not have any extra folders in app webroot

The only thing that remains is to code helpers to provide links to stuff in plugin webroot easily - any ideas on that?

Once again sorry for the deluge of comments - please edit/delete comments as appropriate

Felix Geisendörfer said on Jul 08, 2006:

Hey Tarique,

don't worry about spamming in here, it's definitly on topic and I'm interested in this as well ; ). It's cool to see your solution working, but I would not use it, since I'd consider it a hack. I was just playing around with an index.php kind of solution, but a big problem has been that you have to figure out what headers to sent for which file, so something with .htaccess avoids this problem.

What I think should be done, is to submit a ticket for this issue on Trac and hope for some native cakephp solution to this problem. Meanwhile your solution would qualify as a workaround, but the issue should be adressed in the framework itself.

What do you think?

Tarique Sani said on Jul 08, 2006:

Well till a native solution is not found both our solutions are hacks - mine because it needs one line modification to the .htaccess in the app folder (rest of the steps are needed only if you are going to use webroot of your plugin)

Your solution of index.php would also, I feel, to be a hack as it needs extra folder in webroot some .htaccess rewriting files and some php dispatching. I was interested to see how you worked around the file mime types problem in php but as you gather it is not so simple ;)

Incidentally if you noticed - the .htaccess which are to be placed in the plugins folders are nearly the same as in the native app and webroot of cake.

Yes, this is definitely an issue to be put in the Trac

Dave  said on Oct 26, 2006:

Did this go into Trac ? I cant find it.

Ideally I think it also needs to stay compatible with the javascript helper.

So automatically a command

$javascript->link( 'myfile.js' );

inside a plugin will create a tag linking to /plugins/{pluginname}/js/myfile.js

and to be completely 'correct' surely it should support not having 'plugins' in the url , which is already supported by normal page requests.

So a request for /{pluginname}/js/myfile.js should also work.

Felix Geisendörfer said on Oct 26, 2006:

Dave: I'm not sure if a ticket has been submitted for it yet, however the core has definitly not been changed to reflect this issue yet.

scragz said on Dec 22, 2006:

Any updates to SpliceIt! in the last six months that haven't been checked in yet? You're making *exactly* what I need for my app, but some stuff is still not quite there and a few things are broken with current Cake versions. Figured I'd ask before I get down to hacking so work isn't duplicated.

Felix Geisendörfer said on Dec 22, 2006:

scragz: No, I've not done anything on SpliceIt! myself. I'd really like to work on this project, but my priorities have changed freezing the work on it completly. Anyway I know there are a couple of folks who are interested in SpliceIt! and some have also continued development themselfs for their own projects. If you want to get in touch with those people let me know and I'll introduce you guys. I'd be happy if the result would be a new group of developers taking over the project. I don't have the time to actively take part in that, but I'd be available for reviewing things and making suggestions.

scragz said on Jan 18, 2007:

There's a bug already for the plugin js/css stuff.

Matt Huggins said on May 23, 2008:

Did anyone ever resolve this? The bug listed by scragz (above) is apparently "fixed". However, when I try to include a CSS file via the HtmlHelper [$html->css('filename')], the URL is outputted as "/css/filename.css" instead of "/pluginname/css/filename.css". Anyone know what the deal is and how to fix it?

Jeremy Race  said on Jun 24, 2008:

Yes, it has been fixed: < ?php echo $html->css('filename'); ?> works fine for me as long as there's a webroot folder within the plugin_name(where plugin_name is the name of your plugin) folder, ie /app/plugins/plugin_name/webroot/css/filename.css

Jeremy Race  said on Jun 24, 2008:

Correction: This is what has worked for me...
[$html->css('/calendar/css/filename')]

My directory listing is as follows:
/app/plugins/plugin_name/vendors/css/filename.css

see: https://trac.cakephp.org/ticket/3799

ruxkor  said on Feb 01, 2009:

a pity this one's the only relevant article to be found about serious plugin development in cake.. in one way or another my google searches always end up here at least twice per month :-/ excellent article nevertheless : )

This post is too old. We do not allow comments here anymore in order to fight spam. If you have real feedback or questions for the post, please contact us.