How to build an admin section in CFWheels
November 23, 2010 · Chris Peters
CFWheels routes allow for you to split your admin section up into as many controllers as you need.
Every now and then, someone asks how to build an admin section into their CFWheels application. If they want for it to be in an admin folder, does that mean that you can only have an admin
controller that stores everything needed?
Fortunately, one approach allows for you to split your admin section up into as many controllers as you need using CFWheels routes. Let’s discuss how.
Adding routes for the admin section
CFWheels is pretty darn flexible in allowing you to set up your own URL structures. In fact, CFWheels’s URL routing feature is the main reason why I picked up the framework years ago.
That said, one approach to doing an admin section is to set up some route patterns to set up an admin
folder that doesn’t necessarily exist as a controller (not directly, at least). Let’s say that behind an admin login, we’re going to have a users
section for managing administrators and an events
section that allows administrators to add and edit events on a calendar.
We would set up some routes like this in config/routes.cfm:
<cfscript> | |
// Admin users | |
addRoute(name="adminUsers", pattern="admin/users/[action]/[key]", controller="adminUsers", action="index"); | |
addRoute(name="adminUsers", pattern="admin/users/[action]", controller="adminUsers", action="index"); | |
addRoute(name="adminUsers", pattern="admin/users", controller="adminUsers", action="index"); | |
// Admin events | |
addRoute(name="adminEvents", pattern="admin/events/[action]/[key]", controller="adminEvents", action="index"); | |
addRoute(name="adminEvents", pattern="admin/events/[action]", controller="adminEvents", action="index"); | |
addRoute(name="adminEvents", pattern="admin/events", controller="adminEvents", action="index"); | |
// Admin home | |
addRoute(name="admin", pattern="admin", controller="admin", action="index"); | |
// Site home page | |
addRoute(name="home", pattern="", controller="main", action="home"); | |
</cfscript> |
When defining routes, it’s important to list the most specific route first. The routes defined under adminUsers
and adminEvents
are listed in that order so that CFWheels will match the most specific first and keep looking through the list if the client is requesting something less specific. The home
route is listed last because it is least specific.
We can also name related routes with the same name so we can reference it similarly in calls to linkTo()
, startFormTag()
, and so on. CFWheels will match the route you’re looking for based on which parameters are passed in.
So we can pass different arguments to the same route name like this and get different types of URLs for each:
<cfoutput> | |
<!--- Links to /admin/events ---> | |
#linkTo(text="List Events", route="adminEvents")# | |
<!--- Links to /admin/events/new ---> | |
#linkTo(text="Add an Event", route="adminEvents", action="new")# | |
<!--- Links to /admin/events/view/78 (for example) ---> | |
#linkTo(text="Next Event", route="adminEvents", action="view", key=nextEventId)# | |
<!--- Bonus: Always link to the home page like this! | |
Never link to controller="main", action="home" because you'll get duplicate | |
content at `/` and at `/main/home` ---> | |
#linkTo(text="Home", route="home")# | |
</cfoutput> |
Setting up admin security in the controller layer
Because controllers in CFWheels are defined by CFCs, you can do some nice stuff with inheritance to allow certain behaviors and settings for only a subset of your controllers. For instance, our admin section needs to lock out users behind a login form.
We could put all of the admin authentication filters in the base controller at controllers/Controller.cfc because all controllers extend it, but there is actually a cleaner way to define it just for the controllers that we need it in.
Let’s create a controller at controllers/AdminController.cfc and implement something like this:
component extends="Controller" { | |
//----------------------------------------------------- | |
// Public | |
function init() { | |
filters(through="authenticate"); | |
} | |
//----------------------------------------------------- | |
// Filters | |
private function authenticate() { | |
if(!StructKeyExists(session, "user") { | |
redirectTo(controller="sessions", action="new"); | |
} | |
else { | |
loggedInUser = model("adminUser").findByKey(session.user.id); | |
} | |
} | |
} |
Now through inheritance, we can have the adminUsers
and adminEvents
controllers extend AdminController
instead of just Controller
(which will also included anyway because AdminController
extends Controller
).
Our adminUsers
controller at controllers/AdminUsers.cfc would then look something like this:
component extends="AdminController" { | |
//----------------------------------------------------- | |
// Public | |
function init() { | |
// Be sure to call the parent constructor if you override `init()` | |
super.init(); | |
} | |
function index() { | |
adminUsers = model("adminUser").findAll(order="lastName,firstName"); | |
} | |
} |
Because the adminUsers
controller extends AdminController
, no one will be able to access the index
action until they are logged in. This is enforced by authenticate
filter defined in the parent AdminController
.
Caveat with overriding init()
With this approach, you need to be careful when overriding the constructor in your child controller. If you override init()
, you need to remember to call super.init()
within in order to keep the authentication logic running.
Some may argue that you should only define the authenticate()
method in AdminController
and manually define the filters in each child controller because of this tendency to forget calling super.init()
. I’m fine with that or what I have illustrated above, but remember that it’s just as easy to forget to include the filter either way. So in other words, pick your poison and be sure to pay attention to the details.
Some final notes
I hope that this helps you solve a fairly common design problem in your MVC application. There are a lot of ways that this pattern can be applied when you have a group of controllers that all require some related functionality. I highly recommend trying to name controllers similarly in these situations so that it’s easy to glance at your controllers folder and understand what’s going on.
I might add that this is only one approach too—and it’s the simplest approach. Another approach is to develop your admin section as a completely separate application in its own subfolder or on a different subdomain. (Remember that full CFWheels URL rewriting will still work if your separate admin application is only one subfolder deep.)
The approach of separating your admin into a separate application can get complex pretty fast, but it is something worth pursuing if you find that your admin section tends to have different business logic than your public website.