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.