六、用户CRUD

六、用户CRUD

一、开发目标

前面我们已经成功实现了用户的登录和注册功能,本章我们将补充前面未完成的几个 RESTful 动作:

  • edit 个人资料修改页面

  • update 个人资料提交更改

  • index 用户列表

  • destroy 删除用户

接下来我们将添加更新用户的操作,来支持用户对自己个人资料的更改。然后在开发用户列表页面,将所有用户在网站上以分页的形式显示出来。在本章最后,我们还会为指定用户添加管理员权限,让管理员执行删除用户的操作。

二、更新用户

1、更新用户

创建一个新分支,并在该分支上进行功能开发:

$ git checkout master
$ git checkout -b user-crud

2、编辑表单

由于我们在前面构建用户展示页面时已经使用 resource 方法来为用户添加好完整的 RESTful 动作,因此我们不需要再为用户添加编辑页面的路由。但你需要知道,一个符合 RESTful 架构的用户编辑路由应该是像下面这样:

Route::get('/users/{user}/edit', 'UsersController@edit')->name('users.edit');

也即是说,当我们访问 /users/1/edit 页面时,编辑的是 id 为 1 的用户资料。

接下来让我们为用户控制器上加上编辑用户的操作。

app/Http/Controllers/UsersController.php

<?php

namespace App\Http\Controllers;

use App\Models\User;
use Illuminate\Http\Request;
use Auth;

class UsersController extends Controller
{

    public function edit(User $user)
    {
        return view('users.edit',compact('user'));
    }
}

新增的 edit 动作主要做了以下几个操作:

  1. 利用了 Laravel 的『隐性路由模型绑定』功能,直接读取对应 ID 的用户实例 $user,未找到则报错;

  2. 将查找到的用户实例 $user 与编辑视图进行绑定;

在将用户数据与视图进行绑定之后,便可以在视图上通过 $user 来访问用户对象。接下来让我们接着完成用户编辑页面的构建。

resources/views/users/edit.blade.php

@extends('layouts.default')
@section('title','更新个人资料')

@section('content')
    <div class="offset-md-2 col-md-8">
        <div class="card ">
            <div class="card-header">
                <h5>更新个人资料</h5>
            </div>
            <div class="card-body">

                @include('shared._errors')

                <div class="gravatar_edit">
                    <a href="http://gravatar.com/emails" target="_blank">
                        <img src="{{ $user->gravatar('200') }}" alt="{{ $user->name }}" class="gravatar"/>
                    </a>
                </div>

                <form method="POST" action="{{ route('users.update', $user->id )}}">
                    {{ method_field('PATCH') }}
                    {{ csrf_field() }}

                    <div class="form-group">
                        <label for="name">名称:</label>
                        <input type="text" name="name" class="form-control" value="{{ $user->name }}">
                    </div>

                    <div class="form-group">
                        <label for="email">邮箱:</label>
                        <input type="text" name="email" class="form-control" value="{{ $user->email }}" disabled>
                    </div>

                    <div class="form-group">
                        <label for="password">密码:</label>
                        <input type="password" name="password" class="form-control" value="{{ old('password') }}">
                    </div>

                    <div class="form-group">
                        <label for="password_confirmation">确认密码:</label>
                        <input type="password" name="password_confirmation" class="form-control" value="{{ old('password_confirmation') }}">
                    </div>

                    <button type="submit" class="btn btn-primary">更新</button>
                </form>
            </div>
        </div>
    </div>
@stop

代码讲解:

我们在用户头像编辑的位置使用了外部链接跳转,如果用户有更换头像的需要,则可以跳转到 Gravatar 官网上手动更改。在我们提交用户更新表单之后,将由用户控制器的 update 动作来做处理,因此我们需要把表单提交的请求地址指向用户更新的 URL 上。

<form method="POST" action="{{ route('users.update', $user->id )}}">

上面代码转为 HTML 后如下所示:

<form method="POST" action="http://weibo.test/users/1">

在 RESTful 架构中,我们使用 PATCH 动作来更新资源,但由于浏览器不支持发送 PATCH 动作,因此我们需要在表单中添加一个隐藏域来伪造 PATCH 请求。

{{ method_field('PATCH') }}

转换为 HTML 代码如下所示:

<input type="hidden" name="_method" value="PATCH">

在用户注册成功之后,邮箱便不允许更改,因此我们需要给邮箱输入框加上 disabled 属性来禁止用户输入:

<input type="text" name="email" class="form-control" value="{{ $user->email }}" disabled>

接下来让我们再来加一点样式,优化用户编辑视图。

resources/sass/app.scss

/* Users edit */.gravatar_edit {
  margin: 15px auto;
  text-align: center;
  .gravatar {
    float: none;
    max-width: 100px;
  }}

现在的编辑页面已能正常访问,我们需要将顶部导航栏的编辑资料链接进行更改,提供给用户访问编辑资料的入口。

resources/views/layouts/_header.blade.php

<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
  <div class="container ">
    <a class="navbar-brand" href="{{ route('home') }}">Weibo App</a>
    <ul class="navbar-nav justify-content-end">
      @if (Auth::check())
        <li class="nav-item"><a class="nav-link" href="#">用户列表</a></li>
        <li class="nav-item dropdown">
          <a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
            {{ Auth::user()->name }}
          </a>
          <div class="dropdown-menu" aria-labelledby="navbarDropdown">
            <a class="dropdown-item" href="{{ route('users.show', Auth::user()) }}">个人中心</a>
            <a class="dropdown-item" href="{{ route('users.edit', Auth::user()) }}">编辑资料</a>
            <div class="dropdown-divider"></div>
            <a class="dropdown-item" id="logout" href="#">
              <form action="{{ route('logout') }}" method="POST">
                {{ csrf_field() }}
                {{ method_field('DELETE') }}
                <button class="btn btn-block btn-danger" type="submit" name="button">退出</button>
              </form>
            </a>
          </div>
        </li>
      @else
        <li class="nav-item"><a class="nav-link" href="{{ route('help') }}">帮助</a></li>
        <li class="nav-item" ><a class="nav-link" href="{{ route('login') }}">登录</a></li>
      @endif
    </ul>
  </div>
</nav>

image.png

3、编辑失败

现在我们已完成用户更新表单的构建,接下来需要在用户控制器加上 update 动作来处理用户提交的个人信息。

app/Http/Controllers/UsersController.php

public function update(User $user, Request $request)
{
    $this->validate($request,[
        'name' => 'required|max:50',
        'password' => 'required|confirmed|min:6'
    ]);
    $user->update([
        'name'=> $request->name,
        'password' => bcrypt($request->password)
    ]);

    return redirect()->route('users.show',$user->id);
}

我们可以看到定义的 update 方法接收两个参数,第一个为自动解析用户 id 对应的用户实例对象,第二个则为更新用户表单的输入数据。在我们接收到用户提交的信息时,需要先对用户提交的信息进行验证,最终调用 update 方法对用户对象进行更新。在用户个人资料更新成功后,我们还需要将用户重定向到个人页面,方便用户第一时间查看到自己更改后的个人信息。

这时如果我们尝试提交错误信息并进行提交,将会显示如下界面,代表表单验证功能已可正常使用。

image.png

4、编辑成功

现在的用户编辑功能还有两个地方需要优化:

  1. 在每次更改个人资料的时候都输入完整的密码,才能更新其它信息,对于不想对密码进行更新的用户,这个过程会比较繁琐;

  2. 更新成功之后在页面上没有进行任何提示,而是直接跳转到用户的个人页面,用户体验非常不好;

接下来让我们针对这两个问题对 update 方法进行优化。

app/Http/Controllers/UsersController.php

public function update(User $user, Request $request)
{
    $this->validate($request,[
        'name' => 'required|max:50',
        'password' => 'nullable|confirmed|min:6'
    ]);

    $data = [];
    $data['name'] = $request->name;
    if ($request->password){
        $data['password'] = bcrypt($request->password);
    }
    $user->update($data);

    session()->flash('success','个人资料更新成功!');

    return redirect()->route('users.show',$user->id);
}

Git 代码版本控制

接着让我们将本次更改纳入版本控制中:

$ git add -A
$ git commit -m "更改用户资料"

三、权限系统

1、权限系统

现在的应用存在两个巨大的安全隐患:

  1. 未登录用户可以访问 edit 和 update 动作;

  2. 登录用户可以更新其它用户的个人信息;

接下来让我们针对这两个安全隐患进行修复。

2、必须先登陆

Laravel 中间件 (Middleware) 为我们提供了一种非常棒的过滤机制来过滤进入应用的 HTTP 请求,例如,当我们使用 Auth 中间件来验证用户的身份时,如果用户未通过身份验证,则 Auth 中间件会把用户重定向到登录页面。如果用户通过了身份验证,则 Auth 中间件会通过此请求并接着往下执行。Laravel 框架默认为我们内置了一些中间件,例如身份验证、CSRF 保护等。所有的中间件文件都被放在项目的 app/Http/Middleware 文件夹中。

接下来让我们使用 Laravel 提供身份验证(Auth)中间件来过滤未登录用户的 editupdate 动作。

app/Http/Controllers/UsersController.php

public function __construct()
{
    $this->middleware('auth',[
        'except' => ['show','create','store']
    ]);
}

__construct 是 PHP 的构造器方法,当一个类对象被创建之前该方法将会被调用。我们在 __construct 方法中调用了 middleware 方法,该方法接收两个参数,第一个为中间件的名称,第二个为要进行过滤的动作。我们通过 except 方法来设定 指定动作 不使用 Auth 中间件进行过滤,意为 —— 除了此处指定的动作以外,所有其他动作都必须登录用户才能访问,类似于黑名单的过滤机制。相反的还有 only 白名单方法,将只过滤指定动作。我们提倡在控制器 Auth 中间件使用中,首选 except 方法,这样的话,当你新增一个控制器方法时,默认是安全的,此为最佳实践。

Laravel 提供的 Auth 中间件在过滤指定动作时,如该用户未通过身份验证(未登录用户),默认将会被重定向到 /login 登录页面。

此时退出登录,再次尝试访问 http://weibo.test/users/1/edit 页面将会被重定向到登录页面。

image.png


3、用户只能编辑自己的资料

在完成对未登录用户的限制之后,接下来我们要限制的是已登录用户的操作,当 id 为 1 的用户去尝试更新 id 为 2 的用户信息时,我们应该返回一个 403 禁止访问的异常。在 Laravel 中可以使用 授权策略 (Policy) 来对用户的操作权限进行验证,在用户未经授权进行操作时将返回 403 禁止访问的异常。

我们可以使用以下命令来生成一个名为 UserPolicy 的授权策略类文件,用于管理用户模型的授权。

$ php artisan make:policy UserPolicy

所有生成的授权策略文件都会被放置在 app/Policies 文件夹下。

让我们为默认生成的用户授权策略添加 update 方法,用于用户更新时的权限验证。

app/Policies/UserPolicy.php

<?php

namespace App\Policies;

use App\Models\User;
use Illuminate\Auth\Access\HandlesAuthorization;

class UserPolicy
{
    use HandlesAuthorization;

    /**
     * Create a new policy instance.
     *
     * @return void
     */
    public function __construct()
    {
        //
    }

    public function update(User $currentUser , User $user)
    {
        return $currentUser->id === $user->id;
    }
}

update 方法接收两个参数,第一个参数默认为当前登录用户实例,第二个参数则为要进行授权的用户实例。当两个 id 相同时,则代表两个用户是相同用户,用户通过授权,可以接着进行下一个操作。如果 id 不相同的话,将抛出 403 异常信息来拒绝访问。

使用授权策略需要注意以下两点:

  1. 我们并不需要检查 $currentUser 是不是 NULL。未登录用户,框架会自动为其 所有权限 返回 false

  2. 调用时,默认情况下,我们 不需要 传递当前登录用户至该方法内,因为框架会自动加载当前登录用户(接着看下去,后面有例子);

接下来我们还需要在 AuthServiceProvider 类中对授权策略进行设置。AuthServiceProvider 包含了一个 policies 属性,该属性用于将各种模型对应到管理它们的授权策略上。我们需要为用户模型 User 指定授权策略 UserPolicy

app/Providers/AuthServiceProvider.php

<?php

namespace App\Providers;

use App\Models\User;
use App\Policies\UserPolicy;
use Illuminate\Support\Facades\Gate;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;

class AuthServiceProvider extends ServiceProvider
{
    /**
     * The policy mappings for the application.
     *
     * @var array
     */
    protected $policies = [
        'App\Model' => 'App\Policies\ModelPolicy',
        User::class => UserPolicy::class,
    ];

    /**
     * Register any authentication / authorization services.
     *
     * @return void
     */
    public function boot()
    {
        $this->registerPolicies();

        //
    }
}

授权策略定义完成之后,我们便可以通过在用户控制器中使用 authorize 方法来验证用户授权策略。默认的 App\Http\Controllers\Controller 类包含了 Laravel 的 AuthorizesRequests trait。此 trait 提供了 authorize方法,它可以被用于快速授权一个指定的行为,当无权限运行该行为时会抛出 HttpException。authorize 方法接收两个参数,第一个为授权策略的名称,第二个为进行授权验证的数据。

我们需要为 edit 和 update 方法加上这行:

$this->authorize('update', $user);

这里 update 是指授权类里的 update 授权方法,$user 对应传参 update 授权方法的第二个参数。正如上面定义 update 授权方法时候提起的,调用时,默认情况下,我们 不需要 传递第一个参数,也就是当前登录用户至该方法内,因为框架会自动加载当前登录用户。

书写的位置如下:

app/Http/Controllers/UsersController.php

<?php

namespace App\Http\Controllers;

use App\Models\User;
use Illuminate\Http\Request;
use Auth;

class UsersController extends Controller
{
    public function __construct()
    {
        $this->middleware('auth',[
            'except' => ['show','create','store']
        ]);
    }

    public function create()
    {
        return view('users.create');
    }

    public function show(User $user)
    {
        return view('users.show',compact('user'));
    }

    public function store(Request $request)
    {
        $this->validate($request,[
            'name' => 'required|max:50',
            'email' => 'required|email|unique:users|max:255',
            'password'=>'required|confirmed|min:6'
        ]);
        $user = User::create([
            'name'=>$request->name,
            'email'=>$request->email,
            'password'=>bcrypt($request->password),
        ]);
        Auth::login($user);
        session()->flash('success','欢迎,您将在这里开启一段新的旅程~');
        return redirect()->route('users.show',[$user]);
    }

    public function edit(User $user)
    {
        $this->authorize('update',$user);
        return view('users.edit',compact('user'));
    }

    public function update(User $user, Request $request)
    {
        $this->authorize('update',$user);
        $this->validate($request,[
            'name' => 'required|max:50',
            'password' => 'nullable|confirmed|min:6'
        ]);

        $data = [];
        $data['name'] = $request->name;
        if ($request->password){
            $data['password'] = bcrypt($request->password);
        }
        $user->update($data);

        session()->flash('success','个人资料更新成功!');

        return redirect()->route('users.show',$user->id);
    }

}


最后,我们需要创建第二个用户来测试一下授权功能,首先让我们使用此命令进入 Tinker 环境:

$ php artisan tinker

如果中途想要退出 Tinker,可使用 ctrl + c 快捷键。

通过下面命令创建 ID 为 4 的用户:

>>> App\Models\User::create(['name'=> 'Monkey', 'email'=>'monkey@example.com','password'=>bcrypt('password')])

image.png

现在,使用 id 为 1 的用户登录,当访问 id 为4 的用户编辑页面 —— http://weibo.test/users/4/edit ,系统将会拒绝访问。

image.png


4、友好的转向

当一个未登录的用户尝试访问自己的资料编辑页面时,将会自动跳转到登录页面,这时候如果用户再进行登录,则会重定向到其个人中心页面上,这种方式的用户体验并不好。更好的做法是,将用户重定向到他之前尝试访问的页面,即自己的个人编辑页面。redirect() 实例提供了一个 intended 方法,该方法可将页面重定向到上一次请求尝试访问的页面上,并接收一个默认跳转地址参数,当上一次请求记录为空时,跳转到默认地址上。

app/Http/Controllers/SessionsController.php

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Auth;

class SessionsController extends Controller
{
    public function create()
    {
        return view('sessions.create');
    }

    public function store(Request $request)
    {
        $credentials = $this->validate($request,[
            'email'=>'required|email|max:255',
            'password'=>'required'
        ]);
        if (Auth::attempt($credentials,$request->has('remember'))){
            //登录成功后的相关操作
            session()->flash('success','欢迎回来!');
            $fallback = route('users.show',Auth::user());
            return redirect()->intended($fallback);
        }else{
            //登录失败后的相关操作
            session()->flash('danger','很抱歉,您的邮箱和密码不匹配');
            return redirect()->back()->withInput();
        }

        return ;
    }

    public function destroy()
    {
        Auth::logout();
        session()->flash('success','您已成功推出!');
        return redirect('login');
    }
}

现在尝试退出登录,并访问 http://weibo.test/users/1/edit 页面,页面将重定向到登录页面,这时候接着使用 id 为 1 的用户进行登录,在登录成功后页面将重定向到用户编辑页面上:

5、注册与登录页面访问限制

现在我们的应用还有一个小问题,即已登录用户还能够对注册页面和登录页面进行访问:

image.png


这明显不符合常规逻辑。

我们除了可通过 Auth 中间件的 auth 属性来对控制器的一些动作进行过滤,只允许已登录用户访问之外。还可以使用 Auth 中间件提供的 guest 选项,用于指定一些只允许未登录用户访问的动作,因此我们需要通过对 guest 属性进行设置,只让未登录用户访问登录页面和注册页面。

只让未登录用户访问登录页面:

app/Http/Controllers/SessionsController.php

public function __construct()
{
    $this->middleware('guest',[
        'only'=>['create']
    ]);
}

只让未登录用户访问注册页面:

app/Http/Controllers/UsersController.php

public function __construct()
{
    $this->middleware('auth',[
        'except' => ['show','create','store']
    ]);

    $this->middleware('guest',[
        'only' => ['create']
    ]);
}

这时候访问 http://weibo.test/login :

image.png


会被跳转到 Laravel 默认指定的页面 /home ,因我们并没有此页面,所以会报错 404 找不到页面。我们需要修改下中间件里的 redirect() 方法调用,并加上友好的消息提醒:

app/Http/Middleware/RedirectIfAuthenticated.php

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Support\Facades\Auth;

class RedirectIfAuthenticated
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @param  string|null  $guard
     * @return mixed
     */
    public function handle($request, Closure $next, $guard = null)
    {
        if (Auth::guard($guard)->check()) {
            session()->flash('info','您已登录,无需再次操作');
            return redirect('/');
        }

        return $next($request);
    }
}

这时候当你再次访问 http://weibo.test/login :


image.png

Git 代码版本控制

接着让我们将本次更改纳入版本控制中:

$ git add -A
$ git commit -m "访问策略"

四、列出所有用户

1、列出所有用户

本节我们将从数据库取出所有用户数据,并在用户列表页面将所有用户进行展示,并在顶部导航添加访问入口。最后我们还会为 1 号加上管理员权限,让他可以删除其他的用户。

2、用户列表

根据我们前面使用 resource 方法生成的符合 RESTful 架构的路由可知,用户列表对应用户控制器的 index 动作,页面 URL 对应 /users。接下来我们将在用户控制器中加入 index 动作。并且因为用户列表的访问权限是公开的,所以我们还需要在 Auth 中间件 except 中新增 index 动作来允许游客访问。

app/Http/Controllers/UsersController.php

public function __construct()
{
    $this->middleware('auth',[
        'except' => ['show','create','store','index']
    ]);

    $this->middleware('guest',[
        'only' => ['create']
    ]);
}

public function index(){
    $users = User::all();
    return view('users.index',compact('users'));
}

可以看到,在 index 方法中,我们使用 Eloquent 用户模型将所有用户的数据一下子完全取出来了,这么做会影响应用的性能,后面我们再来对该代码进行优化,通过分页的方式来读取用户数据。在将用户数据取出之后,与 index 视图进行绑定,这样便可以在视图中使用 $users 来访问所有用户实例。

接下来让我们继续创建 index 视图,用于显示所有用户列表的信息。

resources/views/users/index.blade.php

@extends('layouts.default');
@section('title','所有用户');

@section('content')
    <div class="offset-md-2 col-md-8">
        <h2 class="mb-4 text-center">所有用户</h2>
        <div class="list-group list-group-flush">
            @foreach ($users as $user)
                <div class="list-group-item">
                    <img class="mr-3" src="{{ $user->gravatar() }}" alt="{{ $user->name }}" width=32>
                    <a href="{{ route('users.show', $user) }}">
                        {{ $user->name }}
                    </a>
                </div>
            @endforeach
        </div>
    </div>
@stop

我们使用 @foreach 的方法将所有用户的数据逐个输出,并在页面上显示他们的头像和用户名。

现在用户列表页已经可以访问了,接下来让我们对顶部导航进行编辑,为用户列表加上指定链接,方便用户跳转到用户列表页面进行查看。

resources/views/layouts/_header.blade.php

<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
  <div class="container ">
    <a class="navbar-brand" href="{{ route('home') }}">Weibo App</a>
    <ul class="navbar-nav justify-content-end">
      @if (Auth::check())
        <li class="nav-item"><a class="nav-link" href="{{route('users.index')}}">用户列表</a></li>
        <li class="nav-item dropdown">
          <a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
            {{ Auth::user()->name }}
          </a>
          <div class="dropdown-menu" aria-labelledby="navbarDropdown">
            <a class="dropdown-item" href="{{ route('users.show', Auth::user()) }}">个人中心</a>
            <a class="dropdown-item" href="{{ route('users.edit', Auth::user()) }}">编辑资料</a>
            <div class="dropdown-divider"></div>
            <a class="dropdown-item" id="logout" href="#">
              <form action="{{ route('logout') }}" method="POST">
                {{ csrf_field() }}
                {{ method_field('DELETE') }}
                <button class="btn btn-block btn-danger" type="submit" name="button">退出</button>
              </form>
            </a>
          </div>
        </li>
      @else
        <li class="nav-item"><a class="nav-link" href="{{ route('help') }}">帮助</a></li>
        <li class="nav-item" ><a class="nav-link" href="{{ route('login') }}">登录</a></li>
      @endif
    </ul>
  </div>
</nav>

在做完上面这一切工作之后,用户列表页面已经能够正常访问了,但现在还有两个问题:

  1. 注册用户太少;

  2. 用户列表页不支持分页浏览,用户量大的时候会影响性能和用户体验;

接下来让我们来着手解决这两个问题。

3、示例用户

在实际的项目开发过程中,我们经常会用到一些假数据来对数据库进行填充以方便调试程序,原始的做法是手工一个个在数据库中创建,或者从队友的机器那导出数据填充到开发机器中。Laravel 提供了一套更加现代化、非常简单易用的数据填充方案。接下来让我们使用 Laravel 提供的数据填充来批量生成假用户。

假数据的生成分为两个阶段:

  1. 对要生成假数据的模型指定字段进行赋值 - 『模型工厂』

  2. 批量生成假数据模型 - 『数据填充』

4、模型工厂

Laravel 默认为我们集成了 Faker 扩展包,使用该扩展包可以让我们很方便的生成一些假数据。

示例如下:

// 使用 factory 来创建一个 Faker\Generator 实例
$faker = Faker\Factory::create();
// 生成用户名
$faker->name; // "Janie Roob"// 
生成安全邮箱
$faker->safeEmail; // "claire.wuckert@example.net"// 
生成随机日期
$faker->date // "2011-02-10"// 
生成随机时间
$faker->time // "13:03:55"

我们可以借助 Faker 和 Eloquent 模型工厂来为指定模型的每个字段设置随机值。

本项目中生成的模型工厂如下:

database/factories/UserFactory.php

<?php

use Faker\Generator as Faker;

$factory->define(App\Models\User::class, function (Faker $faker) {
    return [
        'name' => $faker->name,
        'email' => $faker->unique()->safeEmail,
        'email_verified_at' => now(),
        'password' => '$2y$10$TKh8H1.PfQx37YgCzwiKb.KjNyWgaHb9cbcoQgdIVFlYg7B77UdFm', // secret
        'remember_token' => str_random(10),
    ];
});

我们使用生成的假日期对用户的创建时间和更新时间进行赋值。

5、数据填充

在 Laravel 中我们使用 Seeder 类来给数据库填充测试数据。所有的 Seeder 类文件都放在 database/seeds 目录下,文件名需要按照『驼峰式』来命名,且严格遵守大小写规范。Laravel 默认为我们定义了一个 DatabaseSeeder类,我们可以在该类中使用 call 方法来运行其它的 Seeder 类,以此控制数据填充的顺序。我们可以使用下面命令来生成一个 UsersTableSeeder 文件,用于填充用户相关的假数据。

$ php artisan make:seeder UsersTableSeeder

在我们定义好了用户模型工厂之后,便可以在生成的用户数据填充文件中使用 factory 这个辅助函数来生成一个使用假数据的用户对象。

现在让我们使用该方法来创建 50 个假用户。

database/seeds/UsersTableSeeder.php

<?php

use Illuminate\Database\Seeder;
use App\Models\User;

class UsersTableSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        $users = factory(User::class)->times(50)->make();
        User::insert($users->makeVisible(['password','remember_token'])->toArray());

        $user = User::find(1);
        $user->name = 'Summer';
        $user->email = 'summer@example.com';
        $user->save;
    }
}

times 和 make 方法是由 FactoryBuilder 类 提供的 API。times 接受一个参数用于指定要创建的模型数量,make 方法调用后将为模型创建一个 集合makeVisible 方法临时显示 User 模型里指定的隐藏属性 $hidden,接着我们使用了 insert 方法来将生成假用户列表数据批量插入到数据库中。最后我们还对第一位用户的信息进行了更新,方便后面我们使用此账号登录。

接着我们还需要在 DatabaseSeeder 中调用 call 方法来指定我们要运行假数据填充的文件。

database/seeds/DatabaseSeeder.php

<?php

use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    /**
     * Seed the application's database.
     *
     * @return void
     */
    public function run()
    {
        Model::unguard();
        $this->call(UsersTableSeeder::class);
        Model::reguard();
    }
}

完成上面操作之后,我们便可以开始为用户生成批量假数据了,在运行生成假数据的命令之前,我们需要使用 migrate:refresh 命令来重置数据库,之后再使用 db:seed 执行数据填充。

$ php artisan migrate:refresh
$ php artisan db:seed

如果我们要单独指定执行 UserTableSeeder 数据库填充文件,则可以这么做:

$ php artisan migrate:refresh
$ php artisan db:seed --class=UsersTableSeeder

你也可以使用下面一条命令来同时完成数据库的重置和填充操作:

$ php artisan migrate:refresh --seed

image.png



6、分页

现在我们已拥有足够多的用户了,接下来让我们着手开发用户列表页面的分页功能。在 Laravel 应用中,分页功能的开发非常简单,接下来让我们看下具体如何实现。

首先,我们需要先对用户控制器中获取所有用户数据的方法进行更改,修改如下。

app/Http/Controllers/UsersController.php

public function index(){
    $users = User::paginate(10);
    return view('users.index',compact('users'));
}

默认状况下,页面的当前页数由 HTTP 请求所带的 page 参数决定,当你访问 http://weibo.test/users?page=2 链接时,获取的是第二页的用户列表信息,Laravel 会自动检测到 page 的值并插入由分页器生成的链接中。在上面代码我们使用 paginate 方法来指定每页生成的数据数量为 10 条,即当我们有 50 个用户时,用户列表将被分为五页进行展示。

在调用 paginate 方法获取用户列表之后,便可以通过以下代码在用户列表页上渲染分页链接。

{!! $users->render() !!}

由 render 方法生成的 HTML 代码默认会使用 Bootstrap 框架的样式,渲染出来的视图链接也都统一会带上 ?page参数来设置指定页数的链接。另外还需要注意的一点是,渲染分页视图的代码必须使用 {!! !!} 语法,而不是 {{ }},这样生成 HTML 链接才不会被转义。

让我们对用户列表页视图进行修改,加上渲染分页视图的代码。

resources/views/users/index.blade.php

@extends('layouts.default');
@section('title','所有用户');

@section('content')
    <div class="offset-md-2 col-md-8">
        <h2 class="mb-4 text-center">所有用户</h2>
        <div class="list-group list-group-flush">
            @foreach ($users as $user)
                <div class="list-group-item">
                    <img class="mr-3" src="{{ $user->gravatar() }}" alt="{{ $user->name }}" width=32>
                    <a href="{{ route('users.show', $user) }}">
                        {{ $user->name }}
                    </a>
                </div>
            @endforeach
        </div>
        <div class="mt-3">
            {!! $users->render() !!}
        </div>
    </div>
@stop

image.png


7、使用局部视图重构

为了对视图模块进行细分,使目录结构更好理解,接下来让我们对用户列表页进行重构,将单个用户视图抽离成一个完整的局部视图。首先我们引入用户局部视图到用户列表上。

resources/views/users/index.blade.php

@extends('layouts.default');
@section('title','所有用户');

@section('content')
    <div class="offset-md-2 col-md-8">
        <h2 class="mb-4 text-center">所有用户</h2>
        <div class="list-group list-group-flush">
            @foreach ($users as $user)
                @include('users._user')
            @endforeach
        </div>
        <div class="mt-3">
            {!! $users->render() !!}
        </div>
    </div>
@stop

接着再对用户局部视图进行创建。

resources/views/users/_user.blade.php

<div class="list-group-item">
    <img class="mr-3" src="{{ $user->gravatar() }}" alt="{{ $user->name }}" width=32>
    <a href="{{ route('users.show', $user) }}">
        {{$user->name}}
    </a>
</div>

修改完成后刷新页面测试:

image.png

Git 代码版本控制

接着让我们将本次更改纳入版本控制中:

$ git add -A
$ git commit -m "查看用户列表"

五、删除用户

1、删除用户

接下来我们要完成删除用户的功能,用户的删除只能通过管理员来操作,因此我们接下来需要为用户表加上管理员字段,以便用来判定该用户是否为管理员。然后将管理员身份授权给某个指定用户,让其得到删除用户的权限。最后我还需要在用户列表页面加上删除按钮,只有当我们登录管理员账号时才能看到删除按钮并对用户进行删除。

2、管理员

我们需要生成一个迁移文件来为用户表新增管理员字段。在生成迁移文件时,带上 --table 选项可以为指定数据表生成迁移文件。现在,让我们运行下面命令来为用户表新增管理员字段。

$ php artisan make:migration add_is_admin_to_users_table --table=users

我们需要在新建的迁移文件中为用户添加一个 is_admin 的布尔值类型字段来判别用户是否拥有管理员身份,该字段默认为 false,在迁移文件执行时对该字段进行创建,回滚时则需要对该字段进行移除。迁移文件最终编写完成的代码如下。

database/migrations/[timestamp]_add_is_admin_to_users_table.php

<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class AddIsAdminToUsersTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::table('users', function (Blueprint $table) {
            $table->boolean('is_admin')->default(false);
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::table('users', function (Blueprint $table) {
            $table->dropColumn('is_admin');
        });
    }
}

可以看到我们使用了 dropColumn 方法来对指定字段进行移除。

在迁移文件创建成功之后,我们还需要运行数据库迁移。

$ php artisan migrate

现在应用中还不存在拥有管理员身份的用户,让我们对数据填充文件进行更改,将第一个生成的用户设置为管理员:

database/seeds/UsersTableSeeder.php

<?php

use Illuminate\Database\Seeder;
use App\Models\User;

class UsersTableSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        $users = factory(User::class)->times(50)->make();
        User::insert($users->makeVisible(['password','remember_token'])->toArray());

        $user = User::find(1);
        $user->name = 'Summer';
        $user->email = 'summer@example.com';
        $user->is_admin = true;
        $user->save();
    }
}

最后让我们对数据库进行重置和填充:

$ php artisan migrate:refresh --seed

现在如果我们使用 tinker 进行查看,可以看到第一位用户已被成功设置成为管理员。

$ php artisan tinker

image.png

创建日期和更新日期是随机生成的

3、destroy 动作

删除用户的动作,有两个逻辑需要提前考虑:

  1. 只有当前登录用户为管理员才能执行删除操作;

  2. 删除的用户对象不是自己(即使是管理员也不能自己删自己)。

我们在开发更新用户功能时,已经创建了用户授权策略类,让我们接着对该授权策略类进行编辑,加上 destroy 删除用户动作相关的授权。

app/Policies/UserPolicy.php

<?php

namespace App\Policies;

use App\Models\User;
use Illuminate\Auth\Access\HandlesAuthorization;

class UserPolicy
{
    use HandlesAuthorization;

    /**
     * Create a new policy instance.
     *
     * @return void
     */
    public function __construct()
    {
        //
    }

    public function update(User $currentUser , User $user)
    {
        return $currentUser->id === $user->id;
    }

    public function destroy(User $currentUser , User $user)
    {
        return $currentUser->is_admin && $currentUser !== $user->id;
    }
}

我们使用了下面这行代码来指明,只有当前用户拥有管理员权限且删除的用户不是自己时才显示链接。

$currentUser->is_admin && $currentUser->id !== $user->id;

Laravel 授权策略提供了 @can Blade 命令,允许我们在 Blade 模板中做授权判断。接下来让我们利用 @can 指令,在用户列表页加上只有管理员才能看到的删除用户按钮。

resources/views/users/_user.blade.php

<div class="list-group-item">
    <img class="mr-3" src="{{ $user->gravatar() }}" alt="{{ $user->name }}" width=32>
    <a href="{{ route('users.show', $user) }}">
        {{$user->name}}
    </a>
    @can('destroy',$user)
        <form action="{{route('users.destroy',$user->id)}}" method="post" class="float-right">
            {{csrf_field()}}
            {{method_field('DELETE')}}
            <button type="submit" class="btn btn-sm badge-danger delete-btn">删除</button>
        </form>
    @endcan
</div>

刷新页面看下效果:

image.png

在管理员点击删除用户按钮之后,删除动作会映射到用户控制器的 destroy 动作上,接下来让我们为控制器添加基本的用户删除动作:

app/Http/Controllers/UsersController.php

public function destroy(User $user)
{
    $user->delete();
    session()->flash('success','成功删除用户!');
    return back();
}

在 destroy 动作中,我们首先会根据路由发送过来的用户 id 进行数据查找,查找到指定用户之后再调用 Eloquent 模型提供的 delete 方法对用户资源进行删除,成功删除后在页面顶部进行消息提示。最后将用户重定向到上一次进行删除操作的页面,即用户列表页。

有了上面的代码,管理员已经能够对用户进行删除操作了。并且我们使用了 Auth 中间件黑名单,也就是说除了 except 数组中指定的动作,其他的动作都必须登录以后才能操作:

app/Http/Controllers/UsersController.php

public function __construct()
{
    $this->middleware('auth',[
        'except' => ['show','create','store','index']
    ]);
}

另外还需要注意的一点是,现在的删除动作是对所有登录用户开放的,为此我们还需要对删除动作加上授权策略,只允许已登录的 管理员 进行删除操作。

删除授权策略 destroy 我们已经在上面创建了,这里我们在用户控制器中使用 authorize 方法来对删除操作进行授权验证即可。在删除动作的授权中,我们规定只有当前用户为管理员,且被删除用户不是自己时,授权才能通过。

app/Http/Controllers/UsersController.php

public function destroy(User $user)
{
    $this->authorize('destroy',$user);
    $user->delete();
    session()->flash('success','成功删除用户!');
    return back();
}

至此,用户删除功能已经完成。

Git 代码版本控制

接着让我们将本次更改纳入版本控制中:

$ git add -A
$ git commit -m "管理员可删除用户"

六、小结

现在让我们将改动的代码进行提交并合并到主分支上。

$ git checkout master
$ git merge user-crud

接着把代码推送到 GitHub 和 Heroku 上。

$ git push
$ git push heroku master

由于本章我们为用户新增了管理员字段,因此需要在 Heroku 上也执行迁移。

$ heroku run php artisan migrate

测试一下线上应用:

image.png

经过本章节的学习,我们学到了以下内容:

  • 通过路由传参与控制器进行交互;

  • 使用 PATCH 动作来更新用户信息;

  • 使用 Auth 中间件来过滤请求;

  • 通过授权策略来授权用户进行编辑资料和删除用户的操作;

  • 通过 intended 方法来提供更加友好的重定向方式;

  • 使用数据填充的方式来生成假数据;

  • Faker 扩展包的基本使用;

  • 借助 Laravel 默认集成的分页功能为用户列表进行分页;

  • 通过授权给管理员来删除用户;

  • 对一个资源进行删除;




回复列表



回复操作

正在加载验证码......

请先拖动验证码到相应位置