Monday 22 October 2012

Hello (Coldbox) world

G'day
OK, so having polled people as to what their thoughts were as to what framework I should investigate, here I am starting my Hello World "project" (that's bigging it up: I intend to get a page that says "Hello World" created, as a first step).  And I'm typing this blog article as I do so.  Very James Joyce and stream-of-consciousnesss indeed.  Except I'll use punctuation and what I write might be comprehensible.  And first year uni students will not be made to read this thing and write essays on it for years to come.  So not much like Joyce at all.



I have done a bit of background prep: I'd D/Led ColdBox and followed the "installation" docs, and had a coupla questions for the Coldbox mob, which I asked (& had answered) on their Google Group.  I got slightly further than that and then decided to bin that approach and start again.  I'm not going to slavishly follow through their docs as if I can't think for myself, I'm gonna follow my nose and see where it takes me.  Well: my nose, Google's nose, and targeting specific parts of the docs as needs must.

It might help if you quickly review y/day's article as an explanation as to why I am not following the "easy route" with how I'm gonna install & configure Coldbox.  I'm not just going bung it in my web root which is the default guidance on the install page, I'll be putting it in a different directory outside my web root, and accessing it via a mapping.  This makes things slightly more difficult, but only slightly.  Well: as near as I can tell anyhow.  I guess we're about to find out.

Baseline Apache / ColdFusion install

This bit is just FYI and where files are located.  It's just prep I did before diving into the Coldbox stuff.

So where am I at?  I have this in my file system:

D:\webapps\helloWorld\www <--- Apache doc root

D:\webapps\helloWorld <--- ColdFusion root
D:\webapps\helloWorld\com\daccf\helloworld <--- my app code will be in here

D:\webapps\helloWorld\org\coldbox\coldbox_3.5.2 <--- this will be my /coldbx mapping location
D:\webapps\helloWorld\org\coldbox\coldbox_3.5.2\system <--- ColdBox files

I have tested everything is serving up fine by creating a coupla files and browsing to them:

<!-- http://helloworld.local/helloWorld.html -->
<html>
    <head>
        <title>Hello World</title>
    </head>
    <body>
        <h1>Hello World</h1>
        <p>Hello World</p>
    </body>
</html>

(this outputs "Hello World", etc)

And the CF version (http://helloworld.local/helloWorld.cfm):

// Application.cfc
component extends="com.daccf.helloworld.Application" {

}

// /com/daccf/helloworld/Application.cfc
component {

    this.name = "HelloWorld";
    this.mappings    = {
        "/"    = getDirectoryFromPath(getCurrentTemplatePath())
    };

}

<!--- helloWorld.cfm --->
<cfinclude template="/com/daccf/helloworld/inc/helloWorld.cfm">

<!--- /com/daccf/helloworld/inc/helloWorld.cfm --->
<cfset message = "Hello World">
<cfoutput>
<html>
<head>
    <title>#message#</title>
</head>
<body>
    <h1>#message#</h1>
    <p>#message#</p>
</body>
</html>
</cfoutput>

This lot also outputs "Hello World", and demonstrates CF is serving stuff fine via Apache's webroot, plus can find the stuff outside the web root, and elsewhere in the CF root.

I'm happy with that baseline config, so I'll install Coldbox now.

ColdBox install and bootstrap

 I googled Coldbox and found their download page.  I just grabbed the stand-alone stuff, and homed it in D:\webapps\helloWorld\org\coldbox\system.  I did actually D/L the other bits 'n' pieces, in case I needed to search them to find out how to do something (I have already had to do this during my "false start" the other day).

Next I return to the installation docs, and scroll down past the "easy install" stuff, and go to the "Alternate Installation Methods". I need to follow this path because I'm using a CF mapping to access Coldbox, not just having it in my web root.  I'm also using an application-specific mapping, not one I've set in CFAdmin.  This is just personal preference: I prefer to do stuff with code rather than via the UI if poss.

So I am now reading the "Bootstrapper" section of the docs, and I'm adapting my Application.cfc to enable Coldbox.

I've based this code on the sample /coldbox_3.5.2/ApplicationTemplates/Advanced/Application_noinheritance.cfc file, some feedback from that forum thread I mentioned before, and some guesswork. I've just included the bare minimum requirements so far.

component {

    variables.thisDir    = getDirectoryFromPath(getCurrentTemplatePath());

    this.name                = "HelloWorld" & createUuid();    // TODO: get rid of UUID
    this.sessionManagement    = true;    // It seems Coldbox requires this to be on, even thoough I'm not going to be using sessions :-(
    this.mappings            = {
        "/"            = variables.thisDir,
        "/coldbox"    = expandPath("/org/coldbox/coldbox_3.5.2")
    };
    
    // reqd COLDBOX settings
    COLDBOX_APP_ROOT_PATH    = variables.thisDir;    // COLDBOX STATIC PROPERTY, DO NOT CHANGE UNLESS THIS IS NOT THE ROOT OF YOUR COLDBOX APP
    COLDBOX_APP_MAPPING        = "";    // The web server mapping to this application. Used for remote purposes or static purposes
    COLDBOX_CONFIG_FILE        = "";    //COLDBOX PROPERTIES
    COLDBOX_APP_KEY            = "";    //COLDBOX APPLICATION KEY OVERRIDE
    
    public boolean function onApplicationStart(){
        //Load ColdBox
        application.cbBootstrap = createObject("coldbox.system.Coldbox").init(COLDBOX_CONFIG_FILE,COLDBOX_APP_ROOT_PATH,COLDBOX_APP_KEY,COLDBOX_APP_MAPPING);
        application.cbBootstrap.loadColdbox();
        return true;
    }

    public boolean function onRequestStart(required string targetPage){
        application.cbBootstrap.onRequestStart(arguments.targetPage);
        return true;
    }

}

I was left none-the-wiser by reading the docs as to what I was supposed to do with those COLDBOX_* variables, but Rob helped me on the forums and clarified a few of them.  I discovered by trial and error that I needed to create a config file:

Config file not located in conventions: config.Coldbox


Having created the file it was looking for it still errors, but this is not surprising as the file is just an empty component:

The method configure was not found in component D:/webapps/helloWorld/com/daccf/helloworld/config/Coldbox.cfc.

Ensure that the method is defined, and that it is spelled correctly.

Putting a configure method in there yields this:

ColdBox settings empty, cannot continue


I'm not discouraged by this because I'm just trying random things, and at least Coldbox is seeing my changes, so I count this as progress. But I'd better stop guessing what to do, and RTFM.

Config

I'm back at the home page of the docs, and there's an option for "Configuration" in the contents, so I'm looking at that... well the link to "The ColdBox Configuration CFC" link on that page actually (I told you this would be a bit stream of consciousness... I truly am trying to work this stuff out as I type).  This does indeed explain this config CFC I need to create.

Following through the docs here, I've created a skeleton Coldbox.cfc with just a configure() method in it, which does get me past that error above (and onto the next one).

component{
    
    public void function configure(){
        variables.coldbox = {
            appName = application.applicationName    // I'm not over-the-moon about this break of encapsulation, but equally I want to be as DRY as possible. I dunno why I need to specify this (again)
        };
    
    }
    
}

The docs say this:
 There you go, that is the simplest that you can go with ColdBox, just the name of your application.

To me that's suggesting if all I have is this, I should have a running app.  Which, in the context of what I've configured so far, should just give me a blank screen, just with no error.  Well I still get an error:

No handlers were found in: D:/webapps/helloWorld/com/daccf/helloworld/handlers or in . So I have no clue how you are going to run this application.


So it seems I need more than just that.  That's no prob, but I think the docs are misleading here.  Do you know what also occurs to me?  That error message reads a bit amateurishly.  Both the missing value in "or in  ." (or in what?), and the "So I have no clue..." bit. but that's trivial, and more an observation than a gripe.

I read some more of the config does to see if there was an explanation of this.  There's a bunch of interesting sounding stuff in there, and instructions how to specify where to look for handlers, but this page doesn't cover what one is, so I'm moving back to the docs home page.

I am guessing there's some "convention over configuration" going on, and I just need to stick a file of some description into that handlers directory, and that might give Coldbox a clue as to how to run my application.

Handlers / Controllers

My first guess as to how to make that error go away was to create a CFC "Main.cfc" and have a method in it "index()".  This is because the default event (as per the config docs sample config CFC) is apparently main.index.  I created that file and that method:

component {

    public function index(){
        writeDump(request);
        abort;
        
    }

}


And - lo and behold - I got my dump coming out on the screen.  This is definitely progress.

Digression:
Actually there was about 1.5h of yelling and swearing between creating that CFC and getting the dump on the screen, but I didn't come up with anything useful to say about it, unless your swear-word vocab needs increasing.  Eventually I worked out whatI was doing wrong.  I explain at the bottom.
 
However if I don't abort at that point I get another error:


Application Execution Exception

Error Type: MissingInclude : [N/A]
Error Messages: Could not find the included template //views/main/index.cfm.

Fair enough.  Another case of files being missing because I haven't actually created them.  but I don't want to skip straight on to a view... I need to tell it to look for a model file first (this will set my "Hello World" message).  I could hazard a guess that adding a directory & file "models/main.cfc" is going to be part of the equation here (actually: no), but I don't know what needs to go into the model method and how to pass the variable to the view, so it's time to stop guessing and go back to the docs.

Incidentally, I hastily added in this:

<!---/com/daccf/helloworld/views/main/index.cfm--->
Hello world

And, indeed, the error went away and I got the "Hello World" message on the screen.  Win!  But I still want a model to set that message.

Model

I went back to the docs home page and located the "Models" page and eyeballed that.  That wasn't really what I was after: it was all the ins and outs of domain modelling and how Wirebox works and stuff like that.  At this stage I don't need to know anything like that, I just need to know how to call a method to get my message.  This is just a basic "Hello World" app, remember?

So I decided on a different approach: there must be something in the controller docs that at least show a model being called... yes: way way way down is a section "Model Integration", which lead me in the right direction, and my controller ended up looking like this:

// /com/daccf/helloworld/handlers/main.cfc
component {

    public string function index(event,rc,prc){
       prc.message = getModel("models.Message").get();
    }

}

And Message.cfc was like this:

// /com/daccf/helloworld/models/Message.cfc
component {

    public string function get(){
        return "Hello World";
    }
    
}

That's all pretty logical.  I still dunno if that worked, but it didn't error.  I need to get that message out of prc and onto the screen.

View

I went back to the view file I created in response to the biege error above, and changed it thus:

<!---/com/daccf/helloworld/views/main/index.cfm--->
<cfoutput>#prc.message#</cfoutput>

And... [deep breath & fingers crossed...] ... when I refreshed my page, I had a "Hello World" message. Just in case it wasn't taking that from the model (I have so many bloody files saying "hello world" or variations thereof littering the place, I wasn't entirely sure!) I changed get() to return "Hello World @ #timeFormat(now())#", and indeed I was indeed the message from the model.

Done

So that's my Coldbox Hello World MVC exercise complete. It wasn't entirely painless.  I got undone a bit between the config and the controllers bit because I had a mapping wrong, and they Coldbox errors were not being helpful.  I was 75% of the way through a posting to the Google Group when I twigged what the story was, and fixed it.  I'm so pleased that for once the watershed moment came before pressing send on the message to the help forum, not 30sec afterwards.  Basically in my Application.cfc I had this:

this.mappings            = {
    "/com/daccf/helloworld"    = variables.thisDir,
    "/coldbox"                = expandPath("/org/coldbox/coldbox_3.5.2")
};

When it needed to be this:

this.mappings  = {
    "/"        = variables.thisDir,
    "/coldbox" = expandPath("/org/coldbox/coldbox_3.5.2")
};

In hindsight this makes sense, but the thing was mostly working, and was just being "enigmatic" in the ways it wasn't working.  It was seeing the /controllers directory (and indeed was complaining if I didn't have it), but it refused to look for files in there, even if I used the handlersExternalLocation setting in my config. If I used that it was ignoring "/controllers", but if I called the directory anything else, eg /controllersx (and set the config value to match) then it would work. But I knew I should not need to set an "external location" for the handlers, as I wasn't using one, so I had something wrong.  Once I set the mapping correctly and got stuff working, the odd behaviour I was seeing seems logical (kinda... some of it, anyhow) once I look back, but at the time when I was trying to nut out what the story was, it all seemed a bit weird.

All in all, it was pretty easy (and easier than it might seem from the length of this article).

Docs

The docs were fine, and I was able to find out everything I needed to know by reading the relevant bits of them, scanning over bits I don't need to know yet.  I did not need to google anythign else, nor ask for help beyond the first post to the forums I made the other day.  I think this is pretty good, actually.

More...

I'm not done with this exercise, because that was a very superficial example (superficial but useful, IMO).  I'm gonna augment it slightly as there's a few things I want to check out before I dive-in too far with actual work.  I'm also gonna distil out the code from the narrative and post an abridged version of what I did, which might be a useful parallel resource.  Or not.  Who knows?

Lastly, if anyone here who knows about Coldbox wants to set me straight on anything I've done here that's dodgy, please tell me.  I don't want dodgy info on the 'net that people might think is correct.

Time to get a beer out of the Coldbox (hohoho, I'm sure I'm the first person ever to say that).  Pity it needs to be a zero-alcohol one :-|

Cheers.

--
Adam