Laravel 5.0 - Route Annotations (removed)

(This is part of a series of posts on New Features in Laravel 5.0.)

  1. Laravel 5.0 - Form Requests
  2. Laravel 5.0 - ValidatesWhenResolved
  3. Laravel 5.0 - Directory structure and namespace
  4. Laravel 5.0 - Route Caching
  5. Laravel 5.0 - Cloud File Drivers
  6. Laravel 5.0 - Method Injection
  7. Laravel 5.0 - Route Annotations (removed)
  8. Laravel 5.0 - Event Annotations (removed)
  9. Laravel 5.0 - Middleware (Filter-style)
  10. Laravel 5.0 - Environment Detection & Environment Variables
  11. Laravel 5.0 - Event Scheduling
  12. Laravel 5.0 - Commands & Handlers
  13. Upgrading from Laravel 4 to Laravel 5
  14. Bringing Whoops Back to Laravel 5
  15. Laravel 5.0 - Events & Handlers
  16. Laravel 5.0 - Generating Missing Events
  17. Laravel 5.0 - Custom Error Pages
  18. Laravel 5.0 - Eloquent Attribute Casting

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

Matt Stauffer headshot

Hi, I'm Matt Stauffer.

I'm partner & technical director at Tighten Co.

You can find me on Twitter at @stauffermatt


Like what you're reading?

I wrote an entire 450+ page book for O'Reilly: Laravel: Up and Running.

You can order the eBook or print book today.