Designing a request object in PHP

In my article on Indigo's core design decisions, I spoke on how to handle the request / response cycle. The direction I chose to go is to create a pair of objects and pass them to the controller. This is in contrast with passing an arbitrary number of arguments (typically pulled from the URL), allowing it to locate GET/POST data, and returning an HTML string. Kohana and Drupal both employ systems like this, but I rejected that pattern. I chose this set up for a few reasons:

  • Testing this system is far easier than one that expects the controllers to seek out GET  or POST data. When combined with dependency injection, it is fairly straightforward to unit test a controller. If I required the controller to directly access the $_GET or $_POST superglobals, it becomes harder to run a series of automated tests.
  • Controller sub-dispatching is easy - a page can create request and response objects and dispatch them to other controllers. Since the controllers are never forced to seek out data in the global state, then the sub-dispatched requests can easily masquerade as the main request.
  • Controllers have more control over the response without having to force anything. The response object contains more than just the HTML to be displayed to the end user - it contains headers and can manage URL redirects if needed. If a controller is not able to dictate these actions to the response, then they are forced to take brutal measures such as calling header('Location: /foo'); directly.
  • The request object can be made from any source - Apache, nginx, CLI, IIS, etc. Rather than making assumptions about the nature of the server and directly looking at things like $_SERVER['DOCUMENT_ROOT'], I can separate out where the data is acquired from what data is needed. The only area that should directly look into the $_SERVER superglobal is the tool that is building the request object.
Of course, this pattern does have its downsides:
  • If every page takes exactly two arguments, it can be difficult to ascertain what input parameters are expected. With Kohana, a page that expects 3 arguments naturally expects three pieces of data passed to the URL. With the setup I am using for Indigo, the only way to know what arguments are expected is to read the $routes array that all controllers must have. Now, if PHP ever got decorators...
  • In a sense, this duplicates data and creates a potential pitfall for developers. For instance, $_SESSION and $_COOKIE will need to be mapped into the request, and pages are expected to read/write to that data, rather than directly to the $_SESSION superglobal or through set_cookie. At the end of each page load, the modified data in the response object is written to $_SESSION and $_COOKIE. If a page writes directly to $_SESSION, those changes will be overwritten at the end of the page load with the unmodified response object session.

The next step is deciding exactly how to lay out the request and response objects. The obvious solution is simply a glorified container. Each object contains a list of parameters and the controller reads/writes to these parameters. I can add some getters and setters to validate data input. It is simple and it does the job, but I feel like I should be able to do more than just that. I could add a set of functions for the objects that provide extended capabilities, but then the question becomes "what functions and capabilities are appropriate for this object?". I could give the response obejct a function for URL redirects, or the request object a function to help deal with CSRF attacks. But what else can I do?

I have been looking through other frameworks to see how they handle their requests and have not been particularly inspired. Kohana makes no attempt to encapsulate a request. NodeJS does a good job creating a request object, but the response object writes directly to the output. I want my response to act more like a staging area. This gives other events a chance to manipulate that data before sending out. 

Drupal's system provides hooks to ask the system for request data, but this is not how I envision Indigo being unit tested. Pyramid has both a request object and a response object, and an initial glance tells me that they are in line with what I am considering doing. I've already created a basic request / response object within Indigo, but will still look into other options to take inspiration from.

Tags: