一、开发目标
前面我们已经成功实现了用户的登录和注册功能,本章我们将补充前面未完成的几个 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 动作主要做了以下几个操作:
利用了 Laravel 的『隐性路由模型绑定』功能,直接读取对应 ID 的用户实例 $user
,未找到则报错;
将查找到的用户实例 $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>
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
方法对用户对象进行更新。在用户个人资料更新成功后,我们还需要将用户重定向到个人页面,方便用户第一时间查看到自己更改后的个人信息。
这时如果我们尝试提交错误信息并进行提交,将会显示如下界面,代表表单验证功能已可正常使用。
4、编辑成功
现在的用户编辑功能还有两个地方需要优化:
在每次更改个人资料的时候都输入完整的密码,才能更新其它信息,对于不想对密码进行更新的用户,这个过程会比较繁琐;
更新成功之后在页面上没有进行任何提示,而是直接跳转到用户的个人页面,用户体验非常不好;
接下来让我们针对这两个问题对 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 add -A $ git commit -m "更改用户资料"
三、权限系统
1、权限系统
现在的应用存在两个巨大的安全隐患:
未登录用户可以访问 edit
和 update
动作;
登录用户可以更新其它用户的个人信息;
接下来让我们针对这两个安全隐患进行修复。
2、必须先登陆
Laravel 中间件 (Middleware) 为我们提供了一种非常棒的过滤机制来过滤进入应用的 HTTP 请求,例如,当我们使用 Auth 中间件来验证用户的身份时,如果用户未通过身份验证,则 Auth 中间件会把用户重定向到登录页面。如果用户通过了身份验证,则 Auth 中间件会通过此请求并接着往下执行。Laravel 框架默认为我们内置了一些中间件,例如身份验证、CSRF 保护等。所有的中间件文件都被放在项目的 app/Http/Middleware
文件夹中。
接下来让我们使用 Laravel 提供身份验证(Auth)中间件来过滤未登录用户的 edit
, update
动作。
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 页面将会被重定向到登录页面。
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 异常信息来拒绝访问。
使用授权策略需要注意以下两点:
我们并不需要检查 $currentUser
是不是 NULL。未登录用户,框架会自动为其 所有权限 返回 false
;
调用时,默认情况下,我们 不需要 传递当前登录用户至该方法内,因为框架会自动加载当前登录用户(接着看下去,后面有例子);
接下来我们还需要在 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')])
现在,使用 id 为 1 的用户登录,当访问 id 为4 的用户编辑页面 —— http://weibo.test/users/4/edit ,系统将会拒绝访问。
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、注册与登录页面访问限制
现在我们的应用还有一个小问题,即已登录用户还能够对注册页面和登录页面进行访问:
这明显不符合常规逻辑。
我们除了可通过 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 :
会被跳转到 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 :
接着让我们将本次更改纳入版本控制中:
$ 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>
在做完上面这一切工作之后,用户列表页面已经能够正常访问了,但现在还有两个问题:
注册用户太少;
用户列表页不支持分页浏览,用户量大的时候会影响性能和用户体验;
接下来让我们来着手解决这两个问题。
3、示例用户
在实际的项目开发过程中,我们经常会用到一些假数据来对数据库进行填充以方便调试程序,原始的做法是手工一个个在数据库中创建,或者从队友的机器那导出数据填充到开发机器中。Laravel 提供了一套更加现代化、非常简单易用的数据填充方案。接下来让我们使用 Laravel 提供的数据填充来批量生成假用户。
假数据的生成分为两个阶段:
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
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
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>
修改完成后刷新页面测试:
接着让我们将本次更改纳入版本控制中:
$ 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
创建日期和更新日期是随机生成的
3、destroy 动作
删除用户的动作,有两个逻辑需要提前考虑:
只有当前登录用户为管理员才能执行删除操作;
删除的用户对象不是自己(即使是管理员也不能自己删自己)。
我们在开发更新用户功能时,已经创建了用户授权策略类,让我们接着对该授权策略类进行编辑,加上 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>
刷新页面看下效果:
在管理员点击删除用户按钮之后,删除动作会映射到用户控制器的 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 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
测试一下线上应用:
经过本章节的学习,我们学到了以下内容:
通过路由传参与控制器进行交互;
使用 PATCH 动作来更新用户信息;
使用 Auth 中间件来过滤请求;
通过授权策略来授权用户进行编辑资料和删除用户的操作;
通过 intended
方法来提供更加友好的重定向方式;
使用数据填充的方式来生成假数据;
Faker 扩展包的基本使用;
借助 Laravel 默认集成的分页功能为用户列表进行分页;
通过授权给管理员来删除用户;
对一个资源进行删除;