By Peter Bell

A Simple OO Controller

Unless you have a pretty unique use case, if you want a MVC controller, you probably want to check out ColdBox, Model Glue, Mach-II or Fusebox. The community frameworks have put a lot of time into getting the little details right and are well architected, executed and tested.

However, sometimes it's useful to see much simpler samples to understand how a simple MVC controller might work (without necessarily supporting implicit invocation). Ben had some problems with his OO controller, so I thought I'd post a very simple sample of the kind of approach I've been playing with . . .

I have a LightBase.cfc bean that does all the heavy lifting, and the main method is processPage():

<cffunction name="processPage" returntype="void" access="public" output="true">
   <cfargument name="PageStruct" required="true" type="struct">
   <cfscript>
      // Create and populate a page request object to hold and pass around all page state information required       var PageRequest = createandPopulatePageRequestObject(PageStruct);

      // Then process the main content area (before displaying the site template) and load the results into the page request       PageRequest.setMainContentArea( mainContentController(PageRequest) );

      // Call the Render component to render the page template (which in turn will call the renderer for the main areas)       Render.page(PageRequest);
   </cfscript>
</cffunction>

All it does is call methods to load up a page request object with all of the information required, call the main controller method to create the data and get the screen name for the primary content area, and then calls another bean to render the page.

createandPopulatePageRequestObject() does all of the bitty work required to load up the PageRequest object with input variables, create a SiteUser object as a meaningful facade to the session scope with real behaviors (like authentication, etc) and the like:

<cffunction name="createandPopulatePageRequestObject" returntype="any" access="public" output="true">
   <cfargument name="PageStruct" required="true" type="struct">
   <cfscript>
      var PageRequest = BeanFactory.getTransient("PageRequest");
      // Create a page object for providing information about the "page" being viewed       var Page = PageService.new();
      // Create an "input" object to handle getting and setting of all inputs (URL and form scope)       var Input = BeanFactory.getTransient("Input");
      // Create a "SiteUser" object to represent the user making the request       var SiteUser = SiteUserService.new();

      // Load the input object with URL and form variables       Input.loadStruct(URL);
      Input.loadStruct(form);
      
      // Populate the page object       if ( StructKeyExists( PageStruct , "FilePath" ) ) {
         // For page controller, load all the page properties          Page.loadStruct( PageStruct );

         // Overload the method with the input method if appropriate          if ( Len( Input.getAction() ) ) {
            Page.setMethod( Input.getAction() ); };
      }
      else
      {
         // For front controller, set the controller and method          Page.setController( ListFirst( Input.getAction() , "." ) );
         if ( Listlen( Input.getAction() , "." ) GT 1 )
         { Page.setMethod( ListGetAt( Input.getAction() , 2, "." ) ); };
      };
      Page.setURL( CGI.script_name);

      // Load the PageRequest object with all the necessary beans       PageRequest.setApplicationConfig( ApplicationConfig );
      PageRequest.setRequestProperties( createandPopulateRequestPropertiesObject() );
      PageRequest.setInput( Input );
      PageRequest.setSiteUser( SiteUser );
      PageRequest.setPage( Page );
      return PageRequest;
   </cfscript>
</cffunction>

The mainContentController() method calls the appropriate controller bean that's been DI'd into this object:

<cffunction name="mainContentController" returntype="struct" access="public" output="true">
   <cfargument name="PageRequest" type="any" required="true">
   <cfscript>
      var MainContentArea = structnew();
      var ControllerName = PageRequest.getPage().getController();
      // Check the controller bean exists
      if ( NOT StructKeyExists( variables , "#ControllerName#Controller" ) )
      { throw ("Invalid controller (#PageRequest.getPage().getController()#)."); }
      else
      {
         MainContentArea = evaluate( "#ControllerName#Controller.call(PageRequest)" );
      };
      return MainContentArea;
   </cfscript>
</cffunction>

And then there is a base call() method in the BaseController that all of my controllers extend for calling the appropriate method in that controller:

<cffunction name="call" output="false" returntype="struct">
   <cfargument name="PageRequest" type="any" required="true">
   <cfscript>
      var Response = StructNew();
      var ControllerName = PageRequest.getPage().getController();
      var MethodName = PageRequest.getPage().getMethod();
      if ( NOT len(MethodName) ) {
         MethodName = DefaultMethodName; };
      if ( StructkeyExists( variables , MethodName ) ) {
         Response = evaluate("#MethodName#( PageRequest=PageRequest )"); }
      else {
         throw( "Invalid method name (#MethodName#) in controller #ControllerName#." ); };
      return Response;
   </cfscript>
</cffunction>

As for the rendering, it is as simple as the page() method within the Render component. You'd extend this to customize the logic for using different page templates depending on the rules for a project - time of day, user, section of the site, etc:

<cffunction name="page" output="true" returntype="void">
   <cfargument name="PageRequest" type="any" required="true">
   <cfscript>
      var TemplateName = "default-page-template";
      variables.PageRequest = PageRequest;
   </cfscript>
   <cfinclude template="#SubdirectoryPath#/lb/template/page/#TemplateName#.cfm">
</cffunction>

Your page template might look something like:

<html>
<head></head>
<body>
Page template with header and the like goes here<br />

#displayMainContentArea()#

Footer goes here<br />
</body>
</html>

And the displayMainContentArea() calls that method on the render() object which simple does the following:

<cffunction name="displayMainContentArea" output="true" returntype="void">
   <cfscript>
      var TemplateName = PageRequest.getMainContentArea().Template;
      var Data = PageRequest.getMainContentArea().Data;
      var Page = PageRequest.getPage();
      var Input = PageRequest.getInput();
      var Message = "";
      if ( StructKeyExists( PageRequest.getMainContentArea() , "Message" ) ) {
         Message = PageRequest.getMainContentArea().Message; };
   </cfscript>
   <cfinclude template="#SubdirectoryPath#/lb/template/screen/#TemplateName#.cfm">
</cffunction>

A ColdBox it isn't, but it works quite well as an example of how simple the basic problem of loading up some kind of event/pagerequest bean and then calling a controller method and passing it to an appropriate renderer can be.

Thoughts?

Comments
Peter,

I'm interested in your take on what the community MVC frameworks give you that this little sketch does not. The kind of approach you outline here avoids the two big drawbacks the community frameworks tend to have:

1. Massive quantities of what are effectively global variables
2. A tendency towards spaghetti XML

So, without starting a religious war (I know some frameworks do avoid one or both of these) - what are the benefits that make those drawbacks worthwhile?

Jaime Metcher
# Posted By Jaime Metcher | 7/24/08 6:40 PM
@Jaime,
You do realize that there is no possible way to answer your question without starting the Nth iteration of the same frameworks discussion! j/k
# Posted By Oscar Arevalo | 7/30/08 1:57 AM
BlogCFC was created by Raymond Camden. This blog is running version 5.005.