Jump to content

plugins


redbullmarky

Recommended Posts

Hi all

Apologies beforehand if this seems like a rehash on any old questions of mine....

 

I've recently finished restructuring the MVC framework I've been using for a while, and now I'm pretty happy with it. Now I'm looking at the best way of implementing plugins to the system - mostly in the View/templates, but I'm also trying to make things as generic as possible so I can drop them in anywhere. My controllers are split into actions, and anything the action returns is what's displayed - then I can use a template system if I choose to format the output before returning it.

 

Now - for the CMS that's being built onto of the framework, there are 2 ways that I need to be able to install/use plugins:

 

1, From the controller action directly

2, From the template, maybe using something like $this->plugin('nav', $args);

 

The View at the moment literally handles just that - it has no access to controllers or models (or anything else for that matter) unless they're passed in as variables via $tpl->set('varname', $object); . I sort of like this "view doesn't concern itself with anything else" but lets say I need a plugin that deals with navigation. I might drop a line into my template such as: $this->plugin('nav', array('type'=>'main')) - as pages are stored as database records, building the nav is going to need the database. It's also going to need the Request object so that it can decide which nav item to highlight as the current one.

 

My questions (at last) - what would be the cleanest way to implement plugins? Should I keep my database and request objects inside my registry and access them from my plugins? How can I make the plugins as generic as possible so that they're not just "view helpers"? Would this be where I'd look at implementing some sort of 'beforeView', 'afterView', 'onPost', etc hooks system instead? How can I make the behaviour of invoking a plugin from a template identical to if I was to install it via a controller?

 

Any help/thoughts on the best way of implementing plugins and hooks would be great

Cheers

Link to comment
Share on other sites

sure, let's take a 'contact form' plugin.

 

My CMS works by reading a page from the database, setting variables in the view, and returning the rendered view, all from within a simple Page Controller. Now - my model has some nice little features, loosely based on Django - ie, each database field is set up in this manner in my Model::init() method:

 

<?php
$this->addField(new PrimaryKey('id'));
$this->addField(new CharField('title', 200));
$this->addField(new TextField('body'));
// etc etc
?>

 

My model has a method called 'getForm' which, given an empty 'Form' object, will attach to it appropriate input fields for my model fields and return the form object back. Rendering this form will produce a full form that I can edit my record with.

 

Now - a "basic" page consists of a page title and a page body. Additional fields are to be provided by plugins. Now - with a contact form plugin, it essentially needs the following (at its most basic):

 

1, a list of recipients the form will go to

2, body text to display after form is posted successfully

3, the ability to add fields to the form mentioned above when editing the page (so probably passing the result of Model::getForm through a plugin method)

4, ability to send a 'you have recieved contact from a form' type message, or perhaps even store the form info in a table.

 

Via the CMS, these plugins would be attached to a page (using plugin table and page_id foreign key, perhaps?) as and when required. So on a regular page, I see a page title and body. When I edit the page, I see two fields - title and body. But when I install the contact plugin, I see the page title, body and contact form, but the body and form is replaced when the form is posted. When I edit the page, I want to see two extra fields - one for form recipients, one for the post-submit body.

 

I suppose where my confusion lies is that I might wish to use these plugins in other controllers that are unrelated to my CMS - so it won't have need for the plugins table but will probably install them manually in my actions. I'm really just looking for the cleanest way to install them (and WHEN down the chain they should be installed), the cleanest interface to use (ie, what to instantiate them with, what to pass into the methods, etc), how they should be used from my views etc. I'm thinking that it's possibly heading into the territory of the 'Observer' pattern to trigger certain events at particular times, but I'm not sure.

 

So really - any thoughts on how the best way to implement this would be great (and apologies if that lot made no sense :D )

 

Cheers

Mark

Link to comment
Share on other sites

I was with ya until the 3rd paragraph. It sounds like you have a good MVC design going.

 

If I understand correctly, what you're trying to add in to the mix is a way to create the Model/View without having to actually write the Model/View?

 

Or, am I missing the intention?

Link to comment
Share on other sites

yeah, i guess - if you take a look at this which is the auto-generated Admin area in Django, that's what I'm trying to achieve, but taking things a step further where plugins can actually add extra fields to the interface or manipulate what the front end displays and react to events such as a form post, etc - but all in a single self-contained plugin. I suppose one of my biggest priorities is to keep things as clean as possible and easy to develop for, which is why it seems like it requires more thought.

 

I'm thinking that attaching my plugins to the controller:

 

<?php
class Controller
{
   protected $plugins = array();

   public function addPlugin(Plugin $plugin) {
      $this->plugins[] = $plugin;
   }
}
?>

 

might be the best way, but I'm not 100% sure whether there may be any pitfalls.

 

Still follow?

Link to comment
Share on other sites

Why not allow the packaging of Model objects with views?

 

 

Pseudo directory structure


/ <- document root
    lib/ <- where your framework is
    config/ <- configuration options
    plugins/
         pluginA/
             model/
                AboutUsModel.class.php
             view/
                AboutUsView.tpl

 

class PluginHelper {
    public function load($name) {
        // load and register (with controller) all files in plugins/$name/model and  plugins/$name/view
    }
}

 

The benefit of this is, you would build your site with a series of 'Plugins' which are completely abstracted away from your framework.

 

If you have a blog plugin, you should be able to plug it in with minimal effort since you have the Model and View dialed.

       

Link to comment
Share on other sites

yeah i've considered that, but the key here is what a CMS user can do. From when I was trying to work it our from a site administrators point of view, I thought like this:

 

1, user sees a page with 2 tabs - Content and Plugins. The Content tab contains editable boxes for 'title' and 'body'. The plugins tab shows a list of plugins, etc that are installed for the current page.

2, On the plugins tab, user clicks on '+' - ie, add a new plugin. They select the plugin they want from the list of available plugins (for example, 'Contact Form') and click on save. The Content form now has two extra fields - Recipient and Body After Post, which can be filled in and saved with the record.

 

Now - the above method assumes that my ContentController::pageAction is called and it's generating a dynamic page. But let's say I have another controller, ExampleController::exampleAction. Within that, I want to use a plugin - but as it's actually not a CMS page, therefore doesn't have a record in the 'pages' table and therefore doesn't have any 'plugins' associated with it, I'd need the ability to add plugins via the controller itself as and when I needed them. In this case, I might consider loading my plugins from my Controller::init() method that I have.

 

Hard to explain, but hopefully you follow.

 

 

Link to comment
Share on other sites

in addition - my structure at the moment is a bit like:

 

/
   framework/
   site/
      config/
         config.php
         routes.php
      models/
         page.php
         user.php
      modules/
         common/ <-- contains site base classes for other modules to extend from
         content/
            content.php
            admin.php
            routes.php
         media_manager/
            admin.php
            routes.php
      plugins/
         nav.php
         contact.php

Link to comment
Share on other sites

yeah Drupal sort of has some nice features in that respect, but I suppose i'm on a bit of an OOP exercise so applying the same ideas that Drupal has is often tricky. But yeah - I suppose a hybrid of Drupal and Django isn't too far from what I'm trying to achieve.

 

Thanks so far

Link to comment
Share on other sites

I think you need to supply a common interface for plugins to be loaded in your controller.

 

If I understand the style you're looking for, what you're trying to do is have a plugin that would be prototyped as follows:

 

<?php

interface Plugin {
    public function getAuthor();
    public function getVersion();
    public function setContext(Context $ctx);

    public function draw(Template $tpl); //draw specific template
    public function handleSubmit(InputParameterList $params) // params would be sanitized so it doesn't have to.
}


class NavigationPlugin implements Plugin {
    private $current_location;
    private $children = null;
    private $context = null;

    public function getAuthor() {return "I am the author";}
    public function getVersion() {return 1.0;}
    public function setContext($ctx) { $this->context = $ctx; }

    public function draw(Template $tpl) {
         //augment, whatever
         $user = $this->context->getUser();
         echo "Welcome, $user";
         $tpl->render();
    }

    public function handleSubmit(InputParameterList $params) {
         if ($this->context == null) throw new ContextNotInitializedException();
         $this->children = $this->context->getPage($params->get('page') /* returns Page object or whatever */)->getChildPages();

         foreach ($this->children as $child) {
               echo '<a href="' . $child->getURL() . '">' . $child->getName() . '</a>';
         }

    }
}

?>

 

I think this handled the cases you wanted to handle, right?

 

What I have introduced which you may not have is a context. This would be kind of like your Registry class, but with more.. context :)

 

Link to comment
Share on other sites

hmm sorta looks more along the lines, yeah. couple of q's about your example:

 

1, would the 'draw' method be better to take a Template object (which it does in your example) and manipulate it rather than outputting anything, before returning the modified object for the controller to render?

2, can you explain more about what you mean by 'context' - sure, i know what the word means but is it a specific pattern/practice? what part of my system does it represent in this case? got any further reading?

 

Great help though - cheers!

Link to comment
Share on other sites

I think it really depends on the amount of complexity you would like to give. I think you should start out by being strict and only loosen restrictions if they're causing you to write shitty code.

 

 

I'm trying to think of an appropriate way to explain Context. I have seen it used in a few enterprise OO applications but I can't give examples directly from them.

 

Basically what a context is is a specialized registry for the current user. What you'd find in there are methods like:

    getUser() # return user model object with reference to current logged in user or 'anonymous' user

    getCurrentPage() # returns the current page

 

Basically anything that can be more specialized than your registry class.

 

I'm sorry, I know that's not really helpful.

   

Link to comment
Share on other sites

hmm. have been playing with a few ideas based on what you've been saying - any thoughts would be great. here's a sample action controller. I've set up a "Context" object like this (for saving space, I've exluded the getXXXX methods as they just return what was set):

 

class Context
{
   function setRequest(Request $rq) {
   }

   function setParams($params) {
   }

   function setOutput($output) {
   }

   ... etc ...
}


class MyPageController extends Controller
{
   function init() {
      $this->loadPlugin('nav');
      $this->context->setRequest($this->rq);
   }

   function viewAction() {
      // load page from DB and returns assoc array of title, body, etc
      $page = Page::loadByRequest($this->context->getRequest());

      // load any plugins attached to the page (as set up in the database)
      $this->loadPlugins($page->getPlugins());

      $this->context->setParams($page);
      $this->event('afterLoad');

      $tpl = new Template();
      $tpl->setAll($this->context->getParams($page));
      $tpl->setOutput($tpl->render('page.tpl'));

      $this->event('afterRender');

      $out = $this->context->getOutput();
      return $out;
   }
}

 

The Context object is instantiated when the controller is. The Controller::event() method basically loops through all loaded plugins looking for an event of that name. If found, it's ran - and has access to the Context object. afterLoad will do any manipulation to page variables, afterRender will take the final HTML output as generate by the template engine and process it as required (like injecting stuff). Of course there could be other "hooks", and these might be unsuitable, but should give an idea of what i'm up to.

 

Am I along the right lines? Is there a cleaner way to achieve the above?

Link to comment
Share on other sites

hmmm. on second thought, this is just one action method. as there are without a doubt going to be many more, I don't want to keep setting up the same old hooks (ie, with $this->event() ) - these are probably things that should trigger automatically at certain points so i can keep my action methods short and sweet. Narrowing things down, I suppose I want to be able to have hooks to:

 

- react to certain requests (in the Request object) - for example, a post, 404, permissions, etc

- manipulate variables before they're passed to the template to be rendered

- manipulate the HTML after it's been rendered

 

plus more.

any thoughts on this as well as my previous post?

Link to comment
Share on other sites

You know, that looks surprisingly similar (though far less complex) to the core Kohana framework event system. I just recently discovered that framework which turned out to be a rather young PHP5 full OOP community fork of Code Igniter. Though it's recently new, what I've seen in the code far surpasses that of CI in my book by all means.

 

If you haven't, take a look: click gently. Particularly, as I said, to the event system that drives the FW. I'm currently seeing how I can adapt something similar to my FW.

 

On a personal note, I've tampered with something similar before in my FW using a chain of command which includes the ActionController's relevant called method in the chain, so I can load/unload and do things before and after the main page execution (afterLoad, afterRender). Sorta like intercepting filters. Though that idea works, it has some minor quirks that made me put it on hold, mainly because it mixed top level procedures (Auth, ACL, environment filtering) with more particular request calls such as Contact Forms in the template  ;D.

I'm now more inclined to move everything to an event system with an observer approach instead of a command chain.

Link to comment
Share on other sites

Great use of the context!

 

Looks like you have a bit more abstraction to do in the viewAction() method. I have prototyped my approach below.

 

The idea is, generate() should be moved to the Controller object, and this way you 'enforce' (via abstract functions in Controller) the use of: afterLoad(), afterRender(), and viewAction().

 

This is all choice of style, of course, but I like to be as strict as possible to ensure everything is implemented that should be.

 

<?php

class MyPageController extends Controller
{
   function init() {
      $this->loadPlugin('nav');
      $this->context->setRequest($this->rq);
   }

   function afterLoad() {
      $page = Page::loadByRequest($this->context->getRequest());

      // load any plugins attached to the page (as set up in the database)
      $this->loadPlugins($page->getPlugins());

      $this->context->setParams($page);
      $this->event('afterLoad');
   }

   function afterRender() {
      $this->event('afterRender');
      $out = $this->context->getOutput();
      return $out;
   }

   function viewAction() {
      $tpl = new Template();
      $tpl->setAll($this->context->getParams($page));
      $tpl->setOutput($tpl->render('page.tpl'));
   }

   function generate() { //cant think of a better name right now
      $this->afterLoad();
      $this->viewAction(); //call it here
      $output = $this->afterRender();
   }

}
?>

 

Does this make sense?

 

Link to comment
Share on other sites

My only query is - let's say I have a route such as  /:controller/:action/* , where does MyPageController::generate() come in to play as far as getting called? (maybe my example use of a 'viewAction' method wasn't the best, but I suppose I meant it would be invoked with a route of something like /my_page/view)

Link to comment
Share on other sites

i have a dispatcher class with a dispatch method. The chunk in question is:

 

// ...do the stuff with whatever we have from the router/request object
$class = $this->getClassName($this->rq->getParams());
$controller = new $class($this->rq);
$controller->init();

if (method_exists($controller, $this->rq->action)) {
$action = $this->rq->action; // could be 'viewAction'
$p = $this->rq->getParams();
$out = call_user_func_array(array($controller, $action), $p['args']);
}

echo $out;

Link to comment
Share on other sites

hmm possibly another thought - adding everything to a context, including the plugins?

 

public function testAction()
{
   $c = new Context($this); // context can access getRequest, getUser, etc methods if required by plugins

   $page = Page::loadByRequest($this->rq);

   $c->setParams($page->getParams()); // pass page vars (title, body, etc) to context - Django-ish, I suppose...
   $c->addPlugin('test'); // add a default plugin that ALWAYS gets used
   $c->addPlugins($page->getPlugins()); // add plugins that are attached to a page.
   $c->addView($this->tpl); // set the view object to use to render page

   return $c->renderContext('page.tpl');
}

 

the thought being that my context object is left to handle plugin calls, variable setting, filters, etc and its render method uses a View class to render the final HTML response.

 

thoughts?

Link to comment
Share on other sites

Archived

This topic is now archived and is closed to further replies.

×
×
  • Create New...

Important Information

We have placed cookies on your device to help make this website better. You can adjust your cookie settings, otherwise we'll assume you're okay to continue.