Lets Do This! Part 3

All about dem Users.

Posted by lagbox on May 28, 2016 laravel tutorial

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.

Part 3 Repository

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 without php infront of it is because I have that command aliased on my shell (php artisan). The same for tinker, its an alias for php 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, and present 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 use deleting event instead of the deleted event. The deleting event is fired before the delete actually happens on the database. The deleted 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.