Adaptring the Front Controller to PHP...

Not for 'how-to' coding questions but PHP theory instead, this forum is here for those of us who wish to learn about design aspects of programming with PHP.

Moderator: General Moderators

Post Reply
User avatar
nielsene
DevNet Resident
Posts: 1834
Joined: Fri Aug 16, 2002 8:57 am
Location: Watertown, MA

Adaptring the Front Controller to PHP...

Post by nielsene »

I'm trying to understand the Front Controller pattern in PHP; I've read the section in PoEAA multiple times and I understand most of the concept, but I'm a little puzzled by their example (probably because I never liked Java for web stuff so I don't really understand their setup).

What is the HttpResponse object? Why is it being passed around through so many layers? The HttpRequest is a combination of _SERVER and _REQUEST in php terms, (broad overview), right?

Another question:
Since PHP doesn't have a notion of application variables, one of the standard Java answers to configuring the Front Controller of using large XML (or other format) file to define the mappings of requests to commands isn't an extremely efficient option. I'm thinking about taking a DNS-inspired option -- the Front Controller will be staticly configured to know about several command, most of which when invoked will delegate again to another command. (Basically chain down the URL). Is this the normal PHP implementation? What are the other options?
timvw
DevNet Master
Posts: 4897
Joined: Mon Jan 19, 2004 11:11 pm
Location: Leuven, Belgium

Re: Adaptring the Front Controller to PHP...

Post by timvw »

nielsene wrote: What is the HttpResponse object? Why is it being passed around through so many layers? The HttpRequest is a combination of _SERVER and _REQUEST in php terms, (broad overview), right?
Although i dont have P of EAA, i think your quite right :)

http://java.sun.com/j2ee/1.4/docs/api/j ... quest.html
http://java.sun.com/j2ee/1.4/docs/api/j ... ponse.html

nielsene wrote: Since PHP doesn't have a notion of application variables, one of the standard Java answers to configuring the Front Controller of using large XML (or other format) file to define the mappings of requests to commands isn't an extremely efficient option.
I'm considering as following: Use XML files to configure. But instead of parsing it each time, store the resulting variables in a file and then let the frontcontroller from that file. (Each time the frontcontroller is loaded it tests if the XML has changed, and eventually generates a new cache)
User avatar
nielsene
DevNet Resident
Posts: 1834
Joined: Fri Aug 16, 2002 8:57 am
Location: Watertown, MA

Re: Adaptring the Front Controller to PHP...

Post by nielsene »

I'm considering as following: Use XML files to configure. But instead of parsing it each time, store the resulting variables in a file and then let the frontcontroller from that file. (Each time the frontcontroller is loaded it tests
if the XML has changed, and eventually generates a new cache)
If you're cache-ing it into PHP (so its a "simple" hash-map of some kinda that's easily includeablee, why keep it in XML to start with? If its not cached into PHP its probably still just about as slow as XML to parse and load.
timvw
DevNet Master
Posts: 4897
Joined: Mon Jan 19, 2004 11:11 pm
Location: Leuven, Belgium

Post by timvw »

The thought was to dump/load php bytecode. That would be faster than xml, ini_get, even php source. But i can't find anymore how that is possible :)
McGruff
DevNet Master
Posts: 2893
Joined: Thu Jan 30, 2003 8:26 pm
Location: Glasgow, Scotland

Re: Adaptring the Front Controller to PHP...

Post by McGruff »

nielsene wrote:What are the other options?
The wact wiki has a good page on FrontControllers: http://www.phpwact.org/pattern/front_controller.

I would probably use a GET parameter to include some file or other - the "Dynamic Invocation" option.
User avatar
Buddha443556
Forum Regular
Posts: 873
Joined: Fri Mar 19, 2004 1:51 pm

Post by Buddha443556 »

Personally I'm very happy with my current Front Controller ... Apache does an excellent job. :wink:
User avatar
nielsene
DevNet Resident
Posts: 1834
Joined: Fri Aug 16, 2002 8:57 am
Location: Watertown, MA

Re: Adaptring the Front Controller to PHP...

Post by nielsene »

McGruff wrote:
nielsene wrote:What are the other options?
The wact wiki has a good page on FrontControllers: http://www.phpwact.org/pattern/front_controller.

I would probably use a GET parameter to include some file or other - the "Dynamic Invocation" option.
That seems to complicately handicap a website into using non-nice URLs. I use POST almost exclusively for forms for cleaner URLs amoung other things. I've been migrating pages to a ForceType style URL parser for isolated chunks of functionality where it makes the most sense (commonly bookmarked/emailed pages).

I'm currently thinking that I'll have the Front Controller dispatch do some limited url parsing (upto the 1st or 2nd subdirectory) and then invoke the ForceType mini-front controller for that module.

And I kinda agree with Buddha443556 that Apache+PHP almost operates as a FC already .. if you make use of the auto_prepend_file php.ini directive to handle any of the "all one place" setup/authentication/logging/etc.

I'm a little unsure how any of these approaches play with Apache's logging though, seems like a true front controller will end up making the logs useless as everything goes through only one file.
User avatar
nielsene
DevNet Resident
Posts: 1834
Joined: Fri Aug 16, 2002 8:57 am
Location: Watertown, MA

Post by nielsene »

Regarding the Request object:

At its minimum its basically this, right:

Code: Select all

class HttpRequest {
   var $_get;
   var $_post;
   var $_cookie;

   function HttpRequest() {
      $this->_get=$_GET;
      $this->_post=$_POST;
      $this->_cookie=$_COOKIE;
   }

   function fetch($type,$varName) {
      if (!in_array($type,"get","post","cookie")) return false;
     $type="_$type";
      return (!isset($this->$type[$varName])? $this->$type[$varName] : false);
   }
   function fetchGet($varName) {
      return $this->fetch("get",$varName);
   }
   // ...
}
I keep wanting to put my "generic cleansing" code in this class ie wrapping all variables in htmlspecialchars(trim()) calls. But that would be wrong, I think, right? Because decisions on what to escape and when should depend on what's being done with the data -- therefore its a command/handler decision not the Request object's?
McGruff
DevNet Master
Posts: 2893
Joined: Thu Jan 30, 2003 8:26 pm
Location: Glasgow, Scotland

Re: Adaptring the Front Controller to PHP...

Post by McGruff »

nielsene wrote:I kinda agree with Buddha443556 that Apache+PHP almost operates as a FC already ..
With PageControllers or a FrontControllers, I don't see any compelling reason to choose one over the other. Both work well.

FrontControllers are sometimes criticised as being inefficient (they have to determine the request type v apache & the file system doing that for you) but there's no real overhead if you use something like dynamic invocation.

Supporters of FrontControllers sometimes claim an advantage from the single point of access which allows you to apply common request tasks like filtering or whatever but you can do the same with a PageController base class extended by page-specific PageControllers.
timvw
DevNet Master
Posts: 4897
Joined: Mon Jan 19, 2004 11:11 pm
Location: Leuven, Belgium

Post by timvw »

I usually choose for a small very basic / not oop front controller (for example this is how http://example.com/cmsuser/index.php would look like) The code for CRUD on news could be in http://example.com/news/index.php ..

Code: Select all

require_once('../../init.php');
require_once(FRAMEWORK_ACTIVERECORDS . '/cmsuser.php');

$task = isset($_GET['task']) ? $_GET['task'] : 'cmsuser_list';
$activerecord = new CMSUser;

switch($task)
{
	// load a view and controller for each task
	case 'cmsuser_create':
		$view = new View(FRAMEWORK_VIEWS . '/detail.php');
		require_once(FRAMEWORK_EXT . '/timvc/createcontroller.php');
		$controller = new CreateController($activerecord, $view);
		break;

	case 'cmsuser_search':
		$view = new View(FRAMEWORK_VIEWS . '/detail.php');
		require_once(FRAMEWORK_EXT . '/timvc/searchcontroller.php');
		$controller = new SearchController($activerecord, $view);
		break;
		
	case 'cmsuser_read':
		$view = new View(FRAMEWORK_VIEWS . '/detail.php');
		require_once(FRAMEWORK_EXT . '/timvc/readcontroller.php');
		$controller = new ReadController($activerecord, $view);
		break;
			
	case 'cmsuser_update':
		$view = new View(FRAMEWORK_VIEWS . '/detail.php');
		require_once(FRAMEWORK_EXT . '/timvc/updatecontroller.php');
		$controller = new UpdateController($activerecord, $view);
		break;	
		
	case 'cmsuser_delete':	
		$view = new View(FRAMEWORK_VIEWS . '/detail.php');
		require_once(FRAMEWORK_EXT . '/timvc/deletecontroller.php');
		$controller = new DeleteController($activerecord, $view);
		break;	
				
	case 'cmsuser_list':
	default:
		$view = new View(FRAMEWORK_VIEWS . '/list.php');
		require_once(FRAMEWORK_EXT . '/timvc/listcontroller.php');
		$controller = new ListController($activerecord, $view);
}

$controller->run();
McGruff
DevNet Master
Posts: 2893
Joined: Thu Jan 30, 2003 8:26 pm
Location: Glasgow, Scotland

Post by McGruff »

nielsene wrote:I keep wanting to put my "generic cleansing" code in this class ie wrapping all variables in htmlspecialchars(trim()) calls. But ... ... ... its a command/handler decision not the Request object's?
Yes. Htmlspecialchars is an output filter - and specifically for html. Trimming I guess can be done anywhere. Probably I'd do that when the input is received.

On planet gruff, Request objects can do quite a few things. I see them as a kind of facade for a "request model".

Input is validated in here and only valid values are allowed out. The $validator->getValidated('key') calls below will return null if any validation rule fails. The rest of the app never accesses raw GPC directly but only safe, validated values via a Request object (which gets passed around almost everywhere). I usually prefer named accessors.

I'd also hide the session mechanism behind the Request object. Cookies/GET are of course specific to http requests. You could easily swap out an http request class for a CLI version which uses some kind of CLI session mechanism, for example - and argv parameters. This would present exactly the same interface. It's one of these Cartesian boundaries I've been talking about: the internal "application mind" doesn't really need to know anything about the external reality, ie whether the context is http or CLI. Named acessors again: persistFoo/forgetFoo/getFoo. The "requested page" is being persisted so that the user can be redirected after a login detour, if they haven't been authenticated.

Incidentally I've never written something for CLI which needs sessions: all this is more of a "thought experiment" which tells you something about the design.

Request objects provide whatever accessors are required. With the ChainOfResponsibility design I've mentioned previously, a BadRequestSyntax object checks $request->hasValidSyntax() and displays a 400 page if something is wrong.

In the example below, hasValidSyntax is asserting that the only cookie allowed is the session cookie (and it need not be present), $_POST must be empty, and a $_GET['county'] parameter must match a regex pattern. The "cup" var means "customised uri parameters" ie parameters parsed from $_SERVER['REQUEST_URI'] if you are using force type tricks. The point is, all that is dealt with by the Request (and associated objects). The application just asks for getCounty() etc.

There's a bit more to the validation code which we maybe don't need to go into here. It'll complain if it finds an "alien" key (something it hasn't been told about - some kind of tampering and possibly a hacking attempt) or if there are missing keys which should be present for the current request type.

Code: Select all

class FooRequestGateway
{
    var $_cup;          // (array) "custom Uri parameters" (mod_rewrite/force type tricks)
    var $_GET_errors;   // (object)
    var $county_name;   // (string)

    /*
    */
    function FooRequestGateway()
    {
        @session_start();
        $uri =& $this->_Uri();
        $this->_cup = $uri->getParameters();
        $this->_extractGET();
    }
    /*
        return (bool)
    */
    function hasValidSyntax() 
    {
        if(     !count($this->_cup)
            and !$this->_GET_errors->hasAlienKeys()
            and !$this->_GET_errors->hasMissingKeys()
            and !$this->_GET_errors->hasValueErrors()
            and !count($_POST)
            and  $this->_cookieIsValid()) {

            return true;

        } else {

            return false;
        }
    }
    /*
        return (string)
    */
    function getCountyName()
    {
        return $this->county_name;
    }
    /*
        param (bool)
    */
    function persistAuthentication($value)
    {
        $_SESSION['authenticated'] = $value;
    }
    /*
        return (bool)
    */
    function isAuthenticated() 
    {
        return (isset($_SESSION['authenticated']) 
            and $_SESSION['authenticated'] === true);
    }
    /*
        param (string)
    */
    function persistRequestedPage($url)
    {
        $_SESSION['requested_page'] = $url;
    }
    /*
        return (void)
    */
    function forgetRequestedPage() 
    {
        unset($_SESSION['requested_page']);
    }
    /*
        return (string/false)
    */
    function getRequestedPage() 
    {
        if(isset($_SESSION['requested_page'])) {
            return $_SESSION['requested_page'];
        } else {
            return false;
        }
    }
    function _extractGET() 
    {
        require_once(APERI_LIB . 'validation/MatchesPattern.php');
        $specs =& $this->_ArraySpecification();
        $specs->setRuleAt(array('county'), new MatchesPattern('/[a-z]+/i'));
        $validator =& $this->_ArrayValidator($specs);
        $validator->setTarget($_GET);
        $this->county_name = $validator->getValidated(array('county'));
        $this->_GET_errors =& $validator->getErrors();
    }
    function _cookieIsValid() 
    {
        if(count($_COOKIE) > 1) {
            return false;
        }
        if(count($_COOKIE) == 1 and !in_array(session_name(), array_keys($_COOKIE))) {
            return false;
        }
        return true;
    }
    function &_Uri() 
    {
        require_once(APERI_LIB . 'control/Uri.php');
        return new Uri;
    }
    function &_ArraySpecification() 
    {
        require_once(APERI_LIB . 'validation/ArraySpecification.php');
        return new ArraySpecification;
    }
    function &_ArrayValidator(&$specs)
    {
        require_once(APERI_LIB . 'validation/ArrayValidator.php');
        return new ArrayValidator($specs);
    }
}
User avatar
nielsene
DevNet Resident
Posts: 1834
Joined: Fri Aug 16, 2002 8:57 am
Location: Watertown, MA

Post by nielsene »

Ah ha, thanks, that helped.

I was thinking that the Request would be a single object for the whole application. There'd be way too many possible variables to make named acccessors for each different variables used in the application.

So the FrontController needs to identify both what Request class to instantiate as well as kick off the Command chain?
McGruff
DevNet Master
Posts: 2893
Joined: Thu Jan 30, 2003 8:26 pm
Location: Glasgow, Scotland

Post by McGruff »

Yes.

You might find a base request class if there are some shared methods.
User avatar
nielsene
DevNet Resident
Posts: 1834
Joined: Fri Aug 16, 2002 8:57 am
Location: Watertown, MA

Post by nielsene »

The inclusion of the $_SESSION into the request is starting to feel a little odd to me.

It seems mo be that the $_SESSION should wrapped more by a "State"/"Mememto"/"UnitOfWork" style object, of which the Request would be one of its composition members?

Yes, something in the Request keyed the Session and thus makes that information available to the application, but it doesn't feel like part of the request.
McGruff
DevNet Master
Posts: 2893
Joined: Thu Jan 30, 2003 8:26 pm
Location: Glasgow, Scotland

Post by McGruff »

In OOP there are always different ways to cut the cake.

In one sense, session vars are not part of the http request. Only the session id is propagated via http.

However, I think it's also valid to say that session parameters are in fact part of the request state. They've deliberately been linked to http requests when they were added to the $_SESSION superglobal.

The application doesn't really need to know whether $foo arrived immediately from user input or if it has been cached locally in a previous request. It just wants $foo. The purpose of the request object is to encapsulate the context - in this case http or CLI.

I think I called my Request object a Facade - really it's more of a Gateway. On the other side of the gateway, the app doesn't know whether it's http or CLI - or even that it's running in a stateless medium. (As is often mentioned, Gateways make nice points to mock/stub). The boundary cuts across the controller layer, separating user input and the http/cli environment from the application controller logic. Other things like $_SERVER vars also have to be kept behind the gateway if the app is to be context-agnostic.

Perhaps I need to rename the object to make things clearer. "RequestContext"?
Post Reply