All about dem Users
As requested we will be adding some User related sh...tuff. Letting users edit their information and allowing Admin users to add new users and update any user.
- We will create a Policy for our User model
- Add a UserController and some views
- Use a Form Request to do our validation
- Do some tricks with our base Form Request
- Use Eloquent Events
- MAWR
artisan
use, as always
If you haven't been following check out Part 1 and Part 2, this is a continuation of the series.
- All about dem Users
- Policy
- Create
- Register
- Fill in the Policy methods
- Controller time ...
- Form Request for something different
- Choices, damn
- Some things to consider about deleting a User
- How about our views
- Index
- Edit
- Update our layout for Users link
- Routes
- Check your User index page
- Finish
- Notes
- Homework, yes really
- Next Up
Policy
Create
No surprise here.
⚡ artisan make:policy UserPolicy
Policy created successfully.
I can't remember if I had explained this. The reason I can call
artisan
withoutphp
infront of it is because I have that command aliased on my shell (php artisan
). The same fortinker
, its an alias forphp artisan tinker
for me.
Register
Now lets register this policy with the Authorization system (Gate)
AuthServiceProvider
protected $policies = [
App\Data::class => App\Policies\DataPolicy::class,
App\User::class => App\Policies\UserPolicy::class,
];
If you look at the repository you will see I chose to use aliases for these, which is why this will look different than the repository. It doesn't really matter as long as the correct class names make it into these arrays. Just showing some variety for y`all. :)
Fill in the Policy methods
We are going to do the same thing we did in the previous part and make a bunch of methods that all call the same private method to decide if the current authed user is the user resource we want to authorize.
Similarly we are going to use a before
method to check if the current authenticated user is an Admin and allow them to have full access.
namespace App\Policies;
use App\User;
use Illuminate\Auth\Access\HandlesAuthorization;
class UserPolicy
{
use HandlesAuthorization;
// allow an admin to pass through
public function before($user, $ability)
{
if ($user->isAdmin()) {
return true;
}
}
public function show(User $user, User $other)
{
return $this->owner($user, $other);
}
public function edit(User $user, User $other)
{
return $this->owner($user, $other);
}
public function update(User $user, User $other)
{
return $this->edit($user, $other);
}
public function delete(User $user, User $other)
{
return $this->edit($user, $other);
}
protected function owner(User $user, User $other)
{
return $user->id == $other->id;
}
}
We are not defining create
or store
as we want regular users to be denied for these actions. The before
will allow admins any abilities, defined or not on this Policy.
Controller time ...
You know what is coming right? ... Thats right more artisan.
⚡ artisan make:controller UserController --resource
Controller created successfully.
As usual, lets do the work and fill in these methods. We are not going to validate via the controller on this one, for some variety we will use a Form Request, which will be defined shortly.
Unlike our DataController
this controller will use the auth
middleware. A user must be logged in to be able to interact with any of this.
namespace App\Http\Controllers;
use App\User;
use Illuminate\Http\Request;
use App\Http\Requests;
class UserController extends Controller
{
public function __construct()
{
$this->middleware('auth');
}
public function index()
{
return view('users.index', [
'users' => User::paginate(20),
]);
}
public function create()
{
// just to see if the current user can do this action
$this->authorize(new User);
return view('users.edit', [
'user' => new User,
'action' => 'Create',
]);
}
public function store(Requests\UserRequest $request)
{
// just to see if the current user can do this action
$this->authorize(new User);
$data = $request->all();
// hash the password
$data['password'] = bcrypt($data['password']);
User::create($data);
return redirect()->route('users.index')
->withMsg('User was created.');
}
public function edit(User $user)
{
// can the current user edit this user
$this->authorize($user);
return view('users.edit', [
'user' => $user,
'action' => 'Update',
]);
}
public function update(Requests\UserRequest $request, User $user)
{
// can the current user update this user
$this->authorize($user);
$data = $request->all();
// if there was no password passed, remove that field
// if there was, lets hash it
if (empty($data['password'])) {
unset($data['password']);
} else {
$data['password'] = bcrypt($data['password']);
}
$user->update($data);
return redirect()->route('users.index')
->withMsg('User was updated.');
}
public function destroy(Request $request, User $user)
{
// can the current user delete this user
$this->authorize('delete', $user);
// if they deleted themselves ... log them out?
if ($user->id == $request->user()->id) {
auth()->logout();
}
$user->delete();
return redirect()->route('users.index')
->withMsg('User was deleted.');
}
}
As previously we will use the authorize
method of the controller to do our ability checking. We are passing in a User model so it knows to use the UserPolicy
we created.
Form Request for something different
We are going to use a Form Request to do our validation this time. The rules will look something like what is in the AuthController@validate
:
[
'name' => 'required|max:255',
'email' => 'required|email|max:255|unique:users',
'password' => 'required|min:6|confirmed',
]
We are going to use the same Form Request for our creation and update, which causes a little bit of an issue. The way the unique
rule works, we will need to have it ignore the current record when updating. If we leave this how it is we will always get an error about email
not being unique when trying to update.
To get around this we will make an adjustment to our Base Request, App\Http\Requests\Request
to add a method, appendUnique
to help us with this. You can read my other post about this method, Form Request for Store and Update
We are going to make that class look like this:
namespace App\Http\Requests;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Foundation\Http\FormRequest;
abstract class Request extends FormRequest
{
protected $updateMethods = ['PUT', 'PATCH'];
protected function appendUnique($key, $keyName = 'id')
{
if (in_array($this->method(), $this->updateMethods) && $param = $this->route($key)) {
if ($param instanceof Model) {
$app = "{$param->getKey()},{$param->getKeyName()}";
} else {
$app = "{$param},{$keyName}";
}
return ','. $app;
}
}
}
Now lets create our UserRequest
Form Request. (Yes, really, with artisan again)
⚡ artisan make:request UserRequest
Request created successfully.
Now lets add our rules to this and be done with it :)
public function rules()
{
return [
'name' => 'required|max:255',
'email' => 'required|email|max:255|unique:users,email'. $this->appendUnique('user'),
'password' => 'required|min:6|confirmed',
];
}
Now we can use this Form Request for our store
method and our update
method.
Choices, damn
The issue now is we have to decide how we want to handle the password field for updates. If we choice the idea that when leaving it blank we are not updating their password, we also need to adjust the password rules to present
as it won't be 'filled' when we are updating a record and not updating the password. Lets just say we will go that way.
We will add another helper method to our base request for this named filledOnCreate
. This method will check what HTTP method the request is using, similar to the other method. If it is a POST we know its for creating a record. If it is PUT or PATCH we know we are updating. When updating we will adjust the rule to use present
, "The field under validation must be present in the input data but can be empty." For create we will use required
like normal.
Lets update our base Request:
protected function filledOnCreate()
{
// if we are updating
if (in_array($this->method(), $this->updateMethods)) {
return 'present';
} else {
return 'required';
}
}
Sweet! Lets update our Form Request now:
return [
'name' => 'required|max:255',
'email' => 'required|email|max:255|unique:users,email'. $this->appendUnique('user'),
'password' => $this->filledOnCreate() .'|min:6|confirmed',
];
Alright, how nice is that, multipurpose.
You don't have to do this at all. You can check the docs for how the
unique
,required
, andpresent
rules work. This is a complicated and tricky example I am showing you here. If this seems too much, you can easily break this up into separate Form Requests to keep the difference in the rules separate for yourself.
Some things to consider about deleting a User
When deleting a User what should happen to their data? Should it also be deleted, or just disassociated from them? This will depend upon what you want to do. For the sake of this tutorial we will delete their Data records.
Lets register an event listener for deleting
event that Eloquent will fire when we are deleting a user. Open up App\Providers\EventServiceProvider
and adjust like so:
use App\User;
...
public function boot(DispatcherContract $events)
{
parent::boot($events);
User::deleting(function (User $user) {
// delete each Data model associated with the user
foreach ($user->data as $data) {
$data->delete();
}
});
}
Now after you delete a user, their Data records will also be removed.
We are using
deleting
event here to avoid any foreign key constraint issues. Depending on what database you are using it may or may not constrain the foreign keys, so to be safe we will usedeleting
event instead of thedeleted
event. Thedeleting
event is fired before the delete actually happens on the database. Thedeleted
event is fired after the record has been deleted from the database.
How about our views
Index
@extends('layouts.app')
@section('content')
<div class="container">
<div class="row">
<div class="col-md-10 col-md-offset-1">
<div class="panel panel-default">
<div class="panel-heading">
Users
</div>
<div class="panel-body">
@if (Session::has('msg'))
<div class="alert alert-info">
<p>
{{ Session::get('msg') }}
</p>
</div>
@endif
@can ('create', new \App\User)
<span class="pull-right">
<a href="{{ route('users.create') }}" class="btn btn-success">Add New User</a>
</span>
@endcan
<table class="table table-striped">
<thead>
<tr>
<th>#</th>
<th>Name</th>
<th>Email</th>
<th>Created At</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
@forelse($users as $user)
<tr>
<th scope="row">{{ $user->id }}</th>
<td>{{ $user->name }}</td>
<td>{{ $user->email }}</td>
<td>{{ $user->created_at->diffForHumans() }}</td>
<td>
@can ('edit', $user)
<a href="{{ route('users.edit', [$user->id]) }}" class="btn btn-primary">Edit</a>
@endcan
@can ('delete', $user)
<form method="POST" action="{{ route('users.destroy', [$user->id]) }}" style="display:inline">
<input type="hidden" name="_method" value="DELETE">
{{ csrf_field() }}
<input type="submit" value="Delete" class="btn btn-danger">
</form>
@endcan
</td>
<td></td>
</tr>
@empty
<tr>
<td colspan="5" align="center">No Data to display.</td>
</tr>
@endforelse
</tbody>
</table>
{{ $users->render() }}
</div>
</div>
</div>
</div>
</div>
@endsection
Edit
@extends('layouts.app')
@section('content')
<div class="container">
<div class="row">
<div class="col-md-10 col-md-offset-1">
<div class="panel panel-default">
<div class="panel-heading">{{ $action }} User</div>
<div class="panel-body">
@if (count($errors) > 0)
<div class="alert alert-danger">
<ul>
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
{{ Form::model($user, [
'route' => $user->exists ? ['users.update', $user] : 'users.store',
'class' => 'form',
'role' => 'form',
'method' => $user->exists ? 'PUT' : 'POST'
]) }}
<div class="form-group {{ $errors->has('name') ? 'has-error' : '' }}">
<label for="name">Name</label>
{{ Form::text('name', null, [
'class' => 'form-control',
'placeholder' => 'Name'
]) }}
</div>
<div class="form-group {{ $errors->has('email') ? 'has-error' : '' }}">
<label for="email">Email</label>
{{ Form::text('email', null, [
'class' => 'form-control',
'placeholder' => 'Email'
]) }}
</div>
<div class="form-group {{ $errors->has('password') ? 'has-error' : '' }}">
<label for="password">Password
@if ($user->exists)
<small>Leave this and the following field blank to not set a new password</small>
@endif
</label>
{{ Form::password('password', [
'class' => 'form-control',
'placeholder' => 'Password'
]) }}
</div>
<div class="form-group {{ $errors->has('password') ? 'has-error' : '' }}">
<label for="password_confirmation">Password Confirm</label>
{{ Form::password('password_confirmation', [
'class' => 'form-control',
'placeholder' => 'Password Confirmation'
]) }}
</div>
<div class="form-group">
<button type="submit" class="btn btn-primary">Submit</button>
</div>
{{ Form::close() }}
</div>
</div>
</div>
</div>
</div>
@endsection
Update our layout for Users link
Remember, we only want logged in users to be able to access this.
<ul class="nav navbar-nav">
<li><a href="{{ url('/home') }}">Home</a></li>
<li><a href="{{ route('data.index') }}">Data</a></li>
@if (Auth::check())
<li><a href="{{ route('users.index') }}">Users</a></li>
@endif
</ul>
That wasn't so bad.
Routes
For this it is pretty simple, its a one liner. We will exclude the show
route as we don't need one for this resource. Add to routes.php
:
Route::resource('users', 'UserController', ['except' => 'show']]);
Now if you are paying attention and know how the Resource Registrar works you may notice a inconsistency between this resource definition and the parameters of the UserController
. We defined the parameters for our UserController
methods as User $user
not User $users
. This will not allow us to use Implicit model bindings as the URI for these routes will contain {users}
not {user}
so it doesn't match our parameters. We can get around this pretty easily.
We are going to tell the Router that all our Resource Parameters should be singular, no matter what the Resource name actually is via our App\Providers\RouteServiceProvider
:
public function boot(Router $router)
{
$router->singularResourceParameters();
parent::boot($router);
}
There you go, now all your Resource routes will use singular parameter names. To read some more about the Resource Registrar check out my article "That Resource Registrar Thingy".
Check your User index page
Depending on which user you are logged in as, you will see different things. If you are logged in as Bob you will be able to edit and delete yourself.
If you are logged in as the admin you will have full access.
Finish
Again, you have done a great job and have been a lovely audience. You have a simple User Administration now.
Notes
As always, this is one way to go about these things. This is all meant to be a quick run through and is using what I think to be the simplest ways, to get these things done fast.
In this section I chose to introduce some new concepts and do some minor things differently. You may have noticed the lack of compact
function being used. Also adding flash data on the redirect was done differently. Just trying to expose you to different means to the same ends.
In all of these parts so far, I have purposefully done some things that could have been done differently. There were some things named differently on purpose, to show how to go about some things in different ways. Can you spot the different places where method names could have been adjusted?
Homework, yes really
See if you can figure out how to make a regular user an Admin user. My suggestion would be to create another method on the UserController
and add a route specifically for this purpose that only and Admin can have access to.
Next Up
So far it looks like perhaps there will be a Part 3.5/4 with some polishing of what has been covered. The next full part will probably be Testing. If anyone has any other suggestions please feel free to leave a comment.