Thursday 16 July 2009

Simple MVC

"Frameworks just complicate things and force you to do their way"
"We don't need Model-View-Controller, we don't do OOP"

Quotes like that proper annoy me. If you want to program unmaintainable procedural spaghetti code on a terminal in vi, then go back to programming in the 70's where you belong.

MVC is just a design pattern. Not being arsed with using an MVC framework on a project doesn't mean you can't write well designed, loosely coupled, easy to maintain programs.

Business logic and state should be encapsulated by your model. Below is a model component for paging through the CFARTGALLERY database that comes with Coldfusion 8:

Model.cfc

<cfcomponent output="false">

  <cffunction access="public" returntype="Model" name="init">
    <cfargument required="true" type="Data" name="data">
    <cfset VARIABLES.data = ARGUMENTS.data>
    <cfreturn this>
  </cffunction>

  <cffunction access="public" returntype="numeric" name="getArtCount">
    <cfreturn VARIABLES.data.countArt().count>
  </cffunction>

  <cffunction access="public" returntype="string" name="getArtNames">

    <cfset var LOCAL = {}>

    <cfparam name="SESSION.firstRecord" default=1>
    <cfparam name="SESSION.maxRecords" default=10>

    <cfset LOCAL.query = VARIABLES.data.findArt(SESSION.firstRecord, SESSION.maxRecords)>
    <cfreturn ValueList(LOCAL.query.artName)>
  </cffunction>

  <cffunction access="public" returntype="boolean" name="showPrev">
    <cfif SESSION.firstRecord GT 1>
      <cfreturn true>
    <cfelse>
      <cfreturn false>
    </cfif>
  </cffunction>

  <cffunction access="public" returntype="boolean" name="showNext">
    <cfif SESSION.firstRecord + SESSION.firstRecord LT getArtCount()>
      <cfreturn true>
    <cfelse>
      <cfreturn false>
    </cfif>
  </cffunction>

  <cffunction access="public" returntype="void" name="nextPage">
    <cfset SESSION.firstRecord = SESSION.firstRecord + SESSION.maxRecords>
  </cffunction>

  <cffunction access="public" returntype="void" name="prevPage">
    <cfset SESSION.firstRecord = SESSION.firstRecord - SESSION.maxRecords>
  </cffunction>
</cfcomponent>

Database code is abstracted to a layer encapsulated by the model, in it's own Data CFC:

Data.cfc

<cfcomponent output="false">

  <cfset VARIABLES.datasource = "cfartgallery">

  <cffunction access="public" returntype="Data" name="init">
    <cfreturn this>
  </cffunction>

  <cffunction access="public" returntype="query" name="countArt">
    <cfset var LOCAL = {}>
    <cfquery name="LOCAL.countArt" datasource="#datasource#">
      SELECT COUNT(*) AS count FROM art
    </cfquery>
    <cfreturn LOCAL.countArt>
  </cffunction>

  <cffunction access="public" returntype="query" name="findArt">
   <cfargument name="firstRecord" required="true" type="numeric">
   <cfargument name="maxRecords" required="true" type="numeric">

   <cfset var LOCAL = {}>
   <cfset LOCAL.lastRecord = firstRecord + maxRecords>
   <cfquery name="LOCAL.findArt" datasource="#datasource#">
      SELECT *
      FROM art
      WHERE artId >= <cfqueryparam cfsqltype="cf_sql_integer" value="#ARGUMENTS.firstRecord#">
      AND artId < <cfqueryparam cfsqltype="cf_sql_integer" value="#LOCAL.lastRecord#">
   </cfquery>
   <cfreturn LOCAL.findArt>
  </cffunction>
</cfcomponent>

The model should have no knowledge of your view and controller code. The view is used to display values from the model, and the controller changes values in the model.

The controller is in two parts. The first part, Controller.cfc contains functions which modify the model. The second part, controller.cfm handles user input, calls the functions in Controller.cfc, and forwards to the next view.

Controller.cfc

<cfcomponent output="false">

  <cffunction access="public" returntype="Controller" name="init">
    <cfargument required="true" type="Model" name="model">
    <cfset VARIABLES.model = ARGUMENTS.model>
    <cfset REQUEST.model = VARIABLES.model>
    <cfreturn this>
  </cffunction>

  <cffunction access="public" returntype="void" name="nextPage">
    <cfset VARIABLES.model.nextPage()>
    <cfset REQUEST.model = VARIABLES.model>
  </cffunction>

  <cffunction access="public" returntype="void" name="prevPage">
    <cfset VARIABLES.model.prevPage()>
    <cfset REQUEST.model = VARIABLES.model>
  </cffunction>
</cfcomponent>

controller.cfm

<cfsilent>
  <cfif isDefined("FORM.next")>
    <cfset APPLICATION.controller.nextPage()>
    <cflocation url="view.cfm">
  </cfif>

  <cfif isDefined("FORM.prev")>
    <cfset APPLICATION.controller.prevPage()>
    <cflocation url="view.cfm">
  </cfif>
</cfsilent>

Lastly, the view read values from the model and posts user click data to the controller:

view.cfm

<html>
  <body>
    <p>
      <table>
        <cfloop index="artName" list="#REQUEST.model.getArtNames()#">
          <tr>
            <td><cfoutput>#artName#</cfoutput></td>
          </tr>
        </cfloop>
      </table>

      <form action="controller.cfm" method="POST">
        <cfif REQUEST.model.showPrev()>
          <input type="submit" id="prev" name="prev" value="←">
        </cfif>
        <cfif REQUEST.model.showNext()>
          <input type="submit" id="next" name="next" value="→">
        </cfif>
      </form>
    </p>
  </body>
</html>

All that's left is to wire it all together in an Application.cfm

Application.cfm

<cfsilent>
  <cfapplication name="mvc" sessionmanagement="true">

  <cfparam
     name="APPLICATION.controller"
     default="#createObject("component", "Controller").init(
       createObject("component", "Model").init(
         createObject("component", "Data").init()))#">
</cfsilent>

Source Code

  • Coldfusion Eclispe/CFEclispe Project - MVC.zip