Context Standard Plugin: Simple MVC
The SimpleMVCPlugin is an HTTPPlugin intended to supply a very simple convention-based model-view-controller framework to any HTTPContext. In fact, the CFCommons.org website is powered by this very lightweight and intuitive framework. Going from zero-to-something is a breeze with this plugin which we'll demonstrate here.
Initializing the Plugin
Controllers
Discovery
The SimpleMVCPlugin discovers controllers in one of two ways - neither of which have anything to do with the location of the CFC within the context. First, the plugin checks to see if any CFC's have the word "controller" (case notwithstanding) in their name, if so the plugin will register that CFC as a controller. Second, you explicitly flag a CFC as a controller with the @cfcommons:mvc:controller annotation. This is a component level annotation that does not require any value in a comment-script annotation. If you're flagging a tag-based CFC, then simply set it's value to true;
Script Based Annotated Controller
Tag Based Annotated Controller
In each of the above examples, both CFC's will be recognized by the plugin as controllers. However, they won't do you much good at this point as neither have defined any controller methods that are able to service requests.
Servicing requests starts with a URL mapping. This also starts off as convention based and is very easy to do. To start, we'll create an index() method on our controller component;
BookController.cfc
Implicit URL Mappings
Again, we're not quite ready to return a meaningful view to the client just yet - but what we did do is implicitly create a URL mapping that the plugin understands. Our BookController.cfc now exposes the public method index(). This means that the parent HTTPContext can map two different request URL's to that exact method;
/index.cfm/book /index.cfm/book/index
Controllers method return types are ignored, so specifying a return type is not necessary - however specifying "void" is also acceptable, albeit a waste of keystrokes.
You can add any number of public methods to a controller and each will bring with it an implicit URL mapping as described above.
The mapping convention being applied here is this; /index.cfm/{controllerName}/{methodName}. When controller CFC's have "controller" in their name, the "controller" portion of their name is ommitted for mapping purposes.
Methods named "index" are treated slightly different, they have two mappings created for them, not only does the /index.cfm/{controllerName}/{methodName} convention apply, but also ommitting the {methodName} will map the request to that method.
Methods other than "index" also have the /index.cfm/{controllerName}/{methodName} URL mapping convention applied - but also have a second that looks like this; /index.cfm/{controllerName}/{methodName}/{id}. For instance, let's add a show() method to our BookController;
The addition of the show() method added two new implicit URL mappings that will bind to that method;
/index.cfm/book/show
/index.cfm/book/show/{somevalue}
Substitute {somevalue} with any alpha-numeric value and the URL will not only map to that method, but the variable content succeeding the /show/ portion of the URL will be bound to a method argument named id. We'll get into request data-binding in a moment.
Overriding URL Mapping Conventions with Annotations
If the above mapping conventions won't work for you, you can easily provide your own with the @cfcommons:mvc:url annotation. Not only does this allow you to fully customize the request URL that will map to your method, but it provides you with some advanced mapping and URL data-binding configuration options as well;
In the above example, we've completely changed the URL mapping from a convention-based assumption, to an explicit URL template with a relatively strict-requirement. The values succeeding the colon ":" in the URL variable indicate a regular expression - any valid regular expression can be provided in an annotated URL mapping. In this case, we're looking for an all-alpha value that is exactly 8 characters long. Any URL that does not meet that requirement will not be mapped to that method. A bit ridiculous of course, but just an example to demonstrate how granular you can get with overriden URL mappings. IF a URL is provided that meets the URL templates requirements, then the value in the {title} position of the URL will be bound to the "title" argument of the mapped method for that request.
MVCURLMappingNotFoundException
When the Plugin cannot match a request to a controller method, an exception of type MVCURLMappingNotFoundException will be thrown. How you handle that exception is entirely up to you - but this plugin also allows you to easily register exception handlers with the parent context, so adding one to gracefully handle this specific exception is up to you.
Controllers are Transient
They are not cached nor are they treated as singletons. An instance of your controller is created for every request. While this doesn't guarantee thread-safety for you it goes a long way to minimizing the risk of memory-leaks and possible thread synchronization concerns. Mix the NeodymiumPlugin into your HTTPContext and you've got a very easy way to have your transient controller instance manage composite relationships with other singleton objects.
Request Data Binding
I've given you a small glimpse of how portions of the request URL can be flagged as variables and bound to your controller method in the form of URL variables. When you override the implicit URL mapping to a controller method, you can declare as many URL variables as you like - each one will be bound to the controller method with an argument of the same name. This is one way request information is made available to your controller. Both form-post and query-string parameters are bound to controller methods in the same way. We'll modify our BookController that we created previously and remove the overriden URL annotation. We're back to using the implicit mapping that the plugin created. You'll notice that the show() method is still requiring an argument named "title";
We can easily bind either a form POST parameter OR a URL query-string parameter to that required argument by simply providing one of the same name in the request. For a query-string, it would look something like this;
/index.cfm/book/show?title=TheRoad
Of course, the aforementioned URL variable approach is much more desirable than this one, this was simply to demonstrate the various other means of binding request information to a controller method.
Lastly, there of course is nothing preventing you from simply referencing the native form OR url structures directly within the controller method itself - it's entirely up to you and the framework does not restrict you to implementing one specific approach.
Views
There are three specific and distinct ways of having your controller methods render an associated view. The first way is to simply have the plugin apply it's intelligent default in which it will attempt to look in your view root for a .cfm template with the same name as your controller method, in a directory with the same name as your controller CFC;
/{view-root}/{controllername}/{methodname}.cfm
View variable
The second and most flexible way of handling view rendering, is to simply set a variable named view in your controller method;
Because this is an actual variable initialized at runtime, it can change however you'd like it to depending upon the logic implemented in your controller;
Annotating the View
Lastly, you can simply annotate the view with the cfcommons:mvc:view annotation;
View Prefixes
You have two opportunities to specify view prefix information. This information will be prepended to any method-level view information. The first place is when the plugin itself is initialized with a plugin constructor argument named viewRoot. This of course can be done programmatically as such;
OR when the Plugin is configured via it's own configuration file as was discussed at the beginning of this document.
Regardless as to how the viewRoot is established, the value that it contains will be prefixed to any view path that the Plugin attempts to locate and render.
The second and more specific location is at the controller CFC level with the @cfcommons:mvc:view annotation - yes, this is exactly the same annotation that you can use at the method level, however this one will concatenate any viewRoot value and it's own value into a prefix that again, the Plugin will use when determining where to locate a view template.