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 授权访问

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

  1. 未登录用户可以访问 edit 和 update 动作,如果你退出登录,以游客身份访问 http://oxxx.test/users/1/edit
  2. 登录用户可以更新其它用户的个人信息,登录 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
日拱一卒
《L03 构架 API 服务器》
你将学到如 RESTFul 设计风格、PostMan 的使用、OAuth 流程,JWT 概念及使用 和 API 开发相关的进阶知识。
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!