Page 1 of 2

chain of commands in MVC

Posted: Wed Aug 01, 2007 10:32 am
by matthijs
I have a frontcontroller which maps to controllers based on the URL. Using the map I can map different URLs to different controllers, actions, parameters and subcontrollers, subactions, etc etc.

Works very well and is quite flexible. However, as you get deeper URL's the controller classes bloat. Say the URL

Code: Select all

mysite/users/
maps to the controller users with some default action
mysite/users/1/
maps to the controller users with the action to view userpage for user 1
mysite/users/1/stuff
maps to the controller users with the action to view userpage for user 1 and subcontroller stuff
mysite/users/1/stuff/edit
maps to the controller users with the action to view userpage for user 1 and subcontroller stuff, subaction edit
etc etc

What I would like to have is suggestions is how to handle the bloat and hierarchical coupling which starts to creep in the controllers by doing it this way. My guess is that I should look into some sort of Chain of Responsibility. Or the way Konstruct handles controllers (at least, from what I understand from a bit of reading).

I need some way to have each controller be more or less independent of the complete chain. Each controller can run several actions, or dispatch to other controllers. Those controllers can again run actions or dispatch to others, etc. All the time, each controller has access to the request object, so it knows what to do. And each controller in the chain returns a response.

The thing is, I don't want to be limited to a single pair of controller/action. I might have

Code: Select all

/admin/users/
which maps to controller admin and action users
/admin/users/edit/
mapping to controller admin and action users and subaction edit
/news/
mapping to controller news
/news/2006/
mapping to controller news and parameter year=2006
etc

One controller runs an action directly. Another controller dispatches to another, which dispatches to another just as long until the right one runs an action

below a simplified example of what I mean:

Frontcontroller:

Code: Select all

require_once('config.php');
require_once('A/DL.php');
require_once('A/Locator.php');
require_once('A/Http/Request.php');
require_once 'A/Http/PathInfo.php';
require_once('A/Http/Response.php');
require_once('A/Controller/Front.php');
require_once('A/Controller/Mapper.php');

$Locator =& new A_Locator();
$Response =& new A_Http_Response();
$Locator->set('Request', new A_Http_Request());
$Locator->set('Response', $Response);

$map = array(
	'' => array(
		'controller',
		'action',
		'id',
		),
	'date' => array(
		'' => array(
			'controller',
			'year',
			'month',
			3 => 'day',
			),
		),
	'users' => array(
		'' => array(
			'controller',
			'id',
			'subcontroller',
			'subaction'
			)
		
		)
	);

$map2 = array(
	'' => array(
		),
	'param1' => array(
		'' => array(
			'subcontroller',
			'subaction',
			'subparam',
			),
		),

	);
$Request = new A_Http_Request();
// set first map and process 
$Mapper = new A_Http_PathInfo($map, false);
$Mapper->run($Request);

// set second map and process to map those routes
$Mapper->setMap($map2);
$Mapper->run($Request);
	
$DefaultAction = new A_DL('', 'home', 'run');
$ErrorAction = new A_DL('', 'error', 'run');

$Mapper = new A_Controller_Mapper('controller/', $DefaultAction);

$Controller = new A_Controller_Front($Mapper, $ErrorAction);
$Controller->run($Locator);
$Response->out();

Controller class:

Code: Select all

class users {
	
	function run($locator) {
		
		$request = $locator->get('Request');
		$response = $locator->get('Response');
		
		// URI /users/1/
		if($request->get('id')) {
			
			// URL / users/1/climbs/
			if($request->get('subcontroller') == 'climbs') {
				// URL /users/1/climbs/add/
				if($request->get('subaction') == 'add') {
					$content = $this->addNewClimb();
					$response->setContent($content);
				// URL /users/1/climbs/view/
				} elseif ($request->get('subaction') == 'view') {
					$content = $this->showClimbsByUserView();
					$response->setContent($content);
				} else {
					$content = $this->showClimbsByUser();
					$response->setContent($content);
				}
			} else {
				$content = $this->showUserById();
				$response->setContent($content);
			}
			
		} 
		else {
			$url = 'http://mysite.com/users/1/someotheraction/';
			$response = $locator->get('Response');
			$response->setRedirect($url);
		
		}
	
	}

	function addNewClimb() {}
	function showClimbsByUserView() {}
	// other functions ....

}
I have used simple functions here, but these would probably be instances of specific classes for each action/subcontroller/subaction. Something like

Code: Select all

class users {
	
	function run($locator) {
		
		$request = $locator->get('Request');
		$response = $locator->get('Response');
		
		// URI /users/1/
		if($request->get('id')) {
                        $dispatchedresponse = new SomeSubcontroller();
                        $content = $dispatchedresponse->getContent();
                } else {
                        $dispatchedresponse = new SomeOtherSubcontroller();
                        $content = $dispatchedresponse->getContent();
                }
                 $response->setContent($content);
                //etc etc
}
class SomeSubcontroller {
	
	function run($locator) {
		
		$request = $locator->get('Request');
		$response = $locator->get('Response');
		
		// URI /users/1/view/
		if($request->get('subcontroller')) {
                        $dispatchedresponse = new SomeSubSubcontroller();
                        $content = $dispatchedresponse->getContent();
                } else {
                        $dispatchedresponse = new SomeOtherSubSubcontroller();
                        $content = $dispatchedresponse->getContent();
                }
                 $response->setContent($content);
                //etc etc
}
Hope you see were I'm getting at. I'm not sure yet what the problems are or might become, but I have a feeling that this might lead to a massive, strongly coupled tree of controllers. Of course, that might not be that bad if you think about the way a RESTful webapplication is indeed a bunch of URLs mapping to certain views (generated by controllers). Hope this isn't too vague.

Basicly my question is: is this the right way to match the mapping to the controllers, actions, subcontrollers, subactions, etc?

Posted: Wed Aug 01, 2007 1:41 pm
by Begby
I think this is the wrong approach. I think your idea of how the URLs are structured isn't bad, but you need to parse the URL out logically instead of having hardcoded paths in every controller. Yes, this will lead to very tight coupling. If you try to move a controller to say another module, or even just rename a controller, then you are going to be in trouble.

Are you doing your URLs like that to save state? For instance viewing if you have

/users/1/stuff/edit

Is this URL just so that when you are done editing you can return to /users/1 thereby saving the page you were on? Also I assume you would want to do something like /products/2/stuff/edit so that you could edit the same thing and then return to products/2 instead of the users page thereby sharing functionality.

Posted: Wed Aug 01, 2007 3:03 pm
by matthijs
I'm not using the URLs to save state. They should just be resources. As I have the framework now, I can just place a new controller file with a controller class in the directory /controllers/, and the Frontcontroller will find that class and use that. So that's quite flexible. However, that is limited to URLs like:

/somecontroller/someaction/
invokes the class somecontroller and action someaction
/users/view
invokes the class users and action view
etc etc

But the thing is, I don't want to be limited to this /controller/action/ couple. In a bigger webapp there are much deeper URL schemes. For example
/articles/somecat/someselection/someauthor/
in which someselection and someauthor are not just some parameters for a single controller, but are controllers them self. And that controller it self can be a page with a form with all the states.

So
/users/germany/new/edit/
is a web page which edits all new users from germany. Without a POST it will show a list to be edited. With a POST it will validate the posted data, etc When everything is ok, it might redisplay or redirect. But that's just an example.

Basically I need a way to have more flexibility (and so less coupling) to design the URLs just the way I want. Therefore, I need a flexible way to dispatch controllers. Each controller will look at the request object and decide what to do. Does the request object have child controllers? Dispatch to that. If not, execute the current controller. Preferably, everything should be kind of pluggable. I just did a bit more study of the code in Konstrukt and I think that that is something I'm after:
http://konstrukt.svn.sourceforge.net/vi ... iew=markup

Speaking of which, you can see the way sourceforge handles URLs? I bet those aren't real, physical directories.

Posted: Wed Aug 01, 2007 5:33 pm
by Christopher
I think the problem is that you are not using actions at all (at least from the code you showed). The Front Controller will call the method specificed by the action, so your controllers should look something like this (CRUD example):

Code: Select all

class users {
       
        function run($locator) {
                // this is default action that is run if no action is supplied, so listing is the default
                $this->listing($locator);
        }
               
        function listing($locator) {
                // show list of users -- mydomain.com/users/listing/
                $this->listing($locator);
        }

        function edit($locator) {
                // edit a specific user record -- mydomain.com/users/edit/foo
                $request = $locator->get('Request');
                $userid = $request->get('id');
        }

        function delete($locator) {
                // delete a specific user record -- mydomain.com/users/delete/foo
                $request = $locator->get('Request');
                $userid = $request->get('id');
        }

}
You can forward to other controllers or load them explicitly, but usually what you really want are sub-Views -- not sub-Controllers. So be clear on what they actually do.

FYI - Latest code is here.

Posted: Wed Aug 01, 2007 9:33 pm
by Begby
Ahh, I see now. You want to reuse controllers and have a lot of depth. You might want to look at some sort of hierarchical MVC. It might be good to take a look at the Claw framework and see how that is setup, within that framework you can do this


mysite.com/controller/action/anothercontroller/action/arg1/10/arg2/31/yetanothercontroller/action


The above would call every controller / action on that url. Within each controller it is up to you whether or not to actually carry out the action, there are controller methods something like OnContinue() which get ran if the controller is not the last one in the chain. You can put any controllers you want in the chain in any order.

The other thing about this, which is nice, is that each controller can modify the view. So on the initialzation of each controller you could have it append a breadcrumb or created nested tables.

I don't suggest using Claw though. I used to be a contributor but then the lead developer guy freaked out and posted some rant about how PHP sucked then abandoned the project. It needs some work. Its worth looking at though to see how its put together and give you some ideas.

As for hierarchical MVC its cool, but it can turn into spaghetti. It looks great on paper but I have yet to see it work good on a big site. I haven't ran into a situation where I absolutely had to have it.

Posted: Thu Aug 02, 2007 1:30 am
by matthijs
arborint wrote:You can forward to other controllers or load them explicitly, but usually what you really want are sub-Views -- not sub-Controllers. So be clear on what they actually do.
Maybe you have a point. My example might not have been very good, I was just experimenting a bit. Seeing how things would go if I would have a depper hierarchy.

So I have a few options:
- the current controller performs an action (your last example)
- forward to other controller
- load (another) controller explicitly (bit like I tried to do)
- create subViews

What Begby shows is indeed what I am thinking about. It's like this handleRequest function in a controller (from Konstrukt):

Code: Select all

function handleRequest() {
 // determine this controllers subspace from name + context->subspace
   $this->subspace = preg_replace('~^'.(preg_quote($this->name, "~")).'/?~', "", $this->context->getSubspace());
 
   $next = $this->findNext();
     if (!is_null($next)) {
       return $this->forward($next);
      }
 
     // execute this controller, since we didn't forward
     $response = $this->execute();
 
     return $response;
   }
The method determines if the controller is final handler or if it should forward, and then either calls forward or executes the action. Maybe I should build something like this in my system.

I'll take a look at Claw, thanks for the suggestion. And I'll think hard about what Arborint said about subcontrollers versus sub-views.

Posted: Thu Aug 02, 2007 5:20 am
by Chris Corbyn
I've written a router which can convert interchangeably between URLs like:

?module=something&action=somethingElse

and

/something/somethingElse

Using basic expressions in a config file like:

/:module/:action

It does the opposite too and reads from the URI to hydrate the request with the relevant values.

I can forward the code if it's of any help in simplifying the URL schema.

EDIT | I wasn't really planning on releasing this but I think I may do now. The code needs a little optimization (maybe some runtime caching) but it does a great job:

http://www.w3style.co.uk/~d11wtq/Roo-0.1.0.tar.gz
http://www.w3style.co.uk/~d11wtq/Roo-0.1.0.zip

It's a bit more complex than other routers I've seen because it's strategy based and allows arrays in the URL.

Example:

Code: Select all

//Routing rule list
//--------------------------------------------------------------------------------------------------------------------------
                //Pattern                                //Defaults                                                  //Name
//--------------------------------------------------------------------------------------------------------------------------
add_route(route("/",                                    array("startPage" => "yes"))                                 );
add_route(route("/do/(:performAction[],)/:page")                                                                     );
add_route(route("/:zip/:foo")                                                                                        );
add_route(route("/documentation/:chapter/:pageNumber",  array("module" => "docs", "do" => "view"))                   );
add_route(route("/:module/:action/*")                                                                                );

//Hydrate the request
execute_routes();
And building a URL:

Code: Select all

echo route_url("?performAction[]=link&performAction[]=viewSource&page=144");
//Yields /do/link,viewSource/144

Posted: Thu Aug 02, 2007 6:15 am
by matthijs
Thanks. From a quick first look it seems like a very interesting approach. At the least it'll teach me a thing or two.

Posted: Thu Aug 02, 2007 6:33 am
by Chris Corbyn
Obviously it's not directly related to your problem, but to save having to parse what's in the URL manually this can simply allow you to build a pattern from the request data. I'm pretty sure the ZF will already include something like this at a more basic level. I know symfony does.

Posted: Fri Aug 03, 2007 9:41 am
by matthijs
d11wtq wrote:I'm pretty sure the ZF will already include something like this at a more basic level.
Indeed, the Zf has http://framework.zend.com/manual/en/zen ... outer.html

Looks pretty complex but also something I'm after.

But I'd also like to try to figure out a way to use my current setup.

Say I have my mapper map:

Code: Select all

$map = array(
	'' => array(
		'controller',
		'action',
		'id',
		
		),
	'date' => array(
		'' => array(
			'controller',
			'year',
			'month',
			3 => 'day',
			),
		),
	'users' => array(
		'' => array(
			'controller',
			'id',
			'subcontroller',
			'subaction',
			''
			),
               ),
      );
This works fine. if I visit /users/12/somecontroller/someaction/ the Request object is:
controller => users,
id => 12
subcontroller => somecontroller
subaction => someaction

So visiting
/
will run the default "home" controller with default action.
/page/
will run controller page, default action
/page/view/
will run controller page and action viewlist
/page/view/23
will run controller page and action viewlist, id 23
etc

/users/
will run controller users, default action
/users/12/
controller users, id 12
/users/12/somecontroller/
controller users, id is 12, request[subcontroller] is somecontroller
etc

As Arborint mentioned, you have to make a distinction between using sub-Views and sub-controllers. I agree that in
/articles/date/2007/10/23/
the controller/action couple is articles/date, and 2007, 10, 23 are parameters used in subViews.

However, something like
/management/users/manage/newusers/edit
is something else. In this case, i would say "management" is some sort of module. You could say a mega-controller. users is a controller. manage is a subcontroller. Newusers is a sub-sub controller. Finally, edit is the action

So at this point I would say that my request object works fine. The mapper does its work. Only thing left: the controllers

As you can see, as your app grows and new paths are added, you'll have a mix of controllers, subcontrollers, subsubcontrollers (etc) and actions. Now the question: what could I do to manage this? As my original post showed, hardcoding everything seems like a bad thing. I think I need some sort of Dispatcher with a forward method. Each controller can use that dispatcher if there is no action for that controller to perform.

What do you think?

Posted: Fri Aug 03, 2007 3:34 pm
by Christopher
The other thing you might want to do is just let the router auto-map the rest of the parameters. Most routers do this. You don't need to provide mappings for everything. I think people tend to overuse fancy routing schemes.

The format is:

domain.com/controller/action/key1/value1/key2/value2

That sets the request so that 'key1'>'value1' and 'key2'=>'value2'

For "/management/users/manage/newusers/edit" you could also do the following with that front controller (using the standard '_' to '/' naming paradigm):

/management_users/manage/newusers/edit

Posted: Fri Aug 03, 2007 3:37 pm
by Chris Corbyn
arborint wrote:The other thing you might want to do is just let the router auto-map the rest of the parameters. Most routers do this. You don't need to provide mappings for everything. I think people tend to overuse fancy routing schemes.

The format is:

domain.com/controller/action/key1/value1/key2/value2

That sets the request so that 'key1'>'value1' and 'key2'=>'value2'
If you tie that into an existing system watch out for this:

?foo&bar&zip

/foo/bar/zip

Unless it does this

/foo//bar//zip/

I've now started to consider empty values a code smell mostly due to this problem.

Posted: Fri Aug 03, 2007 4:24 pm
by Christopher
I believe that /foo//bar//zip/ would be cleaned to 'foo/bar/zip/' (no // allowed) and assign 'foo'=>'bar' (with zip left off for not having a value)

Which way should it work?

Posted: Fri Aug 03, 2007 4:45 pm
by Chris Corbyn
arborint wrote:I believe that /foo//bar//zip/ would be cleaned to 'foo/bar/zip/' (no // allowed) and assign 'foo'=>'bar' (with zip left off for not having a value)

Which way should it work?
That's the way I wrote it in mine :) // gets cleaned to /.

The end one would hold an empty string as per what happens with ?foo.

Posted: Fri Aug 03, 2007 5:17 pm
by matthijs
arborint wrote:The other thing you might want to do is just let the router auto-map the rest of the parameters. Most routers do this. You don't need to provide mappings for everything. I think people tend to overuse fancy routing schemes.

The format is:
domain.com/controller/action/key1/value1/key2/value2
I agree that I shouldn't try to be too fancy, as that might complicate things too much for me. At this point I'm already happy if I can make the next step.

However, my main question now is how the actual processing by the controllers goes. A single controller/actions pair is clear. The Frontcontroller looks for a controller class in a specific directory, runs the given action or the default one. But with a deeper nested set you end up with controllers with massive deeply nested if/then/else loops. Which are basically a copy of the mapper scheme. This controller management. Is it something you solve up front (in the front controller or mapper) or for each controller separately? I hope I'm clear what I mean.