Help with complicated URL routing

Well right now my site has only a simple URL routing mechanism, in which the router/dispatcher simply interprets the url as domain.com/scriptpath/controller/action/param-1/param-2.../param-n. However, real life can get complicated at times, consider the AdminCP url in which you have a user controller that you can add, search, edit and delete users. The add and search actions are pretty easy to set up, but the edit and delete can be a bit tricky as you always need user id to be supplied to carry out these actions(thus always a parameter userid is required).

What if the userid is not supplied? Well, you may either search for it in the search action, or to be directed to a special manager controller for its outer/master user controller(called UserManager in this case). This user manager either displays an HTML table showing users available in the system with links to edit or delete them(its index action), or delegates to the edit or delete action if these actions and user id are specified. If no id is specified, the manager will always show the default user table. With this, the edit and delete actions actually become actions of UserManager and sub-actions of UserController.

Now you see how it gets complicated, the user controller now have two actions add, search, and a manager/sub-controller with two sub-actions edit, delete. The url patterns get a bit complex, as you can see below:

domain.com/scriptpath/user/add
domain.com/scriptpath/user/search
domain.com/scriptpath/user/manage
domain.com/scriptpath/user/manage/edit/1
domain.com/scriptpath/user/manage/delete/1

I want to design the manager as a class using the composition pattern, in which the controller has a manager property. However, the router cannot easily distinguish whether the word ‘manage’ represents an action or an inner controller, which creates routing problem. In simple scenario, I can make the manage a keyword for the router to search when it conducts regular expression matching. It works if the manager is the only special inner-controller, but can get quite messy when you have lots of such inner controllers.

So what would you do if you were facing this problem? I know HMVC may work, but not sure how to design it. How about other possible ways to get it done?

I’ve never seen it done the in the way you describe, with a subcontroller object inside of a controller object.

In this sort of situation, you could use a nested controller structure where your classes might be laid out something like this:


controllers
controllers/user.php
controllers/user/manage.php

and then have your router try matching the nested controllers first, and if they don’t exist, fallback to a main level controller. So if the URL was domain.com/scriptpath/user/manage/edit/1 it would look for controllers/user/manage.php, with an edit action, passing in the parameter 1. If that controller can’t be found, it would try controllers/user.php looking for a manage action, passing edit and 1 as params.

Although to me it seems that overcomplicates things a little. Rather than having a manage sub-controller, why not just make these actions in your user controller?


/user/add
/user/edit/1
/user/delete/1
/user/list

Here, user/list would be your action showing the list of all users (although I’d probably make it the default action for the user controller, so that the URL would simply be /users. For cases where the edit or delete actions are called without an id (or an invalid id) why not show a 404 error? Or if you prefer, just redirect back to the list action.

Developers sometimes talk about convention vs configuration. Your code sounds like it’s heavily built around convention: URLs will always have a specific format and will always map to controllers in the same way. Personally, I think that leaves your code inflexible (hence, your current problem). I think it’s better if your code relies on configuration. It lets you be as specific as you need, and you can also define generic configurations to create conventions only when it’s useful.

A configuration for all your URLs might look something like this:

user_add:
    path: /user/add
    params: { controller: UserController::addAction }

user_search:
    path: /user/search
    params: { controller: UserController::searchAction }

user_manage:
    path: /user/manage
    params: { controller: UserController::manageAction }

user_manage_edit:
    path: /user/manage/edit/{id}
    params: { controller: UserController::manageEditAction }

user_manage_delete:
    path: /user/manage/delete/{id}
    params: { controller: UserController::manageDeleteAction }

Or if you want your “manage” actions to be in a different controller, then that’s easy to do too. (They don’t need to be nested or anything.)

user_manage:
    path: /user/manage
    params: { controller: UserManagerController::indexAction }

user_manage_edit:
    path: /user/manage/edit/{id}
    params: { controller: UserManagerController::editAction }

user_manage_delete:
    path: /user/manage/delete/{id}
    params: { controller: UserManagerController::deleteAction }

And for URLs that do fit a generic template, like your first two, you could define a generic configuration:

generic:
    path: /{controller}/{action}