Laravel 5.0 - Route Annotations (removed)

Posted on October 09, 2014 | By Matt Stauffer


Warning: This post is over a year old. I don't always update old posts with new information, so some of this information may be out of date.

Note: Route Annotations were eventually removed from core, and separated to a package maintained by the Laravel Community. The package should function the same as the documentation here, other than that it requires binding a custom service provider. Feedback can go to the Github issues for the project or to @artisangoose in the Larachat slack.

Precursor: PHP Annotations

If you're not familiar with how (or why) annotations exist, I'd suggest checking out Rafael Dohms' talk PHP Annotations: They Exist!. In short, annotations are notes about your code that live in the DocBlock. But PHP has the ability to read and parse these notes, and so you can use them to give your code directions. Opinions on them are varied, but they've come to Laravel to stay.

Setting the Stage

One of the difficulties on Laravel sites--especially larger sites--is mentally mapping your routes to your controller methods.

Let's assume we're not using route Closures (because it's not the best practice and because we won't be able to take advantage of Laravel 5.0's route caching) and we're not using Implicit or Resource Controller routes, so all of our routes are going to be mapped explicitly to a controller method, somewhere.

So, we have something like this (note that Laravel 5.0 prefers $router->get instead of Route::get):

// routes.php
$router->get('awesome-sauce/{id}', [
    'as' => 'sauce',
    'uses' => 'AwesomeController@sauce'
]);
<?php namespace App\Http\Controllers;

class AwesomeController {

    public function sauce($id) {}

}

...but imagine having dozens or hundreds of those links. What if we were able to make a more direct linkage? Say, if we were able to determine the route in the controller? Bum bum bum...

Note: Laravel 5.0 uses POPO (Plain old PHP Objects) for controllers instead of children of the \Controller class. More on this later.

Introducing Route Annotations

OK, it's clear what I'm leading up to here. Check it out:

<?php namespace App\Http\Controllers;

class AwesomeController {

    /**
     * @Get("/awesome-sauce/{id}", as="sauce")
     */
    public function sauce($id) {}

}

... that's it.

One more step. Open up App/Providers/RouteServiceProvider.php, and add App\Http\Controllers\AwesomeController to the $scan array:

...
    protected $scan = [
        'App\Http\Controllers\HomeController',
        'App\Http\Controllers\Auth\AuthController',
        'App\Http\Controllers\Auth\PasswordController',
        'App\Http\Controllers\AwesomeController'
    ];

Run artisan route:scan and it'll automatically generate your route file at storage/framework/routes.scanned.php. It'll have a lot of default routes, but here is your new route down at the bottom:

<?php
...

$router->get('awesome-sauce/{id}', [
    'uses' => 'App\Http\Controllers\AwesomeController@sauce',
    'as' => 'sauce',
    'middleware' => [],
    'where' => [],
    'domain' => NULL,
]);

You're now determining your routes inline, using annotations, without touching routes.php. DONE.

Two places to annotate

Note that there are two places you can determine your route annotations: on the controller and on the method (or both). Check out the following controller (from the framework tests, but modified for demonstration):

<?php namespace App\Http\Controllers;

/**
 * @Resource("foobar/photos", only={"index", "update"}, names={"index": "index.name"})
 * @Controller(domain="{id}.account.com")
 * @Middleware("FooMiddleware")
 * @Middleware("BarMiddleware", except={"update"})
 * @Middleware("BoomMiddleware", only={"index"})
 * @Where({"id": "regex"})
 */
class BasicController {

    /**
     * @Middleware("BazMiddleware")
     * @return Response
     */
    public function index() {}

    /**
     * @return Response
     */
    public function update($id) {}

    /**
     * @Put("/more/{id}", after="log")
     * @Middleware("QuxMiddleware")
     */
    public function doMore($id) {}

}

Notice that some annotations are set on the controller and others on the methods. Also note the new emphasis on Middleware (and the absence of Before and After); I'll be writing a post soon about the new ways we'll be using Middleware.

Available options

Here are a few more options and use cases:

Basic route mapping

Use the verbs you're used to using in your routes file to annotate simple routes.

<?php namespace App\Http\Controllers;

class BasicController {

    /**
     * @Get("awesome")
     */
    public function awesome() {}

    /**
     * @Post("sauce/{id}")
     */
    public function sauce($id) {}

    /**
     * @Put("foo/{id}", as="foo")
     */
    public function foo($id) {}

}

Resource Controllers

Note that you can define a resource route with @Resource("route-name"); you can choose which routes are shown with only={"method1", "method2"}; and you can name routes with names={"method": "name-for-method"}.

<?php namespace App\Http\Controllers;

/**
 * @Resource("foobar/photos", only={"index", "update"}, names={"index": "index.name"})
 */
class FoobarPhotosController
{

    public function index()
    {
        // Index, named as index.name
    }

    public function update()
    {
        // Update, un-named
    }

}

Sub-Domain Routing

Just like in a normal route definition, annotations can control Sub-Domain Routing:

<?php namespace App\Http\Controllers;

/**
 * @Controller(domain="{user-name}.my-multi-tenant-site.com")
 */
class MyStuffController
{
    // Do stuff 
}

Middleware

Laravel 5.0 replaces Before and After Filters with Middleware; check back soon for a post introducing how the new implementation of Middleware works.

<?php namespace App\Http\Controllers;

/**
 * @Middleware("FooMiddleware")
 */
class MiddlewaredController
{

    /**
     * @Middleware("BarMiddleware")
     */
    public function barred() {}

}

Route constraints

You can apply route constraints, as well:

<?php namespace App\Http\Controllers;

class RegexedController {

    /**
     * @Where({"id": "regex"})
     */
    public function show($id) {}

}

Local Scanning

If your Environment is detected as local, Laravel will auto-scan your controllers on every page view. That way you don't have to artisan route:scan every time you make a change.

Routes.php in Laravel 5

Since I originally wrote this article, the default routes.php has been removed from the default project. In order to bring it back, edit App\Providers\RouteServiceProvider, and in the map() method, un-comment the line that says require app_path('Http/routes.php'). Now you can just create App/Http/routes.php and use it like you used to.

Miscellany

  • If you use routes.php and annotations, the annotations will be listed first in the scanned file.
  • The double quotes in annotations must remain double. No single quotes.

Conclusion

You can still use routes.php if it makes you more comfortable--or if you don't see the value behind this.

Once again, this new Laravel 5.0 feature both opens up new possibilities, and in my mind also helps us to write cleaner, better architected code. Since routes.php is simply a map between URL routes and controllers, route annotations moves the mapping into the controller and removes the need for a separate routes file entirely.


Comments? I'm @stauffermatt on Twitter


Tags: laravel  •  5.0  •  laravel 5