LC02 第 3 遍学习小结 -- 操作记录 3
由于LC02教程和实际需要开发的项目之间存在差异,所以第3遍学习只关注自己实际项目必须会用到的技能点。
第四章. 用户相关
4.1 个人页面
1) 设置路由
注册资源路由:
routes/web.php
Route::resource('users', 'UsersController', ['only' => ['show', 'update', 'edit']]);
2) 创建控制器
$ php artisan make:controller UsersController
增加show方法
public function show(User $user)
{
return view('users.show', compact('user'));
}
3) 创建视图
$ mkdir -p resources/views/users
$ vi resources/views/users/show.blade.php
resources/views/users/show.blade.php
:
@extends('layouts.app')
@section('title', $user->name . ' 的个人中心')
@section('content')
<div class="row">
<div class="col-lg-3 col-md-3 hidden-sm hidden-xs user-info">
<div class="card ">
<img class="card-img-top" src="https://cdn.learnku.com/uploads/images/201709/20/1/PtDKbASVcz.png?imageView2/1/w/600/h/600" alt="{{ $user->name }}">
<div class="card-body">
<h5><strong>个人简介</strong></h5>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. </p>
<hr>
<h5><strong>注册于</strong></h5>
<p>January 01 1901</p>
</div>
</div>
</div>
<div class="col-lg-9 col-md-9 col-sm-12 col-xs-12">
<div class="card ">
<div class="card-body">
<h1 class="mb-0" style="font-size:22px;">{{ $user->name }} <small>{{ $user->email }}</small></h1>
</div>
</div>
<hr>
{{-- 用户发布的内容 --}}
<div class="card ">
<div class="card-body">
暂无数据 ~_~
</div>
</div>
</div>
</div>
@stop
4) 版本控制
$ git add -A
$ git commit -m "用户个人页面原型"
4.2 编辑个人资料
本节都是一些最基本的方法,需要多次重复练习。
1) 新增字段
增加『头像』和『个人简介』字段
$ php artisan make:migration add_avatar_and_introduction_to_users_table --table=users
修改 database/migrations/[timestamp]_add_avatar_and_introduction_to_users_table.php
public function up()
{
Schema::table('users', function (Blueprint $table) {
$table->string('avatar')->nullable();
$table->string('introduction')->nullable();
});
}
public function down()
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('avatar');
$table->dropColumn('introduction');
});
}
$ php artisan migrate
2) 增加入口
增加一个页面链接入口,让登录用户可以很方便地进入到自己的『资料编辑页面』:
vi resources/views/layouts/_header.blade.php
<div class="dropdown-menu" aria-labelledby="navbarDropdown">
<a class="dropdown-item" href="{{ route('users.show', Auth::id()) }}">个人中心</a>
<a class="dropdown-item" href="{{ route('users.edit', Auth::id()) }}">编辑资料</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item" id="logout" href="#">
<form action="{{ route('logout') }}" method="POST">
{{ csrf_field() }}
<button class="btn btn-block btn-danger" type="submit" name="button">退出</button>
</form>
</a>
</div>
UsersController 控制器里创建 edit() 方法:
public function edit(User $user)
{
return view('users.edit', compact('user'));
}
3) 视图文件
vi resources/views/users/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>
<i class="glyphicon glyphicon-edit"></i> 编辑个人资料
</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() }}">
<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="introduction-field">个人简介</label>
<textarea name="introduction" id="introduction-field" class="form-control" rows="3">{{ old('introduction', $user->introduction) }}</textarea>
</div>
<div class="well well-sm">
<button type="submit" class="btn btn-primary">保存</button>
</div>
</form>
</div>
</div>
</div>
</div>
@endsection
4) 更新用户信息
创建update
方法
vi app/Http/Controllers/UsersController.php
public function update(UserRequest $request, User $user)
{
$user->update($request->all());
return redirect()->route('users.show', $user->id)->with('success', '个人资料更新成功!');
}
顶部引入 UserRequest:
use App\Http\Requests\UserRequest;
5) 表单请求 UserRequest
表单请求验证(FormRequest) 是 Laravel 框架提供的用户表单数据验证方案,此方案相比手工调用 validator 来说,能处理更为复杂的验证逻辑,更加适用于大型程序。
创建 UserRequest:
$ php artisan make:request UserRequest
vi app/Http/Requests/UserRequest.php
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Auth;
class UserRequest extends FormRequest
{
public function authorize()
{
return true;
}
public function rules()
{
return [
'name' => 'required|between:3,25|regex:/^[A-Za-z0-9\-\_]+$/|unique:users,name,' . Auth::id(),
'email' => 'required|email',
'introduction' => 'max:80',
];
}
}
6) 渲染错误提示
mkdir -p resources/views/shared
vi resources/views/shared/_error.blade.php
@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
并在表单中加载(只添加一行 @include('shared._error') ):resources/views/users/edit.blade.php
.
.
.
<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() }}">
@include('shared._error')
<div class="form-group">
.
.
.
7) 自定义表单的错误提示信息
vi app/Http/Requests/UserRequest.php
public function messages()
{
return [
'name.unique' => '用户名已被占用,请重新填写',
'name.regex' => '用户名只支持英文、数字、横杠和下划线。',
'name.between' => '用户名必须介于 3 - 25 个字符之间。',
'name.required' => '用户名不能为空。',
];
}
8) 版本管理
$ git add -A
$ git commit -m "编辑个人资料"
4.3 显示个人资料
1) 个人中心显示resources/views/users/show.blade.php
.
.
.
<div class="card-body">
<h5><strong>个人简介</strong></h5>
<p>{{ $user->introduction }}</p>
<hr>
<h5><strong>注册于</strong></h5>
<p>{{ $user->created_at->diffForHumans() }}</p>
</div>
.
.
.
2) 个人简介为空
设置User模型的fillable属性
3) 友好时间戳是英文的
对 Carbon 进行本地化的设置。app/Providers/AppServiceProvider.php
public function boot()
{
\Carbon\Carbon::setLocale('zh');
}
4) 版本管理
$ git add -A
$ git commit -m "显示个人资料"
4.4 上传头像
1) 扩大 fillable
字段清单
在 User 模型里将 avatar 字段加入到允许修改的白名单 $fillable 中
2) 编辑页面
个人简介』编辑框下面,增加头像上传的选项
vi resources/views/users/edit.blade.php
<div class="form-group mb-4">
<label for="" class="avatar-label">用户头像</label>
<input type="file" name="avatar" class="form-control-file">
@if($user->avatar)
<br>
<img class="thumbnail img-responsive" src="{{ $user->avatar }}" width="200" />
@endif
</div>
Laravel 的『用户上传文件对象』底层使用了 Symfony 框架的 UploadedFile 对象进行渲染,为我们提供了便捷的文件读取和管理接口,我们将在后面使用这些方法。
3) 存储用户上传图片
这里虽然只是上传用户头像,但是实际上可以用在任何需要上传图片的场景!所以这里将『图片上传』核心操作做成一个工具类了。
mkdir -p app/Handlers
vi app/Handlers/ImageUploadHandler.php
app/Handlers/ImageUploadHandler.php
<?php
namespace App\Handlers;
class ImageUploadHandler
{
// 只允许以下后缀名的图片文件上传
protected $allowed_ext = ["png", "jpg", "gif", 'jpeg'];
public function save($file, $folder, $file_prefix)
{
// 构建存储的文件夹规则,值如:uploads/images/avatars/201709/21/
// 文件夹切割能让查找效率更高。
$folder_name = "uploads/images/$folder/" . date("Ym/d", time());
// 文件具体存储的物理路径,`public_path()` 获取的是 `public` 文件夹的物理路径。
// 值如:/home/vagrant/Code/larabbs/public/uploads/images/avatars/201709/21/
$upload_path = public_path() . '/' . $folder_name;
// 获取文件的后缀名,因图片从剪贴板里黏贴时后缀名为空,所以此处确保后缀一直存在
$extension = strtolower($file->getClientOriginalExtension()) ?: 'png';
// 拼接文件名,加前缀是为了增加辨析度,前缀可以是相关数据模型的 ID
// 值如:1_1493521050_7BVc9v9ujP.png
$filename = $file_prefix . '_' . time() . '_' . str_random(10) . '.' . $extension;
// 如果上传的不是图片将终止操作
if ( ! in_array($extension, $this->allowed_ext)) {
return false;
}
// 将图片移动到我们的目标存储路径中
$file->move($upload_path, $filename);
return [
'path' => config('app.url') . "/$folder_name/$filename"
];
}
}
在 UsersController 里调用(注意顶部 use 引入):app/Http/Controllers/UsersController.php
use App\Handlers\ImageUploadHandler;
class UsersController extends Controller
{
.
.
.
public function update(UserRequest $request, ImageUploadHandler $uploader, User $user)
{
$data = $request->all();
if ($request->avatar) {
$result = $uploader->save($request->avatar, 'avatars', $user->id);
if ($result) {
$data['avatar'] = $result['path'];
}
}
$user->update($data);
return redirect()->route('users.show', $user->id)->with('success', '个人资料更新成功!');
}
}
4) 版本控制
我们在上传图片的时候,程序自动创建了 public/uploads/images/avatars/ 目录,此文件夹下的文件皆为用户上传的头像文件,我们需要防止这些文件被纳入 Git 版本控制器中,可以利用 Git 的 .gitignore 机制来实现:
vi public/uploads/images/avatars/.gitignore
*
!.gitignore
$ git add -A
$ git commit -m "上传头像"
4.5 显示头像
本节也可以看成如何显示一个上传的图片。
修改个人空间,将头像的 src 属性修改为 {{ $user->avatar }}:
vi resources/views/users/show.blade.php
.
.
.
<div class="card ">
<img class="card-img-top" src="{{ $user->avatar }}" alt="{{ $user->name }}">
<div class="card-body">
.
.
.
vi resources/views/layouts/_header.blade.php
.
.
.
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<img src="{{ Auth::user()->avatar }}" class="img-responsive img-circle" width="30px" height="30px">
{{ Auth::user()->name }}
</a>
.
.
.
版本控制
$ git add -A
$ git commit -m "显示头像"
4.6 限制头像分辨率
即如何限制上传图片的分辨率
1) 在 UserRequest 中增加图片验证规则vi app/Http/Requests/UserRequest.php
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Auth;
class UserRequest extends FormRequest
{
public function authorize()
{
return true;
}
public function rules()
{
return [
'name' => 'required|between:3,25|regex:/^[A-Za-z0-9\-\_]+$/|unique:users,name,' . Auth::id(),
'email' => 'required|email',
'introduction' => 'max:80',
'avatar' => 'mimes:jpeg,bmp,png,gif|dimensions:min_width=208,min_height=208',
];
}
public function messages()
{
return [
'avatar.mimes' =>'头像必须是 jpeg, bmp, png, gif 格式的图片',
'avatar.dimensions' => '图片的清晰度不够,宽和高需要 208px 以上',
'name.unique' => '用户名已被占用,请重新填写',
'name.regex' => '用户名只支持英文、数字、横杆和下划线。',
'name.between' => '用户名必须介于 3 - 25 个字符之间。',
'name.required' => '用户名不能为空。',
];
}
}
版本控制
$ git add -A
$ git commit -m "限制头像分辨率"
4.7 裁剪头像
图片太大会拖慢页面的加载速度,所以接下来我们将对此进行优化
1) 安装扩展包
$ composer require intervention/image
执行以下命令获取配置信息:
$ php artisan vendor:publish --provider="Intervention\Image\ImageServiceProviderLaravel5"
2) 开始裁剪vi app/Handlers/ImageUploadHandler.php
<?php
namespace App\Handlers;
use Image;
class ImageUploadHandler
{
protected $allowed_ext = ["png", "jpg", "gif", 'jpeg'];
public function save($file, $folder, $file_prefix, $max_width = false)
{
// 构建存储的文件夹规则,值如:uploads/images/avatars/201709/21/
// 文件夹切割能让查找效率更高。
$folder_name = "uploads/images/$folder/" . date("Ym/d", time());
// 文件具体存储的物理路径,`public_path()` 获取的是 `public` 文件夹的物理路径。
// 值如:/home/vagrant/Code/larabbs/public/uploads/images/avatars/201709/21/
$upload_path = public_path() . '/' . $folder_name;
// 获取文件的后缀名,因图片从剪贴板里黏贴时后缀名为空,所以此处确保后缀一直存在
$extension = strtolower($file->getClientOriginalExtension()) ?: 'png';
// 拼接文件名,加前缀是为了增加辨析度,前缀可以是相关数据模型的 ID
// 值如:1_1493521050_7BVc9v9ujP.png
$filename = $file_prefix . '_' . time() . '_' . str_random(10) . '.' . $extension;
// 如果上传的不是图片将终止操作
if ( ! in_array($extension, $this->allowed_ext)) {
return false;
}
// 将图片移动到我们的目标存储路径中
$file->move($upload_path, $filename);
// 如果限制了图片宽度,就进行裁剪
if ($max_width && $extension != 'gif') {
// 此类中封装的函数,用于裁剪图片
$this->reduceSize($upload_path . '/' . $filename, $max_width);
}
return [
'path' => config('app.url') . "/$folder_name/$filename"
];
}
public function reduceSize($file_path, $max_width)
{
// 先实例化,传参是文件的磁盘物理路径
$image = Image::make($file_path);
// 进行大小调整的操作
$image->resize($max_width, null, function ($constraint) {
// 设定宽度是 $max_width,高度等比例双方缩放
$constraint->aspectRatio();
// 防止裁图时图片尺寸变大
$constraint->upsize();
});
// 对图片修改后进行保存
$image->save();
}
}
修改 UsersController 的 update() 方法中的调用,修改为:
$result = $uploader->save($request->avatar, 'avatars', $user->id, 416);
3) 开始测试
选择一个较大图片测试即可。
4) 版本控制
$ git add -A
$ git commit -m "裁剪头像"
4.8 授权访问
现在的应用存在两个巨大的安全隐患:
- 未登录用户可以访问 edit 和 update 动作,如果你退出登录,以游客身份访问 http://oxxx.test/users/1/edit :
- 登录用户可以更新其它用户的个人信息,登录 1 用户然后访问 2 用户的编辑资料页面 http://oxxx.test/users/2/edit :
1) 限制游客访问
使用auth中间件, vi app/Http/Controllers/UsersController.php
.
.
class UsersController extends Controller
{
public function __construct()
{
$this->middleware('auth', ['except' => ['show']]);
}
.
.
.
2) 用户只能编辑自己的资料
在 Laravel 中可以使用 授权策略 (Policy) 来对用户的操作权限进行验证,在用户未经授权进行操作时将返回 403 禁止访问的异常。
话说Policy这种方法应该也是小范围使用才行。。
$ php artisan make:policy UserPolicy
vi app/Policies/UserPolicy.php
<?php
namespace App\Policies;
use App\Models\User;
use Illuminate\Auth\Access\HandlesAuthorization;
class UserPolicy
{
use HandlesAuthorization;
public function update(User $currentUser, User $user)
{
return $currentUser->id === $user->id;
}
}
创建了授权策略类,还需要注册才能使用:vi app/Providers/AuthServiceProvider.php
class AuthServiceProvider extends ServiceProvider
{
/**
* The policy mappings for the application.
*
* @var array
*/
protected $policies = [
'App\Model' => 'App\Policies\ModelPolicy',
\App\Models\User::class => \App\Policies\UserPolicy::class,
];
.
.
.
为 edit 和 update 方法加上这行
$this->authorize('update', $user);
class UsersController extends Controller
{
.
.
.
public function edit(User $user)
{
$this->authorize('update', $user);
return view('users.edit', compact('user'));
}
public function update(UserRequest $request, ImageUploadHandler $uploader, User $user)
{
$this->authorize('update', $user);
$data = $request->all();
if ($request->avatar) {
$result = $uploader->save($request->avatar, 'avatars', $user->id, 416);
if ($result) {
$data['avatar'] = $result['path'];
}
}
$user->update($data);
return redirect()->route('users.show', $user->id)->with('success', '个人资料更新成功!');
}
}
3) 版本控制
$ git add -A
$ git commit -m "授权访问"
4.9 小结
- 如何创建一个资源路由
- 如何创建控制器
- 如何使用migration增加字段
- 如何创建表单请求验证类(这是laravel提供的后端验证方案,前端验证自己找前端方法)
- 如何渲染通用的错误提示
- 如何自定义表单请求验证类的错误提示信息
- 如何使用Carbon进行时间的本地化。
- 模型的fillable属性的作用。
- 如何上传图片,写了一个核心工具类。
- 如何防止上传文件被纳入Git版本控制。
- 如何显示上传图片
- 如何限制上传图片的分辨率(通过表单请求验证类)
- 如何裁剪图片(intervention/image 扩展包)
- 如何使用auth中间件
- 如何创建、注册、使用授权策略(Policy)
$ git push