Laravel 4 ACL

A Comprehensive Tutorial

Christopher Pitt
Laravel 4 Tutorials

--

Introduction

Following on from the previous tutorial; I’m going to lead us through creating an ACL (access control list) for our authenticated application.

I have spent two full hours getting the code out of a Markdown document and into Medium. Medium really isn’t designed for tutorials such as this, and while much effort has been spent in the pursuit of accuracy; there’s a good chance you could stumble across a curly quote in a code listing. Please make a note and I will fix where needed.

I have also uploaded this code to Github. You need simply follow the configuration instructions in this tutorial, after downloading the source code, and the application should run fine. This assumes, of course, that you know how to do that sort of thing. If not; this shouldn’t be the first place you learn about making PHP applications.

https://github.com/formativ/tutorial-laravel-4-acl

If you spot differences between this tutorial and that source code, please raise it here or as a GitHub issue. Your help is greatly appreciated.

Getting Started

This tutorial follows on from the previous tutorial in which we set up basic user authentication, along with all the views, migrations, models and actions required. We’re not going to go through all of that again, so if you haven’t done so already; I suggest going through it to familiarise yourself with the existing codebase.

Installation instructions for Laravel 4 can also be found in that tutorial.

Managing Groups

We’re going to be creating an interface for adding, modifying and deleting user groups. Groups will be the containers to which we add various users and resources. We’ll do that by create a migration and a model for groups, but we’re also going to optimise the way we create migrations.

Refactoring Migrations

We’ve got a few more migrations to create in this tutorial; so it’s a good time for us to refactor our approach to creating them…

<?phpuse Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
class BaseMigration
extends Migration
{
protected $table;
public function getTable()
{
if ($this->table == null)
{
throw new Exception("Table not set.");
}
return $this->table;
}
public function setTable(Blueprint $table)
{
$this->table = $table;
return $this;
}
public function addNullable($type, $key)
{
$types = [
"boolean",
"dateTime",
"integer",
"string",
"text"
];
if (in_array($type, $types))
{
$this->getTable()
->{$type}($key)
->nullable()
->default(null);
}
return $this;
}
public function addTimestamps()
{
$this->addNullable("dateTime", "created_at");
$this->addNullable("dateTime", "updated_at");
$this->addNullable("dateTime", "deleted_at");
return $this;
}
public function addPrimary()
{
$this->getTable()->increments("id");
return $this;
}
public function addForeign($key)
{
$this->addNullable("integer", $key);
$this->getTable()->index($key);
return $this;
}
public function addBoolean($key)
{
return $this->addNullable("boolean", $key);
}
public function addDateTime($key)
{
return $this->addNullable("dateTime", $key);
}
public function addInteger($key)
{
return $this->addNullable("integer", $key);
}
public function addString($key)
{
return $this->addNullable("string", $key);
}
public function addText($key)
{
return $this->addNullable("text", $key);
}
}

This file should be saved as app/database/migrations/BaseMigration.php.

We’re going to base all of our models off of a single BaseModel class. This will make it possible for us to reuse a lot of the repeated code we had before.

The BaseModel class has a single protected $table property, for storing the current Blueprint instance we are giving inside our migration callbacks. We have a typical setter for this; and an atypical getter (which throws an exception if $this->table hasn’t been set). We do this as we need a way to validate that the methods which require a valid Blueprint instance have one or throw an exception.

Our BaseMigration class also has a factory method for creating fields of various types. If the type provided is one of those defined; a nullable field of that type will be created. This significantly shortens the code we used previously to create nullable fields.

Following this; we have addPrimary(), addForeign() and addTimestamps(). The addPrimary() method is clearer than the increments() method (in my humble opinion), the addForeign() method adds both a nullable integer field and an index for the foreign key. The addTimestamps() method is similar to the Blueprint’s timestamps() method; except that it also adds the deleted_at timestamp field.

Finally; there are a handful of methods which proxy to the addNullable() method.

Using these methods, the amount of code required for the migrations we will create (and have already created) is drastically reduced.

<?phpuse Illuminate\Database\Schema\Blueprint;class CreateGroupTable
extends BaseMigration
{
public function up()
{
Schema::create("group", function(Blueprint $table)
{
$this
->setTable($table)
->addPrimary()
->addString("name")
->addTimestamps();
});
}
public function down()
{
Schema::dropIfExists("group");
}
}

This file should be saved as app/database/migrations/NNNN_NN_NN_
NNNNNN_CreateGroupTable.php
.

The group table has a primary key, timestamp fields (including created_at, updated_at and deleted_at) as well as a name field.

If you’re skipping migrations; the following SQL should create the same table structure as the migration:

CREATE TABLE `group` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(255) DEFAULT NULL,
`created_at` datetime DEFAULT NULL,
`updated_at` datetime DEFAULT NULL,
`deleted_at` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB CHARSET=utf8;

Listing Groups

We’re going to be creating views to manage group records; more comprehensive than those we created for users previously, but much the same in terms of complexity.

@extends("layout")
@section("content")
@if (count($groups))
<table>
<tr>
<th>name</th>
</tr>
@foreach ($groups as $group)
<tr>
<td>{{ $group->name }}</td>
</tr>
@endforeach
</table>
@else
<p>There are no groups.</p>
@endif
<a href="{{ URL::route("group/add") }}">add group</a>
@stop

This file should be saved as app/views/group/index.blade.php.

The first view is the index view. This should list all the groups that are in the database. We extend the layout as usual, defining a content block for the markup specific to this page.

The main idea is to iterate over the group records, but before we do that we first check if there are any groups. After all, we don’t want to go to the trouble of showing a table if there’s nothing to put in it.

If there are groups, we create a (rough) table and iterate over the groups; creating a row for each. We finish off the view by adding a link to create a new group.

Route::any("/group/index", [
"as" => "group/index",
"uses" => "GroupController@indexAction"
]);

This was extracted from app/routes.php for brevity.

<?phpclass Group
extends Eloquent
{
protected $table = "group";

protected $softDelete = true;
protected $guarded = [
"id",
"created_at",
"updated_at",
"deleted_at"
];
}

This file should be saved as app/models/Group.php.

<?phpclass GroupController
extends Controller
{
public function indexAction()
{
return View::make("group/index", [
"groups" => Group::all()
]);
}
}

This file should be saved as app/controllers/GroupController.php.

In order to view the index page; we need to define a route to it. We also need to define a model for the group table. Lastly; we render the index view, having passed all the groups to the view. Navigating to this route should now display the message “There are no groups.” as we have yet to add any.

You can test out how the index page looks by adding a group to the database directly.

An important thing to note is the use of $softDelete. Laravel 4 provides a new method of ensuring that no data is hastily deleted via Eloquent; so long as this property is set. If true; any calls to the $group->delete() method will set the deleted_at timestamp to the date and time on which the method was invoked. Records with a deleted_at timestamp (which is not null) will not be returned in normal QueryBuilder (including Eloquent) queries.

You can find out more about soft deleting here: http://laravel.com/
docs/eloquent#soft-deleting

Another important thing to note is the use of $guarded. Laravel 4 provides mass assignment protection. What we’re doing by specifying this list of fields; is telling Eloquent which fields should not be settable when providing an array of data in the creation of a new Group instance.

You can find out more about this mass assignment protection here: http://laravel.com/docs/eloquent#mass-assignment

Adding Groups

Following feedback from the previous tutorial; we’re going to be abstracting much of the validation out of the controllers and into new form classes.

<?phpuse Illuminate\Support\MessageBag;class BaseForm
{
protected $passes;
protected $errors; public function __construct()
{
$errors = new MessageBag();
if ($old = Input::old("errors"))
{
$errors = $old;
}
$this->errors = $errors;
}
public function isValid($rules)
{
$validator = Validator::make(Input::all(), $rules);
$this->passes = $validator->passes();
$this->errors = $validator->errors();
return $this->passes;
}
public function getErrors()
{
return $this->errors;
}
public function setErrors(MessageBag $errors)
{
$this->errors = $errors;
return $this;
}
public function hasErrors()
{
return $this->errors->any();
}
public function getError($key)
{
return $this->getErrors()->first($key);
}
public function isPosted()
{
return Input::server("REQUEST_METHOD") == "POST";
}
}

This file should be saved as app/forms/BaseForm.php.

The BaseForm class checks for the error messages we would normally store to flash (session) storage. We would typically pull this data in each action, and now it will happen when each form class instance is created.

The validation takes place in the isValid() method, which gets all the input data and compares it to a set of provided validation rules. This will be used later, in BaseForm subclasses.

BaseForm also has a few methods for managing the $errors property, which should always be a MessageBag instance. They can be used to set and get the MessageBag instance, get an individual message and even tell whether there are any error messages present.

There’s also a method to determine whether the request method, for the current request, is POST.

<?phpclass GroupForm
extends BaseForm
{
public function isValidForAdd()
{
return $this->isValid([
"name" => "required"
]);
}
public function isValidForEdit()
{
return $this->isValid([
"id" => "exists:group,id",
"name" => "required"
]);
}
public function isValidForDelete()
{
return $this->isValid([
"id" => "exists:group,id"
]);
}
}

This file should be saved as app/forms/GroupForm.php.

The first implementation of BaseForm is the GroupForm class. It’s quite simply by comparison; defining three validation methods. These will be used in their respective actions.

We also need a way to generate not only validation error message markup but also a quicker way to create form markup. Laravel 4 has great utilities for creating form and HTML markup, so let’s see how these can be extended.

{{ Form::label("name", "Name") }}
{{ Form::text("name", Input::old("name"), [
"placeholder" => "new group"
]) }}

We’ve already seen this type of Blade template syntax before. The label and text helpers are great for programatically creating the markup we would otherwise have to create; but sometimes it is nice to be able to create our own markup generators for commonly repeated patterns.

What if we, for instance, often use a combination of label, text and error message markup? It would then be ideal for us to create what’s called a macro to generate that markup.

<?phpForm::macro("field", function($options)
{
$markup = "";
$type = "text"; if (!empty($options["type"]))
{
$type = $options["type"];
}
if (empty($options["name"]))
{
return;
}
$name = $options["name"]; $label = ""; if (!empty($options["label"]))
{
$label = $options["label"];
}
$value = Input::old($name); if (!empty($options["value"]))
{
$value = Input::old($name, $options["value"]);
}
$placeholder = ""; if (!empty($options["placeholder"]))
{
$placeholder = $options["placeholder"];
}
$class = ""; if (!empty($options["class"]))
{
$class = " " . $options["class"];
}
$parameters = [
"class" => "form-control" . $class,
"placeholder" => $placeholder
];
$error = ""; if (!empty($options["form"]))
{
$error = $options["form"]->getError($name);
}
if ($type !== "hidden")
{
$markup .= "<div class='form-group";
$markup .= ($error ? " has-error" : "");
$markup .= "'>";
}
switch ($type)
{
case "text":
{
$markup .= Form::label($name, $label, [
"class" => "control-label"
]);
$markup .= Form::text($name, $value, $parameters); break;
}
case "password":
{
$markup .= Form::label($name, $label, [
"class" => "control-label"
]);
$markup .= Form::password($name, $parameters); break;
}
case "checkbox":
{
$markup .= "<div class='checkbox'>";
$markup .= "<label>";
$markup .= Form::checkbox($name, 1, (boolean) $value);
$markup .= " " . $label;
$markup .= "</label>";
$markup .= "</div>";
break;
}
case "hidden":
{
$markup .= Form::hidden($name, $value);
break;
}
}
if ($error)
{
$markup .= "<span class='help-block'>";
$markup .= $error;
$markup .= "</span>";
}
if ($type !== "hidden")
{
$markup .= "</div>";
}
return $markup;
});

This file should be saved as app/macros.php.

This macro evaluates an $options array, generating a label, input element and validation error message. There’s white a lot of checking involved to ensure that all the required data is there, and that optional data affects the generated markup correctly. It supports text inputs, password inputs, checkboxes and hidden fields; but more types can easily be added.

The markup this macro generates is Bootstrap friendly. If you haven’t already heard of Bootstrap (where have you been?) then you can find out more about it at: http://getbootstrap.com/

To see this in action, we need to include it in the startup processes of the application and then modify the form views to use it:

require app_path() . "/macros.php";

This was extracted from app/start/global.php for brevity.

@extends("layout")
@section("content")
{{ Form::open([
"route" => "group/add",
"autocomplete" => "off"
]) }}
{{ Form::field([
"name" => "name",
"label" => "Name",
"form" => $form,
"placeholder" => "new group"
])}}
{{ Form::submit("save") }}
{{ Form::close() }}
@stop
@section("footer")
@parent
<script src="//polyfill.io"></script>
@stop

This file should be saved as app/views/group/add.blade.php.

You’ll notice how much neater the view is; thanks to the form class handling the error messages for us. This view happens to be relatively short since there’s only a single field (name) for groups.

.help-block
{
float : left;
clear : left;
}
.form-group.has-error .help-block
{
color : #ef7c61;
}

This was extracted from public/css/layout.css for brevity.

One last thing we have to do, to get the error messages to look the same as they did before, is to add a bit of CSS to target the Bootstrap-friendly error messages.

With the add view complete; we can create the addAction() method:

public function addAction()
{
$form = new GroupForm();
if ($form->isPosted())
{
if ($form->isValidForAdd())
{
Group::create([
"name" => Input::get("name")
]);
return Redirect::route("group/index");
}
return Redirect::route("group/add")->withInput([
"name" => Input::get("name"),
"errors" => $form->getErrors()
]);
}
return View::make("group/add", [
"form" => $form
]);
}

This was extracted from app/controllers/GroupController.php for brevity.

You can also see how much simpler our addAction() method is; now that we’re using the GroupForm class. It takes care of retrieving old error messages and handling validation so that we can simply create groups and redirect.

Editing Groups

The view and action for editing groups is much the same as for adding groups.


@extends("layout")
@section("content")
{{ Form::open([
"url" => URL::full(),
"autocomplete" => "off"
]) }}
{{ Form::field([
"name" => "name",
"label" => "Name",
"form" => $form,
"placeholder" => "new group",
"value" => $group->name
]) }}
{{ Form::submit("save") }}
{{ Form::close() }}
@stop
@section("footer")
@parent
<script src="//polyfill.io"></script>
@stop

This file should be saved as app/views/group/edit.blade.php.

The only difference here is the form action we’re setting. We need to take into account that a group id will be provided to the edit page, so the URL must be adjusted to maintain this id even after the form is posted. For that; we use the URL::full() method which returns the full, current URL.

public function editAction()
{
$form = new GroupForm();
$group = Group::findOrFail(Input::get("id"));
$url = URL::full();
if ($form->isPosted())
{
if ($form->isValidForEdit())
{
$group->name = Input::get("name");
$group->save();
return Redirect::route("group/index");
}
return Redirect::to($url)->withInput([
"name" => Input::get("name"),
"errors" => $form->getErrors(),
"url" => $url
]);
}
return View::make("group/edit", [
"form" => $form,
"group" => $group
]);
}

This was extracted from app/controllers/GroupController.php for brevity.

In the editAction() method; we’re still create a new instance of GroupForm. Because we’re editing a group, we need to get that group to display its data in the view. We do this with Eloquent’s findOrFail() method; which will cause a 404 error page to be displayed if the id is not found within the database.

The rest of the action is much the same as the addAction() method. We’ll also need to add the edit route to the routes.php file…

Route::any("/group/edit", [
"as" => "group/edit",
"uses" => "GroupController@editAction"
]);

This was extracted from app/routes.php for brevity.

Deleting Groups

There are a number of options we can explore when creating the delete interface, but we’ll go with the quickest which is just to present a link on the listing page.

@extends("layout")
@section("content")
@if (count($groups))
<table>
<tr>
<th>name</th>
<th>&nbsp;</th>
</tr>
@foreach ($groups as $group)
<tr>
<td>{{ $group->name }}</td>
<td>
<a href="{{ URL::route("group/edit") }}?id={{ $group->id }}">edit</a>
<a href="{{ URL::route("group/delete") }}?id={{ $group->id }}" class="confirm" data-confirm="Are you sure you want to delete this group?">delete</a>
</td>
</tr>
@endforeach
</table>
@else
<p>There are no groups.</p>
@endif
<a href="{{ URL::route("group/add") }}">add group</a>
@stop

This file should be saved as app/views/group/index.blade.php.

We’ve modified the group/index view to include two links; which will redirect users either to the edit page or the delete action. Notice the class=”confirm” and data-confirm=”…” attributes we’ve added to the delete link — we’ll use these shortly. We’ll also need to add the delete route to the routes.php file…

Route::any("/group/delete", [
"as" => "group/delete",
"uses" => "GroupController@deleteAction"
]);

This was extracted from app/routes.php for brevity.

Since we’ve chosen such an easy method of deleting groups, the action is pretty straightforward:

public function deleteAction()
{
$form = new GroupForm();
if ($form->isValidForDelete())
{
$group = Group::findOrFail(Input::get("id"));
$group->delete();
}
return Redirect::route("group/index");
}

This was extracted from app/controllers/GroupController.php for brevity.

We simply need to find a group with the provided id (using the findOrFail() method we saw earlier) and delete it. After that; we redirect back to the listing page. Before we take this for a spin, let’s add the following JavaScript:

(function($){
$(".confirm").on("click", function() {
return confirm($(this).data("confirm"));
});
}(jQuery));

This file should be saved as public/js/layout.js.

@section("footer")
@parent
<script src="/js/jquery.js"></script>
<script src="/js/layout.js"></script>
@stop

This was extracted from app/views/group/index.blade.php for brevity.

You’ll notice I have linked to jquery.js (any recent version will do). The code in layout.js adds a click event handler on to every element with class=”confirm” to prompt the user with the message in data-confirm=”…”. If “OK” is clicked; the callback returns true and the browser will redirect to the page on the other end (in this case the deleteAction() method on our GroupController class). Otherwise the click will be ignored.

Adding Users And Resources

Next on our list is making a way for us to specify resource information and add users to our groups. Both of these thing will happen on the group edit page; but before we get there we will need to deal with migrations, models and relationships…

<?phpuse Illuminate\Database\Schema\Blueprint;class CreateResourceTable
extends BaseMigration
{
public function up()
{
Schema::create("resource", function(Blueprint $table)
{
$this
->setTable($table)
->addPrimary()
->addString("name")
->addString("pattern")
->addString("target")
->addBoolean("secure")
->addTimestamps();
});
}
public function down()
{
Schema::dropIfExists("resource");
}
}

This file should be saved as app/database/migrations/NNNN_NN_NN_
NNNNNN_CreateResourceTable.php
.

We are calling them resources to avoid the name collision with the existing Route class.

If you’re skipping migrations; the following SQL should create the same table structure as the migration:

CREATE TABLE `resource` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(255) DEFAULT NULL,
`pattern` varchar(255) DEFAULT NULL,
`target` varchar(255) DEFAULT NULL,
`secure` tinyint(1) DEFAULT NULL,
`created_at` datetime DEFAULT NULL,
`updated_at` datetime DEFAULT NULL,
`deleted_at` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB CHARSET=utf8;

The resource table has fields for the things we usually store in our routes file. The idea is that we keep the route information in the database so we can both programatically generate the routes for our application; and so that we can link various routes to groups for controlling access to various parts of our application.

<?phpclass Resource
extends Eloquent
{
protected $table = "resource";
protected $softDelete = true; protected $guarded = [
"id",
"created_at",
"updated_at",
"deleted_at"
];
public function groups()
{
return $this->belongsToMany("Group")->withTimestamps();
}
}

This file should be saved as app/models/Resource.php.

The Resource model is similar to those we’ve seen before; but it also specifies a many-to-many relationship (in the groups() method). This will allows us to return related groups with $this->groups. We’ll use that later!

The withTimestamps() method will tell Eloquent to update the timestamps of related groups when resources are updated. You can find out more about it at: http://laravel.com/docs/eloquent#working-with-pivot-tables

We also need to add the reverse relationship to the Group model:

public function resources()
{
return $this->belongsToMany("Resource")->withTimestamps();
}

This was extracted from app/models/Group.php for brevity.

There really is a lot to relationships in Eloquent; more than we have time to cover now. I will be going into more detail about these relationships in future tutorials; exploring the different types and configuration options. For now, this is all we need to complete this tutorial.

We can also define relationships for users and groups, as in the following examples:

public function users()
{
return $this->belongsToMany("User")->withTimestamps();
}

This was extracted from app/models/Group.php for brevity.

public function groups()
{
return $this->belongsToMany("Group")->withTimestamps();
}

This was extracted from app/models/User.php for brevity.

Before we’re quite done with the database work; we’ll also need to remember to set up the pivot tables in which the relationship data will be stored.

<?phpuse Illuminate\Database\Schema\Blueprint;class CreateGroupUserTable
extends BaseMigration
{
public function up()
{
Schema::create("group_user", function(Blueprint $table)
{
$this
->setTable($table)
->addPrimary()
->addForeign("group_id")
->addForeign("user_id")
->addTimestamps();
});
}
public function down()
{
Schema::dropIfExists("group_user");
}
}

This file should be saved as app/database/migrations/NNNN_NN_NN_
NNNNNN_CreateGroupUserTable.php
.

<?phpuse Illuminate\Database\Schema\Blueprint;class CreateGroupResourceTable
extends BaseMigration
{
public function up()
{
Schema::create("group_resource", function(Blueprint $table)
{
$this
->setTable($table)
->addPrimary()
->addForeign("group_id")
->addForeign("resource_id")
->addTimestamps();
});
}
public function down()
{
Schema::dropIfExists("group_resource");
}
}

This file should be saved as app/database/migrations/NNNN_NN_NN_
NNNNNN_CreateGroupResourceTable.php
.

We now have a way to manage the data relating to groups; so let’s create the views and actions through which we can capture this data.

If you’re skipping migrations; the following SQL should create the same table structures as the migrations:

CREATE TABLE `group_resource` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`group_id` int(11) DEFAULT NULL,
`resource_id` int(11) DEFAULT NULL,
`created_at` datetime DEFAULT NULL,
`updated_at` datetime DEFAULT NULL,
`deleted_at` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `group_resource_group_id_index` (`group_id`),
KEY `group_resource_resource_id_index` (`resource_id`)
) ENGINE=InnoDB CHARSET=utf8;
CREATE TABLE `group_user` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`group_id` int(11) DEFAULT NULL,
`user_id` int(11) DEFAULT NULL,
`created_at` datetime DEFAULT NULL,
`updated_at` datetime DEFAULT NULL,
`deleted_at` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `group_user_group_id_index` (`group_id`),
KEY `group_user_user_id_index` (`user_id`)
) ENGINE=InnoDB CHARSET=utf8;

The views we need to create are those in which we will select which users and resources should be assigned to a group.

<div class="assign">
@foreach ($resources as $resource)
<div class="checkbox">
{{ Form::checkbox("resource_id[]", $resource->id, $group->resources->contains($resource->id)) }}
{{ $resource->name }}
</div>
@endforeach
</div>

This file should be saved as app/views/resource/assign.blade.php.

<div class="assign">
@foreach ($users as $user)
<div class="checkbox">
{{ Form::checkbox("user_id[]", $user->id, $group->users->contains($user->id)) }}
{{ $user->username }}
</div>
@endforeach
</div>

This file should be saved as app/views/user/assign.blade.php.

These views similarly iterate over resources and users (passed to the group edit view) and render markup for checkboxes. It’s important to note the names of the checkbox inputs ending in [] — this is the recommended way to passing array-like data in HTML forms.

The first parameter of the Form::checkbox() method is the input’s name. The second is its value. The third is whether of not the checkbox should initially be checked. Eloquent models provide a useful contains() method which searches the related rows for those matching the provided id(s).

@extends("layout")
@section("content")
{{ Form::open([
"url" => URL::full(),
"autocomplete" => "off"
]) }}
{{ Form::field([
"name" => "name",
"label" => "Name",
"form" => $form,
"placeholder" => "new group",
"value" => $group->name
])}}
@include("user/assign")
@include("resource/assign")
{{ Form::submit("save") }}
{{ Form::close() }}
@stop
@section("footer")
@parent
<script src="//polyfill.io"></script>
@stop

This file should be saved as app/views/group/edit.blade.php.

We’ve modified the group/edit view to include the new assign views. If you try to edit a group, at this point, you might see an error. This is because we still need to pass the users and resources to the view…

return View::make("group/edit", [
"form" => $form,
"group" => $group,
"users" => User::all(),
"resources" => Resource::where("secure", true)->get()
]);

This was extracted from app/controllers/GroupController.php for brevity.

We return all the users (so that any user can be in any group) and the resources that need to be secure. Right now, that database table is empty, but we can easily create a seeder for it:

<?phpclass ResourceSeeder
extends DatabaseSeeder
{
public function run()
{
$resources = [
[
"pattern" => "/",
"name" => "user/login",
"target" => "UserController@loginAction",
"secure" => false
],
[
"pattern" => "/request",
"name" => "user/request",
"target" => "UserController@requestAction",
"secure" => false
],
[
"pattern" => "/reset",
"name" => "user/reset",
"target" => "UserController@resetAction",
"secure" => false
],
[
"pattern" => "/logout",
"name" => "user/logout",
"target" => "UserController@logoutAction",
"secure" => true
],
[
"pattern" => "/profile",
"name" => "user/profile",
"target" => "UserController@profileAction",
"secure" => true
],
[
"pattern" => "/group/index",
"name" => "group/index",
"target" => "GroupController@indexAction",
"secure" => true
],
[
"pattern" => "/group/add",
"name" => "group/add",
"target" => "GroupController@addAction",
"secure" => true
],
[
"pattern" => "/group/edit",
"name" => "group/edit",
"target" => "GroupController@editAction",
"secure" => true
],
[
"pattern" => "/group/delete",
"name" => "group/delete",
"target" => "GroupController@deleteAction",
"secure" => true
]
];
foreach ($resources as $resource)
{
Resource::create($resource);
}
}
}

This file should be saved as app/database/seeds/ResourceSeeder.php.

We should also add this seeder to the DatabaseSeeder class so that the Artisan commands which deal with seeding pick it up:

<?phpclass DatabaseSeeder
extends Seeder
{
public function run()
{
Eloquent::unguard();
$this->call("ResourceSeeder");
$this->call("UserSeeder");
}
}

This file should be saved as app/database/seeds/DatabaseSeeder.php.

Now you should be seeing the lists of resources and users when you try to edit a group. We need to save selections when the group is saved; so that we can successfully assign both users and resources to groups.

if ($form->isValidForEdit())
{
$group->name = Input::get("name");
$group->save();
$group->users()->sync(Input::get("user_id", []));
$group->resources()->sync(Input::get("resource_id", []));
return Redirect::route("group/index");
}

This was extracted from app/controllers/GroupController.php for brevity.

Laravel 4 provides and excellent method for synchronising related database records — the sync() method. You simply provide it with the id(s) of the related records and it makes sure there is a record for each relationship. It couldn’t be easier!

Finally, we will add a bit of CSS to make the lists less of a mess…

.assign
{
padding : 10px 0 0 0;
line-height : 22px;
}
.checkbox, .assign
{
float : left;
clear : left;
}
.checkbox input[type='checkbox']
{
margin : 0 10px 0 0;
float : none;
}

This was extracted from public/css/layout.css for brevity.

Take it for a spin! You will find that the related records are created (in the pivot) tables, and each time you submit it; the edit page will remember the correct relationships and show them back to you.

Advanced Routes

The final thing we need to do is manage how resources are translated into routes and how the security behaves in the presence of our simple ACL.

<?phpRoute::group(["before" => "guest"], function()
{
$resources = Resource::where("secure", false)->get();
foreach ($resources as $resource)
{
Route::any($resource->pattern, [
"as" => $resource->name,
"uses" => $resource->target
]);
}
});
Route::group(["before" => "auth"], function()
{
$resources = Resource::where("secure", true)->get();
foreach ($resources as $resource)
{
Route::any($resource->pattern, [
"as" => $resource->name,
"uses" => $resource->target
]);
}
});

This file should be saved as app/routes.php.

There are some significant changes to the routes file. Firstly, all the routes are being generated from resources. We no longer need to hard-code routes in this file because we can save them in the database.

It’s more efficient hard-coding them, and we really should be caching them if we have to read them from the database; but that’s the subject of future tutorials — we’ve running out of time here!

All the “insecure” routes are rendered in the first block — the block in which routes are subject to the guest filter. All the “secure” routes are rendered in the secure; where they are subject to the auth filter.

Route::filter("auth", function()
{
if (Auth::guest())
{
return Redirect::route("user/login");
}
else
{
foreach (Auth::user()->groups as $group)
{
foreach ($group->resources as $resource)
{
$path = Route::getCurrentRoute()->getPath();
if ($resource->pattern == $path)
{
return;
}
}
}
return Redirect::route("user/login");
}
});

This was extracted from app/filters.php for brevity.

The new auth filter needs not only to make sure the user is authenticated, but also that one of the group to which they are assigned has the current route assigned to it also. Users can belong to multiple groups and so can resources; so this is the only (albeit inefficient way) to filter allowed resources from those which the user is not allowed access to.

To test this out; alter the group to which your user account belongs to disallow access to the group/add route. When you try to visit it you will be redirected first to the user/login route and the not the user/profile route.

You need to make sure you’re not disallowing access to a route specified in the auth filter. That will probably lead to a redirect loop!

Lastly, we need a way to hide links to disallowed resources…

<?phpif (!function_exists("allowed"))
{
function allowed($route)
{
if (Auth::check())
{
foreach (Auth::user()->groups as $group)
{
foreach ($group->resources as $resource)
{
if ($resource->name == $route)
{
return true;
}
}
}
}
return false;
}
}

This file should be saved as app/helpers.php.

require app_path() . "/helpers.php";

This was extracted from app/start/global.php for brevity.

Once we’ve included that helpers.php file in the startup processes of our application; we can check whether the authenticated user is allowed access to resources simply by passing the resource name to the allowed() method.

The first time you run the migrations (if you’re installing from GitHub); you may see errors relating to the resource table. This is likely caused by the routes.php file trying to load routes from an empty database table before seeding takes place. Try commenting out the code in routes.php until you’ve successfully migrated and seeded your database. There are nicer ways to do this; all of which I will not cover now.

Try this out by wrapping the links of your application in a condition which references this method.

For the sake of brevity; I have not included any examples of this, though many can be found, in the source code, on GitHub.

A Note On Completeness

Due to time constraints; I’ve left a lot of obvious stuff out. The user and token tables could have been optimised, and we could have created new views, form classes etc. for them. Consider this an exercise for the reader.

A Note On Testing

It’s been suggested that I write tests for the code in these tutorials. As they are not focussed on the subject of testing I have chosen to omit testing in the hope that they will remain simple and singular.

That is not to say that tests are bad. By all means; consider writing tests to cover the code in these tutorials. It may also be the focus of future tutorials.

Conclusion

Laravel 4 is packed with excellent tools to create simple, secure login systems. In addition, it also has an excellent ORM, template language, input validator and filter system. And that’s just the tip of the iceberg!

If you found this tutorial helpful, please tell me about it @followchrisp and be sure to recommend it to PHP developers looking to Laravel 4!

This tutorial comes from a book I’m writing. If you like it and want to support future tutorials; please consider buying it. Half of all sales go to Laravel.

--

--