三、个人页面

三、个人页面

一、

1、功能说明

接下来我们将制作用户的个人中心页面,作为用户的个人信息展示页。在此页面中,我们可以看到该用户发过的帖子,发表的评论等。

2、设置路由

我们使用 Laravel 的 资源控制器 功能,接下来我们先给控制器注册一个资源路由:

routes/web.php

Route::get('users','UserController',['only'=>'show','update','edit']);

上面代码将等同于:

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

可以看到使用 resource 方法不仅节省很多代码,且严格遵循了 RESTful URI 的规范,在后续的开发中,我们会优先选择 resource 路由。

生成的资源路由列表信息如下所示:

HTTP 请求URI动作作用
GET/users/{user}UsersController@show显示用户个人信息页面
GET/users/{user}/editUsersController@edit显示编辑个人资料页面
PATCH/users/{user}UsersController@update处理 edit 页面提交的更改


3、创建控制器

接下来我们需要创建一个 UsersController 控制器,这个控制器将负责用户相关的页面和逻辑处理。Laravel 的控制器命名规范统一使用驼峰式大小写和复数形式来命名,在这里我们也应该这么做。一般情况下,我们会使用下面命令来生成控制器:

$ php artisan make:controller UsersController

让我们来看下 UsersController 文件生成的默认代码:

app/Http/Controllers/UsersController.php

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class UsersController extends Controller
{
    //
}

接下来我们增加 show 方法来处理个人页面的展示:

<?php

namespace App\Http\Controllers;

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

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

}
  1. 第一个修改是引入了 App\Models\User 用户模型,因为将要在 show() 方法中使用到 User 模型,所以我们必须先引用。

  2. 接下来我们看下 show() 方法的声明:

public function show(User $user)

Laravel 会自动解析定义在控制器方法(变量名匹配路由片段)中的 Eloquent 模型类型声明。在上面代码中,由于 show() 方法传参时声明了类型 —— Eloquent 模型 User,对应的变量名 $user 会匹配路由片段中的 {user},这样,Laravel 会自动注入与请求 URI 中传入的 ID 对应的用户模型实例。

此功能称为 『隐性路由模型绑定』,是『约定优于配置』设计范式的体现,同时满足以下两种情况,此功能即会自动启用:

1). 路由声明时必须使用 Eloquent 模型的单数小写格式来作为 路由片段参数,User 对应 {user}

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

上面路由部分讲过,在使用资源路由 Route::resource('users', 'UsersController'); 时,默认已经包含了上面的声明。

2). 控制器方法传参中必须包含对应的 Eloquent 模型类型 提示,并且是有序的:

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

当请求 http://larabbs.test/users/1 并且满足以上两个条件时,Laravel 将会自动查找 ID 为 1 的用户并赋值到变量 $user中,如果数据库中找不到对应的模型实例,会自动生成 HTTP 404 响应,例如此刻我们只注册了 Summer 和 Monkey 用户,数据库里只有两条数据,当我们访问 http://larabbs.test/users/10 ID 为 10 的用户时:

image.png

3)继续看 show() 方法里的代码:

return view('users.show', compact('user'));


我们将用户对象变量 $user 通过 compact 方法转化为一个关联数组,并作为第二个参数传递给 view 方法,将变量数据传递到视图中。

show 方法添加完成之后,在视图中,我们即可直接使用 $user 变量来获取 view 方法传递给视图的用户数据。

由于我们还没有创建用户个人页面,因此这时访问用户页面时会出现如下报错。

View [users.show] not found.

image.png


4、创建视图

下面让我们来新建一个用户个人页面。

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://iocaffcdn.phphub.org/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

直接读取 $user 对象的属性:

{{ $user->name }}

这时候我们再访问用户个人页面,便能够看到基本的数据展示:

image.png

如上图所示,头像、个人简介和注册时间还都是假数据,在下一个章节中我们将主要专注于这些功能。

Git 代码标记

我们先把代码纳入到版本管理:

$ git add -A
$ git commit -m "用户个人页面原型"


二、编辑个人资料

接下来我们将一起开发用户编辑资料的功能,用户可以编辑自己的资料,并查看结果。

1、新增字段

查看用户表相关的迁移文件:

database/migrations/2014_10_12_000000_create_users_table.php

public function up()
{
    Schema::create('users', function (Blueprint $table) {
        $table->increments('id');
        $table->string('name');
        $table->string('email')->unique();
        $table->timestamp('email_verified_at')->nullable();
        $table->string('password');
        $table->rememberToken();
        $table->timestamps();
    });
}

以上是 users 表里的所有字段,作为 个人中心页面,可以看出我们还缺少『头像』和『个人简介』字段:

image.png接下来我们使用 Laravel 自带的命令来新建迁移文件。由于我们进行的是字段添加操作,因此在命名迁移文件时需要加上前缀,遵照如 add_column_to_table 这样的命名规范,并在生成迁移文件的命令中设置 --table 选项,用于指定对应的数据库表。最终的生成命令如下:

 php artisan make:migration add_avatar_and_introduction_to_users_table --table=users

接下来我们来设计字段的类型和属性。

我们会将头像的图片以文件形式放置于服务器上,然后将路径子串存储于数据库中,所以我们需要用到 string 类型,用户注册并未提供头像上传功能,因此我们还需要将字段设置为 nullable,意为允许空子串。

个人简介字段存储的是短文本内容,此处我们也选择使用 string 类型,同样的我们也允许用户设置空的简介。现在让我们来为新增的迁移文件加上这两个字段:

database/migrations/[timestamp]_add_avatar_and_introduction_to_users_table.php

<?php

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

class AddAvatarAndIntroductionToUsersTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::table('users', function (Blueprint $table) {
            $table->string('avatar')->nullable();
            $table->string('introduction')->nullable();
        });
    }

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

接着我们还需要运行迁移,将字段加入到用户表中:

$ php artisan migrate

打开数据库查看工具,即可看到我们新添加的两个字段:

image.png

2、增加入口

接下来我们需要增加一个页面链接入口,让登录用户可以很方便地进入到自己的『资料编辑页面』:

resources/views/layouts/_header.blade.php

<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">
        <img src="https://iocaffcdn.phphub.org/uploads/images/201709/20/1/PtDKbASVcz.png?imageView2/1/w/60/h/60" class="img-responsive img-circle" width="30px" height="30px">
        {{Auth::user()->name}}
    </a>
    <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>
</li>

我们在顶部导航里新增了『编辑资料』的链接:

image.png

点击此链接:

image.png

Method [edit] does not exist.

我们需要前往 UsersController 控制器里创建 edit() 方法:

app/Http/Controllers/UsersController.php

<?php

namespace App\Http\Controllers;

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

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

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


edit() 方法接受 $user 用户作为传参,也就是说当 URL 是 http://larabbs.test/users/1/edit 时,读取的是 ID 为 1 的用户。这里使用的是与 show() 方法一致的 『隐性路由模型绑定』 开发范式。

view() 方法加载了 resources/views/users/edit.blade.php 模板,并将用户实例作为变量传置于模板中。

3、页面模板

此时我们再次刷新页面

image.png

View [users.edit] not found.

这是视图文件未找到的错误。接下来我们来创建此视图文件:

resources/views/users/edit.blade.php

@extends('layouts.app');
@section('title',$user->name.'编辑个人资料')

@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',Auth::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="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>
@stop

image.png


4、更新用户信息

在编辑页面的 个人简介 框里随便填写内容,然后点击『保存』按钮,会出现:

image.png

这是我们还未编写处理表单提交的方法,接下来我们先创建此方法:

app/Http/Controllers/UsersController.php

<?php

namespace App\Http\Controllers;

use App\Models\User;
use Illuminate\Http\Request;
use App\Http\Requests\UserRequest;

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

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

    public function update(UserRequest $request,User $user)
    {
        $user->update($request->all());
        return redirect()->route('users.show',$user->id)->with('success','个人资料修改成功');
    }
}
  1. 顶部引入 UserRequest:

use App\Http\Requests\UserRequest;
  1. update() 方法中我们使用了『表单请求验证』(FormRequest)来验证用户提交的数据。

5、表单请求UserRequest

表单请求验证(FormRequest) 是 Laravel 框架提供的用户表单数据验证方案,此方案相比手工调用 validator 来说,能处理更为复杂的验证逻辑,更加适用于大型程序。在本课程中,我们将统一使用 表单请求验证来处理表单验证逻辑。

接下来我们创建 UserRequest ,使用以下命令:

$ php artisan make:request UserRequest

执行成功后会生成以下文件:

app/Http/Requests/UserRequest.php

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class UserRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return false;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
            //
        ];
    }
}

表单请求验证(FormRequest)的工作机制,是利用 Laravel 提供的依赖注入功能,在控制器方法,如上面我们的 update() 方法声明中,传参 UserRequest。这将触发表单请求类的自动验证机制,验证发生在 UserRequest 中,并使用此文件中方法 rules() 定制的规则,只有当验证通过时,才会执行 控制器 update() 方法中的代码。否则抛出异常,并重定向至上一个页面,附带验证失败的信息。

authorize() 方法是表单验证自带的另一个功能 —— 权限验证,本课程中我们不会使用此功能,关于用户授权,我们将会在后面章节中使用更具扩展性的方案,此处我们 return true; ,意味所有权限都通过即可。

接下来我们需要定制 rules() 方法,如下:

app/Http/Requests/UserRequest.php

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class UserRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return false;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    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',
        ];
    }
}

rules() 方法内,对三个字段设置了不同的验证规则,下面分别讲解下:

  • name.required —— 验证的字段必须存在于输入数据中,而不是空。详见文档

  • name.between —— 验证的字段的大小必须在给定的 min 和 max 之间。详见文档

  • name.regex —— 验证的字段必须与给定的正则表达式匹配。详见文档

  • name.unique —— 验证的字段在给定的数据库表中必须是唯一的。详见文档

  • email.required —— 同上

  • email.email —— 验证的字段必须符合 e-mail 地址格式。详见文档

  • introduction.max —— 验证中的字段必须小于或等于 value。详见文档

接下来测试下,在用户名那输入一个字符串,然后点击保存:

image.png

页面刷新了,不过什么都没发生,其实这是因为 UserRequest 验证不通过,带着错误提示跳转回来了,不过因为我们未对错误提示进行处理,所以什么都没发生。接下来我们创建 error.blade.php 文件来渲染错误提示:

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">

此时我们再次前往 http://larabbs.test/users/1/edit 继续刚才的测试:

image.png

此处有一个地方可以优化,错误显示的是 名称,而不是 用户名,这样会对用户造成困扰:

名称 必须介于 3 - 25 个字符之间。

我们需要自定义表单的提示信息,修改 UserRequest,新增方法 messages()

app/Http/Requests/UserRequest.php

    public function messages()
    {
        return [
            'name.unique' => '用户名已被占用,请重新填写',
            'name.regex' => '用户名只支持英文、数字、横杠和下划线。',
            'name.between' => '用户名必须介于 3 - 25 个字符之间。',
            'name.required' => '用户名不能为空。',
        ];
    }

messages() 方法是 表单请求验证(FormRequest)一个很方便的功能,允许我们自定义具体的消息提醒内容,键值的命名规范 —— 字段名 + 规则名称,对应的是消息提醒的内容。效果如下:

image.png


再次测试下编辑资料功能,请在『个人简介』里填入乔老爷子的座右铭作为测试内容 —— Stay hungry, stay foolish.,点击保存按钮:

image.png结果:

image.png

Git 代码版本管理

接下来把代码纳入到版本管理,为下一节做好准备:

$ git add -A
$ git commit -m "编辑个人资料"

三、显示个人资料

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->difforHumans()}}</p>
</div>

源码解读:

  1. $user->introduction 是调用上面我们新添加的字段;

  2. $user->created_at->diffForHumans() 时间戳友好的输出。

刷新页面看效果:

image.png

页面中有两个问题:

  1. 个人简介为空;

  2. 2 days ago 英文输出。

1)个人简介为空

个人简介居然为空,我们明明在最后一次测试中填入内容了,并且也显示成功更新。让我们用数据库工具瞧一瞧是否有内容:

image.png


居然是空的,所以很明显,刚刚数据并没有更新成功。

经过一番调试以后,原来是因为我们没有在 User.php 模型文件中,将 introduction 字段添加至 $fillable 属性中。$fillable 属性的作用是防止用户随意修改模型数据,只有在此属性里定义的字段,才允许修改,否则更新时会被忽略。我们只需请按下图新增字段即可:


再次测试填写简介 Stay hungry, stay foolish.

image.png再次测试填写简介 Stay hungry, stay foolish.

image.png

成功显示:

image.png

2、中文显示友好时间戳

在 Laravel 中,时间戳 created_at 和 updated_at 作为模型属性被调用时,都会自动转换为 Carbon 对象,下面我们使用 Laravel 自带的 dd() 辅助函数验证一下:

resources/views/users/show.blade.php

<div class="card-body">
    <h5><strong>个人简介</strong></h5>
    <p>{{$user->introduction}} </p>
    <hr>
    <h5><strong>注册于</strong></h5>
    {{dd($user->created_at)}}
    <p>{{$user->created_at->diffForHumans()}}</p>
</div>

打印出来的结果:

image.png

Carbon 是 PHP 知名的日期和时间操作扩展,Laravel 将其默认集成到了框架中。diffForHumans 是 Carbon 对象提供的方法,默认情况是英文的,如果要使用中文时间提示,则需要对 Carbon 进行本地化设置。对 Carbon 进行本地化的设置很简单,只在 AppServiceProvider 中调用 Carbon 的 setLocale 方法即可,AppServiceProvider 是框架的核心,在 Laravel 启动时,会最先加载该文件。

app/Providers/AppServiceProvider.php

public function boot()
{
    \Carbon\Carbon::setLocale('zh');
}

设置完成后,打开 resources/views/users/show.blade.php 去掉我们刚刚新增的 dd() 测试信息,刷新页面:

image.png

漂亮!

Git 代码版本管理

接下来把代码纳入到版本管理:

$ git add -A
$ git commit -m "显示个人资料"

四、上传头像

1、上传头像

目前为止,图中的这两张图都是测试图片,接下来我们将一起开发个人资料里的头像上传功能,并将这两张图片换为用户上传的头像。

image.png

2、模型文件修改

首先我们需在 User 模型里将 avatar 字段加入到允许修改的白名单 $fillable 中:

image.png

3、编辑页面

接下来我们在 资料编辑页面 的『个人简介』编辑框下面,增加头像上传的选项:

resources/views/users/edit.blade.php

<div class="form-group mb-4">
    <label for="" class="avatar-label">用户头像</label>
    <input type="file" name="avatar" class="for-control-file">
    
    @if($user->avatar)
        <br>
        <img src="{{$user->avatar}}" width="200px" alt="" class="thumbnail img-responsive">
    @endif
</div>

效果:

image.png

在 Laravel 中,我们可直接通过 请求对象(Request) 来获取用户上传的文件,如以下两种方法:

// 第一种方法
$file = $request->file('avatar');

// 第二种方法,可读性更高
$file = $request->avatar;

接下来我们将在 UsersController 的 update() 方法中,利用 Laravel 的 dd() 调试方法,来查看文件上传的情况:

app/Http/Controllers/UsersController.php

public function update(UserRequest $request, User $user)
{
    dd($request->avatar);

    $user->update($request->all());
    return redirect()->route('users.show', $user->id)->with('success', '个人资料更新成功!');
}


测试一下:

  1. 访问 资料编辑页面 ;

  2. 点击 『choose file』 按钮,选择图片;

  3. 点击『保存』按钮提交表单:

image.png

打印出来是图片文件名称的字符串,与我们的预期不符,图片上传后通过 $request 取到的应是图片对象。

经过一番仔细检查后,发现是因为我们忘记为表单添加 enctype="multipart/form-data" 声明了。请记住,在图片或者文件上传时,为表单添加此句声明是必须的。那我们再次修改下:

resources/views/users/edit.blade.php

<form action="{{ route('users.update', $user->id) }}" method="POST" accept-charset="UTF-8" enctype="multipart/form-data">

重新测试一下:

  1. 访问 资料编辑页面 ;

  2. 点击 『choose file』 按钮,选择图片;

  3. 点击『保存』按钮提交表单:

image.png

这次对了,可以看到是一个对象打印出来。Laravel 的『用户上传文件对象』底层使用了 Symfony 框架的 UploadedFile 对象进行渲染,为我们提供了便捷的文件读取和管理接口,我们将在后面使用这些方法。

现在我们已经能得到用户上传的图片数据了,接下来是对图片进行存储。

4、储存用户上传图片

本项目中,我们不止上传头像需要用到『图片上传功能』,在后面发布帖子功能中,我们也将会允许用户上传图片,所以此处我们需要预先设计一下图片上传相关的逻辑,我们可以将『图片上传』核心操作做成一个工具类:

app/Handlers/ImageUploadHandler.php

<?php
/**
 * Created by PhpStorm.
 * User: SEELE
 * Date: 2019/1/26
 * Time: 14:27
 */

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"
        ];
    }
}

注:请仔细阅读代码注释

我们将使用 app/Handlers 文件夹来存放本项目的工具类,『工具类(utility class)』是指一些跟业务逻辑相关性不强的类,Handlers 意为 处理器 ,ImageUploadHandler 意为图片上传处理器,简单易懂。

接下来我们需要在 UsersController 里调用(注意顶部 use 引入):

app/Http/Controllers/UsersController.php

public function update(UserRequest $request, ImageUploadHandler $uploader, User $user)
{
    $data = $request->all();

    if ($request->avatar){
        $result=$uploader->save($request->avatar,'avatars',$user->id);
        if ($request){
            $data['avatar']=$result['path'];
        }
    }

    $user->update($data);
    return redirect()->route('users.show', $user->id)->with('success', '个人资料更新成功!');
}
  1. 因为我们使用了命名空间,所以需要在顶部加载 use App\Handlers\ImageUploadHandler;

  2. $data = $request->all(); 赋值 $data 变量,以便对更新数据的操作;

  3. 以下代码处理了图片上传的逻辑,注意 if ($result) 的判断是因为 ImageUploadHandler 对文件后缀名做了限定,不允许的情况下将返回 false

if ($request->avatar){
    $result=$uploader->save($request->avatar,'avatars',$user->id);
    if ($request){
        $data['avatar']=$result['path'];
    }
}
  1. $user->update($data); 这一步才是执行更新。

接下来让我们来测试一下:

  1. 访问 资料编辑页面 ;

  2. 点击 『choose file』 按钮,选择图片;

  3. 点击『保存』按钮提交表单。

image.png提交成功,打开项目文件夹,一步步寻找下去,找到我们刚刚上传的图片:

image.png


图片上传成功了,接下来我们做数据嵌套,将我们的头像显示出来。

Git 版本控制

我们在上传图片的时候,程序自动创建了 public/uploads/images/avatars/ 目录,此文件夹下的文件皆为用户上传的头像文件,我们需要防止这些文件被纳入 Git 版本控制器中,可以利用 Git 的 .gitignore 机制来实现:

public/uploads/images/avatars/.gitignore

*
!.gitignore

上面的两行代码意为:当前文件夹下,忽略所有文件,除了 .gitignore

下面把代码纳入到版本管理,为下一节做好准备:

$ git add -A
$ git commit -m "上传头像"

五、显示头像

1、显示头像

目前我们有两个地方用到用户头像,第一个是个人空间,第二个是顶部导航栏。

修改个人空间,将头像的 src 属性修改为 {{ $user->avatar }}

resources/views/users/show.blade.php

<div class="card ">
    <img class="card-img-top" src="{{$user->avatar}}" alt="{{$user->name}}">
    <div class="card-body">

看效果:

image.png

接下来修改顶部导航:

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>

查看效果:

image.png

Git 代码版本控制

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

$ git add -A
$ git commit -m "显示头像"

六、限制头像分辨率

1、图片验证

当用户上传分辨率太小的图片时,会影响网站的美观,所以我们需要对图片的分辨率大小加以限制。得益于 Laravel 强大的表单验证功能,我们只需要在 UserRequest 中增加图片验证规则即可:

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',
        ];
    }

    public function messages()
    {
        return [
            'avatar.mimes' =>'头像必须是 jpeg, bmp, png, gif 格式的图片',
            'avatar.dimensions' => '图片的清晰度不够,宽和高需要 208px 以上',
            'name.unique' => '用户名已被占用,请重新填写',
            'name.regex' => '用户名只支持英文、数字、横杠和下划线。',
            'name.between' => '用户名必须介于 3 - 25 个字符之间。',
            'name.required' => '用户名不能为空。',
        ];
    }
}
  1. rules() 方法中新增了图片比例验证规则 dimensions ,仅允许上传宽和高都大于 208px 的图片;

  2. messages() 方法中新增了头像出错时的提示信息。

宽和高等于 208px 是怎么得到呢?调出 Chrome 开发者工具 进行源代码查看,鼠标放置于图片链接时,即可看到图片尺寸:

image.png

接下来让我们来测试一下:

  1. 访问 资料编辑页面 ;

  2. 点击 『choose file』 按钮,选择一张尺寸小于 208px 的图片:

  3. 点击『保存』按钮提交表单:

  4. image.png


如上图可以看到我们自定义的错误消息提示。

Git 代码版本控制

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

$ git add -A
$ git commit -m "限制头像分辨率"

七、裁剪头像

裁剪图片

我们还有一个地方要优化,用户有时会上传分辨率较大的图片

而我们个人空间里显示区域最大也就 208px,即使要兼容 视网膜屏幕(Retina Screen) 的话,最多也就需要 208px * 2 = 416px 。图片太大会拖慢页面的加载速度,所以接下来我们将对此进行优化。

我们将使用备受欢迎的 Intervention/image 扩展包来处理图片裁切的逻辑,接下来我们需要先安装此扩展包;

1、安装扩展包

(1)Composer安装

$ composer require intervention/image

(2)配置信息

执行以下命令获取配置信息:

$ php artisan vendor:publish --provider="Intervention\Image\ImageServiceProviderLaravel5"

结果如下:

image.png

打开 config/image.php 文件可以看到只有一个驱动器的选项,支持的值有 GD 库 和 ImageMagic

image.png注:此处我们使用默认的 gd 即可,如果将要开发的项目需要较专业的图片,请考虑 ImageMagic。


(2)开始裁剪

我们将裁切的逻辑写在 ImageUploadHandler 中,请将以下代码替换:

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();
    }
}


注:请仔细阅读代码注释,此次新增 reduceSize() 方法,以及此方法的调用。

以上的 save() 方法中,我们新增了 $max_width 参数,用来指定最大图片宽度,我们修改 UsersController 的 update() 方法中的调用,修改为:

$result = $uploader->save($request->avatar, 'avatars', $user->id, 416);

如下图:

image.png

2、开始测试

进入 资料编辑页面 ,选择一张较大的图片(示例图片下载),然后点击保存提交表单:

image.png

可以看到更新成功的提示:

image.png

打开文件夹查看我们刚刚上传的文件:

image.png


至此图片上传功能开发完毕。

Git 代码版本控制

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

$ git add -A
$ git commit -m "裁剪头像"

八、授权访问

1、问题说明

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

  1. 未登录用户可以访问 edit 和 update 动作,如果你退出登录,以游客身份访问 http://larabbs.test/users/1/edit :

image.png

  1. 登录用户可以更新其它用户的个人信息,登录 Summer 用户然后访问 Monkey 用户的编辑资料页面 http://larabbs.test/users/2/edit :

image.png

登录状态的 1 号用户 Summer 居然可以访问 2 号用户 Monkey 的个人编辑页面,甚至是修改内容。

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

2、限制游客访问

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

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

app/Http/Controllers/UsersController.php






回复列表



回复操作

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

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