I seldom think of MVC in terms of applications which don’t really have views. Turns out Laravel 4 is stocked with features which make REST API’s a breeze to create and maintain.
One of the goals, of this tutorial, is to speed up our prototyping. To that end; we’ll be installing Jeffrey Way’s Laravel 4 Generators. This can be done by amending the composer.json file to including the following dependency:
1 "way/generators" : "dev-master" We’ll also need to add the GeneratorsServiceProvider to our app config:
1 "Way\Generators\GeneratorsServiceProvider" You should now see the generate methods when you run artisan.
Artisan has a few tasks which are helpful in setting up resourceful API endpoints. New controllers can be created with the command:
1 php artisan controller:make EventController New migrations can also be created with the command:
1 php artisan migrate:make CreateEventTable These are neat shortcuts but they’re a little limited considering our workflow. What would make us even more efficient is if we had a way to also generate models and seeders. Enter Jeffrey Way’s Laravel 4 Generators…
With the generate methods installed; we can now generate controllers, migrations, seeders and models.
Let’s begin with the migrations:
1 php artisan generate:migration create_event_table This simple command will generate the following migration code:
1 <?php 2 3 use Illuminate\Database\Migrations\Migration; 4 use Illuminate\Database\Schema\Blueprint; 5 6 class CreateEventTable extends Migration { 7 8 /** 9 * Run the migrations. 10 * 11 * @return void 12 */ 13 public function up() 14 { 15 Schema::create('event', function(Blueprint $table) { 16 $table->increments('id'); 17 18 $table->timestamps(); 19 }); 20 } 21 22 /** 23 * Reverse the migrations. 24 * 25 * @return void 26 */ 27 public function down() 28 { 29 Schema::drop('event'); 30 } 31 } We’ve seen these kinds of migrations before, so there’s not much to say about this one. The generators allow us to take it a step further by providing field names and types:
1 php artisan generate:migration --fields="name:string, description:text, started_a\ 2 t:timestamp, ended_at:timestamp" create_event_table This command alters the up() method previously generated:
1 public function up() 2 { 3 Schema::create('event', function(Blueprint $table) { 4 $table->increments('id'); 5 $table->string('name'); 6 $table->text('description'); 7 $table->timestamp('started_at'); 8 $table->timestamp('ended_at'); 9 $table->timestamps(); 10 }); 11 } Similarly, we can create tables for sponsors and event categories:
1 php artisan generate:migration --fields="name:string, description:text" create_ca\ 2 tegory_table 3 4 php artisan generate:migration --fields="name:string, url:string, description:tex\ 5 t" create_sponsor_table 6 These commands generate the following migrations: 1 <?php 2 3 use Illuminate\Database\Migrations\Migration; 4 use Illuminate\Database\Schema\Blueprint; 5 6 class CreateCategoryTable extends Migration { 7 8 /** 9 * Run the migrations. 10 * 11 * @return void 12 */ 13 public function up() 14 { 15 Schema::create('category', function(Blueprint $table) { 16 $table->increments('id'); 17 $table->string('name'); 18 $table->text('description'); 19 $table->timestamps(); 20 }); 21 } 22 23 /** 24 * Reverse the migrations. 25 * 26 * @return void 27 */ 28 public function down() 29 { 30 Schema::drop('category'); 31 } 32 } 1 <?php 2 3 use Illuminate\Database\Migrations\Migration; 4 use Illuminate\Database\Schema\Blueprint; 5 6 class CreateSponsorTable extends Migration { 7 8 /** 9 * Run the migrations. 10 * 11 * @return void 12 */ 13 public function up() 14 { 15 Schema::create('sponsor', function(Blueprint $table) { 16 $table->increments('id'); 17 $table->string('name'); 18 $table->string('url'); 19 $table->text('description'); 20 $table->timestamps(); 21 }); 22 } 23 24 /** 25 * Reverse the migrations. 26 * 27 * @return void 28 */ 29 public function down() 30 { 31 Schema::drop('sponsor'); 32 } 33 } The last couple of migrations we need to create are for pivot tables to connect sponsors and categories to events. Pivot tables are common in HABTM (Has And Belongs To Many) relationships, between database entities.
The command for these is just as easy:
1 php artisan generate:pivot event category 2 3 php artisan generate:pivot event sponsor 4 These commands generate the following migrations: 1 <?php 2 3 use Illuminate\Database\Migrations\Migration; 4 use Illuminate\Database\Schema\Blueprint; 5 6 class PivotCategoryEventTable extends Migration { 7 8 /** 9 * Run the migrations. 10 * 11 * @return void 12 */ 13 public function up() 14 { 15 Schema::create('category_event', function(Blueprint $table) { 16 $table->increments('id'); 17 $table->integer('category_id')->unsigned()->index(); 18 $table->integer('event_id')->unsigned()->index(); 19 $table->foreign('category_id')->references('id')->on('category')->onD\ 20 elete('cascade'); 21 $table->foreign('event_id')->references('id')->on('event')->onDelete(\ 22 'cascade'); 23 }); 24 } 25 26 /** 27 * Reverse the migrations. 28 * 29 * @return void 30 */ 31 public function down() 32 { 33 Schema::drop('category_event'); 34 } 35 } 1 <?php 2 3 use Illuminate\Database\Migrations\Migration; 4 use Illuminate\Database\Schema\Blueprint; 5 6 class PivotEventSponsorTable extends Migration { 7 8 /** 9 * Run the migrations. 10 * 11 * @return void 12 */ 13 public function up() 14 { 15 Schema::create('event_sponsor', function(Blueprint $table) { 16 $table->increments('id'); 17 $table->integer('event_id')->unsigned()->index(); 18 $table->integer('sponsor_id')->unsigned()->index(); 19 $table->foreign('event_id')->references('id')->on('event')->onDelete(\ 20 'cascade'); 21 $table->foreign('sponsor_id')->references('id')->on('sponsor')->onDel\ 22 ete('cascade'); 23 }); 24 } 25 26 /** 27 * Reverse the migrations. 28 * 29 * @return void 30 */ 31 public function down() 32 { 33 Schema::drop('event_sponsor'); 34 } 35 } Apart from the integer fields (which we’ve seen before); these pivot tables also have foreign keys, with constraints. These are common features of relational databases, as they help to maintain referential integrity among database entities.
With all these migration files created; we have only to migrate them to the database with:
1 php artisan migrate (This assumes you have already configured the database connection details, in the configuration files.)
Seeders are next on our list. These will provide us with starting data; so our API responses don’t look so empty.
1 php artisan generate:seed Category 2 php artisan generate:seed Sponsor These commands will generate stub seeders, and add them to the DatabaseSeeder class. I have gone ahead and customised them to include some data (and better formatting):
1 <?php 2 3 class CategoryTableSeeder 4 extends Seeder 5 { 6 public function run() 7 { 8 DB::table("category")->truncate(); 9 10 $categories = [ 11 [ 12 "name" => "Concert", 13 "description" => "Music for the masses.", 14 "created_at" => date("Y-m-d H:i:s"), 15 "updated_at" => date("Y-m-d H:i:s") 16 ], 17 [ 18 "name" => "Competition", 19 "description" => "Prizes galore.", 20 "created_at" => date("Y-m-d H:i:s"), 21 "updated_at" => date("Y-m-d H:i:s") 22 ], 23 [ 24 "name" => "General", 25 "description" => "Things of interest.", 26 "created_at" => date("Y-m-d H:i:s"), 27 "updated_at" => date("Y-m-d H:i:s") 28 ] 29 ]; 30 31 DB::table("category")->insert($categories); 32 } 33 } 1 <?php 2 3 class SponsorTableSeeder 4 extends Seeder 5 { 6 public function run() 7 { 8 DB::table("sponsor")->truncate(); 9 10 $sponsors = [ 11 [ 12 "name" => "ACME", 13 "description" => "Makers of quality dynomite.", 14 "url" => "http://www.kersplode.com", 15 "created_at" => date("Y-m-d H:i:s"), 16 "updated_at" => date("Y-m-d H:i:s") 17 ], 18 [ 19 "name" => "Cola Company", 20 "description" => "Making cola like no other.", 21 "url" => "http://www.cheerioteeth.com", 22 "created_at" => date("Y-m-d H:i:s"), 23 "updated_at" => date("Y-m-d H:i:s") 24 ], 25 [ 26 "name" => "MacDougles", 27 "description" => "Super sandwiches.", 28 "url" => "http://www.imenjoyingit.com", 29 "created_at" => date("Y-m-d H:i:s"), 30 "updated_at" => date("Y-m-d H:i:s") 31 ] 32 ]; 33 34 DB::table("sponsor")->insert($sponsors); 35 } 36 } To get this data into the database, we need to run the seed command:
1 php artisan db:seed At this point; we should have the database tables set up, and some test data should be in the category and sponsor tables.
Before we can output data, we need a way to interface with the database. We’re going to use models for this purpose, so we should generate some:
1 php artisan generate:model Event 2 3 php artisan generate:model Category 4 5 php artisan generate:model Sponsor These commands will generate stub models which resemble the following:
1 <?php 2 3 class Event extends Eloquent { 4 5 protected $guarded = array(); 6 7 public static $rules = array(); 8 } We need to clean these up a bit, and add the relationship data in…
1 <?php 2 3 class Event 4 extends Eloquent 5 { 6 protected $table = "event"; 7 8 protected $guarded = [ 9 "id", 10 "created_at", 11 "updated_at" 12 ]; 13 14 public function categories() 15 { 16 return $this->belongsToMany("Category", "category_event", "event_id", "ca\ 17 tegory_id"); 18 } 19 20 public function sponsors() 21 { 22 return $this->belongsToMany("Sponsor", "event_sponsor", "event_id", "spon\ 23 sor_id"); 24 } 25 } 1 <?php 2 3 class Category 4 extends Eloquent 5 { 6 protected $table = "category"; 7 8 protected $guarded = [ 9 "id", 10 "created_at", 11 "updated_at" 12 ]; 13 14 public function events() 15 { 16 return $this->belongsToMany("Event", "category_event", "category_id", "ev\ 17 ent_id"); 18 } 19 } 1 <?php 2 3 class Sponsor 4 extends Eloquent 5 { 6 protected $table = "sponsor"; 7 8 protected $guarded = [ 9 "id", 10 "created_at", 11 "updated_at" 12 ]; 13 14 public function events() 15 { 16 return $this->belongsToMany("Event", "event_sponsor", "sponsor_id", "even\ 17 t_id"); 18 } 19 } As I mentioned before; we’ve gone with a belongsToMany() relationship to connect the entities together. The arguments for each of these is (1) the model name, (2) the pivot table name, (3) the local key and (4) the foreign key.
The last step in creating our API is to create the client-facing controllers.
The API controllers are different from those you might typically see, in a Laravel 4 application. They don’t load views; rather they respond to the requested content type. They don’t typically cater for multiple request types within the same action. They are not concerned with interface; but rather translating and formatting model data.
Creating them is a bit more tricky than the other classes we’ve done so far:
1 php artisan generate:controller EventController The command isn’t much different, but the generated file is far from ready:
1 <?php 2 class EventController extends BaseController { 3 4 /** 5 * Display a listing of the resource. 6 * 7 * @return Response 8 */ 9 public function index() 10 { 11 return View::make('events.index'); 12 } 13 14 /** 15 * Show the form for creating a new resource. 16 * 17 * @return Response 18 */ 19 public function create() 20 { 21 return View::make('events.create'); 22 } 23 24 /** 25 * Store a newly created resource in storage. 26 * 27 * @return Response 28 */ 29 public function store() 30 { 31 // 32 } 33 34 /** 35 * Display the specified resource. 36 * 37 * @param int $id 38 * @return Response 39 */ 40 public function show($id) 41 { 42 return View::make('events.show'); 43 } 44 45 /** 46 * Show the form for editing the specified resource. 47 * 48 * @param int $id 49 * @return Response 50 */ 51 public function edit($id) 52 { 53 return View::make('events.edit'); 54 } 55 56 /** 57 * Update the specified resource in storage. 58 * 59 * @param int $id 60 * @return Response 61 */ 62 public function update($id) 63 { 64 // 65 } 66 67 /** 68 * Remove the specified resource from storage. 69 * 70 * @param int $id 71 * @return Response 72 */ 73 public function destroy($id) 74 { 75 // 76 } 77 } We’ve not going to be rendering views, so we can remove those statements/actions. We’re also not going to deal just with integer ID values (we’ll get to the alternative shortly).
For now; what we want to do is list events, create them, update them and delete them. Our controller should look similar to the following:
1 <?php 2 3 class EventController 4 extends BaseController 5 { 6 public function index() 7 { 8 return Event::all(); 9 } 10 11 public function store() 12 { 13 return Event::create([ 14 "name" => Input::get("name"), 15 "description" => Input::get("description"), 16 "started_at" => Input::get("started_at"), 17 "ended_at" => Input::get("ended_at") 18 ]); 19 } 20 21 public function show($event) 22 { 23 return $event; 24 } 25 26 public function update($event) 27 { 28 $event->name = Input::get("name"); 29 $event->description = Input::get("description"); 30 $event->started_at = Input::get("started_at"); 31 $event->ended_at = Input::get("ended_at"); 32 $event->save(); 33 return $event; 34 } 35 36 public function destroy($event) 37 { 38 $event->delete(); 39 return Response::json(true); 40 } 41 } We’ve deleted a bunch of actions and added some simple logic in others. Our controller will list (index) events, show them individually, create (store) them, update them and delete (destroy) them.
Before we can access these; we need to add routes for them:
1 Route::model("event", "Event"); 2 3 Route::get("event", [ 4 "as" => "event/index", 5 "uses" => "EventController@index" 6 ]); 7 8 Route::post("event", [ 9 "as" => "event/store", 10 "uses" => "EventController@store" 11 ]); 12 13 Route::get("event/{event}", [ 14 "as" => "event/show", 15 "uses" => "EventController@show" 16 ]); 17 18 Route::put("event/{event}", [ 19 "as" => "event/update", 20 "uses" => "EventController@update" 21 ]); 22 23 Route::delete("event/{event}", [ 24 "as" => "event/destroy", 25 "uses" => "EventController@destroy" 26 ]); There are two important things here:
If you go to the index route; you will probably see an error. It might say something like “Call to undefined method IlluminateEventsDispatcher::all()”. This is because there is already a class (or rather an alias) called Event. Event is the name of our model, but instead of calling the all() method on our model; it’s trying to call it on the event disputer class baked into Laravel 4.
I’ve lead us to this error intentionally, to demonstrate how to overcome it if you ever have collisions in your applications. Most everything in Laravel 4 is in a namespace. However, to avoid lots of extra keystrokes; Laravel 4 also offers a configurable list of aliases (in app/config/app.php).
In the list of aliases; you will see an entry which looks like this:
1 'Event' => 'Illuminate\Support\Facades\Event', I changed the key of that entry to Events but you can really call it anything you like.
It’s not always easy to test REST API’s simply by using the browser. Often you will need to use an application to perform the different request methods. Thankfully modern *nix systems already have the Curl library, which makes these sorts of tests easier.
You can test the index endpoint with the console command:
1 curl http://dev.tutorial-laravel-4-api/event Unless you’ve manually populated the event table, or set up a seeder for it; you should see an empty JSON array. This is a good thing, and also telling of some Laravel 4 magic. Our index() action returns a collection, but it’s converted to JSON output when passed in a place where a Response is expected.
Let’s add a new event:
1 curl -X POST -d "name=foo&description=a+day+of+foo&started_at=2013-10-03+09:00&en\ 2 ded_at=2013-10-03+12:00" http://dev.tutorial-laravel-4-api:2080/event ..now, when we request the index() action, we should see the new event has been added. We can retrieve this event individually with a request similar to this:
1 curl http://dev.tutorial-laravel-4-api:2080/event/1 There’s a lot going on here. Remember how we bound the model to a specific parameter name (in app/routes.php)? Well Laravel 4 sees that ID value, matches it to the bound model parameter and fetches the record from the database. If the ID does not match any of the records in the database; Laravel will respond with a 404 error message.
If the record is found; Laravel returns the model representation to the action we specified, and we get a model to work with.
Let’s update this event:
1 curl -X PUT -d "name=best+foo&description=a+day+of+the+best+foo&started_at=2013-1\ 2 0-03+10:00&ended_at=2013-10-03+13:00" http://dev.tutorial-laravel-4-api:2080/even\ 3 t/1 Notice how all that’s changed is the data and the request type — even though we’re doing something completely different behind the scenes.
Lastly, let’s delete the event:
1 curl -X DELETE http://dev.tutorial-laravel-4-api:2080/event/1 So far we’ve left the API endpoints unauthenticated. That’s ok for internal use but it would be far more secure if we were to add an authentication layer.
We do this by securing the routes in filtered groups, and checking for valid credentials within the filter:
1 Route::group(["before" => "auth"], function() 2 { 3 // ...routes go here 4 }); 1 Route::filter("auth", function() 2 { 3 // ...get database user 4 5 if (Input::server("token") !== $user->token) 6 { 7 App::abort(400, "Invalid token"); 8 } 9 }); Your choice for authentication mechanisms will greatly affect the logic in your filters. I’ve opted not to go into great detail with regards to how the tokens are generated and users are stored. Ultimately; you can check for token headers, username/password combos or even IP addresses.
What’s important to note here is that we check for this thing (tokens in this case) and if they do not match those stored in user records, we abort the application execution cycle with a 400 error (and message).
You can find out more about filters at: http://laravel.com/docs/routing#route-filters.
There are times when we need to customise how model attributes are stored and retrieved. Laravel 4 lets us do that by providing specially named methods for accessors and mutators:
1 public function setNameAttribute($value) 2 { 3 $clean = preg_replace("/\W/", "", $value); 4 $this->attributes["name"] = $clean; 5 } 6 7 public function getDescriptionAttribute() 8 { 9 return trim($this->attributes["description"]); 10 } You can catch values, before they hit the database, by creating public set*Attribute() methods. These should transform the $value in some way and commit the change to the internal $attributes array.
You can also catch values, before they are returned, by creating get*Attribute() methods.
In the case of these methods; I am removing all non-word characters from the name value, before it hits the database; and trimming the description before it’s returned by the property accessor. Getters are also called by the toArray() and toJson() methods which transform model instances into either arrays or JSON strings.
You can also add attributes to models by creating accessors and mutators for them, and mentioning them in the $appends property:
1 protected $appends = ["hasCategories", "hasSponsors"]; 2 3 public function getHasCategoriesAttribute() 4 { 5 $hasCategories = $this->categories()->count() > 0; 6 return $this->attributes["hasCategories"] = $hasCategories; 7 } 8 9 public function getHasSponsorsAttribute() 10 { 11 $hasSponsors = $this->sponsors()->count() > 0; 12 return $this->attributes["hasSponsors"] = $hasSponsors; 13 } Here we’ve created two new accessors which check the count for categories and sponsors. We’ve also added those two attributes to the $appends array so they are returned when we list (index) all events or specific (show) events.
Laravel 4 provides a great cache mechanism. It’s configured in the same was as the database:
1 <?php 2 3 return [ 4 "driver" => "memcached", 5 "memcached" => [ 6 [ 7 "host" => "127.0.0.1", 8 "port" => 11211, 9 "weight" => 100 10 ] 11 ], 12 "prefix" => "laravel" 13 ]; I’ve configured my cache to use the Memcached provider. This needs to be running on the specified host (at the specified port) in order for it to work.
No matter the provider you choose to use; the cache methods work the same way:
1 public function index() 2 { 3 return Cache::remember("events", 15, function() 4 { 5 return Event::all(); 6 }); 7 } The Cache::remember() method will store the callback return value in cache if it’s not already there. We’ve set it to store the events for 15 minutes.
The primary use for cache is in key/value storage:
1 public function total() 2 { 3 if (($total = Cache::get("events.total")) == null) 4 { 5 $total = Event::count(); 6 Cache::put("events.total", $total, 15); 7 } 8 9 return Response::json((int) $total); 10 } You can also invoke this cache on Query Builder queries or Eloquent queries:
1 public function today() 2 { 3 return Event::where(DB::raw("DAY(started_at)"), date("d")) 4 ->remember(15) 5 ->get(); 6 } …we just need to remember to add the remember() method before we call the get() or first() methods.