Previously we looked at how to set up a basic authentication system. In this chapter; we’re going to continue to improve the authentication system by adding what’s called ACL (Access Control List) to the authentication layer.
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.
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…
1 <?php 2 3 use Illuminate\Database\Migrations\Migration; 4 use Illuminate\Database\Schema\Blueprint; 5 6 class BaseMigration 7 extends Migration 8 { 9 protected $table; 10 11 public function getTable() 12 { 13 if ($this->table == null) 14 { 15 throw new Exception("Table not set."); 16 } 17 18 return $this->table; 19 } 20 21 public function setTable(Blueprint $table) 22 { 23 $this->table = $table; 24 return $this; 25 } 26 27 public function addNullable($type, $key) 28 { 29 $types = [ 30 "boolean", 31 "dateTime", 32 "integer", 33 "string", 34 "text" 35 ]; 36 37 if (in_array($type, $types)) 38 { 39 $this->getTable() 40 ->{$type}($key) 41 ->nullable() 42 ->default(null); 43 } 44 45 return $this; 46 } 47 48 public function addTimestamps() 49 { 50 $this->addNullable("dateTime", "created_at"); 51 $this->addNullable("dateTime", "updated_at"); 52 $this->addNullable("dateTime", "deleted_at"); 53 return $this; 54 } 55 56 public function addPrimary() 57 { 58 $this->getTable()->increments("id"); 59 return $this; 60 } 61 62 public function addForeign($key) 63 { 64 $this->addNullable("integer", $key); 65 $this->getTable()->index($key); 66 return $this; 67 } 68 69 public function addBoolean($key) 70 { 71 return $this->addNullable("boolean", $key); 72 } 73 74 public function addDateTime($key) 75 { 76 return $this->addNullable("dateTime", $key); 77 } 78 79 public function addInteger($key) 80 { 81 return $this->addNullable("integer", $key); 82 } 83 84 public function addString($key) 85 { 86 return $this->addNullable("string", $key); 87 } 88 89 public function addText($key) 90 { 91 return $this->addNullable("text", $key); 92 } 93 } 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 a bit clearer than the increments() method, 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.
1 <?php 2 3 use Illuminate\Database\Schema\Blueprint; 4 5 class CreateGroupTable 6 extends BaseMigration 7 { 8 public function up() 9 { 10 Schema::create("group", function(Blueprint $table) 11 { 12 $this 13 ->setTable($table) 14 ->addPrimary() 15 ->addString("name") 16 ->addTimestamps(); 17 }); 18 } 19 20 public function down() 21 { 22 Schema::dropIfExists("group"); 23 } 24 } 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:
1 CREATE TABLE `group` ( 2 `id` int(10) unsigned NOT NULL AUTO_INCREMENT, 3 `name` varchar(255) DEFAULT NULL, 4 `created_at` datetime DEFAULT NULL, 5 `updated_at` datetime DEFAULT NULL, 6 `deleted_at` datetime DEFAULT NULL, 7 PRIMARY KEY (`id`) 8 ) ENGINE=InnoDB CHARSET=utf8; 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.
1 @extends("layout") 2 @section("content") 3 @if (count($groups)) 4 <table> 5 <tr> 6 <th>name</th> 7 </tr> 8 @foreach ($groups as $group) 9 <tr> 10 <td>{{ $group->name }}</td> 11 </tr> 12 @endforeach 13 </table> 14 @else 15 <p>There are no groups.</p> 16 @endif 17 <a href="{{ URL::route("group/add") }}">add group</a> 18 @stop 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.
1 Route::any("/group/index", [ 2 "as" => "group/index", 3 "uses" => "GroupController@indexAction" 4 ]); 1 <?php 2 3 class Group 4 extends Eloquent 5 { 6 protected $table = "group"; 7 8 protected $softDelete = true; 9 10 protected $guarded = [ 11 "id", 12 "created_at", 13 "updated_at", 14 "deleted_at" 15 ]; 16 } 1 <?php 2 3 class GroupController 4 extends Controller 5 { 6 public function indexAction() 7 { 8 return View::make("group/index", [ 9 "groups" => Group::all() 10 ]); 11 } 12 } 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.
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.
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.
We’re going to be abstracting much of the validation out of the controllers and into new form classes.
1 <?php 2 3 use Illuminate\Support\MessageBag; 4 5 class BaseForm 6 { 7 protected $passes; 8 protected $errors; 9 10 public function __construct() 11 { 12 $errors = new MessageBag(); 13 14 if ($old = Input::old("errors")) 15 { 16 $errors = $old; 17 } 18 19 $this->errors = $errors; 20 } 21 22 public function isValid($rules) 23 { 24 $validator = Validator::make(Input::all(), $rules); 25 $this->passes = $validator->passes(); 26 $this->errors = $validator->errors(); 27 return $this->passes; 28 } 29 30 public function getErrors() 31 { 32 return $this->errors; 33 } 34 35 public function setErrors(MessageBag $errors) 36 { 37 $this->errors = $errors; 38 return $this; 39 } 40 41 public function hasErrors() 42 { 43 return $this->errors->any(); 44 } 45 46 public function getError($key) 47 { 48 return $this->getErrors()->first($key); 49 } 50 51 public function isPosted() 52 { 53 return Input::server("REQUEST_METHOD") == "POST"; 54 } 55 } 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.
1 <?php 2 3 class GroupForm 4 extends BaseForm 5 { 6 public function isValidForAdd() 7 { 8 return $this->isValid([ 9 "name" => "required" 10 ]); 11 } 12 13 public function isValidForEdit() 14 { 15 return $this->isValid([ 16 "id" => "exists:group,id", 17 "name" => "required" 18 ]); 19 } 20 21 public function isValidForDelete() 22 { 23 return $this->isValid([ 24 "id" => "exists:group,id" 25 ]); 26 } 27 } 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.
1 {{ Form::label("name", "Name") }} 2 {{ Form::text("name", Input::old("name"), [ 3 "placeholder" => "new group" 4 ]) }} 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.
1 <?php 2 3 Form::macro("field", function($options) 4 { 5 $markup = ""; 6 7 $type = "text"; 8 9 if (!empty($options["type"])) 10 { 11 $type = $options["type"]; 12 } 13 14 if (empty($options["name"])) 15 { 16 return; 17 } 18 19 $name = $options["name"]; 20 21 $label = ""; 22 23 if (!empty($options["label"])) 24 { 25 $label = $options["label"]; 26 } 27 28 $value = Input::old($name); 29 30 if (!empty($options["value"])) 31 { 32 $value = Input::old($name, $options["value"]); 33 } 34 35 $placeholder = ""; 36 37 if (!empty($options["placeholder"])) 38 { 39 $placeholder = $options["placeholder"]; 40 } 41 42 $class = ""; 43 44 if (!empty($options["class"])) 45 { 46 $class = " " . $options["class"]; 47 } 48 49 $parameters = [ 50 "class" => "form-control" . $class, 51 "placeholder" => $placeholder 52 ]; 53 54 $error = ""; 55 56 if (!empty($options["form"])) 57 { 58 $error = $options["form"]->getError($name); 59 } 60 61 if ($type !== "hidden") 62 { 63 $markup .= "<div class='form-group"; 64 $markup .= ($error ? " has-error" : ""); 65 $markup .= "'>"; 66 } 67 68 switch ($type) 69 { 70 case "text": 71 { 72 $markup .= Form::label($name, $label, [ 73 "class" => "control-label" 74 ]); 75 76 $markup .= Form::text($name, $value, $parameters); 77 78 break; 79 } 80 81 case "password": 82 { 83 $markup .= Form::label($name, $label, [ 84 "class" => "control-label" 85 ]); 86 87 $markup .= Form::password($name, $parameters); 88 89 break; 90 } 91 92 case "checkbox": 93 { 94 $markup .= "<div class='checkbox'>"; 95 $markup .= "<label>"; 96 $markup .= Form::checkbox($name, 1, !!$value); 97 $markup .= " " . $label; 98 $markup .= "</label>"; 99 $markup .= "</div>"; 100 101 break; 102 } 103 104 case "hidden": 105 { 106 $markup .= Form::hidden($name, $value); 107 break; 108 } 109 } 110 111 if ($error) 112 { 113 $markup .= "<span class='help-block'>"; 114 $markup .= $error; 115 $markup .= "</span>"; 116 } 117 118 if ($type !== "hidden") 119 { 120 $markup .= "</div>"; 121 } 122 123 return $markup; 124 }); 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.
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:
1 require app_path() . "/macros.php"; 1 @extends("layout") 2 @section("content") 3 {{ Form::open([ 4 "route" => "group/add", 5 "autocomplete" => "off" 6 ]) }} 7 {{ Form::field([ 8 "name" => "name", 9 "label" => "Name", 10 "form" => $form, 11 "placeholder" => "new group" 12 ])}} 13 {{ Form::submit("save") }} 14 {{ Form::close() }} 15 @stop 16 @section("footer") 17 @parent 18 <script src="//polyfill.io"></script> 19 @stop 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.
1 .help-block 2 { 3 float : left; 4 clear : left; 5 } 6 7 .form-group.has-error .help-block 8 { 9 color : #ef7c61; 10 } 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:
1 public function addAction() 2 { 3 $form = new GroupForm(); 4 5 if ($form->isPosted()) 6 { 7 if ($form->isValidForAdd()) 8 { 9 Group::create([ 10 "name" => Input::get("name") 11 ]); 12 13 return Redirect::route("group/index"); 14 } 15 16 return Redirect::route("group/add")->withInput([ 17 "name" => Input::get("name"), 18 "errors" => $form->getErrors() 19 ]); 20 } 21 22 return View::make("group/add", [ 23 "form" => $form 24 ]); 25 } 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.
The view and action for editing groups is much the same as for adding groups.
1 @extends("layout") 2 @section("content") 3 {{ Form::open([ 4 "url" => URL::full(), 5 "autocomplete" => "off" 6 ]) }} 7 {{ Form::field([ 8 "name" => "name", 9 "label" => "Name", 10 "form" => $form, 11 "placeholder" => "new group", 12 "value" => $group->name 13 ]) }} 14 {{ Form::submit("save") }} 15 {{ Form::close() }} 16 @stop 17 @section("footer") 18 @parent 19 <script src="//polyfill.io"></script> 20 @stop 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.
1 public function editAction() 2 { 3 $form = new GroupForm(); 4 5 $group = Group::findOrFail(Input::get("id")); 6 $url = URL::full(); 7 8 if ($form->isPosted()) 9 { 10 if ($form->isValidForEdit()) 11 { 12 $group->name = Input::get("name"); 13 $group->save(); 14 return Redirect::route("group/index"); 15 } 16 17 return Redirect::to($url)->withInput([ 18 "name" => Input::get("name"), 19 "errors" => $form->getErrors(), 20 "url" => $url 21 ]); 22 } 23 24 return View::make("group/edit", [ 25 "form" => $form, 26 "group" => $group 27 ]); 28 } 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…
1 Route::any("/group/edit", [ 2 "as" => "group/edit", 3 "uses" => "GroupController@editAction" 4 ]); 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.
1 @extends("layout") 2 @section("content") 3 @if (count($groups)) 4 <table> 5 <tr> 6 <th>name</th> 7 <th> </th> 8 </tr> 9 @foreach ($groups as $group) 10 <tr> 11 <td>{{ $group->name }}</td> 12 <td> 13 <a href="{{ URL::route("group/edit") }}?id={{ $group->id \ 14 }}">edit</a> 15 <a href="{{ URL::route("group/delete") }}?id={{ $group->i\ 16 d }}" class="confirm" data-confirm="Are you sure you want to delete this group?">\ 17 delete</a> 18 </td> 19 </tr> 20 @endforeach 21 </table> 22 @else 23 <p>There are no groups.</p> 24 @endif 25 <a href="{{ URL::route("group/add") }}">add group</a> 26 @stop 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…
1 Route::any("/group/delete", [ 2 "as" => "group/delete", 3 "uses" => "GroupController@deleteAction" 4 ]); Since we’ve chosen such an easy method of deleting groups, the action is pretty straightforward:
1 public function deleteAction() 2 { 3 $form = new GroupForm(); 4 5 if ($form->isValidForDelete()) 6 { 7 $group = Group::findOrFail(Input::get("id")); 8 $group->delete(); 9 } 10 11 return Redirect::route("group/index"); 12 } 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:
1 (function($){ 2 $(".confirm").on("click", function() { 3 return confirm($(this).data("confirm")); 4 }); 5 }(jQuery)); 1 @section("footer") 2 @parent 3 <script src="/js/jquery.js"></script> 4 <script src="/js/layout.js"></script> 5 @stop 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.
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…
1 <?php 2 3 use Illuminate\Database\Schema\Blueprint; 4 5 class CreateResourceTable 6 extends BaseMigration 7 { 8 public function up() 9 { 10 Schema::create("resource", function(Blueprint $table) 11 { 12 $this 13 ->setTable($table) 14 ->addPrimary() 15 ->addString("name") 16 ->addString("pattern") 17 ->addString("target") 18 ->addBoolean("secure") 19 ->addTimestamps(); 20 }); 21 } 22 23 public function down() 24 { 25 Schema::dropIfExists("resource"); 26 } 27 } If you’re skipping migrations; the following SQL should create the same table structure as the migration:
1 CREATE TABLE `resource` ( 2 `id` int(10) unsigned NOT NULL AUTO_INCREMENT, 3 `name` varchar(255) DEFAULT NULL, 4 `pattern` varchar(255) DEFAULT NULL, 5 `target` varchar(255) DEFAULT NULL, 6 `secure` tinyint(1) DEFAULT NULL, 7 `created_at` datetime DEFAULT NULL, 8 `updated_at` datetime DEFAULT NULL, 9 `deleted_at` datetime DEFAULT NULL, 10 PRIMARY KEY (`id`) 11 ) 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.
1 <?php 2 3 class Resource 4 extends Eloquent 5 { 6 protected $table = "resource"; 7 8 protected $softDelete = true; 9 10 protected $guarded = [ 11 "id", 12 "created_at", 13 "updated_at", 14 "deleted_at" 15 ]; 16 17 public function groups() 18 { 19 return $this->belongsToMany("Group")->withTimestamps(); 20 } 21 } 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!
We also need to add the reverse relationship to the Group model:
1 public function resources() 2 { 3 return $this->belongsToMany("Resource")->withTimestamps(); 4 } We can also define relationships for users and groups, as in the following examples:
1 public function users() 2 { 3 return $this->belongsToMany("User")->withTimestamps(); 4 } 1 public function groups() 2 { 3 return $this->belongsToMany("Group")->withTimestamps(); 4 } 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.
1 <?php 2 3 use Illuminate\Database\Schema\Blueprint; 4 5 class CreateGroupUserTable 6 extends BaseMigration 7 { 8 public function up() 9 { 10 Schema::create("group_user", function(Blueprint $table) 11 { 12 $this 13 ->setTable($table) 14 ->addPrimary() 15 ->addForeign("group_id") 16 ->addForeign("user_id") 17 ->addTimestamps(); 18 }); 19 } 20 21 public function down() 22 { 23 Schema::dropIfExists("group_user"); 24 } 25 } 1 <?php 2 3 use Illuminate\Database\Schema\Blueprint; 4 5 class CreateGroupResourceTable 6 extends BaseMigration 7 { 8 public function up() 9 { 10 Schema::create("group_resource", function(Blueprint $table) 11 { 12 $this 13 ->setTable($table) 14 ->addPrimary() 15 ->addForeign("group_id") 16 ->addForeign("resource_id") 17 ->addTimestamps(); 18 }); 19 } 20 21 public function down() 22 { 23 Schema::dropIfExists("group_resource"); 24 } 25 } 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:
1 CREATE TABLE `group_resource` ( 2 `id` int(10) unsigned NOT NULL AUTO_INCREMENT, 3 `group_id` int(11) DEFAULT NULL, 4 `resource_id` int(11) DEFAULT NULL, 5 `created_at` datetime DEFAULT NULL, 6 `updated_at` datetime DEFAULT NULL, 7 `deleted_at` datetime DEFAULT NULL, 8 PRIMARY KEY (`id`), 9 KEY `group_resource_group_id_index` (`group_id`), 10 KEY `group_resource_resource_id_index` (`resource_id`) 11 ) ENGINE=InnoDB CHARSET=utf8; 12 13 CREATE TABLE `group_user` ( 14 `id` int(10) unsigned NOT NULL AUTO_INCREMENT, 15 `group_id` int(11) DEFAULT NULL, 16 `user_id` int(11) DEFAULT NULL, 17 `created_at` datetime DEFAULT NULL, 18 `updated_at` datetime DEFAULT NULL, 19 `deleted_at` datetime DEFAULT NULL, 20 PRIMARY KEY (`id`), 21 KEY `group_user_group_id_index` (`group_id`), 22 KEY `group_user_user_id_index` (`user_id`) 23 ) 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.
1 <div class="assign"> 2 @foreach ($resources as $resource) 3 <div class="checkbox"> 4 {{ Form::checkbox("resource_id[]", $resource->id, $group->resources->\ 5 contains($resource->id)) }} 6 {{ $resource->name }} 7 </div> 8 @endforeach 9 </div> 1 <div class="assign"> 2 @foreach ($users as $user) 3 <div class="checkbox"> 4 {{ Form::checkbox("user_id[]", $user->id, $group->users->contains($us\ 5 er->id)) }} 6 {{ $user->username }} 7 </div> 8 @endforeach 9 </div> 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).
1 @extends("layout") 2 @section("content") 3 {{ Form::open([ 4 "url" => URL::full(), 5 "autocomplete" => "off" 6 ]) }} 7 {{ Form::field([ 8 "name" => "name", 9 "label" => "Name", 10 "form" => $form, 11 "placeholder" => "new group", 12 "value" => $group->name 13 ])}} 14 @include("user/assign") 15 @include("resource/assign") 16 {{ Form::submit("save") }} 17 {{ Form::close() }} 18 @stop 19 @section("footer") 20 @parent 21 <script src="//polyfill.io"></script> 22 @stop 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…
1 return View::make("group/edit", [ 2 "form" => $form, 3 "group" => $group, 4 "users" => User::all(), 5 "resources" => Resource::where("secure", true)->get() 6 ]); 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:
1 <?php 2 3 class ResourceSeeder 4 extends DatabaseSeeder 5 { 6 public function run() 7 { 8 $resources = [ 9 [ 10 "pattern" => "/", 11 "name" => "user/login", 12 "target" => "UserController@loginAction", 13 "secure" => false 14 ], 15 [ 16 "pattern" => "/request", 17 "name" => "user/request", 18 "target" => "UserController@requestAction", 19 "secure" => false 20 ], 21 [ 22 "pattern" => "/reset", 23 "name" => "user/reset", 24 "target" => "UserController@resetAction", 25 "secure" => false 26 ], 27 [ 28 "pattern" => "/logout", 29 "name" => "user/logout", 30 "target" => "UserController@logoutAction", 31 "secure" => true 32 ], 33 [ 34 "pattern" => "/profile", 35 "name" => "user/profile", 36 "target" => "UserController@profileAction", 37 "secure" => true 38 ], 39 [ 40 "pattern" => "/group/index", 41 "name" => "group/index", 42 "target" => "GroupController@indexAction", 43 "secure" => true 44 ], 45 [ 46 "pattern" => "/group/add", 47 "name" => "group/add", 48 "target" => "GroupController@addAction", 49 "secure" => true 50 ], 51 [ 52 "pattern" => "/group/edit", 53 "name" => "group/edit", 54 "target" => "GroupController@editAction", 55 "secure" => true 56 ], 57 [ 58 "pattern" => "/group/delete", 59 "name" => "group/delete", 60 "target" => "GroupController@deleteAction", 61 "secure" => true 62 ] 63 ]; 64 65 foreach ($resources as $resource) 66 { 67 Resource::create($resource); 68 } 69 } 70 } We should also add this seeder to the DatabaseSeeder class so that the Artisan commands which deal with seeding pick it up:
1 <?php 2 3 class DatabaseSeeder 4 extends Seeder 5 { 6 public function run() 7 { 8 Eloquent::unguard(); 9 10 $this->call("ResourceSeeder"); 11 $this->call("UserSeeder"); 12 } 13 } 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.
1 if ($form->isValidForEdit()) 2 { 3 $group->name = Input::get("name"); 4 $group->save(); 5 6 $group->users()->sync(Input::get("user_id", [])); 7 $group->resources()->sync(Input::get("resource_id", [])); 8 9 return Redirect::route("group/index"); 10 } 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…
1 .assign 2 { 3 padding : 10px 0 0 0; 4 line-height : 22px; 5 } 6 .checkbox, .assign 7 { 8 float : left; 9 clear : left; 10 } 11 .checkbox input[type='checkbox'] 12 { 13 margin : 0 10px 0 0; 14 float : none; 15 } 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.
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.
1 <?php 2 3 Route::group(["before" => "guest"], function() 4 { 5 $resources = Resource::where("secure", false)->get(); 6 7 foreach ($resources as $resource) 8 { 9 Route::any($resource->pattern, [ 10 "as" => $resource->name, 11 "uses" => $resource->target 12 ]); 13 } 14 }); 15 16 Route::group(["before" => "auth"], function() 17 { 18 $resources = Resource::where("secure", true)->get(); 19 20 foreach ($resources as $resource) 21 { 22 Route::any($resource->pattern, [ 23 "as" => $resource->name, 24 "uses" => $resource->target 25 ]); 26 } 27 }); 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.
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.
1 Route::filter("auth", function() 2 { 3 if (Auth::guest()) 4 { 5 return Redirect::route("user/login"); 6 } 7 else 8 { 9 foreach (Auth::user()->groups as $group) 10 { 11 foreach ($group->resources as $resource) 12 { 13 $path = Route::getCurrentRoute()->getPath(); 14 15 if ($resource->pattern == $path) 16 { 17 return; 18 } 19 } 20 } 21 22 return Redirect::route("user/login"); 23 } 24 }); 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.
Lastly, we need a way to hide links to disallowed resources…
1 <?php 2 3 if (!function_exists("allowed")) 4 { 5 function allowed($route) 6 { 7 if (Auth::check()) 8 { 9 foreach (Auth::user()->groups as $group) 10 { 11 foreach ($group->resources as $resource) 12 { 13 if ($resource->name == $route) 14 { 15 return true; 16 } 17 } 18 } 19 } 20 21 return false; 22 } 23 } 1 require app_path() . "/helpers.php"; 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.
Try this out by wrapping the links of your application in a condition which references this method.