上次我们在用户注册页面添加了一个字段username,并且用这个username来登录。在我们了解了如何做Laravel脚手架(scaffold)及基本设置后,在这个基础上我们可以建立一个单个用户的编辑,修改页面供用户查看和修改个人资料用。这个是目前很常见的一个功能了。用户在这个页面上可以修改个人头像(有默认头像),生日,昵称等个人信息。
1. 单个用户资料的展示和修改页面
首先让我们来做一个路由和控制器来展示和编辑用户的个人信息。在routes/web.php里面增加一行:
Route::resource('users', 'UsersController', ['only' => ['update', 'edit']]);
上面这一行我们要了解的知识点是laravel的资源控制器,如果对这一段知识不熟悉的话可以参见Laravel的官网,这里就不再赘述了。
我们不能赋予用户删除的权限,所以移除了删除权限。上一句就等于:
Route::get('/users/{user}/edit', 'UsersController@edit')->name('users.edit');
Route::patch('/users/{user}', 'UsersController@update')->name('users.update');
这里是edit是展示页面,update进行数据的修改,然后我们执行下如下命令来创建控制器:
php artisan make:controller UsersController
我们打开创建好的控制器,并把这一个方法放进去:
class UsersController extends Controller
{
//展示个人资料页面方法
public function edit(User $user)
{
return view('users.edit', compact('user'));
}
}
当然我们要在文件的头部引入模型文件App\User来做数据支持。
use App\User;
接下来我们做下个人资料页面的视图文件,compact('user')这个函数就会把user数据传到视图中。
我们的设计是展示和编辑个人资料的页面放在一个页面上,方便管理。下面是edit.blade.php的代码:
@extends('layouts.app')
@section('content')
<div class="container">
<div class="col-md-8 offset-md-2">
<div class="card">
<div class="card-header">
<h4>
编辑个人资料
</h4>
</div>
<div class="card-body">
<form action="{{ route('users.update', $user->id) }}" method="POST" accept-charset="UTF-8">
<input type="hidden" name="_method" value="PUT">
<input type="hidden" name="_token" value="{{ csrf_token() }}">
<div class="form-group">
<label for="name-field">用户名</label>
<input class="form-control" type="text" name="name" id="name-field" value="{{ old('name', $user->name) }}" />
</div>
<div class="form-group">
<label for="email-field">邮 箱</label>
<input class="form-control" type="text" name="email" id="email-field" value="{{ old('email', $user->email) }}" />
</div>
<div class="well well-sm">
<button type="submit" class="btn btn-primary">保存</button>
</div>
</form>
</div>
</div>
</div>
</div>
@endsection
接下来我们还要写具体的处理资料更新的方法,在控制器中增加如下方法:
public function update(UserRequest $request, User $user)
{
$user->update($request->all());
return redirect()->route('users.edit', $user->id)->with('success', '个人资料更新成功!');
}
这里的UserRequest是要我们单独创建的。在这个类中,我们会处理三个问题,1.是否是认证使用。2.验证规则。3.错误信息。下面我们就来创建它吧:
php artisan make:request UserRequest
这个命令会生成App\Http\Requests\UserRequest.php,我们直接把代码展示下:
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Auth;
class UserRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'username' => 'required|between:3,25|regex:/^[A-Za-z0-9\-\_]+$/|unique:users,username,' . Auth::id(),
'email' => 'required|email'
];
}
public function messages()
{
return [
'username.unique' => '用户名已被占用,请重新填写',
'username.regex' => '用户名只支持英文、数字、横杠和下划线。',
'username.between' => '用户名必须介于 3 - 25 个字符之间。',
'username.required' => '用户名不能为空。',
];
}
}
authorize方法中return true使得用户必须在登录情况下才能操作;rules是进行验证规则的;messages是展示错误信息的。
这些做完了之后别忘了把引入到控制里面也就是UsersController.php里。
use App\Http\Requests\UserRequest.php
我们要在edit.blade.php的头部插入这个代码来展示其错误信息。
...
<input type="hidden" name="_token" value="{{ csrf_token() }}">
@if (count($errors) > 0)
<div class="alert alert-danger">
<div class="mt-2"><b>有错误发生:</b></div>
<ul class="mt-2 mb-2">
@foreach ($errors->all() as $error)
<li> {{ $error }}</li>
@endforeach
</ul>
</div>
@endif
<div class="form-group">
2.安全隐患
在这种情况下,你如果访问单个用户的编辑页面,不管登不登陆的状况下都可以看见并编辑,规则是单个用户只能查看和修改自己的资料。如果其他用户登录,通过修改ID看到别人的资料也是不行的。所以这就有很大的安全隐患,通常情况下我们在控制器加一个构造函数便可以解决这种情况。
public function __construct()
{
$this->middleware('auth');
}
加完这个方法之后再访问这个页面的话我们就会看到, 在访问users/1/edit这个路由的时候就会跳到登录页面,这个就是此中间件的作用。如果想让某个路由移除这个规则,那么就可以改成:
public function __construct()
{
$this->middleware('auth', ['except' => ['show']]);
}
上面的语句意味着类似users/show/1这个路由是可以不用登录就能访问的。当然中间件也可以直接在路由上加上如
Route::get('/home', 'HomeController@index')->name('home')->middleware('auth');
这种方式。这时我们会发现一个问题,例如,用户1和2注册后,当用户1登录以后可以看到交叉看到用户2的个人页面甚至修改它,只要知道用户2的ID即可,这样肯定是不行的,这里我们会提供两种方法。第一是直接在某个方法的开头插入能返回true或者false的一段判断代码。例如:
if($user->id != Auth()->id()){
return "you are not authorized.";
};
这段代码的意思就是说如果当前登录用户和模型的用户不一致的话即可停止执行代码。当然laravel有一个机制叫policy来解决这个问题。下面我们来创建policy。
php artisan make:policy UsersPolicy
然后我们在里面定义我们的授权方法。如:
public function view_user(User $currentUser, User $user)
{
return $currentUser->id === $user->id;
}
$currentUser返回的是当前登录的用户,所以和模型里的ID用户相匹配的返回为真,否则是false。然后我们需要在app/Providers里面,修改AuthServiceProvider.php里面插入:
protected $policies = [
'App\User' => 'App\Policies\UserPolicy'
];
左面的是模型,右面的是对应的政策。然后我们需要在控制器中的对应方法里写下:
$this->authorizie('view_user', $user);
第一个是政策里面的方法名,第二个是模型。这时我们再交叉查看会员资料的时候,就会发现显示的是403。从 Laravel 5.8 起,我们可以定义一个回调函数来让 Laravel 自己去寻找模型所对应的授权策略文件:
public function boot()
{
$this->registerPolicies();
}
这样的话我们不需要在AuthServiceProvider.php里面进行模型和策略对应的操作了。
3.添加用户头像功能
图片得上传和文字得提交有很多处是不一样的,有时候要复杂得多,比如验证格式,大小,裁剪等等,所以我们要单独拿出来说。
首先我们要添加一个自定义字段,把头像的URL放进去。在上一篇文章中提到如何添加一个自定义字段到用户注册表单,而这一次这个字段不是注册的时候填的,所以有些不一样。如何添加呢?我们现在数据库迁移文件加一个头像的字段,然后进行迁移命令:
php artisan make:migration add_avatar_to_users_table --table=users
在迁移文件中的两个方法中分别添加这两行:
public function up()
{
Schema::table('users', function (Blueprint $table) {
$table->string('avatar')->nullable();
});
}
public function down()
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('avatar');
});
}
后面的nullable()表面这个字段可以为空。我们在前端做一个默认头像,如果头像为空的时候就显示这个默认头像,如果头像不为空则显示用户上传的头像。然后我们再运行迁移命令:
php artisan migrate
这样用户表就多了一个字段avatar. 接下来我们需在 User 模型里将 avatar 字段加入到允许修改的白名单 $fillable 中,打开app/user.php, 在19行和20行添加:
protected $fillable = [
'name', 'email', 'username','password','avatar',
];
edit.blade.php
@extends('layouts.app')
@section('content')
<div class="container">
<div class="col-md-8 offset-md-2">
<div class="card">
<div class="card-header">
<h4>
编辑个人资料
</h4>
</div>
<div class="card-body">
<form action="{{ route('users.update', $user->id) }}" method="POST" accept-charset="UTF-8" enctype="multipart/form-data">
<input type="hidden" name="_method" value="PUT">
<input type="hidden" name="_token" value="{{ csrf_token() }}">
@if(Session::has('success'))
<div class="alert alert-success">
<div class="alert alert-success"> {{ Session::get('success') }}</div>
</div>
@endif
@if (count($errors) > 0)
<div class="alert alert-danger">
<div class="mt-2"><b>有错误发生:</b></div>
<ul class="mt-2 mb-2">
@foreach ($errors->all() as $error)
<li><i class="glyphicon glyphicon-remove"></i> {{ $error }}</li>
@endforeach
</ul>
</div>
@endif
@if($user->avatar)
<div class="avatar">
<img src="{{$user->avatar}}" width="150" height="150">
</div>
@else
<div class="avatar">
<img src="{{ asset('img/user.jpg') }}" width="150" height="150">
</div>
@endif
<div class="form-group">
<label for="avatar">头像上传</label>
<input type="file" name="avatar" id="avatar" class="form-control-file">
</div>
<input type="file" name="avatar" class="form-control-file">
<div class="form-group">
<label for="name-field">名称</label>
<input class="form-control" type="text" name="name" id="name-field" value="{{ old('name', $user->name) }}" />
</div>
<div class="form-group">
<label for="email-field">邮 箱</label>
<input class="form-control" type="text" name="email" id="email-field" value="{{ old('email', $user->email) }}" />
</div>
<div class="form-group">
<label for="username-field">用户名</label>
<input class="form-control" type="text" name="username" id="username-field" value="{{ old('username', $user->username) }}" />
</div>
<div class="well well-sm">
<button type="submit" class="btn btn-primary">保存</button>
</div>
</form>
</div>
</div>
</div>
</div>
@endsection
这里要显示成功信息也要显示错误信息,成功信息是控制器传过来的,成功信息存储在session里面。
@if(Session::has('success'))
<div class="alert alert-success">
{{ Session::get('success') }}
</div>
@endif
错误信息可能有多个,所以我们要遍历:
@if (count($errors) > 0)
<div class="alert alert-danger">
<div class="mt-2"><b>有错误发生:</b></div>
<ul class="mt-2 mb-2">
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
这里别忘了要在表单上加一个enctype="multipart/form-data",否额无法上传图片。
我们现在看下图片:
图片上传要经过一些复杂的步骤,我们要验证是否是图片,也就是图片扩展名验证,还有就是图片文件的最大大小等等,但我们可以用laravel的验证机制来一下子解决这些问题。
我们在上面的UserRequest.php里面的验证机制上添加:
'avatar' => 'image|mimes:jpeg,png,jpg,gif|max:2048'
在违反此验证规则的返回错误信息里写上:
'avatar.image' => '上传的必须是图片类型',
'avatar.mimes' => '上传格式只支持jpeg,png,jpg,gif',
'avatar.max' => '图片大小不能超过2兆'
这个验证规则不言而喻大家一看就懂,在此就不再赘述了,最后我们要解决的是如何储存到相应位置的问题。上传图片的时候通常也要进行重命名,在此我们就拿用户的user id和时间戳组成的字符串来命名吧。然后我们把绝对路径放到数据库里,然后储存图片就可以了。为此我们把UserController.php里面的update方法改成:
public function update(UserRequest $request, User $user)
{
$data = $request->all();
if($request->avatar){
$user_id = Auth::user()->id;
$imageName = $user_id.'_'.time().'.'.request()->avatar->getClientOriginalExtension();
request()->avatar->move(public_path('img'), $imageName);
$data["avatar"] = env('APP_URL')."/img/".$imageName;
}
$user->update($data);
return redirect()->route('users.edit', $user->id)->with('success', '个人资料更新成功!');
}
我们在public文件夹下面创建一个img文件夹作为上传的目标文件夹。命名规则,用户名加时间戳加扩展名。在数据库里存入的是绝对路径。当然图片我们没有进行裁剪和缩小,至于更详细地讲解让Laravel如何处理图片,我们要在专门的教程里讲解。