Laravel Vuejs 实战:开发知乎 (18-19)用户关注问题
1.添加授权策略
为了让问题只能被登录用户关注 且用户关注与未关注的区别不同显示
提示:使用policy
1 php artisan make:policy QuestionPolicy
QuestionPolicy文件:
1 <?php 2 3 namespace App\Policies; 4 5 use App\Models\Question; 6 use App\User; 7 use Illuminate\Auth\Access\HandlesAuthorization; 8 9 class QuestionPolicy 10 { 11 use HandlesAuthorization; 12 13 /** 14 * Create a new policy instance. 15 * 16 * @return void 17 */ 18 public function __construct() 19 { 20 // 21 22 } 23 24 25 /** 26 * 判断用户是否有权编辑更新问题 27 * @param User $user 28 * @param Question $question 29 * @return bool 30 */ 31 public function update(User $user, Question $question) 32 { 33 return $user->id === $question->user_id; 34 } 35 36 37 /** 38 * 判断用户是否有权删除问题 39 * @param User $user 40 * @param Question $question 41 * @return bool 42 */ 43 public function destroy(User $user, Question $question) 44 { 45 return $user->id === $question->user_id; 46 } 47 48 49 /** 用户是否可以关注问题,未登录不行,关注了不行 50 * @param User $user 51 * @param Question $question 52 * @return bool 53 */ 54 public function follow(User $user, Question $question) 55 { 56 return auth()->user()->id === $user->id && !$user->followQuestions->contains('id', $question->id); 57 } 58 } 59 60
修改show页面对关注后和未关注的不同显示
1 @extends('layouts.app') 2 @section('content') 3 <div class="container"> 4 <div class="row"> 5 <div class="col-md-8 col-md offset-1"> 6 {{--问题--}} 7 <div class="card"> 8 <div class="card-header"> 9 {{ $question->title }} 10 11 @foreach(['success','warning','danger'] as $info) 12 @if(session()->has($info)) 13 <div class="alert alert-{{$info}}">{{ session()->get($info) }}</div> 14 @endif 15 @endforeach 16 17 @can('update',$question) 18 <a href="{{ route('questions.edit',$question) }}" class="btn btn-warning">编辑</a> 19 @endcan 20 21 @can('destroy',$question) 22 <form action="{{ route('questions.destroy',$question) }}" method="post"> 23 @csrf 24 @method('DELETE') 25 <button type="submit" class="btn btn-danger">删除</button> 26 </form> 27 @endcan 28 29 @forelse($question->topics as $topic) 30 <button class="btn btn-secondary float-md-right m-1">{{ $topic->name }}</button> 31 @empty 32 <p class="text text-warning float-md-right"> "No Topics"</p> 33 @endforelse 34 35 <p class="text text-info float-md-right"> 已有{{ count($question->answers) }}个回答</p> 36 37 </div> 38 <div class="card-body"> 39 {!! $question->content !!} 40 </div> 41 </div> 42 43 44 {{--回答提交form--}} 45 {{--只有登录用户可以提交回答--}} 46 @if(auth()->check()) 47 <div class="card mt-2"> 48 <div class="card-header"> 49 提交回答 50 </div> 51 <div class="card-body"> 52 <form action="{{ route('answers.store',$question) }}" method="post"> 53 @csrf 54 <!-- 回答编辑器容器 --> 55 <script id="container" name="content" type="text/plain" 56 style="width: 100%;height: 200px">{!! old('content') !!}</script> 57 <p class="text text-danger"> @error('content') {{ $message }} @enderror </p> 58 <!--提交按钮--> 59 <button type="submit" class="btn btn-primary float-md-right mt-2">提交回答</button> 60 </form> 61 </div> 62 </div> 63 @else 64 {{--显示请登录--}} 65 <a href="{{ route('login') }}" class="btn btn-success btn-block mt-4">登录提交答案</a> 66 @endif 67 {{--展示答案--}} 68 @forelse($question->answers as $answer) 69 <div class="card mt-4"> 70 <div class="card-header"> 71 <div class="float-left"> 72 <img src="{{ $answer->user->avatar }}" class="img-thumbnail imgWrap" 73 style="height: 50px" alt="{{ $answer->user->name }}"> 74 <span class="text text-info">{{ $answer->user->name }}</span> 75 </div> 76 <span class="float-right text text-info m-auto">{{ $answer->updated_at }}</span> 77 </div> 78 79 <div class="card-body"> 80 {!! $answer->content !!} 81 </div> 82 </div> 83 84 @empty 85 86 @endforelse 87 </div> 88 89 <div class="col-md-3"> 90 <div class="card"> 91 <div class="card-header"> 92 <h2> {{ $question->followers_count }}</h2> 93 <span>关注者</span> 94 </div> 95 96 <div class="card-body"> 97 @if(auth()->check()) 98 {{-- 如果用户已经关注过,则显示取消关注按钮--}} 99 @can('follow',$question) 100 <a href="{{ route('questions.follow',$question) }}" 101 class="btn btn-primary btn-block">点击关注</a> 102 @else 103 <a href="{{ route('questions.follow',$question) }}" 104 class="btn btn-danger btn-block">取消关注</a> 105 @endcan 106 @else 107 <a href="{{ route('questions.follow',$question) }}" 108 class="btn btn-primary btn-block">点击关注</a> 109 110 @endif 111 </div> 112 113 </div> 114 </div> 115 </div> 116 </div> 117 @endsection 118 @section('footer-js') 119 @include('questions._footer_js') 120 @endsection 121 122
修改跳转位置:
在RouteServiceProvider中添加:
1 public const QUESTION = '/questions';
LoginController中:
将RouteServiceProvider::Home 改为 RouteServiceProvider::QUESTION
RedirectIfAuthenticated中:
将RouteServiceProvider::Home 改为 RouteServiceProvider::QUESTION
2. 添加一张表用于管理用户关注问题,多对多 一个用户可以关注多个问题,一个问题可以被多个用户关注
可以参考follow系统原理:
Laravel 6 | Follow Unfollow System Example From Scratch
Laravel Eloquent followers relationship
【之前创建的表是用户创建问题 及 tags与questions之间的表】
1 php artisan make:migration create_users_questions_table
migration文件代码:
1 <?php 2 3 use Illuminate\Database\Migrations\Migration; 4 use Illuminate\Database\Schema\Blueprint; 5 use Illuminate\Support\Facades\Schema; 6 7 class CreateUsersQuestionsTable extends Migration 8 { 9 /** 10 * Run the migrations. 11 * 12 * @return void 13 */ 14 public function up() 15 { 16 //用户关注的问题 与 问题下关注的用户 的表 17 Schema::create('users_questions', function (Blueprint $table) { 18 $table->bigIncrements('id'); 19 $table->unsignedBigInteger('user_id')->index(); 20 $table->unsignedBigInteger('question_id')->index(); 21 $table->timestamps(); 22 }); 23 } 24 25 /** 26 * Reverse the migrations. 27 * 28 * @return void 29 */ 30 public function down() 31 { 32 Schema::dropIfExists('users_questions'); 33 } 34 } 35 36
然后:
1 php artisan migrate
model关联关系:
Question.php
1 <?php 2 3 namespace App\Models; 4 5 use App\Answer; 6 use App\Topic; 7 use App\User; 8 use Illuminate\Database\Eloquent\Model; 9 use Illuminate\Database\Eloquent\SoftDeletes; 10 11 class Question extends Model 12 { 13 //软删除 添加 14 use SoftDeletes; 15 // 16 protected $fillable = ['title', 'content', 'user_id']; 17 //支持软删除 添加 18 protected $dates = ['deleted_at']; 19 20 public function topics() 21 { 22 return $this->belongsToMany( 23 Topic::class, 24 'questions_topics' //表名我设置的是questions_topics,可能不是系统自动解析的question_topic 25 )->withTimestamps();//withTimestamps操作questions_topics表中create_at及updated_at字段的 26 } 27 28 public function user() 29 { 30 return $this->belongsTo(User::class); 31 } 32 33 /** scope+请求名命名的 34 * @return bool 35 */ 36 public function scopePublished($query) 37 { 38 return $query->where('is_hidden', 'F');//等于F表示不隐藏 39 } 40 41 42 /** 一个问题有多个回答 43 * @return \Illuminate\Database\Eloquent\Relations\HasMany 44 */ 45 public function answers() 46 { 47 return $this->hasMany(Answer::class); 48 } 49 50 51 public function followUsers() 52 { 53 //默认表名 可以不设置后面三个参数,自定义表名需要设置 54 return $this->belongsToMany(Question::class, 'users_questions', 'user_id', 'question_id'); 55 } 56 57 } 58
User.php
1 <?php 2 3 namespace App; 4 5 use App\Models\Question; 6 use Illuminate\Contracts\Auth\MustVerifyEmail; 7 use Illuminate\Database\Eloquent\SoftDeletes; 8 use Illuminate\Foundation\Auth\User as Authenticatable; 9 use Illuminate\Notifications\Notifiable; 10 11 class User extends Authenticatable implements MustVerifyEmail 12 { 13 use Notifiable; 14 #region 支持软删除 15 use SoftDeletes; 16 protected $dates = ['deleted_at']; 17 #endregion 18 /** 19 * The attributes that are mass assignable. 20 * 21 * @var array 22 */ 23 protected $fillable = [ 24 'name', 'email', 'password', 'avatar', 'activation_token' 25 ]; 26 27 /** 28 * The attributes that should be hidden for arrays. 29 * 30 * @var array 31 */ 32 protected $hidden = [ 33 'password', 'remember_token', 34 ]; 35 36 /** 37 * The attributes that should be cast to native types. 38 * 39 * @var array 40 */ 41 protected $casts = [ 42 'email_verified_at' => 'datetime', 43 ]; 44 45 46 /**添加用户模型和问题模型的模型关联 47 * @return \Illuminate\Database\Eloquent\Relations\HasMany 48 */ 49 public function questions() 50 { 51 return $this->hasMany(Question::class); 52 } 53 54 55 /** 添加用户模型和回答模型的模型关联 一个用户可以有多个回答 56 * @return \Illuminate\Database\Eloquent\Relations\HasMany 57 */ 58 public function answers() 59 { 60 return $this->hasMany(Answer::class); 61 } 62 63 64 public function followQuestions() 65 { 66 //默认表名 可以不设置后面三个参数,自定义表名需要设置 67 return $this->belongsToMany(Question::class, 'users_questions', 'question_id', 'user_id'); 68 } 69 } 70 71
QuestionController.php
1 <?php 2 3 namespace App\Http\Controllers; 4 5 use App\Http\Requests\QuestionStoreRequest; 6 use App\Models\Question; 7 use App\Repositories\QuestionRepository; 8 9 10 class QuestionController extends Controller 11 { 12 13 /** 14 * @var QuestionRepository 15 */ 16 private $questionRepository; 17 18 public function __construct(QuestionRepository $questionRepository) 19 { 20 $this->middleware( 21 'auth', 22 [ 23 'except' => 24 [ 25 'index', 26 'show', 27 ]//非注册用户只能查看不能编辑添加更改删除 28 ] 29 ); 30 31 $this->questionRepository = $questionRepository; 32 } 33 34 35 /** Display a listing of the resource. 36 * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View 37 */ 38 public function index() 39 { 40 // 41 $questions = $this->questionRepository->getQuestionPublished(); 42 return view('questions.index', compact('questions')); 43 } 44 45 46 /** 47 * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View 48 */ 49 public function create() 50 { 51 // 52 return view('questions.create'); 53 } 54 55 56 /** 57 * @param QuestionStoreRequest $request 58 * @return \Illuminate\Http\RedirectResponse 59 */ 60 public function store(QuestionStoreRequest $request)//依赖注入QuestionStoreRequest实例 61 { 62 // 63 // $data = $request->validate([ 64 // 'title' => 'required|min:8', 65 // 'content' => 'required|min:28', 66 // ]); 67 //存储topics 68 $topics = $this->questionRepository->normalizeTopics($request->get('topics')); 69 //初始化question要用到的数据 70 $data = $request->all(); 71 $data['user_id'] = auth()->user()->id; 72 73 // $question=Question::create($data); 被下方代码取代 74 $question = $this->questionRepository->create($data); 75 76 //使用我们再question model里面添加的topics方法获得 topics关联,再使用attach方法 77 $question->topics()->attach($topics); 78 79 return redirect()->route('questions.show', $question); 80 } 81 82 83 /** 84 * @param Question $question 85 * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View 86 */ 87 public function show(Question $question) 88 { 89 //使用关系关联加载,with方法会将分类之下的主题一起查询出来,而且不会出现N+1影响性能的问题 90 $question->with('topics')->get(); 91 //使用关系关联加载,with方法会将分类之下的回答一起查询出来,而且不会出现N+1影响性能的问题 92 $question->with('answers')->get(); 93 94 return view('questions.show', compact('question')); 95 } 96 97 98 /**判断权限 返回视图 99 * @param Question $question 100 * @return \Illuminate\Contracts\View\Factory|\Illuminate\Http\RedirectResponse|\Illuminate\View\View 101 */ 102 public function edit(Question $question) 103 { 104 if (auth()->user()->can('update', $question)) //判断当前用户是否有权编辑更新该question实例 105 { 106 //返回编辑视图 107 return view('questions.edit', compact('question')); 108 } else { 109 //返回警告 没有权限 110 return redirect()->back()->with('warning', '你不能编辑不属于你的问题!'); 111 } 112 } 113 114 115 /** Update the specified resource in storage. 116 * @param QuestionStoreRequest $questionStoreRequest 117 * @param Question $question 118 * @return \Illuminate\Http\RedirectResponse 119 */ 120 public function update(QuestionStoreRequest $questionStoreRequest, Question $question) 121 { 122 //更新前 判断下权限 123 if (!(auth()->user()->can('update', $question))) { 124 //返回警告 没有权限 125 return redirect()->back()->with('warning', '你不能编辑不属于你的问题!'); 126 } 127 //取得更新的字段 使用Eloquent提供的update方法执行问题更新 128 $question->update([ 129 'title' => $questionStoreRequest->get('title'), 130 'content' => $questionStoreRequest->get('content'), 131 ]); 132 133 134 //topics的操作这时候看起来有点臃肿 可以使用TopicController来管理,暂时省略 135 //存储topics 136 $topics = $this->questionRepository->normalizeTopics($questionStoreRequest->get('topics')); 137 //使用我们再question model里面添加的topics方法获得 topics关联, 138 //再使用sync方法同步tag 【删除的会被删除掉,没删除的就保留,新的就增加】 139 $question->topics()->sync($topics); 140 141 //更新完成,跳转回去 142 return redirect()->back(); 143 } 144 145 146 /**Remove the specified resource from storage. 147 * @param Question $question 148 * @return \Illuminate\Http\RedirectResponse 149 * @throws \Exception 150 */ 151 public function destroy(Question $question) 152 { 153 // 154 if (auth()->user()->can('destroy', $question)) { 155 $question->delete(); 156 return redirect()->route('questions.index')->with('success', "删除成功!"); 157 } 158 return redirect()->back()->with('danger', "你不能删除不属于你的问题!"); 159 } 160 161 162 public function follow(Question $question) 163 { 164 if (auth()->user()->can('follow', $question)) //通过QuestionPolicy的follow方法判断用户是否可以关注问题 165 { 166 //同步记录 167 auth()->user()->followQuestions()->sync($question); 168 //关注数加1 169 $question->increment('followers_count'); 170 $message = "关注"; 171 } else { 172 //取消记录 173 auth()->user()->followQuestions()->detach($question); 174 //关注数减1 175 $question->decrement('followers_count'); 176 $message = "取关"; 177 } 178 179 return redirect()->back()->with('success', $message . '成功!'); 180 } 181 182 } 183
3.优化:
关注 使用toggle方法,tags 与 questions 适合用sync detach attach方法 具体参考官方文档: 切换关联 或 laravel toggle方法
QuestionController.php1 <?php 2 3 namespace App\Http\Controllers; 4 5 use App\Http\Requests\QuestionStoreRequest; 6 use App\Models\Question; 7 use App\Repositories\QuestionRepository; 8 9 10 class QuestionController extends Controller 11 { 12 13 /** 14 * @var QuestionRepository 15 */ 16 private $questionRepository; 17 18 public function __construct(QuestionRepository $questionRepository) 19 { 20 $this->middleware( 21 'auth', 22 [ 23 'except' => 24 [ 25 'index', 26 'show', 27 ]//非注册用户只能查看不能编辑添加更改删除 28 ] 29 ); 30 31 $this->questionRepository = $questionRepository; 32 } 33 34 35 /** Display a listing of the resource. 36 * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View 37 */ 38 public function index() 39 { 40 // 41 $questions = $this->questionRepository->getQuestionPublished(); 42 return view('questions.index', compact('questions')); 43 } 44 45 46 /** 47 * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View 48 */ 49 public function create() 50 { 51 // 52 return view('questions.create'); 53 } 54 55 56 /** 57 * @param QuestionStoreRequest $request 58 * @return \Illuminate\Http\RedirectResponse 59 */ 60 public function store(QuestionStoreRequest $request)//依赖注入QuestionStoreRequest实例 61 { 62 // 63 // $data = $request->validate([ 64 // 'title' => 'required|min:8', 65 // 'content' => 'required|min:28', 66 // ]); 67 //存储topics 68 $topics = $this->questionRepository->normalizeTopics($request->get('topics')); 69 //初始化question要用到的数据 70 $data = $request->all(); 71 $data['user_id'] = auth()->user()->id; 72 73 // $question=Question::create($data); 被下方代码取代 74 $question = $this->questionRepository->create($data); 75 76 //使用我们再question model里面添加的topics方法获得 topics关联,再使用attach方法 77 $question->topics()->attach($topics); 78 79 return redirect()->route('questions.show', $question); 80 } 81 82 83 /** 84 * @param Question $question 85 * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View 86 */ 87 public function show(Question $question) 88 { 89 //使用关系关联加载,with方法会将分类之下的主题一起查询出来,而且不会出现N+1影响性能的问题 90 $question->with('topics')->get(); 91 //使用关系关联加载,with方法会将分类之下的回答一起查询出来,而且不会出现N+1影响性能的问题 92 $question->with('answers')->get(); 93 94 return view('questions.show', compact('question')); 95 } 96 97 98 /**判断权限 返回视图 99 * @param Question $question 100 * @return \Illuminate\Contracts\View\Factory|\Illuminate\Http\RedirectResponse|\Illuminate\View\View 101 */ 102 public function edit(Question $question) 103 { 104 if (auth()->user()->can('update', $question)) //判断当前用户是否有权编辑更新该question实例 105 { 106 //返回编辑视图 107 return view('questions.edit', compact('question')); 108 } else { 109 //返回警告 没有权限 110 return redirect()->back()->with('warning', '你不能编辑不属于你的问题!'); 111 } 112 } 113 114 115 /** Update the specified resource in storage. 116 * @param QuestionStoreRequest $questionStoreRequest 117 * @param Question $question 118 * @return \Illuminate\Http\RedirectResponse 119 */ 120 public function update(QuestionStoreRequest $questionStoreRequest, Question $question) 121 { 122 //更新前 判断下权限 123 if (!(auth()->user()->can('update', $question))) { 124 //返回警告 没有权限 125 return redirect()->back()->with('warning', '你不能编辑不属于你的问题!'); 126 } 127 //取得更新的字段 使用Eloquent提供的update方法执行问题更新 128 $question->update([ 129 'title' => $questionStoreRequest->get('title'), 130 'content' => $questionStoreRequest->get('content'), 131 ]); 132 133 134 //topics的操作这时候看起来有点臃肿 可以使用TopicController来管理,暂时省略 135 //存储topics 136 $topics = $this->questionRepository->normalizeTopics($questionStoreRequest->get('topics')); 137 //使用我们再question model里面添加的topics方法获得 topics关联, 138 //再使用sync方法同步tag 【删除的会被删除掉,没删除的就保留,新的就增加】 139 $question->topics()->sync($topics); 140 141 //更新完成,跳转回去 142 return redirect()->back(); 143 } 144 145 146 /**Remove the specified resource from storage. 147 * @param Question $question 148 * @return \Illuminate\Http\RedirectResponse 149 * @throws \Exception 150 */ 151 public function destroy(Question $question) 152 { 153 // 154 if (auth()->user()->can('destroy', $question)) { 155 $question->delete(); 156 return redirect()->route('questions.index')->with('success', "删除成功!"); 157 } 158 return redirect()->back()->with('danger', "你不能删除不属于你的问题!"); 159 } 160 161 162 public function follow(Question $question) 163 { 164 if (auth()->user()->can('follow', $question)) //通过QuestionPolicy的follow方法判断用户是否可以关注问题 165 { 166 $message = "关注"; 167 } else { 168 $message = "取关"; 169 } 170 //同步记录 171 auth()->user()->followQuestions()->toggle($question); 172 $question->followers_count = $question->followUsers()->count(); 173 $question->save(); 174 return redirect()->back()->with('success', $message . '成功!'); 175 } 176 177 } 178 179更新模型关系,加时间数据存储:
User.php
User.php1 <?php 2 3 namespace App; 4 5 use App\Models\Question; 6 use Illuminate\Contracts\Auth\MustVerifyEmail; 7 use Illuminate\Database\Eloquent\SoftDeletes; 8 use Illuminate\Foundation\Auth\User as Authenticatable; 9 use Illuminate\Notifications\Notifiable; 10 11 class User extends Authenticatable implements MustVerifyEmail 12 { 13 use Notifiable; 14 #region 支持软删除 15 use SoftDeletes; 16 protected $dates = ['deleted_at']; 17 #endregion 18 /** 19 * The attributes that are mass assignable. 20 * 21 * @var array 22 */ 23 protected $fillable = [ 24 'name', 'email', 'password', 'avatar', 'activation_token' 25 ]; 26 27 /** 28 * The attributes that should be hidden for arrays. 29 * 30 * @var array 31 */ 32 protected $hidden = [ 33 'password', 'remember_token', 34 ]; 35 36 /** 37 * The attributes that should be cast to native types. 38 * 39 * @var array 40 */ 41 protected $casts = [ 42 'email_verified_at' => 'datetime', 43 ]; 44 45 46 /**添加用户模型和问题模型的模型关联 47 * @return \Illuminate\Database\Eloquent\Relations\HasMany 48 */ 49 public function questions() 50 { 51 return $this->hasMany(Question::class); 52 } 53 54 55 /** 添加用户模型和回答模型的模型关联 一个用户可以有多个回答 56 * @return \Illuminate\Database\Eloquent\Relations\HasMany 57 */ 58 public function answers() 59 { 60 return $this->hasMany(Answer::class); 61 } 62 63 64 public function followQuestions() 65 { 66 //默认表名 可以不设置后面三个参数,自定义表名需要设置 67 return $this->belongsToMany(Question::class, 'users_questions', 'question_id', 'user_id')->withTimestamps(); 68 } 69 } 70 71Question.php
Question.php1 <?php 2 3 namespace App\Models; 4 5 use App\Answer; 6 use App\Topic; 7 use App\User; 8 use Illuminate\Database\Eloquent\Model; 9 use Illuminate\Database\Eloquent\SoftDeletes; 10 11 class Question extends Model 12 { 13 //软删除 添加 14 use SoftDeletes; 15 // 16 protected $fillable = ['title', 'content', 'user_id']; 17 //支持软删除 添加 18 protected $dates = ['deleted_at']; 19 20 public function topics() 21 { 22 return $this->belongsToMany( 23 Topic::class, 24 'questions_topics' //表名我设置的是questions_topics,可能不是系统自动解析的question_topic 25 )->withTimestamps();//withTimestamps操作questions_topics表中create_at及updated_at字段的 26 } 27 28 public function user() 29 { 30 return $this->belongsTo(User::class); 31 } 32 33 /** scope+请求名命名的 34 * @return bool 35 */ 36 public function scopePublished($query) 37 { 38 return $query->where('is_hidden', 'F');//等于F表示不隐藏 39 } 40 41 42 /** 一个问题有多个回答 43 * @return \Illuminate\Database\Eloquent\Relations\HasMany 44 */ 45 public function answers() 46 { 47 return $this->hasMany(Answer::class); 48 } 49 50 51 public function followUsers() 52 { 53 //默认表名 可以不设置后面三个参数,自定义表名需要设置 54 return $this->belongsToMany(Question::class, 'users_questions', 'user_id', 'question_id')->withTimestamps(); 55 } 56 57 } 58 59