redbullmarky Posted June 26, 2008 Share Posted June 26, 2008 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 Quote Link to comment Share on other sites More sharing options...
keeB Posted June 26, 2008 Share Posted June 26, 2008 Can you give me an example of what type of plugin you'd be looking to develop? Quote Link to comment Share on other sites More sharing options...
redbullmarky Posted June 27, 2008 Author Share Posted June 27, 2008 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 ) Cheers Mark Quote Link to comment Share on other sites More sharing options...
keeB Posted June 27, 2008 Share Posted June 27, 2008 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? Quote Link to comment Share on other sites More sharing options...
redbullmarky Posted June 27, 2008 Author Share Posted June 27, 2008 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? Quote Link to comment Share on other sites More sharing options...
keeB Posted June 27, 2008 Share Posted June 27, 2008 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. Quote Link to comment Share on other sites More sharing options...
redbullmarky Posted June 27, 2008 Author Share Posted June 27, 2008 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. Quote Link to comment Share on other sites More sharing options...
redbullmarky Posted June 27, 2008 Author Share Posted June 27, 2008 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 Quote Link to comment Share on other sites More sharing options...
keeB Posted June 27, 2008 Share Posted June 27, 2008 I'd take a look at how Drupal does it. In the meantime, I have to go to the pediatrician. Back in 2 hours Quote Link to comment Share on other sites More sharing options...
redbullmarky Posted June 27, 2008 Author Share Posted June 27, 2008 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 Quote Link to comment Share on other sites More sharing options...
redbullmarky Posted June 27, 2008 Author Share Posted June 27, 2008 in fact - is there anything that's similar to Drupal, but more object based? Quote Link to comment Share on other sites More sharing options...
keeB Posted June 27, 2008 Share Posted June 27, 2008 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 Quote Link to comment Share on other sites More sharing options...
redbullmarky Posted June 27, 2008 Author Share Posted June 27, 2008 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! Quote Link to comment Share on other sites More sharing options...
keeB Posted June 27, 2008 Share Posted June 27, 2008 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. Quote Link to comment Share on other sites More sharing options...
redbullmarky Posted June 27, 2008 Author Share Posted June 27, 2008 no no, it's fine - i think i've seen some frameworks using a 'Request' object in this fashion but I'm guessing things could get more specific than that. Quote Link to comment Share on other sites More sharing options...
keeB Posted June 27, 2008 Share Posted June 27, 2008 You could think of it as an alternative to the $_SESSION super global. Quote Link to comment Share on other sites More sharing options...
redbullmarky Posted June 28, 2008 Author Share Posted June 28, 2008 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? Quote Link to comment Share on other sites More sharing options...
redbullmarky Posted June 28, 2008 Author Share Posted June 28, 2008 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? Quote Link to comment Share on other sites More sharing options...
Aeglos Posted June 28, 2008 Share Posted June 28, 2008 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 . I'm now more inclined to move everything to an event system with an observer approach instead of a command chain. Quote Link to comment Share on other sites More sharing options...
keeB Posted June 28, 2008 Share Posted June 28, 2008 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? Quote Link to comment Share on other sites More sharing options...
redbullmarky Posted June 28, 2008 Author Share Posted June 28, 2008 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) Quote Link to comment Share on other sites More sharing options...
keeB Posted June 28, 2008 Share Posted June 28, 2008 Where would it normally call viewAction() ? If you could post that code, I'll help Quote Link to comment Share on other sites More sharing options...
redbullmarky Posted June 28, 2008 Author Share Posted June 28, 2008 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; Quote Link to comment Share on other sites More sharing options...
redbullmarky Posted June 28, 2008 Author Share Posted June 28, 2008 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? Quote Link to comment Share on other sites More sharing options...
redbullmarky Posted June 28, 2008 Author Share Posted June 28, 2008 Aeglos - thanks for the link. Not came across that one before but the haphazardness of PHP4-OOP was one of the main reasons I steered clear of CodeIgniter even though it has much to be admired. Quote Link to comment Share on other sites More sharing options...
Recommended Posts
Join the conversation
You can post now and register later. If you have an account, sign in now to post with your account.