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.