Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What's the reason behind using routes file in modern frameworks?

In modern web frameworks (Laravel, Symfony, Silex, to name a few), there seems to be a pattern of using a routes.php file or similar to attach URIs to controllers. Laravel makes it a bit easier with an option to use PHP annotations.

But to me all this feels like a bit of a code repetition, and when you are creating/modifying controller logic, you have to keep routes file always at hand. Interestingly, there's a simpler way I saw in several old frameworks, and I used to use this in my old projects as well:


Controller. All classes in src/controllers folder (old way) or all classes in YourApp\Controllers namespace are being automatically mapped to the first part of the URL by adding "Controller" to it. Example: /auth gets mapped to AuthController, /product/... — to ProductController, and / — to default IndexController.

Action. Action is the second part of the URL, and it gets mapped to the method name. So, /auth/login will call AuthController::loginAction() method. If no second part provided, we try indexAction(). Don't want people to access some internal method? Don't make it public.

Parameters. Next parts of the URL are being mapped to the arguments of the method; if there are Application and/or Request type hintings in the argument list, they are skipped so they can be properly injected; we can access GET/POST variables as usual through Request.

Here's the full example, using all these features together:

URL: https://example.com/shop/category/computers?country=US&sort=brand

namespace MyApp\Controllers;

class ShopController extends BaseController {
    public function categoryAction(Application $app, Request $req, $category, $subcategory = null) {
        echo $category; // computers
        echo $subcategory; // null, it's optional here
        echo $req->get('country'); // US
        echo $req->get('sort'); // brand
    }
}

I'm sure it seems to lack some familiar features at first, but all features that I can think of could be easily added if needed — using attachable providers, connecting middlewares, branching controllers to subcontrollers, specifying HTTP methods, and even performing a bit of a pre-validation on the arguments. It's very flexible.


This approach would really speed up the routing creation and management. So besides having all the routes in one file (which is also not always true, considering various providers, using ->mount() in Silex or bundles in Symfony), what are the reasons modern frameworks seem to prefer this way of doing MVC routing over the simpler way I've described? What am I missing?

like image 633
Serge Uvarov Avatar asked Jul 04 '16 05:07

Serge Uvarov


3 Answers

I'll speak here from Symfony/Silex perspective:

  1. Decoupling. routes.php provide separation of URL mapping and controllers. Do you need to add or change URL? You go straight to routes.php. Very handy if you want to change a lot of them.
  2. Flexibility. Generally, routes.php is a bit more flexible approach. SEO might go crazy and might require you to have route like /shop_{shopName}/{categoryName}/someStaticKeyword/{anotherVar}. With routes.php you can easily map this to your code, but this might become a problem if your routes are directly mapped to the code. Even more, you can have this only controller, you don't need to write controllers for each slash part. You can even have different controllers handling the same URL with different variable parts, for example /blog/[\d]+ or /blog/[a-z-]+ being handled by different controllers (one might generate redirect to another). You might never need to do something like this, but this is just a demonstration of flexibility of this approach - with it anything is possible.
  3. Validation. Another thing to consider is that routes.php provide simple means of validation via ->assert method. That is, routes.php does not only map URLs to controller methods, but also ensure that these URLs match specific requirements, and you don't have to do this in code (which in most scenarios would take a bit more code to write). Also, you can create default asserts for some variables, for instance, you may ensure that {page} or {userId} are always \d+, single line in routes.php that would take care of all usages of {page} or {userId}.
  4. URL Generation. One more feature of routes.php is url generation. You can assign any route any name (via ->bind() method) and then generate URLs based on that name, and providing variables for parts of URL that change. And once we have this system and use URL generator throughout our code we can change URLs as much as we want, but we won't have any need to edit anything but routes.php. And once again - these are flexible names, that you won't have to change everywhere throughout your project once URL has changed and you are not limited choosing the name. It might be much shorter than the URL, or a bit more verbose.
  5. Maintainability. Say, you might want to change some urls (as in example above - from /blog/[\d+] to /blog/[a-z-]+. Also, you might want to keep both of them for some time, and make old one redirect to the new one). With routes.php you simply add a new line and add a todo memo to remove it in some time if you want to remove it later.

Sure, all of this might be achieved with any aproach. But would that be this simple, flexible, transparent and compact as this approach?

like image 124
TiGR Avatar answered Oct 03 '22 05:10

TiGR


Just as note, the standard edition of Symfony is shipped with SensioFrameworkExtraBundle, that allows to use annotations instead of file-based declaration:

/**
 * @Route("/")
 */
public function indexAction()
{
    // ...
}

In the same manner, you can set the prefix for the whole controller file:

/**
 * @Route("/blog")
 */
class PostController extends Controller
{
    /**
     * @Route("/{id}")
     */
    public function showAction($id)
    {
    }
}

Still, we have to declare it. I think the main reason is that routing requirements are heavily influenced by SEO optimizations. For anything that faces the public you want some keyword-rich URL. The "public" logic organization may also be different, you may have only one controller to deliver all your static pages, still you want those to be /contact, /about...

In some domains, routes can be inferred with conventions. If you create a REST API using FosRestBundle, it will auto-generate routes based on your controllers/actions names on the ressource approach. In general I think the FosRestBundle got a lot of things in their approach, you can easily parse and validate query parameters at the same time:

class FooController extends Controller
{
    /**
     * This action route will be /foo/articles, GET method only
     *
     * @QueryParam(name="val", default="75 %%")
     */
    public function getArticlesAction(ParamFetcher $paramFetcher)
    {
        ...
    }
like image 36
romaricdrigon Avatar answered Oct 03 '22 05:10

romaricdrigon


I always prefer having the routes in a config file rather than using annotations because, firstly, I feel it's more maintainable and easier to have an oversight. It's easier to ensure that you don't have any conflicting routes when you can see them all together.

Additionally, it's theoretically faster. Annotations require reflection, where the application needs to scan the file system and parse each controller to collect the set of routes.

like image 26
Scott Finlay Avatar answered Oct 03 '22 04:10

Scott Finlay