Controlling the Controller

Posted Dec 31, 2008 by Kris Jordan | Comments ( 4 ) | Filed in: | | | | |

What is a Controller?

In Recess a Controller is an object responsible for taking a web request, invoking the intended logic in a 'Model', and then selecting a 'View' which will generate the response, likely in HTML, to be sent back to the client.

Getting Started

Recess applications are broken down into Models, Views, and Controllers. To kick off a new application open Recess Tools (/recess), navigate to 'Apps', and 'New Application'. Our new application will be named 'Hello World', programmatic 'HelloWorld', and with route prefix 'helloWorld/'.

In an applications directory structure there is a controllers directory where your Controllers are placed. Creating a new controller is simply a matter of creating a new PHP file containing a class which extends Controller. Here is an example:

class HelloWorldController extends Controller { }

The file will be named 'HelloWorldController.class.php'. The '.class.php' extension is important in Recess because it implies to the Recess Library that the file has a single class defined in it. This is similar to Java's single class per file convention.

Let's add some functionality to the controller. We will begin by adding a simple method to print 'Hello World' and exit.

class HelloWorldController extends Controller {

	function printIt() {
		print 'Hello World!'; exit;
	}

}

If we now navigate to 'helloWorld/printIt' we will see 'Hello World!' printed. How did the request for 'helloWorld/printIt' wind up getting mapped to the printIt method? This process is called Routing.

Routing

Routing is the subsystem in Recess that maps a URL to a method in your controller. For more information on routing details refer to this article on Routing

The Request Object

An HTTP request contains a variety of information: variables, headers, cookies, a URL, etc. Routing takes care of mapping an HTTP method (GET/POST/PUT/DELETE) to a controller method. Within your controller method you'll likely need to perform logic based on information in the request. Controllers have a Request object which can be referenced using $this->request. Let's take a look:

class HelloWorldController extends Controller {

	function printIt() {
		print $this->request->resource . '<br />';
		print_r($this->request); 
		exit;
	}

}

Now refresh 'helloWorld/printIt' in your browser. If the output runs together in one line view the source. These are the variables available on the Request object. For example, $this->request->resource is '/helloWorld/printIt'. Try navigating to 'helloWorld/printIt?foo=bar' and notice how $this->request->get['foo'] is set to 'bar'. In a Request object 'get', 'post', and 'put' hold the variables passed into the request when using the GET, POST, or PUT method like PHP's $_GET and $_POST.

The Controller-View Relationship

The Controller is responsible for indicating which view template to use. If no response is returned from a controller method the default view template will be nameOfTheControllerMethod.php in the 'views/' directory. Try removing the body of the printIt method and refresh to get an error indicating no view template at 'helloWorld/views/printIt.php' can be found. To change the view template file we can use the 'ok' helper method.

class HelloWorldController extends Controller {

	function printIt() {
		return $this->ok('the-view');
	}

}

After refreshing the error message will change to indicate the new location 'helloWorld/views/the-view.php'. The 'ok' helper method will be discussed soon. Let's create a new file named 'the-view.php' with the content below and save it into the views folder:

<html>
	<head><title>Hello World View</title></head>
	<body>
		<?php echo 'Hello World!'; ?>
	</body>
</html>

Try refreshing, you should see 'Hello World!'. Lets pass some variables from the Controller to the View Template.

Controller:

class HelloWorldController extends Controller {

	function printIt() {
		$this->message = 'Hello World';
		$this->repeat = 10;
		return $this->ok('the-view');
	}

}

View Template:

<html>
	<head><title>Hello World View</title></head>
	<body>
		<?php for($i = 0; $i < $repeat; $i++): ?>
			<?php echo $message, '<br />'; ?>
		<?php endfor; ?>		
	</body>
</html>

Refresh to see 'Hello World!' printed 10 times. How do those variables propagate to the view template? The public instance variables in a controller are copied into the Response object which gets passed to a View and the View then sets those variables in the context of the view template. This process is vaguely similar to a 'Memento' pattern.

When writing view templates a great trick to quickly see what variables are available is to force an error in the view and look at the Recess Diagnostics screen. Try replacing the for loop in your view template with this code:

<html>
	<head><title>Hello World View</title></head>
	<body>
		<?php echo $fail; ?>
	</body>
</html>

This will trigger an undefined variable error in PHP that will bring up Recess Diagnostics. On the Diagnostics screen there is a 'Context' table that shows all of the variables available in the local context. Here we can see $message and $repeat set as well as some other variables, one of which is the Response object.

The Response Object

Controller methods are allowed to return either nothing at all or a Response object. When a controller method does not return anything Recess assumes an OkResponse is intended with a view template that has the same name as the controller method. The 'Ok' prefix of OkResponse corresponds to the HTTP 200 OK response code. A Recess Response object contains the information Recess needs to respond to a request including: the response code, data to be passed to the view, headers to be sent back, cookies, a reference to the request, and some additional meta data used by Recess.

In the base AbstractController class there are a number of helper methods which will import and instantiate a response for you. The 'ok' method is an example of a helper method which returns an OkResponse. Other helpers include: conflict, redirect, forwardOk, forwardNotFound, created, and unauthorized. The 'forward'ing responses, and created, are a special kind of Response.

A ForwardingResponse causes Recess to handle another request and sends the body of that response to the client. For example, imagine you would like to build a PHP REST interface for creating Posts. After creating a Post you would like to send a 201 CREATED response that contains a Location header informing the client where to find that resource. For web browsers you likely want to send back meaningful content, perhaps the new list of Posts. In Recess this would look like:

class PostsController extends Controller {

	/** !Route POST, /posts */
	function insertPost() {
		$post = Make::a('Post')->copy($this->request->data('Post'))->insert();
		return $this->created('/post/' . $post->id, '/posts');
	}

	/** !Route GET, /posts */
	function listPosts() {
		$this->posts = Make::a('Post')->all();
	}

	/** !Route GET, /posts/$id */
	function showPost($id) {
		$this->post = Make::a('Post')->equal('id', $id)->first();
	}

}

The important line is @$this->created('/post/' . $post->id, '/posts');@ The created helper method takes two arguments, the first is the URL to the created resource that will be sent in the Location header, the second is the URL to the 'content' to respond with. In this case the REST resource created is at /posts/$id but the response will render the HTML for the list of all posts at /posts.

The urlTo Helper

Introducing dependencies on specific URLs in your controllers (and views!) is a bad practice because these URLs may change due to refactoring. Recess decouples this knowledge by providing a helper method that returns the URL to a controller method. Lets take another stab at the Posts controller using urlTo.

class PostsController extends Controller {

	/** !Route POST, /posts */
	function insertPost() {
		$post = Make::a('Post')->copy($this->request->data('Post'))->insert();
		return $this->created(
			$this->urlTo('showPost', $post->id), 
			$this->urlTo('listPosts'));
	}

	/** !Route GET, /posts */
	function listPosts() {
		$this->posts = Make::a('Post')->all();
	}

	/** !Route GET, /posts/$id */
	function showPost($id) {
		$this->post = Make::a('Post')->equal('id', $id)->first();
	}

}

The urlTo helper method will return the URL which maps to the controller method passed as an argument. Notice that methods which take parameters must be passed the parameters as subsequent arguments as shown in urlTo('showPosts', $post->id). Now if we change URLs using the relative routing techniques shown in the Routing Screencast we do not have to find all of the points where that URL was referenced. Also, if the name of a method changes and urlTo('thatMethod') is called Recess will throw an error which simplifies debugging.

Conclusion

Controllers in Recess simplify the process of accepting a request, delegating to application logic in Models, and passing off responsibility for responding to a view all in a RESTful manner. The conventions of selecting a view name based on the controller method name and returning a 200 OK response by default can be overriden with ease using helper methods. Finally, the urlTo method helps keep controllers and views DRY.

Comments

Comments are moderated. It may take some time for your comment to appear.

Recess can only be as good as the thoughts that go into it. Let us hear yours...

  1. Posted by IvanK on Dec 31, 2008
    That's all very interesting, and I'm already sold to the idea of routes next to controller methods, but up to now that's the only really different feature compared to other frameworks. The thing I'm really interested in is the model - up to now all the PHP ORMs suck, so I'm hoping there's a gem hidden somewhere here too. And what's up with the "Make" class, sounds kinda strange, I mean you don't "make" the objects, you retrieve them from the database... not that it really matters of course, but still sounds strange. Anyway, please add a post about the model soon.
  2. Posted by Kris Jordan on Dec 31, 2008
    Ivan - Models are up next!

    As for the 'Make::a' it's a shortcut method for instantiating a new class, nothing more.

    $people = Make::a('person')->all()

    is equivalent to:

    $person = new Person();
    $people = $person->all();

    The problem with the new keyword is that you can't immediately chain after it, i.e. this doesn't work: $people = (new Person())->all();
  3. Posted by Christoph on Mar 23, 2009
    dear kris,

    first of all, love your oop-freaky-code style!
    recess seems to be a new aproach for frameworkers - light, easy to use, fast!

    100% agree with IvanK "up to now all the PHP ORMs suck". From that point of view, beeing able to use recess-models without the whole need of the framework itself, enabling to use its orm in existing codebase, uhhhhhh - nice!!!

    cheers,
    chris
  4. Posted by jobedom on Apr 30, 2010
    With PHP 5.3 you could do something like (oversimplified for the sake of explanation):

    class Make {
    public static function __callStatic($name, $args) { return new $name; }
    }

    Then, you instantiate classes with:

    $p = Make::Person()->fluid1()->fluid2()...

You must login to comment on this page.