SitePoint Sponsor |
|
User Tag List
Results 1 to 22 of 22
Thread: My bare bones approach to MVC
-
Mar 26, 2006, 23:37 #1
My bare bones approach to MVC
Let me start be clearing up a few things:
- I know MVC has been discussed many, many times here, but most approaches to it I've seen are way too bloated with stuff I won't use, or they simply don't tie up as it should (IMHO). I'll probably be using Zend when it's stable, so I'm doing this mostly for the sake of learning it.
- I'm a single developer and I use PHP for simple things. I don't need my code to be highly flexible, therefore I'm not using class abstraction or interfaces. All I need is a simple, yet powerful way to develop simple applications faster. That's it.
- Most of this code is based on Zend and Code Igniter, so similarities are not coincidences.
As the title says, this is just a bare bones approach to the MVC pattern. I haven't given the view much thought (I wont use any template system, just php syntax) and the model should be kind of standalone, so it's pretty much only the controller.
I divided the controller in 4, very simple components: Front Controller => Router => Dispatcher => Page Controller. Those should be able to handle the main model-view-controller division and allow me to have a decent and easy URL routing system.
I know there's not much to be reviewed here, but I want to know what is your opinion on this approach (as I'm stuck on the MVC paradigm for quite a few weeks now), and I'm really hoping to get some suggestions to improve it.
Enough talk, here's the zipped code with a working example (edit paths on config first) and bellow is the most important code snippets:
My bootstrap file (all requests are redirected to it using .htaccess):
PHP Code:<?php
require 'app/config/config.php';
require 'lib/z.php';
function __autoload($class)
{
Z::loadClass($class);
}
$FrontController = new FrontController;
$FrontController->run();
?>
PHP Code:<?php
class FrontController
{
function __construct()
{
}
public function run()
{
$Router = new Router;
$route = $Router->getRoute();
$Dispatcher = new Dispatcher;
$Dispatcher->dispatch($route);
}
}
PHP Code:<?php
class Router
{
protected $segments;
protected $controller;
protected $action;
protected $params;
function __construct()
{
$this->parseSegments();
}
public function getRoute()
{
$this->route();
$route = array(
'controller' => $this->controller,
'action' => $this->action,
'params' => $this->params
);
return $route;
}
public function route()
{
/*
* TODO:
* First call a custom router, and if it can't find a custom route calls the default router.
*/
$this->routeDefault();
}
public function routeDefault()
{
$segments = $this->segments;
if (!$segments[0]) {
if (CFG_DEFAULT_CONTROLLER) {
$controller = CFG_DEFAULT_CONTROLLER;
$action = 'index';
} else {
$controller = 'index';
$action = 'index';
}
} elseif ($segments[0] && !$segments[1]) {
$controller = $segments[0];
$action = 'index';
} elseif ($segments[0] && $segments[1]) {
$controller = $segments[0];
$action = $segments[1];
}
if ($segments[2]) {
$params = array_slice($segments, 2);
}
$this->controller = $controller;
$this->action = $action;
$this->params = $params;
}
public function routeCustom()
{
}
protected function parseSegments()
{
$uri = $_SERVER['REQUEST_URI'];
if (strstr($uri, '?')) {
$uri = substr($uri, 0, strpos($uri, '?'));
}
$segments = explode('/', trim($uri, '/'));
$this->segments = $segments;
}
}
?>
PHP Code:<?php
class Dispatcher
{
function __construct()
{
}
public function dispatch($route)
{
$file = CFG_APP_PATH . 'controllers/' . strtolower($route['controller']) . '.php';
$controller = ucfirst($route['controller']) . 'Controller';
$action = strtolower($route['action']);
@Z::loadFile($file);
if (@class_exists($controller) && is_subclass_of($controller, 'PageController')) {
$controller = new $controller;
if (method_exists($controller, $route['action'])) {
$controller->run($route['action'], $route['params']);
} else {
if (method_exists($controller, 'noAction')) {
$controller->run('noAction');
} else {
/*
* TODO:
* Throw exception or raise error
*/
die('invalid action');
}
}
} else {
/*
* TODO:
* Throw exception or raise error
*/
die('invalid controller');
}
}
}
?>
/test/ or /test/index/
/test/testing/
/test/anything => /test/noAction/
PHP Code:<?php
class TestController extends PageController
{
public function index()
{
echo 'This is the test controller, index action.';
}
public function testing()
{
$data = array(
'title' => 'View Test',
'name' => 'John'
);
$View = new View('test', $data);
$View->render();
}
public function noAction()
{
echo '404';
}
}
?>
-
Mar 27, 2006, 00:12 #2
- Join Date
- Sep 2003
- Location
- Glasgow
- Posts
- 1,690
- Mentioned
- 0 Post(s)
- Tagged
- 0 Thread(s)
You'll often hear people talk about presentation (UI) / domain / data access layers. MVC finesses this by separating presentation into view and controller. However, if controller and view are to be separate, controllers can't pass data to views as in TestController: views are responsible for gathering their own data. (A controller could tell the view to render itself though). A view is more than just an html template. The question to ask of the design is can you create a different view without also changing the controller?
Of course you don't have to use an MVC layering scheme. If views and controllers don't vary indepedently, there would be no need to do this. I find that they do. For example, I'm working on something where I haven't decided what kind of messages to put in form processing scripts yet. While I'm developing, I'm just sticking in low-level error etc messages (which is handy to check everything's working) but these would not be appropriate for the client who will need something less technical. I'll change that later - and may have to change it again once he's had a chance to use it. When I start getting some feedback I might need to make several UI changes. Some of these might require fine tuning the options available in the UI (buttons, links and the actions associated with them). That'll require new controllers and views. Some may simply mean attaching a different view to the same domain manipulation - eg different kinds of reports as above. If controllers and views are well separated, I won't need to edit the controller as well as the view to achieve this.
-
Mar 27, 2006, 00:17 #3
- Join Date
- Jan 2005
- Location
- United Kingdom
- Posts
- 208
- Mentioned
- 0 Post(s)
- Tagged
- 0 Thread(s)
How is this different to Zend Framework? It looks practically the same to me. I know you've said you base you work on ZF but copying it tit for tat is probably not going to teach you much
-
Mar 27, 2006, 01:24 #4
McGruff,
that makes a lot of sense, but I'm not sure if my views/controllers will change that much. Anyway, could you provide an example with the view getting the data from the controller instead of the controller sending to the view?
From other frameworks I've seen the view part where it is an actual class instead of a simple template, and while that might be very welcome on a big application that changes a lot it's certainly does not fit my case.
Shrike,
for this part of the code it really is pretty much zend, except for the added simplicity, but as I said, this is just the bare bones. I'll be implementing more features (common helpers, custom and simple URL routing) as soon as I'm confident with what I have.
The point here was really gather opinions on that base structure, and looking at McGruff comments it's pretty obvious that it's not a perfect solution.
-
Mar 27, 2006, 11:30 #5
- Join Date
- Sep 2003
- Location
- Glasgow
- Posts
- 1,690
- Mentioned
- 0 Post(s)
- Tagged
- 0 Thread(s)
Originally Posted by dreaz
Something I call a "Wiretap" pattern can be useful. It's a variation on Observer which allows you to add in the observation without making any changes to either of the objects involved in the observed interaction. For example, suppose you have an ActiveRecord and in a controller you make a call:
PHP Code:$record->foo();
PHP Code:class Wiretap {
function Wiretap(&$record, &$listener) {
$this->_record =& $record;
$this->_listener =& $listener;
}
function foo() {
$result = $this->_record->foo();
$this->_listener->receiveFooResult($result);
return $result;
}
}
You could edit the domain objects themselves, simply adding an addObserver() method. However, you might use Wiretap if you don't want to make these kind of changes. In the example I gave above, I'm expecting to want to change the messages displayed in the report. A very simple "processing failed" message can maybe pick up a simple boolean from somewhere. A detailed report - with information on affected database rows, files written and so on - may need to observe a number of different interactions. I don't want to keep editing domain objects every time I change this. Setting up different wiretaps allows me to change the information gathered by the view without affecting either the controller or any of the domain objects which it manipulates.
I should say the Wiretap idea is experimental and I haven't been using it for long enough to discover any pitfalls. You may need to be careful with the listener interface. For example, there might be a dozen different record objects each with a foo() method. If you're listening in to all of these, you may not be able use the same $listener->receiveFooResult() message each time. If you need to tell them apart, you could pass some kind of identifier as a parameter or (as I think I'd prefer) send different messages "blueThingHasBeenFooed", "redThingHasBeenFooed" and so on. That means a different wiretap class for each domain object.
-
Mar 27, 2006, 13:27 #6
- Join Date
- Jan 2003
- Posts
- 5,748
- Mentioned
- 0 Post(s)
- Tagged
- 0 Thread(s)
Umm...
PHP Code:class View { // listener?
private $strategy;
public function setStrategy( $strategy ) {
$this -> strategy = $strategy;
}
public function recieveFooResult( $result) {
return $this -> strategy -> foo( $result );
}
// ...
}
PHP Code:// different strategies
class BlueThingHasBeenFooed {}
class RedThingHasBeenFooed {}
class GreenThingHasBeenFooed {}
// et al
-
Mar 27, 2006, 13:47 #7
- Join Date
- Jun 2004
- Location
- Copenhagen, Denmark
- Posts
- 6,157
- Mentioned
- 0 Post(s)
- Tagged
- 0 Thread(s)
I'm not sure I completely grasped where you're getting at with Wiretrap, but it reminds me somewhat of Deferred.
-
Mar 27, 2006, 15:54 #8
- Join Date
- Sep 2003
- Location
- Glasgow
- Posts
- 1,690
- Mentioned
- 0 Post(s)
- Tagged
- 0 Thread(s)
It sounds like Deferred is dealing with a similar problem "interleaving several tasks". Specifically, the idea of Wiretap is to weave in a secondary task which depends on the result of an interaction between another pair of objects but without affecting this in any way. The interacting objects aren't aware of the wiretap (hence the name) and their classes don't need to be edited. I'm not familiar with Deferred but it sounds like it could be implemented with a Wiretap although not necessarily so. Another option is to add in observer methods (eg addObserver, notify) to the observable but if that's not a good choice you can do it with a wiretap and don't have to change any existing code.
Almost. The only change is in the way you stitch everything together. Normally a reference to the service object at one end of the observed interaction would be passed to the client at the other. With Wiretap, the client instead gets a reference to the wiretap object and the wiretap object gets a reference to the service object and a listener. The wiretap will basically just pass through method calls from the client to the service object and at the same time send any relevant messages to the listener.
With the view example, you can chop and change the "listening points" and the information which is being gathered without having to make any changes to the controller or domain code.
-
Mar 27, 2006, 16:34 #9
- Join Date
- Jun 2004
- Location
- Copenhagen, Denmark
- Posts
- 6,157
- Mentioned
- 0 Post(s)
- Tagged
- 0 Thread(s)
Thanks for the explanation, I think I tossed in the wrong buzz-word - AOP would have been more appropriate.
While I think I understand the technicalities, I'm not sure I see how you would use it in context. Could you show a mock of how you'll use it for a real-world situation, say for controlling a form ?
-
Mar 27, 2006, 16:34 #10
Ok,
I have looked over the observer and strategy pattern and while I can very much see it being used on a big, ever-changing application, it looks a bit too overwhelming for my needs.
An observer/wiretap makes a lot of sense to me, so does the strategy, but I'll end up with an entire class for something that could be easily handled by a simple template and a couple of if's (if that). Is it that wrong to use the controller to get the data from the model and send it to the view?
I don't want to sound like I'm just too lazy to learn new things, but I really think that for what I'm doing I can keep it simple without compromising too much of the maintainability of my code. That's why I looked into MVC in the first place, being able to separate view from the logic really appeals to me, but I don't want to go so far way that it will stop making sense to me.
What do you suggest?
-
Mar 27, 2006, 17:00 #11
- Join Date
- Jun 2003
- Location
- Melbourne, Australia
- Posts
- 440
- Mentioned
- 0 Post(s)
- Tagged
- 0 Thread(s)
McGruff's Wiretap also seems quite like WACT's Delegate. (Well, it was called Delegate in release 0.2a, but has since been renamed Callback with various 'Event' classes extending it... I think -- it's been a while since I looked in CVS.)
Wiretap calls the listener's "receiving" method, whereas Wact's Delegate/Callback doesn't have a receiving method. Rather, the "listener" just has to know the Delegate's invoke() method. It's nice because the method wrapped by the Delegate/Callback can be called anonymously.Zealotry is contingent upon 100 posts and addiction 200?
-
Mar 27, 2006, 17:10 #12
- Join Date
- Sep 2003
- Location
- Glasgow
- Posts
- 1,690
- Mentioned
- 0 Post(s)
- Tagged
- 0 Thread(s)
Originally Posted by kyberfabrikken
Originally Posted by kyberfabrikken
Does that help explain it better? I'll post more code if need be.
-
Mar 27, 2006, 17:19 #13
- Join Date
- Sep 2003
- Location
- Glasgow
- Posts
- 1,690
- Mentioned
- 0 Post(s)
- Tagged
- 0 Thread(s)
Originally Posted by dreaz
What I originally meant to do was give an example of how views can be varied independently of controllers (which I'd see as a prerequisite for MVC). If the controller passes all the view data on to the view, a change to the information displayed in a view will also require a change to the controller. If you don't think that will be a problem I'd just ignore it.
-
Mar 27, 2006, 17:26 #14
Originally Posted by McGruff
What I mean is, why is the view supposed to be independent from the controller, if they're made to be used with it. I just don't see the case where this would be needed, but please, show me.
Does it helps if I tell you that pretty much all the data I pass to the view from the controller will come from the model? That's how I see it being done on the MVC examples and frameworks all around (including rails and zend).
-
Mar 27, 2006, 19:21 #15
- Join Date
- Jun 2004
- Location
- Copenhagen, Denmark
- Posts
- 6,157
- Mentioned
- 0 Post(s)
- Tagged
- 0 Thread(s)
Originally Posted by McGruff
Originally Posted by McGruff
http://dojotoolkit.org/docs/dojo_eve...seeking-advice
http://alex.dojotoolkit.org/wp-conte...d_FP_in_JS.pdf
-
Mar 28, 2006, 02:55 #16
- Join Date
- Jan 2004
- Location
- Oslo, Norway
- Posts
- 894
- Mentioned
- 0 Post(s)
- Tagged
- 0 Thread(s)
Maybe I missed something, but it seems to me the Wiretap is basically Observer implemented in a Decorator instead of in the observed class itself. That's clever. The observed class knows nothing about what's going on.
Another alternative I've been thinking about (but never actually implemented) is to replace the Observer pattern with notification to a Composite. More intrusive than the Wiretap, but less so than the classical observer:
PHP Code:class ActiveRecord {
public function __construct($compositeListener) {
$this->listener = $compositeListener;
}
public function foo() {
// do stuff
$this->listener->update($something);
}
PHP Code:$record->getCompositeListener->add($listener);
Dagfinn Reiersøl
PHP in Action / Blog / Twitter
"Making the impossible possible, the possible easy,
and the easy elegant" -- Moshe Feldenkrais
-
Mar 28, 2006, 08:12 #17
- Join Date
- Jan 2003
- Posts
- 5,748
- Mentioned
- 0 Post(s)
- Tagged
- 0 Thread(s)
> but isn't the view directly related to the controller anyway?
Well no... I would say that the View and the Model have more of a relationship within the MVC mould, rather than the View and the Controller
Try to understand that with Views, there may well be multiple Views for example, for a given request; Note the singular tone in reference to the Request - with those multiple Views, you may have numerous Models as well, or vice versa, ie The same data model, but different ways of showing the data.
-
Mar 28, 2006, 19:31 #18
- Join Date
- Sep 2003
- Location
- Glasgow
- Posts
- 1,690
- Mentioned
- 0 Post(s)
- Tagged
- 0 Thread(s)
Originally Posted by dagfinn
Wiretap observes method calls and their return values. It listens in to messages rather than objects. Normally, listener objects are known to an observable object and they can receive anything from within the observable's class scope including results from private method calls. Wiretap is a little bit more limited in what it can see. It listens in to messages (public methods) not objects.
Originally Posted by dreaz
Suppose we have a controller:
PHP Code:class DrinkOrder {
/* param (object) $input encapsulates user input
param (object) $customer
param (object) $barman
param (object) $view
*/
function DrinkOrder(&$input, &$customer, &$barman, &$view) {
$this->_input =& $input;
$this->_customer =& $customer;
$this->_barman =& $barman;
$this->_view =& $view;
}
/*
*/
function try() {
$this->_barman->serve(
$this->_customer,
$this->_input->getDrinkType(),
$this->_input->getPayment());
$this->_view->render();
}
}
PHP Code:class Barman {
/* param (object) $bar
param (object) $till
param (object) $price_list
*/
function Barman(&$bar, &$till, &$price_list) {
$this->_bar =& $bar;
$this->_till =& $till;
$this->_price_list =& $price_list;
}
/* param (object) $customer
param (string) $drink_type
param (integer) $payment
return (bool)
*/
function serve(&$customer, $drink_type, $payment) {
if( !$this->_bar->has($drink_type)) {
return false;
}
$cost = $this->_price_list->getCost($drink_type);
if( !($payment < $cost)) {
return false;
}
$this->_till->enter($payment);
$customer->receiveMoney($this->_till->remove($payment - $cost));
$customer->receiveDrink($this->_bar->getDrink($drink_type));
return true;
}
}
PHP Code:class DrinkOrderFactory {
function &getController() {
require_once(...);
$controller =& new DrinkOrder(
$this->_Input(),
$this->_Customer(),
$this->_Barman(),
$this->_View());
return $controller;
}
function &_Bar() {
if( !isset($this->_bar) {
require_once(...);
$this->_bar =& new Bar;
}
return $this->_bar;
}
function &_Till() {
}
function &_PriceList() {
}
function &_Barman() {
}
function &_Customer() {
}
function &_View() {
}
// etc
}
PHP Code:class BarmanWiretap
{
/* param (object) $barman
param (object) $listener
*/
function BarmanWiretap(&$barman, &$listener) {
$this->_barman =& $barman;
$this->_listener =& $listener;
}
/* param (object) $customer
param (string) $drink_type
param (integer) $payment
return (bool)
*/
function serve(&$customer, $drink_type, $payment) {
$is_served = $this->_barman->serve(
$customer,
$drink_type,
$payment);
$this->_listener->newDrinkOrder($is_served);
return $is_served;
}
}
PHP Code:class DrinkOrderFactory {
function &getController() {
require_once(...);
$controller =& new DrinkOrder(
$this->_Input(),
$this->_Customer(),
$this->_BarmanWiretap(), # wiretap replaces the std Barman
$this->_View());
return $controller;
}
function &_BarmanWiretap() {
if( !isset($this->_barman_wiretap) {
require_once(...);
$this->_barman_wiretap =& new BarmanWiretap(
$this->_Barman(),
$this->_View());
}
return $this->_barman_wiretap;
}
//etc
PHP Code:class DrinkOrderView {
function newDrinkOrder($is_served) {
if($is_served) {
$this->message = 'Enjoy your drink :)';
} else {
$this->message = 'Cannot serve drink :(';
}
}
function render() {
// print the message
}
}
Changing the view like this will not require any changes to the controller or domain objects (I'd really rather not muck about with the latter if I can avoid it). The domain manipulation defined in DrinkOrder remains exactly the same regardless of the view. However, with the controller passing data to the view, DrinkOrder would have had to be edited - it needs to hand over a different set of data. You might end up passing values through a whole chain of domain objects all the way back to the controller before finally being able to pass them to the view. Setting up "listening points" (with one flavour of Observer or another) means you can get them directly.
-
Mar 29, 2006, 06:25 #19
- Join Date
- Jan 2004
- Location
- Oslo, Norway
- Posts
- 894
- Mentioned
- 0 Post(s)
- Tagged
- 0 Thread(s)
Duh...I love it
, but couldn't the Controller just pass the domain objects to the View? Give the Barman and the Customer to the View for interrogation, so to speak? It does assume that the Barman and the Customer remember what's happened, but it's a heck of a lot simpler in principle, and there's still no need to change the domain objects or the Controller if you want to display something else in the View.
It's not clear to me what is the actual benefit of your setup.Dagfinn Reiersøl
PHP in Action / Blog / Twitter
"Making the impossible possible, the possible easy,
and the easy elegant" -- Moshe Feldenkrais
-
Mar 29, 2006, 11:01 #20
- Join Date
- Sep 2003
- Location
- Glasgow
- Posts
- 1,690
- Mentioned
- 0 Post(s)
- Tagged
- 0 Thread(s)
I'd agree you shouldn't make things more complicated than you have to. The benefit in the kind of scenario I've sketched out is that views become well-separated.
Generally, controllers might make a few calls to Barman-like objects before telling the view to render itself. The Barman-like objects may provide a simpler front to other objects, and so on. Depending on the complexity of the domain, you may have a long route to travel to get information - a boolean, an error message, etc - back to the top-level controller from an object much lower down in the structure. That, I'd suggest, is a smell.
Altering domain objects so that they record state required solely by the view might also be a smell. If they need to know this anyway fine but if not I might be adding baggage to some nice, clean domain objects. A traditional Observer implementation does something similar: although it keeps the view nicely separated I've still got to add some message sending code directly into a domain object.
If the controller already knows about the objects which the view wants to interrogate it could pass them straight to the view - sure. Often it will and I can see how tempting that is. However, if they're buried away down in the hierarchy it won't and once again you'll have to travel the scenic route all the way back to the controller rather than communicate directly with the view using Observer.
The question to ask is: how well does the design cope if I decide to change the information displayed in the view? If you need this kind of separation it can be done - but maybe you don't.
-
Mar 30, 2006, 04:00 #21
- Join Date
- Jan 2004
- Location
- Oslo, Norway
- Posts
- 894
- Mentioned
- 0 Post(s)
- Tagged
- 0 Thread(s)
Originally Posted by McGruff
Originally Posted by McGruff
Originally Posted by McGruff
I still don't get how the Wiretap is necessary to avoid having to change the Controller. And when you say the Controller shouldn't pass data to the View, I'm not sure what you mean by data.
What I would say in answer to your stated requirements is that whatever the Controller passes to the View should be coarse-grained objects rather than single data items. If there's enough information in there for the View to serve its varying needs, you won't have to change the Controller every time there is a new UI requirement. That much seems reasonable and should be sufficient for the separation you're talking about.
-
Mar 30, 2006, 10:30 #22
- Join Date
- Sep 2003
- Location
- Glasgow
- Posts
- 1,690
- Mentioned
- 0 Post(s)
- Tagged
- 0 Thread(s)
Originally Posted by dagfinn
Originally Posted by dagfinn
Originally Posted by dagfinn
Originally Posted by dagfinn
The common Observer implementation would also help but, when the view changes, there's some vestigial machinery in a domain object to be cleared up.
Originally Posted by dagfinn
Bookmarks