导航
导航
文章目录
  1. 第一章 基础设置
    1. 1.1 开发环境
    2. 1.2 配置国内软件源
    3. 1.3 创建 Laravel 项目
    4. 1.4 更改 User 模型位置
    5. 1.5 数据库 User 表设计
  2. 第二章 用户注册后发送验证邮件
    1. 2.1 阿里云邮件推送服务
    2. 2.2 发送注册验证邮件
    3. 2.3 Sendcloud 邮件服务
  3. 第三章 用户注册与登录消息提示
    1. 3.1 引入 laracasts/flash 包
    2. 3.2 设置帐号未激活不能登录
  4. 第四章 视图文件汉化、增加中文语言包
    1. 4.1 简体中文语言包
    2. 4.2 更新前端资源
  5. 第五章 实现找回密码
  6. 第六章 设计问题表
  7. 第七章 发布问题
    1. 7.1 安装 UEditor 编辑器
    2. 7.2 发布问题
  8. 第八章 问题表单字段验证
    1. 8.1 方法一:快速验证
    2. 8.2 方法二:表单请求验证
  9. 第九章 美化编辑器
    1. 9.1 登录后才能发布问题
    2. 9.2 自定义 toolbars
  10. 第十章 定义话题与问题关系
    1. 10.1 Topic 模型
    2. 10.2 Question 模型
  11. 第十一章 使用 Select2 优化话题选择
    1. 11.1 编译压缩 Select2 资源
    2. 11.2 多选框(静态)
    3. 11.3 多选框(动态)
  12. 第十二章 创建和显示话题
    1. 12.1 新建话题
    2. 12.2 显示问题的所有话题
  13. 第十三章 使用 Repository 模式
  14. 第十四章 实现编辑问题
    1. 14.1 编辑问题
    2. 14.2 更新问题
  15. 第十五章 问题列表和删除问题
    1. 15.1 问题列表
    2. 15.2 删除问题
  16. 第十六章 创建问题的答案 Answer 模型
  17. 第十七章 实现提交回答
    1. 17.1 创建 Answer 控制器
    2. 17.2 验证 Answer 表单
    3. 17.3 视图中显示 Answer
  18. 第十八章 用户关注问题
    1. 18.1 提交回答需要登录
    2. 18.2 关注问题
      1. 18.2.1 数据库迁移
      2. 18.2.2 视图文件
      3. 18.2.3 路由
      4. 18.2.4 「关注问题」控制器
      5. 18.2.5 模型
  19. 第十九章 用户关注问题(下)
    1. 19.1 使用 toggle 方法避免重复关注
    2. 19.2 获取关注状态
  20. 第二十章 使用 Vuejs 实现关注问题
  21. 第二十一章 前后端分离 API token 认证
    1. 21.1 在 users 表新增 api_token 字段
    2. 21.2 获取前端生成的 api_token 字符串
    3. 21.3 前端安全优化
    4. 21.4 优化路由
  22. 第二十二章 关注用户(上)
    1. 22.1 新增 followers 表
    2. 22.2 视图
  23. 第二十三章 关注用户(下)
    1. 23.1 vuejs 组件
    2. 23.2 路由
    3. 23.3 控制器
    4. 23.4 用户模型
  24. 第二十四章 站内信通知
  25. 第二十五章 关注用户之邮件通知
    1. 25.1 Sendcloud 邮件服务
    2. 25.2 Directmail 邮件服务
  26. 第二十六章 重构邮件通知代码
  27. 第二十七章 对答案进行点赞
  28. 第二十八章 私信功能(上)
  29. 第二十九章 私信功能(下)
  30. 第三十章 实现评论(上)
  31. 第三十一章 Vuejs 实现评论组件
  32. 第三十二章 Repository 模式重构代码
  33. 第三十三章 自定义 helper
  34. 第三十四章 私信列表
  35. 第三十五章 私信列表(下)
  36. 第三十六章 回复私信
  37. 第三十七章 标记私信已读
  38. 第三十八章 显示未读私信
  39. 第三十九章 私信实现 Repository 模式
  40. 第四十章 私信通知
  41. 第四十一章 notifications 已读
  42. 第四十二章 上传头像组件
  43. 第四十三章 头像上传到服务器
  44. 第四十四章 头像上传到七牛
  45. 第四十五章 实现修改密码
  46. 第四十六章 用户个人设置
  47. 第四十七章 重构用户设置

Laravel 5 开发知乎笔记

开发一个类似于知乎的站点:用户可以发布问题,提交回答,发表评论等,用户之间可以相互关注,发送私信等。

第一章 基础设置

1.1 开发环境

  • OS: macOS High Sierra 10.13
  • Homestead: 6.5
  • Vagrant box: parallels 4.0.0
  • Laravel 5.5
  • PHP 7.1.10 , MySQL 5.7.19 , Nginx 1.13.3

1.2 配置国内软件源

Composer

composer config -g repo.packagist composer https://packagist.laravel-china.org

NodeJS

# 推荐使用 yarn 
yarn config set registry https://registry.npm.taobao.org --global && \
yarn config set disturl https://npm.taobao.org/dist --global && \
yarn config set sass-binary-site https://npm.taobao.org/mirrors/node-sass

# 二选一
npm config set registry https://registry.npm.taobao.org --global && \
npm config set disturl https://npm.taobao.org/dist --global && \
npm config set sass-binary-site https://npm.taobao.org/mirrors/node-sass

# 查看效果
yarn config list

Ubuntu 16.04

sed -i 's/archive.ubuntu.com/mirrors.163.com/g' /etc/apt/sources.list && \
sed -i 's/deb http:\/\/security/#deb http:\/\/security/g' /etc/apt/sources.list && \
sed -i 's/deb-src http:\/\/security/#deb-src http:\/\/security/g' /etc/apt/sources.list && \
apt-get -y update

1.3 创建 Laravel 项目

# 创建项目
composer create-project --prefer-dist laravel/laravel zhihu "5.5.*"

# 更新依赖库
yarn install && \
composer update && \
composer dump-autoload 

# 其他参考命令
yarn install --no-bin-links (windows)
yarn install --force (重构某个包)
npm rebuild node-sass

# 导入 Git 配置文件
cat .gitconfig > ~/.gitconfig
git config --global --edit

1.4 更改 User 模型位置

User.php 放置到 app/Models/ 文件夹,命名空间改成:

namespace App\Models;

修改 auth.php 配置文件:

<?php
// config\auth.php
    'providers' => [
        'users' => [
            'driver' => 'eloquent',
            'model' => App\Models\User::class,
        ],
    ],

1.5 数据库 User 表设计

文档:《Laravel 的数据库迁移 Migrations》

设计 User 表结构

<?php
// 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')->unique();
            $table->string('email')->unique();
            $table->string('password');
            $table->string('avatar');
            $table->string('confirmation_token');
            $table->smallInteger('is_active')->default(0);
            $table->string('questions_count')->default(0);
            $table->string('answers_count')->default(0);
            $table->string('comments_count')->default(0);
            $table->string('favorites_count')->default(0);
            $table->string('likes_count')->default(0);
            $table->string('followers_count')->default(0);
            $table->string('following_count')->default(0);
            $table->json('settings')->nullable();
            $table->rememberToken();
            $table->timestamps();
        });
    }

修改 .env 配置信息

运行数据库迁移

php artisan migrate

第二章 用户注册后发送验证邮件

2.1 阿里云邮件推送服务

修改 composer.json 然后 composer update

"require": {
    "wang_yan/directmail": "dev-master"
},

或者在项目目录下执行

composer require wang_yan/directmail:dev-master

然后修改 config/app.php,添加服务提供者

<?php
'providers' => [
   // 添加这行
    WangYan\DirectMail\DirectMailTransportProvider::class,
];

最后在 .env 中配置你的密钥, 并修改邮件驱动为 directmail

MAIL_DRIVER=directmail

DIRECT_MAIL_KEY=     # AccessKeyId
DIRECT_MAIL_SECRET=  # AccessSecret

2.2 发送注册验证邮件

初始化 Laravel 认证模块

php artisan make:auth

修改 RegisterController 控制器

<?php 
// app\Http\Controllers\Auth\RegisterController.php

    use App\Models\User;
    use Illuminate\Support\Facades\Mail;

    protected function create(array $data)
    {
        $user =  User::create([
            'name' => $data['name'],
            'email' => $data['email'],
            'avatar' => 'default.jpg',
            'confirmation_token' => str_random(40),
            'password' => bcrypt($data['password']),
        ]);

        $this->sendVerifyEmailTo($user);
        return $user;
    }

    private function sendVerifyEmailTo($user)
    {
        $data = [
            'name' => $user->name,
            'url'  => Route('email.verify',['token' => $user->confirmation_token])
        ];

        Mail::send('emails.register', $data, function ($message) use ($user) {
            $message->from('service@dm.mail.wangyan.org', env('APP_NAME','Laravel'));
            $message->to($user->email);
            $message->subject('请验证您的 Email 地址');
        });
    }

修改 User 模型

<?php 
// app\Models\User.php
    protected $fillable = [
        'name', 'email', 'password', 'avatar', 'confirmation_token',
    ];

增加邮件模板

vim resources\views\emails\register.blade.php

增加路由

<?php
// routes\web.php
Route::get('/email/verify/{token}', 'EmailController@verify')->name('email.verify');

增加控制器

php artisan make:controller EmailController

编辑控制器

<?php
// app\Http\Controllers\EmailController.php
use App\Models\User;

class EmailController extends Controller
{
    /**
     * @param $token
     * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
     */
    function verify($token)
    {
        $user = User::where('confirmation_token',$token)->first();

        if(is_null($user)){
            return redirect('/');
        }

        $user->is_active = 1;

        $user->confirmation_token= str_random(40);
        $user->save();

        return redirect('/home');
    }
}

2.3 Sendcloud 邮件服务

修改 composer.json 然后 composer update

"require": {
    "naux/sendcloud": "^1.1",
},

或者在项目目录下执行

composer require naux/sendcloud

修改 config/app.php,添加服务提供者

<?php
'providers' => [
   // 添加这行
    Naux\Mail\SendCloudServiceProvider::class,
];

.env 中配置你的密钥, 并修改邮件驱动为 sendcloud

MAIL_DRIVER=sendcloud

SEND_CLOUD_USER=   # 创建的 api_user
SEND_CLOUD_KEY=    # 分配的 api_key

第三章 用户注册与登录消息提示

3.1 引入 laracasts/flash

这个包的作用是将消息提示放到 session 中

项目:《Easy Flash Messages for Laravel App》

用法:

  • flash('Message')->success(): success 样式
  • flash('Message')->error(): error 样式
  • flash('Message')->warning(): warning 样式
  • flash('Message')->overlay(): 弹窗
  • flash()->overlay('Modal Message', 'Modal Title'): 有标题的弹窗
  • flash('Message')->important(): 有关闭按钮
  • flash('Message')->error()->important(): 可以关闭的错误提示

修改 composer.json 然后 composer update

"require": {
    "laracasts/flash": "^3.0"
},

或者在项目目录下执行

composer require laracasts/flash

新增服务提供者

<?php
// config\app.php
'providers' => [
    Laracasts\Flash\FlashServiceProvider::class,
];

修改 EmailController 控制器

<?php
// app\Http\Controllers\EmailController.php
    function verify($token)
    {
        $user = User::where('confirmation_token',$token)->first();

        if(is_null($user)){
            // Render the message as an overlay.
            flash()->overlay('邮箱验证失败!', '温馨提示');
            return redirect('/');
        }

        $user->is_active = 1;

        $user->confirmation_token= str_random(40);
        $user->save();

        flash('邮箱验证成功!')->success()->important();
        return redirect('/home');
    }

修改 HomeController 控制器

<?php
// app\Http\Controllers\HomeController.php
    public function index()
    {
        flash('登入成功!')->success()->important();
        return view('home');
    }

修改视图模板

<?php
// 使用到 overlay() 弹窗时引入
// resources\views\layouts\app.blade.php
    <script>
        $('#flash-overlay-modal').modal();
    </script>
<?php
// 在显示消息的位置引入
// resources\views\home.blade.php
<div class="panel-body">
    @include('flash::message')
</div>
<?php
// resources\views\welcome.blade.php
// 将下面代码放在适当位置
<link href="https://cdn.bootcss.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">

@include('flash::message')

<script src="https://cdn.bootcss.com/jquery/3.2.1/jquery.min.js"></script>
<script src="https://cdn.bootcss.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
<script>
    $('#flash-overlay-modal').modal();
</script>

3.2 设置帐号未激活不能登录

<?php
// app\Http\Controllers\Auth\LoginController.php
use Illuminate\Http\Request;

protected function attemptLogin(Request $request)
{
    $credentials = array_merge($this->credentials($request),['is_active' => '1']);
    return $this->guard()->attempt(
        $credentials,$request->has('remember')
    );
}

第四章 视图文件汉化、增加中文语言包

4.1 简体中文语言包

  • 汉化默认视图文件
  • 将Google字体换成中科大源
  • 增加 cmn-Hans 简体中文语言包

4.2 更新前端资源

项目:《Laravel 资源任务编译器 Laravel Mix》

/* resources/assets/sass/app.scss */
@import url(https://fonts.lug.ustc.edu.cn/css?family=Raleway:300,400,600);

编译

yarn run production

其他详细过程,略

laravel-zhihu-01

第五章 实现找回密码

详细原理(略),可从 app\Http\Controllers\Auth\ForgotPasswordController.php 开始理解

<?php
// app\Models\User.php
use Illuminate\Support\Facades\Mail;

public function sendPasswordResetNotification($token)
{
    $data = [
        'title' => env('APP_NAME','Laravel'),
        'name'  => $this->name,
        'url'   => url('password/reset',$token)
    ];
    Mail::send('emails.reset', $data, function ($message) {
        $message->from('service@sc.mail.wangyan.org', env('APP_NAME','Laravel'));
        $message->to($this->email);
        $message->subject('重设密码');
    });
}

增加邮件模板

vim resources\views\emails\reset.blade.php

第六章 设计问题表

文档:Laravel 的数据库迁移 Migrations

创建 Model 的同时,生成 migration 迁移文件

# 注意双反斜杆
php artisan make:model Models\\Question -m

修改 migrate,设计表结构

<?php
// database/migrations/create_questions_table.php
    public function up()
    {
        Schema::create('questions', function (Blueprint $table) {
            $table->increments('id');
            $table->string('title');
            $table->string('body');
            $table->integer('user_id')->unsigned();
            $table->integer('comments_count')->default(0);
            $table->integer('followers_count')->default(0);
            $table->integer('answers_count')->default(0);
            $table->string('close_comment',8)->default('F');
            $table->string('is_hidden',8)->default('F');
            $table->timestamps();
        });
    }

运行迁移

php artisan migrate

第七章 发布问题

7.1 安装 UEditor 编辑器

修改 composer.json 然后 composer update

"require": {
    "overtrue/laravel-ueditor": "~1.0"
},

或者在项目目录下执行

composer require "overtrue/laravel-ueditor:~1.0"

添加下面一行到 config/app.phpproviders 部分

Overtrue\LaravelUEditor\UEditorServiceProvider::class,

发布配置文件与资源

php artisan vendor:publish --provider='Overtrue\LaravelUEditor\UEditorServiceProvider'

附件存储(需要以管理员身份允许终端)

文档:Laravel 的文件系统和云存储功能集成

php artisan storage:link

7.2 发布问题

创建 QuestionsController 控制器

项目:《Laravel 的 HTTP 控制器》

php artisan make:controller QuestionsController --resource

定义 questions 资源路由

<?php
// routes/web.php
Route::resource('questions','QuestionsController');

修改 QuestionsController 控制器

<?php
// app\Http\Controllers\QuestionsController.php
    use App\Models\Question;
    use Illuminate\Support\Facades\Auth;

    // 首页显示所有问题
    public function index()
    {
        $questions = Question::all();
        return $questions;
    }

    // 返回创建问题视图
    public function create()
    {
        return view('questions.create');
    }

    // 保存问题,然后返回该问题视图
    public function store(Request $request)
    {
        $data = [
            'title' => $request->get('title'),
            'body' => $request->get('body'),
            'user_id' => Auth::id() // 获取登陆用户 ID
        ];
        $question = Question::create($data); // 保存问题
        return redirect()->route('questions.show',$question->id); // 重定向
    }

    // 返回问题视图
    public function show($id)
    {
        $question = Question::findOrfail($id);
        return view('questions.show',compact('question'));
    }

修改 Question 模型中的 fillable

<?php
// app\Models\Question.php
class Question extends Model
{
    protected $fillable = ['title','body','user_id'];
}

新建「创建问题」视图文件

<?php
//resources\views\questions\create.blade.php
@extends('layouts.app')

@section('content')
    @include('vendor.ueditor.assets')
    <div class="container">
        <div class="row">
            <div class="col-md-8 col-md-offset-2">
                <div class="panel panel-default">
                    <div class="panel-heading">发布问题</div>
                    <div class="panel-body">
                        @include('flash::message')
                        <form action="/questions" method="post">
                            {!! csrf_field() !!}
                            <div class="form-group">
                                <label for="title">标题</label>
                                <input type="text" name="title" class="form-control" placeholder="标题" class="title">
                            </div>
                            <script id="container" name="body" type="text/plain"></script><br>
                            <button type="submit" class="btn btn-block btn-success pull-right">发布问题</button>
                        </form>
                    </div>
                    <script type="text/javascript">
                        var ue = UE.getEditor('container');
                        ue.ready(function() {
                            ue.execCommand('serverparam', '_token', '{{ csrf_token() }}');
                        });
                    </script>
                </div>
            </div>
        </div>
    </div>
@endsection

提交后问题后显示的页面

<?php
//resources\views\questions\show.blade.php
@extends('layouts.app')

@section('content')
    <div class="container">
        <div class="row">
            <div class="col-md-8 col-md-offset-2">
                <div class="panel panel-default">
                    <div class="panel-heading">{{$question->title}}</div>
                    <div class="panel-body">
                        {!! $question->body !!}
                    </div>
                </div>
            </div>
        </div>
    </div>
@endsection

第八章 问题表单字段验证

文档:《Laravel 的表单验证机制详解》

8.1 方法一:快速验证

将验证逻辑直接写到控制器中,使用 Illuminate\Http\Request 对象提供的 validate 方法进行验证。

<?php
// app\Http\Controllers\QuestionsController.php
    public function store(Request $request)
    {
        $rules = [
            'title' => 'required|min:6|max:128',
            'body' => 'required|min:12',
        ];
        $messages = [
            'title.required' => '标题不能为空',
            'title.min' => '标题长度至少要6个字符',
            'title.max' => '标题最长不能超过128个字符',
            'body.required' => '内容不能为空',
            'body.min' => '内容至少要有12个字符',
        ];
        $this->validate($request,$rules,$messages);
        $data = [
            'title' => $request->get('title'),
            'body' => $request->get('body'),
            'user_id' => Auth::id()
        ];
        $question = Question::create($data);
        return redirect()->route('questions.show',$question->id);
    }

编辑视图文件

<?php
// resources\views\questions\create.blade.php
@extends('layouts.app')

@section('content')
    @include('vendor.ueditor.assets')
    <div class="container">
        <div class="row">
            <div class="col-md-8 col-md-offset-2">
                <div class="panel panel-default">
                    <div class="panel-heading">发布问题</div>
                    <div class="panel-body">
                        @include('flash::message')
                        <form action="/questions" method="post">
                            {!! csrf_field() !!}
                            <div class="form-group{{ $errors->has('title') ? ' has-error' : '' }}">
                                <label for="title">标题</label>
                                <input type="text" name="title" class="form-control" placeholder="标题" value="{{ old('title') }}" required autofocus>
                                @if ($errors->has('title'))
                                    <span class="help-block">
                                        <strong>{{ $errors->first('title') }}</strong>
                                    </span>
                                @endif
                            </div>
                            <div class="form-group{{ $errors->has('body') ? ' has-error' : '' }}">
                                <script id="container" name="body" type="text/plain">
                                    {!! old('title') !!}
                                </script><br>
                                @if ($errors->has('body'))
                                    <span class="help-block">
                                        <strong>{{ $errors->first('body') }}</strong>
                                    </span>
                                @endif
                            </div>
                            <button type="submit" class="btn btn-block btn-success pull-right">发布问题</button>
                        </form>
                    </div>
                    <script type="text/javascript">
                        var ue = UE.getEditor('container');
                        ue.ready(function() {
                            ue.execCommand('serverparam', '_token', '{{ csrf_token() }}');
                        });
                    </script>
                </div>
            </div>
        </div>
    </div>
@endsection

8.2 方法二:表单请求验证

使用 Artisan 命令 make:request 创建表单请求类来处理更复杂的验证。

php artisan make:request StoreQuestionRequest

添加验证规则到 rules 方法中

注意:表单请求类内也包含了 authorize 方法,用来判断用户是否有权限做出此请求。

<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreQuestionRequest extends FormRequest
{
    // 如果你打算在其他地方处理授权逻辑,这里返回 true
    public function authorize()
    {
        return true;
    }

    // 规则
    public function rules()
    {
        return [
            'title' => 'required|min:6|max:128',
            'body' => 'required|min:12',
        ];
    }

    // 自定义错误消息
    public function messages()
    {
        return [
            'title.required' => '标题不能为空',
            'title.min' => '标题长度至少要6个字符',
            'title.max' => '标题最长不能超过128个字符',
            'body.required' => '内容不能为空',
            'body.min' => '内容至少要有12个字符',
        ];
    }
}

修改控制器使用 StoreQuestionRequest 类替换默认的 Request

<?php
// app\Http\Controllers\QuestionsController.php
    use App\Http\Requests\StoreQuestionRequest;
    public function store(StoreQuestionRequest $request)
    {
        //
    }

第九章 美化编辑器

9.1 登录后才能发布问题

登录后才能发布问题,indexshow 页面例外。

<?php
// app\Http\Controllers\QuestionsController.php
    public function __construct()
    {
        $this->middleware('auth')->except(['index','show']);
    }

9.2 自定义 toolbars

<!-- 定义容器的高度为200 -->
<script id="container" style="height: 200px" name="body" type="text/plain">
    {!! old('title') !!}
</script>

<script type="text/javascript">
    var ue = UE.getEditor('container', {
        toolbars: [
            ['bold', 'italic', 'underline', 'strikethrough', 'blockquote', 'insertunorderedlist', 'insertorderedlist', 'justifyleft','justifycenter', 'justifyright',  'link', 'insertimage', 'fullscreen']
        ],
        elementPathEnabled: false,
        enableContextMenu: false,
        autoClearEmptyNode:true,
        wordCount:false,
        imagePopup:false,
        autotypeset:{ indent: true,imageBlockLine: 'center' }
    });
    ue.ready(function() {
        ue.execCommand('serverparam', '_token', '{{ csrf_token() }}');
    });
</script>

第十章 定义话题与问题关系

10.1 Topic 模型

创建「话题」模型与数据表(请留意单、复数)

question_topicquestionstopics 的多对多中间表
数据库表名是复数,中间表是单数

# 创建模型同时创建 topics 表(复数)
php artisan make:model Models\\Topic -m

# 注意是 create_questions_topics_table 
php artisan make:migration create_questions_topics_table --create=question_topic

定义「话题」数据表结构

<php
// database/migrations/create_topics_table.php
    public function up()
    {
        Schema::create('topics', function (Blueprint $table) {
            $table->increments('id');
            $table->string('name');
            $table->text('bio')->nullable();
            $table->integer('questions_count')->default(0);
            $table->integer('followers_count')->default(0);
            $table->timestamps();
        });
    }

定义「问题」——「话题」数据表结构

// database\migrations\create_questions_topics_table.php
    public function up()
    {
        Schema::create('question_topic', function (Blueprint $table) {
            $table->increments('id');
            $table->integer('question_id')->unsigned()->index();
            $table->integer('topic_id')->unsigned()->index();
            $table->timestamps();
        });
    }
php artisan migrate

定义「问题」——「话题」多对多关系

文档:《Eloquent:关联:多对多》

10.2 Question 模型

问题模型:一个问题属于多个话题

<?php
// app/Models/Question.php
class Question extends Model
{
    protected $fillable = ['title','body','user_id'];

    // 获得此问题所属话题(复数)
    public function topics()
    {
        //  一个问题属于多个话题
        // belongsToMany('关联模型「Topic」','中间表表名','当前模型「Question」在中间表的外键','关联模型「Topic」在中间表的外键')
        // 中间表表名,Eloquent 会按照字母顺序合并两个关联模型的名称
        // question_id 默认采用的是下划线方式命名 
        return $this->belongsToMany(Topic::class)->withTimestamps();
        return $this->belongsToMany('App\Models\Topic', 'question_topic','question_id','topic_id')->withTimestamps();
    }
}

话题模型:一个话题下有多个问题

<?php
// app\Models\Topic.php
class Topic extends Model
{
    protected $fillable = ['name','questions_count','followers_count'];

    public function questions()
    {
        // 参数也可以使用 App\Models\Qeustion
        return $this->belongsToMany(Qeustion::class)->withTimestamps();
    }
}

第十一章 使用 Select2 优化话题选择

11.1 编译压缩 Select2 资源

文档:《Laravel 的资源任务编译器 Laravel Mix》

引人 Select2 的 JS、CSS 文件

https://github.com/select2/select2

resources\assets\css\select2.min.css
resources\assets\js\select2.min.js
resources\assets\js\select2.zh-CN.js

引入 JS

// resources\assets\js\app.js
require('./select2.min');
require('./select2.zh-CN');

引入 css

/* resources\assets\sass\app.scss */
@import "../css/select2.min";

禁用缓存

// webpack.mix.js
mix.js('resources/assets/js/app.js', 'public/js')
   .sass('resources/assets/sass/app.scss', 'public/css')
   .version(['public/js/app.js','public/css/app.css']);

编译资源

yarn run production

修改引入文件地址

<?php
// resources\views\layouts\app.blade.php
<link href="{{ mix('css/app.css') }}" rel="stylesheet">

<script src="{{ mix('js/app.js') }}"></script>
@yield('js');

11.2 多选框(静态)

http://select2.github.io/examples.html

<?php
// resources\views\questions\create.blade.php

<div class="form-group">
    <label for="topic">话题</label>
    <select class="js-example-basic-multiple form-control" multiple="multiple">
        <option value="AL">Alabama</option>
        <option value="WY">Wyoming</option>
    </select>
</div>

@section('js')
    <script type="text/javascript">
        $(".js-example-basic-multiple").select2();
    </script>
@endsection

11.3 多选框(动态)

生成测试数据

文档:《Laravel 数据库之:数据填充》

php artisan make:factory TopicFactory
<?php
// database\factories\TopicFactory.php
use Faker\Generator as Faker;

$factory->define(App\Models\Topic::class, function (Faker $faker) {
    return [
        'name' => $faker->word,
        'bio' => $faker->paragraph,
    ];
});

修改 Topic 模型中的 fillable

<?php
// app\Models\Topic.php
protected $fillable = ['name','questions_count','followers_count','bio'];

使用 tinker 填充数据

php artisan tinker
factory(App\Models\Topic::class,10)->create();
quit

定义 Topic 的 api 路由

<?php
// routes\api.php
Route::get('/topics', function (Request $request) {
    $topics = App\Models\Topic::select(['id','name'])->where('name','like','%'.$request->query('q').'%')->get();
    return $topics;
});

修改「创建问题」视图文件

<?php
// resources\views\questions\create.blade.php
<div class="form-group">
    <label for="topic">话题</label>
    <select name="topic[]" class="select2-placeholder-multiple form-control" multiple="multiple"></select>
</div>

@section('js')
    <script type="text/javascript">
        $(document).ready(function() {
            function formatTopic (topic) {
                return  "<div class='select2-result-repository clearfix'>" +
                        "<div class='select2-result-repository__meta'>" +
                        "<div class='select2-result-repository__title'>" +
                        topic.name ? topic.name : "Laravel"   +
                        "</div></div></div>";
            }

            function formatTopicSelection (topic) {
                return topic.name || topic.text;
            }

            $(".select2-placeholder-multiple").select2({
                language: "zh-CN",
                tags: true,
                placeholder: '选择相关话题',
                minimumInputLength: 1,
                ajax: {
                    url: '/api/topics',
                    dataType: 'json',
                    delay: 250,
                    data: function (params) {
                        return {
                            q: params.term,
                        };
                    },
                    processResults: function (data) {
                        return {
                            results: data,
                        };
                    },
                    cache: true
                },
                templateResult: formatTopic,
                templateSelection: formatTopicSelection,
                escapeMarkup: function (markup) { return markup; }
            });
        });
    </script>
@endsection

第十二章 创建和显示话题

12.1 新建话题

文档:《Laravel 的集合 Collection》文档:《Eloquent:多对多关联》

<?php
// app\Http\Controllers\QuestionsController.php
    use App\Models\Topic;
    public function store(StoreQuestionRequest $request)
    {
        // 返回「话题」数组
        // 如果不是数组,则「话题」($topicsArray) 为空
        $topics = $request->get('topic');
        if(is_array($topics)){
            $topicsArray = $this->normalizeTopic($topics);
        }
        $data = [
            'title' => $request->get('title'),
            'body' => $request->get('body'),
            'user_id' => Auth::id()
        ];
        $question = Question::create($data);
        // 使用 attach 方法向「问题」——「话题」中间表插入一条数据
        // 前提是 Question 模型下有 topics() 方法
        $question->topics()->attach($topicsArray);
        return view('questions.show',compact('question'));
    } 
    // collect 函数:从数组中创建一个全新的「集合」实例
    // map 方法遍历集合并将每一个值传入给定的回调
    // map 返回一个新的集合实例,它不会修改它所调用的集合
    private function normalizeTopic(array $topics)
    {
        return collect($topics)->map(function($topic){
            // 如果话题存在,则$topic返回话题的ID(数字类型),否则返回新话题名称
            if(is_numeric($topic)){
                // 该话题的问题数+1
                Topic::find($topic)->increment('questions_count');
                return (int) $topic;
            }
            // 新建话题后返回该话题ID
            $newTopic = Topic::create(['name'=>$topic,'questions_count'=>1]);
            return $newTopic->id;
        })->toArray();
    }

12.2 显示问题的所有话题

QuestionsController 控制器

<?php
// app\Http\Controllers\QuestionsController.php
    public function show($id)
    {
        // 使用 with 方法进行预加载,减少查询次数
        // Question 模型下要有 topics() 方法
        $question = Question::where('id',$id)->with('topics')->first();
        return view('questions.show',compact('question'));
    }

「问题」视图

<?php
// resources\views\questions\show.blade.php
@extends('layouts.app')

@section('content')
    <div class="container">
        <div class="row">
            <div class="col-md-8 col-md-offset-2">
                <div class="panel panel-default">
                    <div class="panel-heading">
                        {{$question->title}}
                        @foreach($question->topics as $topic)
                            <a class="topic btn btn-link pull-right" href="/topic/{{$topic->id}}">{{$topic->name}}</a>
                        @endforeach
                    </div>
                    <div class="panel-body">
                        {!! $question->body !!}
                    </div>
                </div>
            </div>
        </div>
    </div>
@endsection

css 美化

resources\assets\css\style.css

/* resources\assets\sass\app.scss */
@import "../css/style";
yarn run production

第十三章 使用 Repository 模式

Repository 模式的目的是将一些高频操作放在一起

App\Repositories 文件夹创建 QuestionsRepository

<?php`

// app\Repositories\QuestionsRepository.php
namespace App\Repositories;
use App\Models\Question;
class QuestionsRepository
{
//通过「问题ID」查找带有「话题」的「问题」
public function byIdWithTopics($id)
{
$questions = Question::where(‘id’,$id)->with(‘topics’)->first();
return $questions;
}

// 根据 $attributes 数组来创建「问题」 
public function create(array $attributes)
{
    return Question::create($attributes);
}

}


编辑 `QuestionsController` 控制器

```php
<?php
// app\Http\Controllers\QuestionsController.php
use App\Repositories\QuestionsRepository;
class QuestionsController extends Controller
{
    protected $questionRepository;

    public function __construct(QuestionsRepository $questionRepository)
    {
        $this->questionRepository = $questionRepository;
        $this->middleware('auth')->except(['index','show']);
    }

    public function store(StoreQuestionRequest $request)
    {
        // 修改 $question = Question::create($data);
        $question = $this->questionRepository->create($data);
    }

    public function show($id)
    {
        // 修改 $question = Question::where('id',$id)->with('topics')->first();
        $question = $this->questionRepository->byIdWithTopics($id);
    }
}

第十四章 实现编辑问题

14.1 编辑问题

路由情况

route-question

修改 QuestionsController 控制器

<?php
// app\Http\Controllers\QuestionsController.php
    public function edit($id)
    {
        $question = $this->questionRepository->byID($id);
        // 只有问题创建者才能编辑
        if (Auth::user()->owns($question)){
            return view('questions.edit',compact('question'));
        }
        return back();
    }

QuestionsRepository 中,通过ID获取问题

<?php
// app\Repositories\QuestionsRepository.php
class QuestionsRepository
{
    public function byID($id)
    {
        $question = Question::findOrfail($id);
        return $question;
    }
}

只有问题创建者才能编辑自己的问题

<?php
// app\Models\User.php
    use Illuminate\Database\Eloquent\Model;
    public function owns(Model $model)
    {
        // 判断当前用户Id是否等于问题创建者的ID
        // Auth::user()->id == $question->user_id
        return $this->id == $model->user_id;
    }

「编辑问题」视图文件

复制 create.blade.php 文件为edit.blade.php,并修改以下内容:

<?php
// resources/views/questions/edit.blade.php
// PATCH 方法对应的是 update 路由
<form action="/questions/{{ $question->id }}" method="post">
    {{ method_field('PATCH') }}
</form>

14.2 更新问题

QuestionsController 控制器

文档:《多对多关联》

StoreQuestionRequest 是验证规则,sync 用于同步关联

<?php
// app\Http\Controllers\QuestionsController.php
    public function update(StoreQuestionRequest $request, $id)
    {

        $question = $this->questionRepository->byID($id);
        $question->update([
            'title' => $request->get('title'),
            'body' => $request->get('body'),
        ]);
        // sync 方法可以接收 ID 数组,向中间表插入对应关联数据记录。
        // 所有没放在数组里的 IDs 都会从中间表里移除。
        $topics = $request->get('topic');
        if(is_array($topics)){
            $topicsArray = $this->normalizeTopic($topics);
        }
        $question->topics()->sync($topicsArray);
        return redirect()->route('questions.show',$question->id);
    }

「问题」视图文件

<?php
// resources/views/questions/show.blade.php
@extends('layouts.app')

@section('content')
    <div class="container">
        <div class="row">
            <div class="col-md-8 col-md-offset-2">
                <div class="panel panel-default">
                    <div class="panel-heading">
                        {{$question->title}}
                        @foreach($question->topics as $topic)
                            <a class="topic btn btn-link pull-right" href="/topic/{{$topic->id}}">{{$topic->name}}</a>
                        @endforeach
                    </div>
                    <div class="panel-body">
                        {!! $question->body !!}
                    </div>
                    <div class="actions panel-footer">
                        @if(Auth::check()  && Auth::user()->owns($question))
                            <span class="edit">
                                <a href="/questions/{{$question->id}}/edit">编辑</a>
                            </span>
                        @endif
                    </div>
                </div>
            </div>
        </div>
    </div>
@endsection

第十五章 问题列表和删除问题

15.1 问题列表

编辑问题控制器

http://php.net/manual/zh/function.compact.php

<?php
// app\Http\Controllers\QuestionsController.php
    public function index()
    {
        $questions = $this->questionRepository->getQuestionsFeed();
        return view('questions.index',compact('questions'));
    }

编辑 QuestionsRepository

文档:《本地作用域》

本地作用域允许我们定义通用的约束集合以便在应用中复用,只需简单在对应 Eloquent 模型方法前加上一个 scope 前缀。

<?php
// app\Repositories\QuestionsRepository.php
    public function getQuestionsFeed()
    {
        // 获取 is_hidden 为 false 的问题,并按更新时间排序,同时获取该问题对应的用户信息。
        return Question::published()->latest('updated_at')->with('user')->get();
    }

Eloquent 模型中任何以 scope 开始的方法都被当做 Eloquent scope

<?php
// app\Models\Question.php
    public function scopePublished($query)
    {
        //  约束条件是 is_hidden 为 false
        return $query->where('is_hidden','F');
    }

    public function user()
    {
        return $this->belongsTo(User::class);
    }

问题首页视图文件

<?php
// resources\views\questions\index.blade.php
@extends('layouts.app')
@section('content')
    <div class="container">
        <div class="row">
            <div class="col-md-8 col-md-offset-2">
                @foreach($questions as $question)
                    <div class="media">
                        <div class="media-left">
                            <a href="">
                                <!-- 头像地址:public/images/avatar.png -->
                                <img width="48" src="{{ url('images',$question->user->avatar) }}" alt="{{$question->user->name}}" >
                            </a>
                        </div>
                        <div class="media-body">
                            <h4 class="media-heading">
                                <a href="/questions/{{$question->id}}">{{$question->title}}</a>
                            </h4>
                        </div>
                    </div>
                @endforeach
            </div>
        </div>
    </div>
@endsection

15.2 删除问题

视图文件

<?php
// resources\views\questions\show.blade.php
<div class="actions panel-footer">
    @if(Auth::check()  && Auth::user()->owns($question))
        <span class="edit">
            <a href="/questions/{{$question->id}}/edit">编辑</a>
        </span>
        <form action="/questions/{{$question->id}}" method="post" class="delete-form">
            {{method_field('DELETE')}}
            {{csrf_field()}}
            <button class="button delete-button is-naked">删除</button>
        </form>
    @endif
</div>

「删除问题」控制器

<?php
// app\Http\Controllers\QuestionsController.php
    public function destroy($id)
    {
        $question = $this->questionRepository->byID($id);
        if (Auth::user()->owns($question)){
            $question->delete();
            return redirect('/questions/');
        }
        abort(403,'Forbidden');
    }

第十六章 创建问题的答案 Answer 模型

# 模型名称首字母大写且是单数,自动创建的数据库表是复数
php artisan make:model models\\Answer -m
<?php
    public function up()
    {
        Schema::create('answers', function (Blueprint $table) {
            $table->increments('id');
            $table->integer('user_id')->index()->unsigned();
            $table->integer('question_id')->index()->unsigned();
            $table->text('body');
            $table->integer('votes_count')->default(0);
            $table->integer('comments_count')->default(0);
            $table->string('is_hidden',8)->default('F');
            $table->string('close_comment',8)->default('F');
            $table->timestamps();
        });
    }
php artisan migrate

Answer 模型

<?php
// app\Models\Answer.php
class Answer extends Model
{
    protected $fillable = ['user_id', 'question_id', 'body'];
    public function user()
    {
        return $this->belongsTo(User::class);
    }
    public function question()
    {
        return $this->belongsTo(Question::class);
    }
}

Question 模型

<?php
// app\Models\Question.php
    public function answers()
    {
        return $this->hasMany(Answer::class);
    }

User 模型

<?php
// app\Models\User.php
    public function answers()
    {
        return $this->hasMany(Answer::class);
    }

第十七章 实现提交回答

17.1 创建 Answer 控制器

# 单数
php artisan make:controller AnswerController

定义保存 Answer 的路由

<?php
// routes\web.php
Route::post('questions/{question}/answer','AnswerController@store');

Answer 控制器

<?php
// app\Http\Controllers\AnswerController.php
use App\Repositories\AnswerRepository;
use Illuminate\Support\Facades\Auth;
use Illuminate\Http\Request;

class AnswerController extends Controller
{
    protected $answer;

    public function __construct(AnswerRepository $answer)
    {
        $this->answer = $answer;
    }

    public function store(Request $request, $question)
    {
        // ID 是当前登录用户ID
        $answer = $this->answer->create([
            'question_id' => $question,
            'user_id'     => Auth::id(),
            'body'        => $request->get('body')
        ]);
        // 问题回答数+1
        $answer->question()->increment('answers_count');
        return back();
    }

AnswerRepository 模式

<?php
// app\Repositories\AnswerRepository.php
use App\Models\Answer;
class AnswerRepository
{
    public function create(array $attributes)
    {
        return Answer::create($attributes);
    }
}

17.2 验证 Answer 表单

新建规则文件

php artisan make:request StoreAnswerRequest
<?php
// app\Http\Requests\StoreAnswerRequest.php
public function authorize()
{
    return true;
}
public function rules()
{
    return [
        'body' => 'required|min:12'
    ];
}

将规则依赖注入

<?php
// app\Http\Controllers\AnswerController.php
use App\Http\Requests\StoreAnswerRequest;
public function store(StoreAnswerRequest $request, $question)
{
    //
}

17.3 视图中显示 Answer

修改 byIdWithTopicsbyIdWithTopicsAndAnswers

<?php
// app\Http\Controllers\QuestionsController.php
    public function show($id)
    {
        $question = $this->questionRepository->byIdWithTopicsAndAnswers($id);
        return view('questions.show',compact('question'));
    }
<?php
// app\Repositories\QuestionsRepositories.php
    public function byIdWithTopicsAndAnswers($id)
    {
        $question = Question::where('id',$id)->with('topics','answers')->first();
        return $question;
    }

「问题」视图

<?php
// resources\views\questions\show.blade.php
@extends('layouts.app')

@section('content')
    @include('vendor.ueditor.assets')
    <div class="container">
        <div class="row">
            <div class="col-md-8 col-md-offset-2">
            <!-- ...... -->
            </div>

            <div class="col-md-8 col-md-offset-2">
                <div class="panel panel-default">
                    <div class="panel-heading">
                        {!! $question->answers_count !!} 个答案
                    </div>
                    <div class="panel-body">
                        @foreach($question->answers as $answer)
                            <div class="media">
                                <div class="media-left">
                                    <a href="">
                                        <img width="48" src="{{ url('images',$answer->user->avatar) }}" alt="{{$answer->user->name}}" >
                                    </a>
                                </div>
                                <div class="media-body">
                                    <h4 class="media-heading">
                                        <a href="/user/{{$answer->user->name}}">
                                            {{$answer->user->name}}
                                        </a>
                                    </h4>
                                    {!! $answer->body !!}
                                </div>
                            </div>
                        @endforeach
                        <form action="/questions/{{$question->id}}/answer" method="post">
                            {!! csrf_field() !!}
                            <div class="form-group{{ $errors->has('body') ? ' has-error' : '' }}">
                                <label for="body">内容</label>
                                <script id="container" style="height: 120px" name="body" type="text/plain">
                                    {!! old('body') !!}
                                </script><br/>
                                @if ($errors->has('body'))
                                <span class="help-block">
                                    <strong>{{ $errors->first('body') }}</strong>
                                </span>
                                @endif
                            </div>
                            <button type="submit" class="btn btn-block btn-success pull-right">提交答案</button>
                        </form>
                    </div>
                </div>
            </div>

        </div>
    </div>
@endsection

@section('js')
<script type="text/javascript">
    var ue = UE.getEditor('container', {
        toolbars: [
            ['bold', 'italic', 'underline', 'strikethrough', 'blockquote', 'insertunorderedlist', 'insertorderedlist', 'justifyleft','justifycenter', 'justifyright',  'link', 'insertimage', 'fullscreen']
        ],
        elementPathEnabled: false,
        enableContextMenu: false,
        autoClearEmptyNode:true,
        wordCount:false,
        imagePopup:false,
        autotypeset:{ indent: true,imageBlockLine: 'center' }
    });
    ue.ready(function() {
        ue.execCommand('serverparam', '_token', '{{ csrf_token() }}');
    });
</script>
@endsection

第十八章 用户关注问题

18.1 提交回答需要登录

在前端限制

<?php
// resources\views\questions\show.blade.php
@if(Auth::check())
    <form action="/questions/{{$question->id}}/answer" method="post">
    </form>
@else
    <a href="{{ url('login') }}" class="btn btn-warning btn-block">登录提交答案</a>
@endif

在后端限制

<?php
// app/Http/Controllers/AnswerController.php
public function __construct(AnswerRepository $answer)
{
    $this->middleware('auth');
}

18.2 关注问题

实质是创建 user_question 表,记录了登录用户ID和所关注问题的ID,点击关注该问题便生成一条记录。

18.2.1 数据库迁移

注意 user_question 表是复数

<?php
php artisan make:migration create_user_question --create=user_question

注意 user_id question_id 外键是单数

<?php
// database/migrations/create_user_question.php
Schema::create('user_question', function (Blueprint $table) {
    $table->increments('id');
    $table->integer('user_id')->unsigned()->index();
    $table->integer('question_id')->unsigned()->index();
    $table->timestamps();
});
php artisan migrate

18.2.2 视图文件

<?php
// resources\views\questions\show.blade.php
<div class="container">
        <div class="row">
            <!-- offset-2 改成 offset-1  -->
            <div class="col-md-8 col-md-offset-1">
            ......
            </div>
            <div class="col-md-3">
                <div class="panel panel-default">
                    <div class="panel-heading question-follow">
                        <h2>{{ $question->followers_count }}</h2>
                        <span>关注者</span>
                    </div>
                    <div class="panel-body">
                        <a href="/question/{{$question->id}}/follow" class="btn btn-default">
                            关注该问题
                        </a>
                        <a href="#editor" class="btn btn-primary pull-right">撰写答案</a>
                    </div>
                </div>
            </div>
            <!-- offset-2 改成 offset-1  -->
            <div class="col-md-8 col-md-offset-1">
                <div class="panel panel-default">
                    <div class="panel-heading">
                        {!! $question->answers_count !!} 个答案
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>

18.2.3 路由

<?php
// zhihu\routes\web.php
Route::get('question/{question}/follow','QuestionFollowController@follow');

18.2.4 「关注问题」控制器

php artisan make:controller QuestionFollowController
<?php
// app\Http\Controllers\QuestionFollowController.php
use App\Models\Question;
use Illuminate\Support\Facades\Auth;
public function __construct()
{
    // 需要登录,否则下面 Auth::user() 爆错
    $this->middleware('auth');
}

public function follow($question)
{
    // 别忘了 use Auth
    // $question 是从路由传入的问题ID
    Auth::user()->follows($question);
    Question::find($question)->increment('followers_count');
    return back();
}

18.2.5 模型

<?php
// app\Models\User.php
public function follows($question)
{
    return Follow::create([
        'question_id' => $question,
        'user_id' => $this->id
    ]);
}
`
php artisan make:model models\\Follow
<?php
// app\Models\Follow.php
class Follow extends Model
{
    protected $table = 'user_question';
    protected $fillable = ['user_id', 'question_id'];
}

第十九章 用户关注问题(下)

19.1 使用 toggle 方法避免重复关注

<?php
// app\Http\Controllers\QuestionFollowController.php
public function follow($question)
{
    Auth::user()->followThis($question);
    ......
}

文档:《多对多关联:同步关联》

<?php
// app\Models\User.php
    public function follows()
    {
        // 多对多,一个用户可以关注多个问题,一个问题也能被多个用户关注
        return $this->belongsToMany(Question::class,'user_question')->withTimestamps();
    }

    public function followThis($question)
    {
        // 如果给定 ID 已附加,就会被移除。
        // 同样的,如果给定 ID 已移除,就会被附加:
        return $this->follows()->toggle($question);

        // 关注成功则+1,否则反之
        if ($this->followed($question))
            Question::find($question)->increment('followers_count');
        else
            Question::find($question)->decrement('followers_count');
    }
`

19.2 获取关注状态

<?php
// app\Models\User.php
public function followed($question)
{
    // 通过问题ID查询该用户是否关注该问题 
    return  $this->follows()->where('question_id',$question)->count();
}
`

前端

<?php
// resources\views\questions\show.blade.php
<div class="panel-body">
    @if(Auth::check())
        <a href="/question/{{$question->id}}/follow"
            class="btn btn-default {{Auth::user()->followed($question->id) ? 'btn-success' : ''}}">
            {{Auth::user()->followed($question->id) ? '已关注' : '关注该问题'}}
        </a>
    @else
        <a href="/question/{{$question->id}}/follow" class="btn btn-warning">关注该问题</a>
    @endif
    <a href="#editor" class="btn btn-primary pull-right">撰写答案</a>
</div>
`

第二十章 使用 Vuejs 实现关注问题

在上一节使用 toggle 避免重复关注,并且使用超链接来实现关注和取消关注。而使用 Vuejs 可以避免跳转,用户体验更佳。

路由

<?php
// routes/api.php

// 根据用户ID和问题ID,查询是否关注成功
Route::post('/question/follower',function (Request $request){
    $followed = \App\Models\Follow::where('question_id',$request->get('question'))
        ->where('user_id',$request->get('user'))
        ->count();
    if ($followed) {
        return response()->json(['followed' => true]);
    }
    return response()->json(['followed' => false]);

})->middleware('api');

// 关注成功后写入数据到 user_question 表
Route::post('/question/follow',function (Request $request){
    $question_id =  $request->get('question');
    $followed = \App\Models\Follow::where('question_id',$question_id)
        ->where('user_id',$request->get('user'))
        ->first();
    //若已经关注,则取消关注
    if ($followed !== null) {
        $followed->delete();
        \App\Models\Question::find($question_id)->decrement('followers_count');
        return response()->json(['followed' => false]);
    }
    //否则写入数据
    \App\Models\Follow::create([
        'question_id'=>$request->get('question'),
        'user_id'=>$request->get('user'),
    ]);
    \App\Models\Question::find($question_id)->increment('followers_count');
    return response()->json(['followed' => true]);

})->middleware('api');

安装 props

yarn add props
// resources\assets\js\components\QuestionFollowButton.vue
<template>
    <button
            class="btn btn-default"
            v-bind:class="{'btn-success' : followed}"
            v-text="text"
            v-on:click="follow"
    ></button>
</template>

<script>
    export default {
        props:['question','user'],
        mounted() {
            // 查询是否已经关注
            axios.post('/api/question/follower',{'question':this.question,'user':this.user}).then(response => {
                //console.log(response.data);
                this.followed = response.data.followed
            })
        },
        data(){
            return {
                followed:false //默认值
            }
        },
        computed:{
            text(){
                return this.followed ? '已关注' : '关注该问题'
            }
        },
        methods:{
            // 关注
            follow() {
                axios.post('/api/question/follow',{'question':this.question,'user':this.user}).then(response => {
                    this.followed = response.data.followed
                })
            }
        }
    }
</script>
`
// resources\assets\js\app.js
Vue.component('question-follow-button', require('./components/QuestionFollowButton.vue'));

视图

 <div class="panel-body">
    @if(Auth::check())
        <question-follow-button question="{{$question->id}}" user="{{Auth::id()}}"></question-follow-button>
    @else
        <a href="/question/{{$question->id}}/follow" class="btn btn-warning">关注该问题</a>
    @endif
    <a href="#editor" class="btn btn-primary pull-right">撰写答案</a>
</div>

编译

yarn run prod

第二十一章 前后端分离 API token 认证

21.1 在 users 表新增 api_token 字段

php artisan make:migration add_api_token_to_users --table=users
<?php
// database\migrations\2017_09_19_133744_add_api_token_to_users.php
    public function up()
    {
        Schema::table('users', function (Blueprint $table) {
            $table->string('api_token',64)->unique();
        });
    }
    public function down()
    {
        Schema::table('users', function (Blueprint $table) {
            $table->dropColumn(['api_token']);
        });
    }
php artisan migrate

将生成的随机字符串手动粘贴到 user 表中

php artisan migrate
php artisan tinker
>>> str_random(60)
=> "79hiXQBXMSWwrZDqybMTxZGjbB0nkFv2DkrAqyGoQ1f9WscoHwTSaB0btktm"
=> "ry8k58UcyZoSZJtIyZtegp3M5pqJbkceEPyC2dxm8zH5NaUoWDR9ykvY1EMX"

注册时自动生成随机字符串

<?php
// app\Http\Controllers\Auth\RegisterController.php
    protected function create(array $data)
    {
        $user = User::create([
            'api_token' => str_random(60),
        ]);
    }
<?php
// app\Models\User.php
  protected $fillable = [
        'api_token'
    ];

21.2 获取前端生成的 api_token 字符串

<?php
// resources\assets\js\bootstrap.js
// 注意 Authorization
let apiToken = document.head.querySelector('meta[name="api-token"]');

if (apiToken) {
    window.axios.defaults.headers.common['Authorization'] = apiToken.content;
} else {
    console.error('API token not found');
}

前端从数据库读取 api_token 内容

<?php
// resources\views\layouts\app.blade.php
<meta name="api-token" content="{{  Auth::check() ? 'Bearer '.Auth::user()->api_token : 'Bearer ' }}">

21.3 前端安全优化

去掉 user 部分

// resources\assets\js\components\QuestionFollowButton.vue
    export default {
        props:['question'],
        mounted() {
            axios.post('/api/question/follower',{'question':this.question}).then(response => {
                //
            })
        },
        methods:{
            follow() {
                axios.post('/api/question/follow',{'question':this.question}).then(response => {
                    //
                })
            }
        }
    }

路由部分,新增 $user 并且将 $request->get('user') 替换成 $user->id

<?php
// routes\api.php
Route::post('/question/follower',function (Request $request){
    $user = Auth::guard('api')->user(); //新增内容
    $followed = \App\Models\Follow::where('question_id',$request->get('question'))
        ->where('user_id',$user->id) // 注意 $user->id
        ->count();
})->middleware('auth:api');

Route::post('/question/follow',function (Request $request){
    $user = Auth::guard('api')->user(); //新增内容
    $question_id =  $request->get('question');
    $followed = \App\Models\Follow::where('question_id',$question_id)
        ->where('user_id',$user->id) //注意 $user->id
        ->first();
    \App\Models\Follow::create([
        'question_id'=>$request->get('question'),
        'user_id'=>$user->id, //注意 $user->id
    ]);
})->middleware('auth:api');

视图

<?php
// resources\views\questions\show.blade.php
// 移除了 user="{{Auth::id()}}
<question-follow-button question="{{$question->id}}"></question-follow-button>

编译

yarn run prod

21.4 优化路由

https://d.laravel-china.org/docs/5.5/eloquent-relationships#updating-many-to-many-relationships

<?php
// routes\api.php
Route::post('/question/follower',function (Request $request){
    $user = Auth::guard('api')->user();
    // 原路由需要提供 user_id ,优化后将 Follow::where 封装成 $user->followed
    //  $followed = \App\Models\Follow::where('question_id',$request->get('question'))
    //   ->where('user_id',$user->id)
    //   ->count();
    $followed = $user->followed($request->get('question'));
    if ($followed) {
        return response()->json(['followed' => true]);
    }
    return response()->json(['followed' => false]);

})->middleware('auth:api');

Route::post('/question/follow',function (Request $request){
    $user = Auth::guard('api')->user();
    $question_id =  $request->get('question');
    // 原路由需要提供 user_id ,优化后将 Follow::where 封装成 $user->followThis
    // followThis 这里用到了 toggle 特性,省去了 Follow::create 步骤
    // $followed = \App\Models\Follow::where('question_id',$question_id)
    //    ->where('user_id',$user->id)
    //    ->first();
    $question = \App\Models\Question::find($question_id);
    $followed = $user->followThis($question_id);
    if (count($followed['detached']) > 0){
        $question->decrement('followers_count');
        return response()->json(['followed' => false]);
    }
    $question->increment('followers_count');
    return response()->json(['followed' => true]);

})->middleware('auth:api');

第二十二章 关注用户(上)

22.1 新增 followers 表

该表存放关注者 ID 和被关注者 ID

php artisan make:migration create_followers_table --create=followers
<?php
// database/migrations/create_followers_table.php
public function up()
    {
        Schema::create('followers', function (Blueprint $table) {
            $table->increments('id');
            $table->integer('follower_id')->unsigned()->index();
            $table->integer('followed_id')->unsigned()->index();
            $table->timestamps();
        });
    }
php artisan migrate

一个用户(follower)既可以关注多个用户(正在关注他人,即following),也能被多个用户(关注者,followers)所关注。

<?php
// app\Models\User.php

// 用户(follower)关注了哪些人(followed)
public function following()
{
    // 用户(follower) 和已经关注的人(followed)均在 user 表,故self::class
    // 顺序:类 -- 中间表 -- 用户在中间表的外键 -- 已经关注的人在中间表的外键
    return $this->belongsToMany(self::class, 'followers', 'follower_id', 'followed_id')->withTimestamps();
}

22.2 视图

<?php
// resources/views/questions/show.blade.php
<div class="container">
    <div class="row">
        ......
        <!-- start -->
        <div class="col-md-3">
            <div class="panel panel-default">
                <div class="panel-heading question-follow">
                    <h5>关于作者</h5>
                </div>
                <div class="panel-body">
                    <div class="media">
                        <div class="media-left">
                            <a href="#">
                                <img width="36" src="{{ url('images',$question->user->avatar) }}" alt="{{$question->user->name}}">
                            </a>
                        </div>
                        <div class="media-body">
                            <h4 class="media-heading"><a href="">
                                    {{ $question->user->name }}
                                </a>
                            </h4>
                        </div>
                        <div class="user-statics" >
                            <div class="statics-item text-center">
                                <div class="statics-text">问题</div>
                                <div class="statics-count">{{ $question->user->questions_count }}</div>
                            </div>
                            <div class="statics-item text-center">
                                <div class="statics-text">回答</div>
                                <div class="statics-count">{{ $question->user->answers_count }}</div>
                            </div>
                            <div class="statics-item text-center">
                                <div class="statics-text">关注者</div>
                                <div class="statics-count">{{ $question->user->followers_count }}</div>
                            </div>
                        </div>
                    </div>
                    <user-follow-button user="{{$question->user_id}}"></user-follow-button>
                    <send-message user="{{$question->user_id}}"></send-message>
                </div>
            </div>
        </div>
        <!-- end -->
    </div>
</div>

第二十三章 关注用户(下)

23.1 vuejs 组件

这个按钮在关注用户(上)已经添加了

<?php
// resources/views/questions/show.blade.php
<user-follow-button user="{{$question->user_id}}"></user-follow-button>

所以,直接新增 UserFollowButton.vue

// resources\assets\js\components\UserFollowButton.vue
// 页面载入时就向 /api/user/followers/ 发起get请求,查询是否关注了该问题的作者。
// 点击按钮时向 /api/user/follow 发起post请求,直接关注该问题的作者。
<template>
    <button
            class="btn btn-default"
            v-bind:class="{'btn-success': followed}"
            v-text="text"
            v-on:click="follow"
    ></button>
</template>

<script>
    export default {
        props:['user'],
        mounted() {
            axios.get('/api/user/followers/' + this.user).then(response => {
                this.followed = response.data.followed
            })
        },
        data() {
            return {
                followed: false
            }
        },
        computed: {
            text() {
                return this.followed ? '已关注' : '关注他'
            }
        },
        methods:{
            follow() {
                axios.post('/api/user/follow',{'user':this.user}).then(response => {
                    this.followed = response.data.followed
                })
            }
        }
    }
</script>

运行

// resources\assets\js\app.js
Vue.component('user-follow-button', require('./components/UserFollowButton.vue'));
yarn run dev

23.2 路由

<?php
// routes\api.php
//  这里的ID是该问题的作者ID 
Route::get('/user/followers/{id}','FollowersController@index');
Route::post('/user/follow','FollowersController@follow');

23.3 控制器

UserRepository

<?php
namespace App\Repositories;
use App\Models\User;
class UserRepository
{
    public function byId($id)
    {
        return User::find($id);
    }
}

新增 FollowersController 控制器

# 控制器是复数
php artisan make:controller FollowersController
<?php
//app\Http\Controllers\FollowersController.php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Repositories\UserRepository;
use Auth;
class FollowersController extends Controller
{
    protected $user;

    public function __construct(UserRepository $user)
    {
        $this->user = $user;
    }

    public function index($id)
    {
        $user = $this->user->byId($id);
        $followers = $user->followers()->pluck('follower_id')->toArray();
        if ( in_array(Auth::guard('api')->user()->id, $followers) ) {
            return response()->json(['followed' => true]);
        }
        return response()->json(['followed' => false]);
    }

    public function follow()
    {
        $user = $this->user->byId(request('user'));
        $followed = Auth::guard('api')->user()->follow($user->id);
        if ( count($followed['attached']) > 0 ) {
            $user->increment('followers_count');
            return response()->json(['followed' => true]);
        }
        $user->decrement('followers_count');
        return response()->json(['followed' => false]);
    }
}

23.4 用户模型

<?php
// app\Models\User.php

public function following()
{
    return $this->belongsToMany(self::class, 'followers', 'follower_id', 'followed_id')->withTimestamps();
}

// 用户(follower)被哪些人(followed)所关注(关注者)
public function followers()
{
    return $this->belongsToMany(self::class, 'followers', 'followed_id', 'follower_id')->withTimestamps();
}

// 根据用户id发起关注
public function follow($user)
{
    return $this->following()->toggle($user);
}

第二十四章 站内信通知

https://laravel-china.org/docs/laravel/5.5/notifications

点击关注后发送通知

<?php
// app\Http\Controllers\FollowersController.php
use App\Notifications\NewUserFollowNotification;
public function follow()
{
    if ( count($followed['attached']) > 0 ) {
        //......
        $user->notify(new NewUserFollowNotification());
    }
}

生成 notification 通知类

php artisan make:notification NewUserFollowNotification
php artisan notifications:table
php artisan migrate
<?php
// app\Notifications\NewUserFollowNotification.php
use Auth;
public function via($notifiable)
{
    return ['database'];
}

public function toDatabase($notifiable)
{
    return [
        'name' => Auth::guard('api')->user()->name,
    ];
}

web 路由

<?php
// routes\web.php
Route::get('notifications','NotificationsController@index');

控制器

php artisan make:controller NotificationsController
<?php
// app\Http\Controllers\NotificationsController.php
use Auth;
public function index()
{
    $user = Auth::user();

    return view('notifications.index', compact('user'));
}

视图

<?php
// resources\views\notifications\index.blade.php
@extends('layouts.app')

@section('content')
    <div class="container">
        <div class="row">
            <div class="col-md-8 col-md-offset-2">
                <div class="panel panel-default">
                    <div class="panel-heading">消息通知</div>
                    <div class="panel-body">
                        @foreach($user->notifications as $notification)
                            @include('notifications.'.snake_case(class_basename($notification->type)))
                        @endforeach
                    </div>
                </div>
            </div>
        </div>
    </div>
@endsection
<?php
// resources\views\notifications\new_user_follow_notification.blade.php
<li class="notifications {{ $notification->unread() ? 'unread' : ' ' }}">
    <a href="{{ $notification->data['name'] }}">
        {{ $notification->data['name'] }}
    </a> 关注了你。
</li>

第二十五章 关注用户之邮件通知

25.1 Sendcloud 邮件服务

<?php
// app\Notifications\NewUserFollowNotification.php
use App\Channels\SendcloudChannel;
use Mail;
public function via($notifiable)
{
    return ['database', SendcloudChannel::class];
}

public function toSendcloud($notifiable)
{
    $data = [
        'yourName' => $notifiable->name,
        'followerName' => Auth::guard('api')->user()->name,
        'url'  => 'http://zhihu.dev/user/'.Auth::guard('api')->user()->id,
    ];
    Mail::send('emails.follow', $data, function ($message) use ($data,$notifiable) {
        $message->from('service@sc.mail.wangyan.org', env('APP_NAME','Laravel'));
        $message->to($notifiable->email);
        $message->subject($data['followerName'].'关注了你');
    });
}
<?php
// app\Channels\SendcloudChannel.php
namespace App\Channels;
use Illuminate\Notifications\Notification;
class SendcloudChannel
{
    public function send($notifiable, Notification $notification)
    {
        $message = $notification->toSendcloud($notifiable);
    }
}

视图略

resources/views/emails/follow.blade.php

25.2 Directmail 邮件服务

<?php
// .env
MAIL_DRIVER=sendcloud
SEND_CLOUD_USER=wang_yan
SEND_CLOUD_KEY=ZN1XDQ8q6jRfS3JU
<?php
// app\Notifications\NewUserFollowNotification.php
use App\Channels\DirectmailChannel;
use Mail;
public function via($notifiable)
{
    return ['database', DirectmailChannel::class];
}

public function toDirectmail($notifiable)
{
    $data = [
        'yourName' => $notifiable->name,
        'followerName' => Auth::guard('api')->user()->name,
        'url'  => 'http://zhihu.dev/user/'.Auth::guard('api')->user()->id,
    ];
    Mail::send('emails.follow', $data, function ($message) use ($data,$notifiable) {
        $message->from('service@dm.mail.wangyan.org', env('APP_NAME','Laravel'));
        $message->to($notifiable->email);
        $message->subject($data['followerName'].'关注了你');
    });
}
<?php
// app\Channels\DirectmailChannel.php
namespace App\Channels;
use Illuminate\Notifications\Notification;
class DirectmailChannel
{
    public function send($notifiable, Notification $notification)
    {
        $message = $notification->toDirectmail($notifiable);
    }
}

第二十六章 重构邮件通知代码

创建基类

<?php
// app/Mailer/Mailer.php
namespace App\Mailer;
use Illuminate\Support\Facades\Mail;
use Naux\Mail\SendCloudTemplate;
class Mailer
{
    public function sendTo($template, $email, array $data)
    {
        $content = new SendCloudTemplate($template,$data);

        Mail::raw($content,  function ($message) use ($email) {
            $message->from('service@sc.mail.wangyan.org', env('APP_NAME','Laravel'));
            $message->to($email);
        });
    }
}

关注通知

<?php
// app/Mailer/UserMailer.php
namespace App\Mailer;
use Auth;
class UserMailer extends Mailer
{
    public function followNotifyEmail($email,$name)
    {
        $data = [
            'yourName' => $name,
            'followerName' => Auth::guard('api')->user()->name,
            'url'  => 'http://zhihu.dev/user/'.Auth::guard('api')->user()->id,
        ];

        $this->sendTo('zhihu_new_user_follow',$email,$data);
    }
}
<?php
// app/Notifications/NewUserFollowNotification.php
public function toSendcloud($notifiable)
{
    (new UserMailer())->followNotifyEmail($notifiable->email,$notifiable->name);
}

密码重置

<?php
// app/Mailer/UserMailer.php
class UserMailer extends Mailer
{
    public function passwordReset($name,$email,$token)
    {
        $data = [
            'title' => env('APP_NAME','Laravel'),
            'name'  => $name,
            'url'   => url('password/reset',$token)
        ];

        $this->sendTo('zhihu_password_reset',$email,$data);
    }
}
<?php
// app/Models/User.php
public function sendPasswordResetNotification($token)
{
    (new UserMailer())->passwordReset($this->name,$this->email,$token);
}

新用户注册邮件验证

<?php
// app/Mailer/UserMailer.php
class UserMailer extends Mailer
{
    public function verifyEmail($name,$email,$confirmation_token)
    {
        $data = [
            'name' => $name,
            'url'  => Route('email.verify',['token' => $confirmation_token])
        ];

        $this->sendTo('zhihu_user_register',$email,$data);
    }
}
<?php
// app/Http/Controllers/Auth/RegisterController.php
private function sendVerifyEmailTo($user)
{
    (new UserMailer())->verifyEmail($user->name,$user->email,$user->confirmation_token);
}

第二十七章 对答案进行点赞

模型

php artisan make:model Vote -m
<?php
// database/migrations/create_votes_table.php
Schema::create('votes', function (Blueprint $table) {
    $table->increments('id');
    $table->unsignedInteger('user_id')->index();
    $table->unsignedInteger('answer_id')->index();
    $table->timestamps();
});
php artisan migrate

路由

<?php
// 都是post
Route::post('/answer/{id}/votes/users','VotesController@users');
Route::post('/answer/vote','VotesController@vote');

控制器

php artisan make:controller VotesController
<?php
// app/Http/Controllers/VotesController.php
namespace App\Http\Controllers;
use App\Repositories\AnswerRepository;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;

class VotesController extends Controller
{
    // AnswerRepository
    protected $answer;
    public function __construct(AnswerRepository $answer)
    {
        $this->answer = $answer;
    }

    //根据用户id查询是否已经点赞
    public function users($id)
    {
        $user = Auth::guard('api')->user();
        // hasVoteFor()
        if($user->hasVoteFor($id)){
            return response()->json(['voted' => true]);
        }
        return response()->json(['voted' => false]);
    }

    //点赞
    public function vote()
    {
        // AnswerRepository
        $answer = $this->answer->byId(request('answer'));

        // voteFor()
        $voted = Auth::guard('api')->user()->voteFor(request('answer'));
        if ( count($voted['attached']) > 0 ) {
            $answer->increment('votes_count');
            return response()->json(['voted' => true]);
        }
        $answer->decrement('votes_count');
        return response()->json(['voted' => false]);
    }
}

AnswerRepository

<?php
public function byId($id)
{
    return Answer::find($id);
}

user 模型

<?php
// app/Models/User.php
    // 一个用户可以点赞多个问题,一个问题可以被多个用户点赞,中间表是votes
    public function votes()
    {
        return $this->belongsToMany(Answer::class,'votes');
    }

    /**
     * @param $answer
     * @return array
     */
    public function voteFor($answer)
    {
        return $this->votes()->toggle($answer);
    }

    /**
     * 根据回答id查询是否有点赞记录
     *
     * @param $answer
     * @return bool
     */
    public function hasVoteFor($answer)
    {
        return !! $this->votes()->where('answer_id',$answer)->count();
    }

vuejs组件

<template>
    <button
            class="btn btn-default"
            v-bind:class="{'btn-primary': voted}"
            v-text="text"
            v-on:click="vote"
    ></button>
</template>

<script>
    export default {
        props:['answer','count'],
        mounted() {
            axios.post('/api/answer/' + this.answer + '/votes/users').then(response => {
                this.voted = response.data.voted
            })
        },
        data() {
            return {
                voted: false
            }
        },
        computed: {
            text() {
                return this.count
            }
        },
        methods:{
            vote() {
                axios.post('/api/answer/vote',{'answer':this.answer}).then(response => {
                    this.voted = response.data.voted
                    response.data.voted ? this.count ++ : this.count --
                })
            }
        }
    }
</script>

视图

<?php
<user-vote-button answer="{{$answer->id}}" count="{{$answer->votes_count}}"></user-vote-button>

运行

yarn run dev

第二十八章 私信功能(上)

创建数据表

php artisan make:model Modles\\Message -m
<?php
public function up()
    {
        Schema::create('messages', function (Blueprint $table) {
            $table->increments('id');
            $table->unsignedInteger('from_user_id');
            $table->unsignedInteger('to_user_id');
            $table->text('body');
            $table->string('has_read',8)->default('F');
            $table->timestamp('read_at')->nullable();
            $table->timestamps();
        });
    }
php artisan migrate

文档:《Eloquent:关联》

私信模型

<?php
// app/Models/Message.php
class Message extends Model
{
    protected $table = 'messages';
    protected $fillable = ['from_user_id', 'to_user_id', 'body'];

    protected function fromUser()
    {
        // 反向一对一,私信由某个用户发送的
        return $this->belongsTo(User::class,'from_user_id');
    }

    public function toUser()
    {
        // 反向一对多,私信可以发送给多个用户
        return !! $this->belongsTo(User::class,'to_user_id');
    }
}

用户模型

<?php
// app/Models/User.php
    public function messages()
    {
        //用户可以向多个用户发送私信
        return $this->hasMany(Message::class,'to_user_id');
    }

第二十九章 私信功能(下)

路由

<?php
// routes/api.php
Route::post('/message/store','MessagesController@store');

私信控制器

php artisan make:controller MessagesController
<?php
namespace App\Http\Controllers;

use App\Repositories\MessageRepository;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;

class MessagesController extends Controller
{
    protected $message;
    public function __construct(MessageRepository $message)
    {
        $this->message = $message;
    }

    public function store()
    {
        $message = $this->message->create([
            'to_user_id' => request('user'),
            'from_user_id' => Auth::guard('api')->user()->id,
            'body' => request('body')
        ]);

        if ($message) {
            return response()->json(['status' => true]);
        }

        return response()->json(['status' => false]);
    }
}

MessageRepository

<?php
// /app/Repositories/MessageRepository.php
namespace App\Repositories;
use App\Models\Message;
class MessageRepository
{
    public function create(array $attributes)
    {
        return Message::create($attributes);
    }
}

视图

<?php
// resources/views/questions/show.blade.php
<send-message user="{{$question->user_id}}"></send-message>

vuejs 组件

Vue.component('send-message', require('./components/SendMessage.vue'));
<!-- resources/assets/js/components/SendMessage.vue -->
<template>
    <div>

    <button
            class="btn btn-default pull-right"
            style="margin-top: -36px;"
            @click="showSendMessageForm"
    >发送私信</button>
        <div class="modal fade" id="modal-send-message" tabindex="-1" role="dialog">
            <div class="modal-dialog">
                <div class="modal-content">
                    <div class="modal-header">
                        <button type="button " class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
                        <h4 class="modal-title">
                            发送私信
                        </h4>
                    </div>
                    <div class="modal-body">
                        <textarea name="body" class="form-control" v-model="body" v-if="!status"></textarea>
                        <div class="alert alert-success" v-if="status">
                            <strong>私信发送成功</strong>
                        </div>
                    </div>
                    <div class="modal-footer">
                        <button type="button" class="btn btn-primary" @click="store">
                            发送私信
                        </button>
                        <button type="button" class="btn btn-default" data-dismiss="modal">关闭</button>
                    </div>
                </div>
            </div>
        </div>
    </div>
</template>

<script>
    export default {
        props:['user'],
        data() {
            return {
                body:'',
                status: false
            }
        },
        methods:{
            store() {
                axios.post('/api/message/store',{'user':this.user,'body':this.body}).then(response => {
                    this.status = response.data.status
                    this.body = ''
                    setTimeout(function () {
                        $('#modal-send-message').modal('hide')
                    }, 2000)
                })
            },
            showSendMessageForm() {
                $('#modal-send-message').modal('show')
            }
        }
    }
</script>

运行

yarn run dev

第三十章 实现评论(上)

创建数据表

php artisan make:model Models\\Comment -m
<?php
// database/migrations/create_comments_table.php
// commentable_id 和 commentable_type 用于多态关联
public function up()
    {
        Schema::create('comments', function (Blueprint $table) {
            $table->increments('id');
            $table->unsignedInteger('user_id');
            $table->text('body');
            $table->unsignedInteger('commentable_id');
            $table->string('commentable_type');
            $table->unsignedInteger('parent_id')->nullable();
            $table->smallInteger('level')->default(1);
            $table->string('is_hidden',8)->default('F');
            $table->timestamps();
        });
    }
php artisan migrate

多态关联

文档:《多态关联》

<?php
// app/Models/Comment.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Comment extends Model
{
    protected $table = 'comments';
    protected $fillable = ['user_id', 'body', 'commentable_id', 'commentable_type'];
    // 获得拥有此评论的模型
    public function commentable()
    {
        return $this->morphTo();
    }
}

获得答案的所有评论

<?php
// app/Models/Answer.php
public function comments()
{
    return $this->morphMany('App\Models\Comment','commentable');
}

获得问题的所有评论

<?php
// app/Models/Question.php
public function comments()
{
    return $this->morphMany('App\Models\Comment','commentable');
}

第三十一章 Vuejs 实现评论组件

路由

<?php
// routes/api.php
Route::get('/answer/{id}/comments','CommentsController@answer');
Route::get('/question/{id}/comments','CommentsController@question');

Route::post('comment','CommentsController@store');

控制器

php artisan make:controller CommentsController

获取多态关联

文档:《Eloquent 预加载》

<?php
// app/Http/Controllers/CommentsController.php
namespace App\Http\Controllers;
use App\models\Answer;
use App\Models\Comment;
use App\Models\Question;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;

class CommentsController extends Controller
{
    // 获取答案所以评论
    public function answer($id)
    {
        $answer = Answer::with('comments','comments.user')->where('id', $id)->first();
        return $answer->comments;
    }

    // 获取问题所有评论
    public function question($id)
    {
        $question = Question::with('comments','comments.user')->where('id', $id)->first();
        return $question->comments;

    }

    // 保存评论
    public function store()
    {
        $model = $this->getModelNameFromType(request('type'));
        $comment = Comment::create([
            'commentable_id' => request('model'),
            'commentable_type' => $model,
            'user_id' => Auth::guard('api')->user()->id,
            'body' => request('body')
        ]);
        return $comment;
    }

    private function getModelNameFromType($type)
    {
        return $type === 'question' ? 'App\Models\Question' : 'App\Models\Answer';
    }
}

vuejs

<!-- resources/assets/js/components/Comments.vue -->
<template>
    <div>

        <button
                class="button is-naked delete-button"
                @click="showCommentsForm"
                v-text="text"
        ></button>
        <div class="modal fade" :id=dialog tabindex="-1" role="dialog">
            <div class="modal-dialog">
                <div class="modal-content">
                    <div class="modal-header">
                        <button type="button " class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
                        <h4 class="modal-title">
                            评论列表
                        </h4>
                    </div>
                    <div class="modal-body">
                       <div v-if="comments.length > 0">
                           <div class="media" v-for="comment in comments">
                               <div class="media-left">
                                   <a href="#">
                                       <img width="24" class="media-object" :src="'http://zhihu.test/images/' + comment.user.avatar">
                                   </a>
                               </div>
                               <div class="media-body">
                                   <h4 class="media-heading">{{comment.user.name}}</h4>
                                   {{comment.body}}
                               </div>
                           </div>
                       </div>
                    </div>
                    <div class="modal-footer">
                        <input type="text" class="form-control" v-model="body">
                        <button type="button" class="btn btn-primary" @click="store">
                            评论
                        </button>
                    </div>
                </div>
            </div>
        </div>
    </div>
</template>

<script>
    export default {
        props:['type','model','count'],
        data() {
            return {
                body:'',
                comments: []
            }
        },
        computed:{
            dialog() {
                return 'comments-dialog-' + this.type + '-' + this.model
            },
            dialogId() {
                return '#' + this.dialog
            },
            text() {
                return this.count + '评论'
            },
            total() {
                return this.count
            }
        },
        methods:{
            store() {
                axios.post('/api/comment',{'type':this.type,'model':this.model,'body':this.body}).then(response => {
                    let comment = {
                        user:{
                            name:Zhihu.name,
                            avatar:Zhihu.avatar
                        },
                        body: response.data.body
                    }
                    this.comments.push(comment)
                    this.body = ''
                    this.total ++
                })
            },
            showCommentsForm() {
                this.getComments()
                $(this.dialogId).modal('show')
            },
            getComments() {
                axios.get('/api/' + this.type +'/' + this.model + '/comments').then(response => {
                    this.comments = response.data
                })
            }
        }
    }
</script>

vuejs 组件

Vue.component('comments', require('./components/Comments.vue'));

视图

<?php
// resources/views/questions/show.blade.php
<comments type="question"
    model="{{$question->id}}"
    count="{{$question->comments()->count()}}">
</comments>
// ......
@foreach($question->answers as $answer)
    // ......
    <comments type="answer"
        model="{{$answer->id}}"
        count="{{$answer->comments()->count()}}">
    </comments>
    // ......
@endforeach

由前端传入用户名和头像

<?php
// resources/views/layouts/app.blade.php
    <script>
        @if(Auth::check())
            window.Zhihu = {
                name:"{{Auth::user()->name}}",
                avatar:"{{Auth::user()->avatar}}"
            }
        @endif
    </script>

第三十二章 Repository 模式重构代码

<?php
// app/Http/Controllers/CommentsController.php
namespace App\Http\Controllers;

use App\Repositories\AnswerRepository;
use App\Repositories\CommentRepository;
use App\Repositories\QuestionsRepository;
use Illuminate\Support\Facades\Auth;

class CommentsController extends Controller
{
    protected $answer;
    protected $question;
    protected $comment;

    public function __construct(AnswerRepository $answer, QuestionsRepository $question, CommentRepository $comment)
    {
        $this->answer = $answer;
        $this->question = $question;
        $this->comment = $comment;
    }

    public function answer($id)
    {
        return $this->answer->getAnswerCommentsById($id);
    }

    public function question($id)
    {
        return $this->question->getQuestionCommentsById($id);
    }

    public function store()
    {
        $model = $this->getModelNameFromType(request('type'));

        return $this->comment->create([
            'commentable_id' => request('model'),
            'commentable_type' => $model,
            'user_id' => Auth::guard('api')->user()->id,
            'body' => request('body')
        ]);
    }

    private function getModelNameFromType($type)
    {
        return $type === 'question' ? 'App\Models\Question' : 'App\Models\Answer';
    }
}

AnswerRepository

<?php
// app/Repositories/AnswerRepository.php
public function getAnswerCommentsById($id)
{
    $answer = Answer::with('comments', 'comments.user')->where('id', $id)->first();
    return $answer->comments;
}

QuestionsRepository

<?php
// app/Repositories/QuestionsRepository.php
public function getQuestionCommentsById($id)
{
    $question = Question::with('comments','comments.user')->where('id',$id)->first();
    return $question->comments;
}

CommentRepository

<?php
// app/Repositories/CommentRepository.php
namespace App\Repositories;
use App\Models\Comment;
class CommentRepository
{
    public function create(array $attributes)
    {
        return Comment::create($attributes);
    }
}

第三十三章 自定义 helper

33.1 优化话题路由

<?php
// routes/api.php
Route::get('/topics','TopicsController@index')->middleware('api');
php artisan make:Controller TopicsController
<?php
// app/Http/Controllers/TopicsController.php
namespace App\Http\Controllers;
use App\Repositories\TopicRepository;
use Illuminate\Http\Request;
class TopicsController extends Controller
{
    protected $topics;

    /**
     * TopicsController constructor.
     * @param $topics
     */
    public function __construct(TopicRepository $topics)
    {
        $this->topics = $topics;
    }

    public function index(Request $request)
    {
        return $this->topics->getTopicsForTagging($request);
    }
}
<?php
// app/Repositories/TopicRepository.php
namespace App\Repositories;
use App\Models\Topic;
use Illuminate\Http\Request;
class TopicRepository
{
    public function getTopicsForTagging(Request $request)
    {
        return Topic::select(['id','name'])
            ->where('name','like','%'.$request->query('q').'%')
            ->get();
    }
}

33.2 优问题路由

<?php
// routes/api.php
Route::post('/question/follower','QuestionFollowController@follower')->middleware('auth:api');
Route::post('/question/follow','QuestionFollowController@followThisQuestion')->middleware('auth:api');
<?php
// app/Http/Controllers/QuestionFollowController.php
public function follower(Request $request)
    {
        $user = Auth::guard('api')->user();
        $followed = $user->followed($request->get('question'));
        if ($followed) {
            return response()->json(['followed' => true]);
        }
        return response()->json(['followed' => false]);
    }

public function followThisQuestion(Request $request)
    {
        $user = Auth::guard('api')->user();
        $question = $this->question->byID($request->get('question'));
        $followed = $user->followThis($question->id);
        if (count($followed['detached']) > 0){
            $question->decrement('followers_count');
            return response()->json(['followed' => false]);
        }
        $question->increment('followers_count');
        return response()->json(['followed' => true]);
    }

33.3 Helpers 函数

<?php
// app/Support/Helpers.php
if ( !function_exists('user') ) {
    function user($driver = null)
    {
        if ( $driver ) {
            return app('auth')->guard($driver)->user();
        }

        return app('auth')->user();
    }
}
<?php
// composer.json
"autoload": {
    "files":[
        "app/Support/Helpers.php"
    ]
}
composer dump-autoload
<?php
// 在所有控制器中将
Auth::guard('api')->user();  
// 替换成
user('api')

第三十四章 私信列表

路由

<?php
// routes/web.php
Route::get('inbox','InboxController@index');
Route::get('inbox/{userId}','InboxController@show');

控制器

php artisan make:controller InboxController
<?php
// app/Http/Controllers/InboxController.php
namespace App\Http\Controllers;

use App\Models\Message;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;

class InboxController extends Controller 
{
    public function __construct()
    {
        $this->middleware('auth');
    }

    public function index()
    {
        $messages = Auth::user()->messages->groupBy('from_user_id');
        return view('inbox.index',compact('messages'));
    }

    public function show($userId)
    {
        $messages = Message::where('from_user_id',$userId)->get();
        return $messages;
    }
}

视图

<?php
// resources/views/inbox/index.blade.php
@extends('layouts.app')

@section('content')
    <div class="container">
        <div class="row">
            <div class="col-md-8 col-md-offset-2">
                <div class="panel panel-default">
                    <div class="panel-heading">私信列表</div>
                    <div class="panel-body">
                        @foreach($messages as $messageGroup)
                            <div class="media">
                                <div class="media-left">
                                    <a href="#">
                                        <img src="{{ url('images', $messageGroup->first()->fromUser->avatar) }}" width="48" alt="">
                                    </a>
                                </div>
                                <div class="media-body">
                                    <h4 class="media-heading">
                                        <a href="#">
                                            {{ $messageGroup->first()->fromUser->name }}
                                        </a>
                                    </h4>
                                    <p>
                                        <a href="/inbox/{{ $messageGroup->first()->fromUser->id }}">
                                            {{ $messageGroup->first()->body }}
                                        </a>
                                    </p>
                                </div>
                            </div>
                        @endforeach
                    </div>
                </div>
            </div>
        </div>
    </div>
@endsection

第三十五章 私信列表(下)

解决发件人看不到已发私信

改造数据表

php artisan make:migration add_dialog_id_to_messages --table=messages
<?php
// database/migrations/add_dialog_id_to_messages.php
    public function up()
    {
        Schema::table('messages', function (Blueprint $table) {
            $table->bigInteger('dialog_id')->default('24');
        });
    }

    public function down()
    {
        Schema::table('messages', function (Blueprint $table) {
            $table->dropColumn(['dialog_id']);
        });
    }
<?php
// app/Models/Message.php
protected $fillable = ['from_user_id', 'to_user_id', 'body', 'dialog_id'];
<?php
// app/Http/Controllers/MessagesController.php
public function store()
{
    $message = $this->message->create([
        // ......
        'dialog_id' => time().Auth::id()
    ]);
}

控制器

<?php
// app/Http/Controllers/InboxController.php
    public function index()
    {
        $messages = Message::where('to_user_id',Auth::id())
            ->orWhere('from_user_id',Auth::id())
            ->with(['fromUser','toUser'])->get();
        return view('inbox.index',['messages' => $messages->groupBy('to_user_id')]);
    }

    public function show($dialogId)
    {
        $messages = Message::where('dialog_id',$dialogId)->get();
        return $messages;
    }

路由

<?php
// routes/web.php
Route::get('inbox/{dialogId}','InboxController@show');

视图

Auth::id() == $key

按收件人分组后,如果当前登陆用户就是收件人,则显示发件人信息,否则显示收件人信息。

<?php
@extends('layouts.app')

@section('content')
    <div class="container">
        <div class="row">
            <div class="col-md-8 col-md-offset-2">
                <div class="panel panel-default">
                    <div class="panel-heading">私信列表</div>
                    <div class="panel-body">
                        @foreach($messages as $key => $messageGroup)
                            <div class="media">
                                <div class="media-left">
                                    <a href="#">
                                        @if(Auth::id() == $key)
                                            <img src="{{ url('images', $messageGroup->last()->fromUser->avatar) }}" width="48" alt="">
                                        @else
                                            <img src="{{ url('images', $messageGroup->last()->toUser->avatar) }}" width="48" alt="">
                                        @endif
                                    </a>
                                </div>
                                <div class="media-body">
                                    <h4 class="media-heading">
                                        <a href="#">
                                            @if(Auth::id() == $key)
                                                {{ $messageGroup->last()->fromUser->name }}
                                            @else
                                                {{ $messageGroup->last()->toUser->name }}
                                            @endif
                                        </a>
                                    </h4>
                                    <p>
                                        <a href="/inbox/{{ $messageGroup->last()->dialog_id }}">
                                            {{ $messageGroup->last()->body }}
                                        </a>
                                    </p>
                                </div>
                            </div>
                        @endforeach
                    </div>
                </div>
            </div>
        </div>
    </div>
@endsection

第三十六章 回复私信

路由

<?php
// routes/web.php
Route::post('inbox/{dialogId}/store','InboxController@store');
<?php
// app/Http/Controllers/InboxController.php
<?php
    public function index()
    {
        $messages = Message::where('to_user_id',Auth::id())
            ->orWhere('from_user_id',Auth::id())
            ->with(['fromUser','toUser'])->get();
        return view('inbox.index',['messages' => $messages->unique('dialog_id')->groupBy('to_user_id')]);
    }

    public function show($dialogId)
    {
        $messages = Message::where('dialog_id',$dialogId)->latest()->get();
        return view('inbox.show',compact('messages','dialogId'));
    }

    public function store($dialogId)
    {
        $message = Message::where('dialog_id',$dialogId)->first();
        $toUserId = $message->from_user_id === Auth::id() ? $message->to_user_id : $message->from_user_id;
        Message::create([
            'from_user_id' => Auth::id(),
            'to_user_id' => $toUserId,
            'body' => request('body'),
            'dialog_id' => $dialogId
        ]);
        return back();
    }
}

视图

<?php
// resources/views/inbox/show.blade.php
@extends('layouts.app')

@section('content')
    <div class="container">
        <div class="row">
            <div class="col-md-8 col-md-offset-2">
                <div class="panel panel-default">
                    <div class="panel-heading">对话列表</div>
                    <div class="panel-body">
                        <form action="/inbox/{{$dialogId}}/store" method="post">
                            {{ csrf_field() }}
                            <div class="form-group">
                                <textarea name="body" class="form-control"></textarea>
                            </div>
                            <div class="form-group pull-right">
                                <button class="btn btn-success">发送私信</button>
                            </div>
                        </form>
                        <div class="messages-list">
                            @foreach($messages as $message)
                                <div class="media">
                                    <div class="media-left">
                                        <a href="#">
                                            <img src="{{ url('images',$message->fromUser->avatar) }}"  width="48" alt="">
                                        </a>
                                    </div>
                                    <div class="media-body">
                                        <h4 class="media-heading">
                                            <a href="#">
                                                {{ $message->fromUser->name }}
                                            </a>
                                        </h4>
                                        <p>
                                            {{ $message->body }} <span class="pull-right">{{ $message->created_at->format('Y-m-d') }}</span>
                                        </p>
                                    </div>
                                </div>
                            @endforeach
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
@endsection

第三十七章 标记私信已读

<?php
// app/Http/Controllers/InboxController.php
public function show($dialogId)
{
    $messages->markAsRead();
}
<?php
// app/Models/Message.php
 public function markAsRead()
    {
        if(is_null($this->read_at)) {
            $this->forceFill(['has_read' => 'T','read_at' => $this->freshTimestamp()])->save();
        }
    }

    public function newCollection(array $models = [])
    {
        return new MessageCollection($models);
    }
<?php
// /app/Models/MessageCollection.php
namespace App\Models;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Facades\Auth;
class MessageCollection extends Collection
{
    public function markAsRead()
    {
        $this->each(function($message) {
            if($message->to_user_id === Auth::id() ){
                $message->markAsRead();
            }
        });
    }
}

第三十八章 显示未读私信

<?php
// app/Http/Controllers/InboxController.php
// 根据 dialog_id 会话分组(一个会话有多条私信),倒序(每个会话中最后的私信排最前)。
    public function index()
    {
        $messages = Message::where('to_user_id',Auth::id())
            ->orWhere('from_user_id',Auth::id())
            ->with(['fromUser','toUser'])->latest()->get();
        return view('inbox.index',['messages' => $messages->groupBy('dialog_id')]);
    }

    public function show($dialogId)
    {
        $messages = Message::where('dialog_id',$dialogId)->latest()->get();
        $messages->markAsRead();
        return view('inbox.show',compact('messages','dialogId'));
    }

视图

<?php
// resources/views/inbox/index.blade.php
// 每个会话显示一个列表,如果登陆用户就是发件人,那么显示收件人头像,否则显示发件人头像。
// $messageGroup->first() 指回话中的最后一条私信记录(messages已经倒序)
@extends('layouts.app')

@section('content')
    <div class="container">
        <div class="row">
            <div class="col-md-8 col-md-offset-2">
                <div class="panel panel-default">
                    <div class="panel-heading">私信列表</div>
                    <div class="panel-body">
                        @foreach($messages as $messageGroup)
                            <div class="media {{ $messageGroup->first()->shouldAddUnreadClass() ? 'unread' : '' }}">
                                <div class="media-left">
                                    <a href="#">
                                        @if(Auth::id() == $messageGroup->last()->from_user_id)
                                            <img src="{{ url('images', $messageGroup->last()->toUser->avatar) }}" width="48" alt="">
                                        @else
                                            <img src="{{ url('images', $messageGroup->last()->fromUser->avatar) }}" width="48" alt="">
                                        @endif
                                    </a>
                                </div>
                                <div class="media-body">
                                    <h4 class="media-heading">
                                        <a href="#">
                                            @if(Auth::id() == $messageGroup->last()->from_user_id)
                                                {{ $messageGroup->last()->toUser->name }}
                                            @else
                                                {{ $messageGroup->last()->fromUser->name }}
                                            @endif
                                        </a>
                                    </h4>
                                    <p>
                                        <a href="/inbox/{{ $messageGroup->first()->dialog_id }}">
                                            {{ $messageGroup->first()->body }}
                                        </a>
                                    </p>
                                </div>
                            </div>
                        @endforeach
                    </div>
                </div>
            </div>
        </div>
    </div>
@endsection

模型

<?php
// app/Models/Message.php
    public function read()
    {
        return $this->has_read === 'T';
    }

    public function unread()
    {
        return $this->has_read === 'F';
    }

    public function shouldAddUnreadClass()
    {
        // 如果登陆用户就是发件人,那么始终未读
        if(Auth::id() === $this->from_user_id) {
            return false;
        }
        return $this->unread();
    }

优化

<?php
// app/Http/Controllers/InboxController.php
public function index()
    {
        $messages = Message::where('to_user_id',Auth::id())
            ->orWhere('from_user_id',Auth::id())
            ->with(['fromUser' => function ($query){
                return $query->select(['id','name','avatar']);
            },'toUser' => function ($query){
                return $query->select(['id','name','avatar']);
            }])->latest()->get();
        return view('inbox.index',['messages' => $messages->groupBy('dialog_id')]);
    }
    public function show($dialogId)
    {
        $messages = Message::where('dialog_id',$dialogId)->with(['fromUser' => function ($query){
                return $query->select(['id','name','avatar']);
            },'toUser' => function ($query){
                return $query->select(['id','name','avatar']);
            }])->latest()->get();
        $messages->markAsRead();
        return view('inbox.show',compact('messages','dialogId'));
    }

第三十九章 私信实现 Repository 模式

<?php
// app/Repositories/MessageRepository.php
namespace App\Repositories;

use App\Models\Message;
use Illuminate\Support\Facades\Auth;

class MessageRepository
{
    public function create(array $attributes)
    {
        return Message::create($attributes);
    }

    public function getAllMessages()
    {
        return $messages = Message::where('to_user_id',Auth::id())
            ->orWhere('from_user_id',Auth::id())
            ->with(['fromUser' => function ($query){
                return $query->select(['id','name','avatar']);
            },'toUser' => function ($query){
                return $query->select(['id','name','avatar']);
            }])->latest()->get();
    }

    public function getDialogMessagesBy($dialogId)
    {
        return Message::where('dialog_id',$dialogId)->with(['fromUser' => function ($query){
            return $query->select(['id','name','avatar']);
        },'toUser' => function ($query){
            return $query->select(['id','name','avatar']);
        }])->latest()->get();
    }

    public function getStingleMessageBy($dialogId)
    {
        return Message::where('dialog_id',$dialogId)->first();
    }
}

控制器

<?php
// app/Http/Controllers/InboxController.php
namespace App\Http\Controllers;

use App\Repositories\MessageRepository;
use Illuminate\Support\Facades\Auth;

class InboxController extends Controller
{
    protected $message;

    public function __construct(MessageRepository $message)
    {
        $this->middleware('auth');
        $this->message = $message;
    }

    public function index()
    {
        $messages = $this->message->getAllMessages();
        return view('inbox.index',['messages' => $messages->groupBy('dialog_id')]);
    }

    public function show($dialogId)
    {
        $messages = $this->message->getDialogMessagesBy($dialogId);
        $messages->markAsRead();
        return view('inbox.show',compact('messages','dialogId'));
    }

    public function store($dialogId)
    {
        $message = $this->message->getStingleMessageBy($dialogId);
        $toUserId = $message->from_user_id === Auth::id() ? $message->to_user_id : $message->from_user_id;
        $this->message->create([
            'from_user_id' => Auth::id(),
            'to_user_id' => $toUserId,
            'body' => request('body'),
            'dialog_id' => $dialogId
        ]);
        return back();
    }
}

第四十章 私信通知

 php artisan make:notification NewMessageNotification
<?php
// app/Notifications/NewMessageNotification.php
namespace App\Notifications;

use App\Models\Message;
use Illuminate\Notifications\Notification;

class NewMessageNotification extends Notification
{
    public $message;

    public function __construct(Message $message)
    {
        $this->message = $message;
    }

    public function via()
    {
        return ['database'];
    }

    public function toDatabase()
    {
         return [
             'name' => $this->message->fromUser->name,
             'dialog' => $this->message->dialog_id,
         ];
    }
}

控制器

<?php
// app/Http/Controllers/InboxController.php
public function store($dialogId)
    {
        $message = $this->message->getStingleMessageBy($dialogId);
        $toUserId = $message->from_user_id === Auth::id() ? $message->to_user_id : $message->from_user_id;
        $newMessage = $this->message->create([
            'from_user_id' => Auth::id(),
            'to_user_id' => $toUserId,
            'body' => request('body'),
            'dialog_id' => $dialogId
        ]);
        $newMessage->toUser->notify(new NewMessageNotification($newMessage));
        return back();
    }

视图

<?php
// resources/views/notifications/new_message_notification.blade.php
<li class="notifications">
    <a href="/inbox/{{$notification->data['dialog']}}">
        {{ $notification->data['name'] }} 给你发了一条私信
    </a>
</li>

第四十一章 notifications 已读

路由

# routes/web.php
Route::get('notifications/{notification}','NotificationsController@show');

控制器

<?php
// app/Http/Controllers/NotificationsController.php
public function show(DatabaseNotification $notification)
    {
        $notification->markAsRead();
        return redirect(\Request::query('redirect_url'));
    }

视图

<?php
// resources/views/notifications/new_message_notification.blade.php
<li class="notifications {{ $notification->unread() ? 'unread' : ' ' }}">
    <a href="/notifications/{{$notification->id}}?redirect_url=/inbox/{{$notification->data['dialog']}}">
        {{ $notification->data['name'] }} 给你发了一条私信
    </a>
</li>

第四十二章 上传头像组件

路由

<?php
// routes/web.php
Route::get('avatar','UsersController@avatar');

控制器

php artisan make:controller UsersController
<?php
// app/Http/Controllers/UsersController.php
class UsersController extends Controller
{
    public function avatar()
    {
        return view('users.avatar');
    }
}

视图

<?php
// resources/views/users/avatar.blade.php
@extends('layouts.app')

@section('content')
    <div class="container">
        <div class="row">
            <div class="col-md-8 col-md-offset-2">
                <div class="panel panel-default">
                    <div class="panel-heading">更换头像</div>
                    <div class="panel-body">
                        <user-avatar avatar="{{ url('images',Auth::user()->avatar) }}" ></user-avatar>
                    </div>
                </div>
            </div>
        </div>
    </div>
@endsection

vue 组件

http://github.com/dai-siki/vue-image-crop-upload

yarn add vue-image-crop-upload babel-polyfill
<?php
// /resources/assets/js/app.js
Vue.component('user-avatar', require('./components/Avatar.vue'));
<!-- resources/assets/js/components/Avatar.vue -->
<template>
    <div>
        <my-upload field="img"
                   @crop-success="cropSuccess"
                   @crop-upload-success="cropUploadSuccess"
                   @crop-upload-fail="cropUploadFail"
                   v-model="show"
                   :width="300"
                   :height="300"
                   url="/upload"
                   :params="params"
                   :headers="headers"
                   img-format="png"></my-upload>
        <img :src="imgDataUrl" width="48">
        <a class="btn" @click="toggleShow">设置头像</a>
    </div>
</template>

<script>
    import 'babel-polyfill'; // es6 shim
    import myUpload from 'vue-image-crop-upload';
    export default {
        props:['avatar'],
        data() {
            return {
                show: false,
                params: {
                    token: '123456798',
                    name: 'avatar'
                },
                headers: {
                    smail: '*_~'
                },
                imgDataUrl: this.avatar
            }
        },
        components: {
            'my-upload': myUpload
        },
        methods: {
            toggleShow() {
                this.show = !this.show;
            },
            cropSuccess(imgDataUrl, field){
                console.log('-------- crop success --------');
                this.imgDataUrl = imgDataUrl;
            },
            cropUploadSuccess(jsonData, field){
                console.log('-------- upload success --------');
                console.log(jsonData);
                console.log('field: ' + field);
            },
            cropUploadFail(status, field){
                console.log('-------- upload fail --------');
                console.log(status);
                console.log('field: ' + field);
            }
        }
    }
</script>
yarn run dev

第四十三章 头像上传到服务器

路由

<?php
// routes/web.php
Route::get('avatar','UsersController@avatar');

控制器

<?php
// app/Http/Controllers/UsersController.php
public function changeAvatar(Request $request)
    {
        $file = $request->file('img');
        $filename = md5(time().user()->id).'.'.$file->getClientOriginalExtension();
        $file->move(public_path('avatars'),$filename);
        user()->avatar = asset('avatars/'.$filename);
        user()->save();
        return ['url' => user()->avatar];
    }

vue 组件

<!-- resources/assets/js/components/Avatar.vue -->
<template>
    <div style="text-align: center;">
        <my-upload field="img"
                   @crop-success="cropSuccess"
                   @crop-upload-success="cropUploadSuccess"
                   @crop-upload-fail="cropUploadFail"
                   v-model="show"
                   :width="300"
                   :height="300"
                   url="/avatar"
                   :params="params"
                   :headers="headers"
                   img-format="png"></my-upload>
        <img :src="imgDataUrl" style="width: 80px;">
        <div style="margin-top: 20px;">
            <button class="btn btn-default" @click="toggleShow">设置头像</button>
        </div>
    </div>
</template>

<script>
    import 'babel-polyfill';
    import myUpload from 'vue-image-crop-upload';

    export default {
        props:['avatar','token'],
        data() {
            return {
                show: false,
                params: {
                    name: 'img'
                },
                headers: {
                    smail: '*_~',
                    'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
                },
                imgDataUrl: this.avatar
            }
        },
        components: {
            'my-upload': myUpload
        },
        methods: {
            toggleShow() {
                this.show = !this.show;
            },
            cropSuccess(imgDataUrl, field){
                this.imgDataUrl = imgDataUrl;
            },
            cropUploadSuccess(response, field){
                this.imgDataUrl = response.url
                this.toggleShow()
            },
            cropUploadFail(status, field){
                console.log('-------- upload fail --------');
                console.log(status);
                console.log('field: ' + field);
            }
        }
    }
</script>
yarn run dev

第四十四章 头像上传到七牛

安装扩展包

https://github.com/overtrue/laravel-filesystem-qiniu

composer require "overtrue/laravel-filesystem-qiniu" -vvv
<?php
// config/app.php
'providers' => [
    // Other service providers...
    Overtrue\LaravelFilesystem\Qiniu\QiniuStorageServiceProvider::class,
],
<?php
// config/filesystems.php
return [
   'disks' => [
        //...
        'qiniu' => [
           'driver'     => 'qiniu',
           'access_key' => env('QINIU_ACCESS_KEY', 'xxxxxxxxxxxxxxxx'),
           'secret_key' => env('QINIU_SECRET_KEY', 'xxxxxxxxxxxxxxxx'),
           'bucket'     => env('QINIU_BUCKET', 'test'),
           'domain'     => env('QINIU_DOMAIN', 'xxx.clouddn.com'), // or host: https://xxxx.clouddn.com
        ],
        //...
    ]
];

控制器

<?php
// app/Http/Controllers/UsersController.php
public function changeAvatar(Request $request)
    {
        $file = $request->file('img');
        $filename = 'avatars/'.md5(time().user()->id).'.'.$file->getClientOriginalExtension();

        Storage::disk('qiniu')->writeStream($filename,fopen($file->getRealPath(),'r'));
        user()->avatar = 'http://'.config('filesystems.disks.qiniu.domain').'/'.$filename;

        user()->save();
        return ['url' => user()->avatar];
    }

第四十五章 实现修改密码

路由

<?php
// routes/web.php
Route::get('password','PasswordController@password');
Route::post('password/update','PasswordController@update');

验证密码

php artisan make:request ChangePasswordRequest
<?php
// app/Http/Requests/ChangePasswordRequest.php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class ChangePasswordRequest extends FormRequest
{
    public function authorize()
    {
        return true;
    }

    public function rules()
    {
        return [
            'old_password' => 'required|min:6',
            'password' => 'required|min:6|confirmed',
        ];
    }

    public function messages()
    {
        return [
            'old_password.required' => '原始密码不能为空',
            'old_password.min' => '原始密码不能少于6个字符',
            'password.required' => '新始密码不能为空',
            'password.min' => '新密码不能少于6个字符',
            'password.confirmed' => '两次输入新密码不符',
        ];
    }
}

控制器

php artisan make:controller PasswordController
<?php
// app/Http/Controllers/PasswordController.php
namespace App\Http\Controllers;
use Hash;
use App\Http\Requests\ChangePasswordRequest;
class PasswordController extends Controller
{
    public function password()
    {
        return view('users.password');
    }

    public function update(ChangePasswordRequest $request)
    {
        if(Hash::check($request->get('old_password'),user()->password)) {
            user()->password = bcrypt($request->get('password'));
            user()->save();
            flash('密码修改成功','success');
            return back();
        }
        flash('密码修改失败','danger');
        return back();
    }
}

视图

<?php
// resources/views/users/password.blade.php
@extends('layouts.app')

@section('content')
    <div class="container">
        <div class="row">
            <div class="col-md-8 col-md-offset-2">
                <div class="panel panel-default">
                    <div class="panel-heading">修改密码</div>
                    <div class="panel-body">
                        @include('flash::message')
                        <form class="form-horizontal" role="form" method="POST" action="/password/update">
                            {{ csrf_field() }}
                            <div class="form-group{{ $errors->has('old_password') ? ' has-error' : '' }}">
                                <label for="old_password" class="col-md-4 control-label">原始密码</label>
                                <div class="col-md-6">
                                    <input id="old_password" type="password" class="form-control" name="old_password" value="{{ old('old_password') }}" required>
                                    @if ($errors->has('old_password'))
                                        <span class="help-block">
                                        <strong>{{ $errors->first('old_password') }}</strong>
                                    </span>
                                    @endif
                                </div>
                            </div>
                            <div class="form-group{{ $errors->has('password') ? ' has-error' : '' }}">
                                <label for="password" class="col-md-4 control-label">输入新密码</label>
                                <div class="col-md-6">
                                    <input id="password" type="password" class="form-control" name="password" required>
                                    @if ($errors->has('password'))
                                        <span class="help-block">
                                        <strong>{{ $errors->first('password') }}</strong>
                                    </span>
                                    @endif
                                </div>
                            </div>
                            <div class="form-group">
                                <label for="password-confirm" class="col-md-4 control-label">确认新密码</label>
                                <div class="col-md-6">
                                    <input id="password-confirm" type="password" class="form-control" name="password_confirmation" required>
                                </div>
                            </div>
                            <div class="form-group">
                                <div class="col-md-6 col-md-offset-4">
                                    <button type="submit" class="btn btn-primary btn-block">
                                        更改密码
                                    </button>
                                </div>
                            </div>
                        </form>
                    </div>
                </div>
            </div>
        </div>
    </div>
@endsection

第四十六章 用户个人设置

路由

<?php
// routes/web.php
Route::get('setting','SettingController@index');
Route::post('setting','SettingController@store');

控制器

php artisan make:controller SettingController
<?php
// app/Http/Controllers/SettingController.php
namespace App\Http\Controllers;

use Illuminate\Http\Request;

class SettingController extends Controller
{
    public function index()
    {
        return view('users.setting');
    }

    public function store(Request $request)
    {
        $settings = array_merge(user()->settings,array_only($request->all(),['city','bio']));
        user()->update(['settings' => $settings]);
        return back();
    }
}

模型

<?php
// app/Models/User.php
protected $fillable = [
    'settings',
];

protected $casts = [
    'settings' => 'array'
];

默认值

<?php
// app/Http/Controllers/Auth/RegisterController.php
$user =  User::create([
    'settings' => ['city' => ''],
]);

视图

<?php
// /resources/views/users/setting.blade.php
@extends('layouts.app')

@section('content')
    <div class="container">
        <div class="row">
            <div class="col-md-8 col-md-offset-2">
                <div class="panel panel-default">
                    <div class="panel-heading">设置个人信息</div>
                    <div class="panel-body">
                        <form class="form-horizontal" role="form" method="POST" action="/setting">
                            {{ csrf_field() }}
                            <div class="form-group{{ $errors->has('city') ? ' has-error' : '' }}">
                                <label for="city" class="col-md-4 control-label">现居城市</label>
                                <div class="col-md-6">
                                    <input id="city" type="text" class="form-control" name="city" value="{{ user()->settings['city'] }}"  required>
                                    @if ($errors->has('city'))
                                        <span class="help-block">
                                        <strong>{{ $errors->first('city') }}</strong>
                                    </span>
                                    @endif
                                </div>
                            </div>
                            <div class="form-group{{ $errors->has('city') ? ' has-error' : '' }}">
                                <label for="bio" class="col-md-4 control-label">个人简介</label>
                                <div class="col-md-6">
                                    <textarea id="bio" type="text" class="form-control" name="bio"  required>{{ user()->settings['bio'] }}</textarea>
                                    @if ($errors->has('bio'))
                                        <span class="help-block">
                                        <strong>{{ $errors->first('bio') }}</strong>
                                    </span>
                                    @endif
                                </div>
                            </div>
                            <div class="form-group">
                                <div class="col-md-6 col-md-offset-4">
                                    <button type="submit" class="btn btn-primary btn-block">
                                        更新资料
                                    </button>
                                </div>
                            </div>
                        </form>
                    </div>
                </div>
            </div>
        </div>
    </div>
@endsection

第四十七章 重构用户设置

控制器

<?php
// app/Http/Controllers/SettingController.php
public function store(Request $request)
    {
        user()->settings()->merge($request->all());
        return back();
    }

模型

<?php
// app/Models/User.php
public function settings()
{
    return new Setting($this);
}
<?php
namespace App\Models;
class Setting
{
    protected $allowed = ['city','bio'];
    protected $user;

    public function __construct(User $user)
    {
        $this->user = $user;
    }

    public function merge(array $attributes)
    {
        $settings = array_merge($this->user->settings,array_only($attributes,$this->allowed));

        return $this->user->update(['settings' => $settings]);
    }
}
支持一下
扫一扫,支持一下