1.执行命令:
1 php artisan make:model Comment -mc
2.数据库迁移文件:
****_create_comments_table.php文件:
评论与之前的不同在于,评论可以评论问题,也可以评论答案,还可以评论其他评论;也就是多态多对多。
可以参考: Laravel Polymorphic Relationship Example
Laravel Many to Many Polymorphic Relationship Tutorial
其次,评论可以嵌套,所以需要注意层级,上层评论的id,以及评论隐藏状态。
1 <?php 2 3 use IlluminateDatabaseMigrationsMigration; 4 use IlluminateDatabaseSchemaBlueprint; 5 use IlluminateSupportFacadesSchema; 6 7 class CreateCommentsTable extends Migration 8 { 9 /** 10 * Run the migrations. 11 * 12 * @return void 13 */ 14 public function up() 15 { 16 Schema::create('comments', function (Blueprint $table) { 17 $table->bigIncrements('id'); 18 $table->unsignedBigInteger('user_id')->comment("发出评论的用户的id"); 19 $table->text('content')->comment("评论的内容"); 20 $table->unsignedBigInteger('commentable_id')->comment("被评论的对象的id"); 21 $table->string('commentable_type')->comment("被评论的类型 问题或者答案或者评论"); 22 $table->unsignedBigInteger('parent_id')->nullable()->comment("嵌套评论的上级id"); 23 $table->unsignedSmallInteger('level')->default(1)->comment("评论属于第几层"); 24 $table->string('is_hidden', 8)->default("F")->comment("是否隐藏状态"); 25 $table->timestamps(); 26 }); 27 } 28 29 /** 30 * Reverse the migrations. 31 * 32 * @return void 33 */ 34 public function down() 35 { 36 Schema::dropIfExists('comments'); 37 } 38 } 39 40
执行:
1 php artisan migrate
3.comment模型初始设置:
1 //表名 2 protected $table = 'comments'; 3 4 //必须初始赋值的 5 protected $fillable = ['user_id', 'content', 'commentable_id', 'commentable_type']; 6
4.模型关联
多态多对多
Comment.php中添加:
1 public function commentable() 2 { 3 return $this->morphTo(); 4 } 5
Answer.php中添加:
1 public function comments() 2 { 3 return $this->morphMany(Comment::class, 'commentable'); 4 } 5
1 <?php 2 3 namespace App; 4 5 use AppModelsQuestion; 6 use IlluminateDatabaseEloquentModel; 7 use IlluminateDatabaseEloquentSoftDeletes; 8 9 class Answer extends Model 10 { 11 #region 支持软删除添加 12 use SoftDeletes; 13 protected $dates = ['deleted_at']; 14 15 #endregion 16 17 protected $fillable = ['user_id', 'question_id', 'content']; 18 19 /** 一个回答只有一个回答主人 20 * @return IlluminateDatabaseEloquentRelationsBelongsTo 21 */ 22 public function user() 23 { 24 return $this->belongsTo(User::class); 25 } 26 27 /** 一个回答只针对一个问题 28 * @return IlluminateDatabaseEloquentRelationsBelongsTo 29 */ 30 public function question() 31 { 32 return $this->belongsTo(Question::class); 33 } 34 35 /** 36 * @return IlluminateDatabaseEloquentRelationsBelongsToMany 37 */ 38 public function userVotes() 39 { 40 return $this->belongsToMany(User::class, 'votes')->withTimestamps(); 41 } 42 43 public function comments() 44 { 45 return $this->morphMany(Comment::class, 'commentable'); 46 } 47 } 48 49
Question.php中添加:
1 public function comments() 2 { 3 return $this->morphMany(Comment::class, 'commentable'); 4 } 5
1 <?php 2 3 namespace AppModels; 4 5 use AppAnswer; 6 use AppComment; 7 use AppTopic; 8 use AppUser; 9 use IlluminateDatabaseEloquentModel; 10 use IlluminateDatabaseEloquentSoftDeletes; 11 12 class Question extends Model 13 { 14 //软删除 添加 15 use SoftDeletes; 16 // 17 protected $fillable = ['title', 'content', 'user_id']; 18 //支持软删除 添加 19 protected $dates = ['deleted_at']; 20 21 public function topics() 22 { 23 return $this->belongsToMany( 24 Topic::class, 25 'questions_topics' //表名我设置的是questions_topics,可能不是系统自动解析的question_topic 26 )->withTimestamps();//withTimestamps操作questions_topics表中create_at及updated_at字段的 27 } 28 29 public function user() 30 { 31 return $this->belongsTo(User::class); 32 } 33 34 /** scope+请求名命名的 35 * @return bool 36 */ 37 public function scopePublished($query) 38 { 39 return $query->where('is_hidden', 'F');//等于F表示不隐藏 40 } 41 42 43 /** 一个问题有多个回答 44 * @return IlluminateDatabaseEloquentRelationsHasMany 45 */ 46 public function answers() 47 { 48 return $this->hasMany(Answer::class); 49 } 50 51 52 public function followUsers() 53 { 54 //默认表名 可以不设置后面三个参数,自定义表名需要设置 55 return $this->belongsToMany(Question::class, 'users_questions', 'user_id', 'question_id')->withTimestamps(); 56 } 57 58 public function comments() 59 { 60 return $this->morphMany(Comment::class, 'commentable'); 61 } 62 63 } 64 65
5.view部分 vue组件
1 <template> 2 <div> 3 <button class="btn btn-sm btn-secondary ml-2" 4 @click="showCommentsForm" v-text="showCommentText"> 5 </button> 6 7 <div class="modal fade" :id="id" tabindex="-1" role="dialog"> 8 <div class="modal-dialog"> 9 <div class="modal-content"> 10 <div class="modal-header"> 11 <h4 class="modal-title"> 12 评论 13 </h4> 14 <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> 15 </div> 16 17 <div class="modal-body"> 18 <div v-if="comments.length>0"> 19 <div class="card" v-for="comment in comments"> 20 <div class="card-body"> 21 <div class="row"> 22 <div class="col-md-2"> 23 <img :src="comment.user.avatar" class="img img-rounded img-fluid" 24 :alt="comment.user.name"> 25 <p class="text-secondary text-center"> {{ 26 comment.created_at }}</p> 27 </div> 28 <div class="col-md-10"> 29 <p> 30 <a class="float-left" href="#"> 31 <strong>{{ comment.user.name}}</strong></a> 32 </p> 33 <div class="clearfix"></div> 34 <p> {{ comment.content }}</p> 35 <p> 36 <a class="float-right btn btn-outline-primary ml-2"> <i 37 class="fa fa-reply"></i> 回复</a> 38 <a class="float-right btn text-white btn-danger"> <i 39 class="fa fa-heart"></i> 点赞</a> 40 </p> 41 </div> 42 </div> 43 </div> 44 <!-- <div class="card card-inner">--> 45 <!-- <div class="card-body">--> 46 <!-- <div class="row">--> 47 <!-- <div class="col-md-2">--> 48 <!-- <img src="https://image.ibb.co/jw55Ex/def_face.jpg"--> 49 <!-- class="img img-rounded img-fluid"/>--> 50 <!-- <p class="text-secondary text-center">15 Minutes Ago</p>--> 51 <!-- </div>--> 52 <!-- <div class="col-md-10">--> 53 <!-- <p><a--> 54 <!-- href="https://maniruzzaman-akash.blogspot.com/p/contact.html"><strong>Maniruzzaman--> 55 <!-- Akash</strong></a></p>--> 56 <!-- <p>Lorem Ipsum is simply dummy text of the pr make but also the leap--> 57 <!-- into electronic typesetting, remaining essentially unchanged. It was--> 58 <!-- popularised in the 1960s with the release of Letraset sheets--> 59 <!-- containing Lorem Ipsum passages, and more recently with desktop--> 60 <!-- publishing software like Aldus PageMaker including versions of Lorem--> 61 <!-- Ipsum.</p>--> 62 <!-- <p>--> 63 <!-- <a class="float-right btn btn-outline-primary ml-2"> <i--> 64 <!-- class="fa fa-reply"></i> Reply</a>--> 65 <!-- <a class="float-right btn text-white btn-danger"> <i--> 66 <!-- class="fa fa-heart"></i> Like</a>--> 67 <!-- </p>--> 68 <!-- </div>--> 69 <!-- </div>--> 70 <!-- </div>--> 71 <!-- </div>--> 72 </div> 73 </div> 74 <input class="form-control" v-model="postComment" v-if="!success"></input> 75 <button type="button" class="btn btn-success" @click="store" v-if="!success">发送</button> 76 <div class="alert alert-success" v-if="success">评论发送成功!</div> 77 </div> 78 79 <!-- Modal Actions --> 80 <div class="modal-footer"> 81 <button type="button" class="btn btn-secondary" data-dismiss="modal">关闭</button> 82 </div> 83 </div> 84 </div> 85 </div> 86 </div> 87 88 </template> 89 90 <script> 91 export default { 92 props: ['type', 'commentable_id'], 93 name: "Comments.vue", 94 data: function () { 95 return { 96 comments: [], 97 postComment: null, 98 success: false, 99 } 100 }, 101 computed: { 102 id() { 103 return 'model-comment' + '-' + this.type + '-' + this.commentable_id; 104 }, 105 showCommentText() { 106 return this.comments.length + "条评论"; 107 }, 108 }, 109 mounted() { 110 this.getComments(); 111 }, 112 methods: { 113 showCommentsForm() { 114 this.getComments(); 115 $('#' + this.id).modal('show');//显示评论框 116 }, 117 getComments() { 118 let currentObject = this; 119 axios.get('/api/' + this.type + '/' + this.commentable_id + '/comments').then(function (response) { 120 currentObject.comments = response.data.comments; 121 }).catch(function (e) { 122 console.log(e); 123 }).finally(function () { 124 125 }); 126 }, 127 store() { 128 let currentObject = this; 129 axios.post('/api/comments', { 130 'type': this.type, 131 'commentable_id': this.commentable_id, 132 'postComment': this.postComment 133 }).then(function (response) { 134 currentObject.comments.push(response.data.comment[0]); 135 }).catch(function (e) { 136 console.log(e); 137 }).finally(function () { 138 139 }); 140 } 141 } 142 } 143 </script> 144 145 <style scoped> 146 147 </style> 148 149
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=" 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 @include('users._small_icon',['userable'=>$answer]) 72 <span class="float-right text text-info text-center"> 73 {{ $answer->updated_at->diffForHumans() }}</span> 74 @if(auth()->check()) 75 <user-vote-button answer="{{ $answer->id }}" 76 vote_count="{{ $answer->userVotes->count() }}" 77 class="float-right"></user-vote-button> 78 @endif 79 </div> 80 81 <div class="card-body"> 82 {!! $answer->content !!} 83 </div> 84 <div class="card-footer"> 85 <comments type="answer" commentable_id="{{ $answer->id }}"></comments> 86 </div> 87 </div> 88 89 @empty 90 91 @endforelse 92 </div> 93 94 <div class="col-md-3"> 95 <div class="card"> 96 <div class="card-header"> 97 <h2> {{ $question->followers_count }}</h2> 98 <span>关注者</span> 99 </div> 100 <div class="card-body"> 101 <question-follow-button question="{{$question->id}}"id}}"> 102 </question-follow-button> 103 </div> 104 </div> 105 106 <div class="card mt-4"> 107 <div class="card-header"> 108 <h2> 提问者 </h2> 109 </div> 110 <div class="card-body"> 111 @include('users._small_icon',['userable'=>$question]) 112 </div> 113 @include('users._user_stats') 114 </div> 115 </div> 116 117 118 </div> 119 </div> 120 @endsection 121 @section('footer-js') 122 @include('questions._footer_js') 123 @endsection 124 125
1 /** 2 * First we will load all of this project's JavaScript dependencies which 3 * includes Vue and other libraries. It is a great starting point when 4 * building robust, powerful web applications using Vue and Laravel. 5 */ 6 7 require('./bootstrap'); 8 require('../../vendor/select2/select2/dist/js/select2.js'); 9 // 将views/vendor/ueditor/assets.blade.php中的引用换到本处 10 require('../../public/vendor/ueditor/ueditor.config.js'); 11 require('../../public/vendor/ueditor/ueditor.all.js'); 12 13 window.Vue = require('vue'); 14 15 /** 16 * The following block of code may be used to automatically register your 17 * Vue components. It will recursively scan this directory for the Vue 18 * components and automatically register them with their "basename". 19 * 20 * Eg. ./components/ExampleComponent.vue -> <example-component></example-component> 21 */ 22 23 // const files = require.context('./', true, /.vue$/i) 24 // files.keys().map(key => Vue.component(key.split('/').pop().split('.')[0], files(key).default)) 25 26 // Vue.component('example-component', require('./components/ExampleComponent.vue').default); 27 Vue.component('question-follow-button', require('./components/QuestionFollowButton').default); 28 Vue.component('user-follow-button', require('./components/UserFollowButton').default); 29 Vue.component('user-vote-button', require('./components/UserVoteButton').default); 30 Vue.component('send-message', require('./components/SendMessage').default); 31 Vue.component('comments', require('./components/Comments').default); 32 /** 33 * Next, we will create a fresh Vue application instance and attach it to 34 * the page. Then, you may begin adding components to this application 35 * or customize the JavaScript scaffolding to fit your unique needs. 36 */ 37 38 const app = new Vue({ 39 el: '#app', 40 }); 41 42
6.CommentContoller及路由api.php
api.php:
1 #region 2 Route::middleware('api')->get('/answer/{id}/comments', 'CommentController@showAnswerComment'); 3 Route::middleware('api')->get('/question/{id}/comments', 'CommentController@showQuestionComment'); 4 5 Route::middleware('auth:api')->post('/comments', 'CommentController@store'); 6 #endregion 7
1 <?php 2 3 namespace AppHttpControllers; 4 5 use AppComment; 6 use IlluminateHttpRequest; 7 use IlluminateDatabaseEloquentBuilder; 8 9 class CommentController extends Controller 10 { 11 // 12 13 public function showAnswerComment($id) 14 { 15 //https://laravel.com/docs/5.8/eloquent-relationships#querying-polymorphic-relationships 16 $comments = Comment::whereHasMorph('commentable', ['AppAnswer'], function (Builder $query) use ($id) { 17 $query->where('id', '=', $id); 18 })->with('user')->get(); 19 20 return response()->json( 21 [ 22 'comments' => $comments, 23 ] 24 ); 25 } 26 27 public function showQuestionComment($id) 28 { 29 $comments = Comment::query()->whereHasMorph('commentable', 'AppModelsQuestion', function (Builder $query) use ($id) { 30 $query->where('id', $id); 31 })->with('user')->get(); 32 return response()->json( 33 [ 34 'comments' => $comments, 35 ] 36 ); 37 } 38 39 public function store(Request $request) 40 { 41 $type = ($request->get('type') === 'answer') ? 'AppAnswer' : 'AppModelsQuestion'; 42 $comment = Comment::create([ 43 'commentable_type' => $type, 44 'commentable_id' => $request->get('commentable_id'), 45 'user_id' => auth()->user()->id, 46 'content' => $request->get('postComment'), 47 ]); 48 $comment = Comment::query()->where('id', $comment->id)->with('user')->get(); 49 return response()->json( 50 [ 51 'comment' => $comment, 52 ] 53 ); 54 } 55 56 } 57 58
7.添加comment与用户模型的关联:
Comment.php中:
1 public function user() 2 { 3 return $this->belongsTo(User::class); 4 } 5
1 <?php 2 3 namespace App; 4 5 use IlluminateDatabaseEloquentModel; 6 7 class Comment extends Model 8 { 9 //表名 10 protected $table = 'comments'; 11 12 //必须初始赋值的 13 protected $fillable = ['user_id', 'content', 'commentable_id', 'commentable_type']; 14 15 16 public function commentable() 17 { 18 return $this->morphTo(); 19 } 20 21 public function user() 22 { 23 return $this->belongsTo(User::class); 24 } 25 } 26 27
User.php中:
1 public function comments() 2 { 3 return $this->hasMany(Comment::class); 4 } 5
1 <?php 2 3 namespace App; 4 5 use AppModelsQuestion; 6 use IlluminateContractsAuthMustVerifyEmail; 7 use IlluminateDatabaseEloquentSoftDeletes; 8 use IlluminateFoundationAuthUser as Authenticatable; 9 use IlluminateNotificationsNotifiable; 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', 'api_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 IlluminateDatabaseEloquentRelationsHasMany 48 */ 49 public function questions() 50 { 51 return $this->hasMany(Question::class); 52 } 53 54 55 /** 添加用户模型和回答模型的模型关联 一个用户可以有多个回答 56 * @return IlluminateDatabaseEloquentRelationsHasMany 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 71 /** 用户的粉丝 72 * @return IlluminateDatabaseEloquentRelationsBelongsToMany 73 */ 74 public function followers() 75 { 76 77 return $this->belongsToMany 78 ( 79 self::class, 80 'followers', 81 'user_id', //foreignPivotKey:当前模型在中间表的字段(当前模型类的外键) //【当前模型是leader】的外键id 82 'follower_id'//relatedPivotKey:另一模型在中间表的字段(另一模型类的外键) 83 )->withTimestamps(); 84 } 85 86 87 /** 用户关注的作者 88 * @return IlluminateDatabaseEloquentRelationsBelongsToMany 89 */ 90 public function followings() 91 { 92 return $this->belongsToMany 93 ( 94 self::class, 95 'followers', 96 'follower_id',//foreignPivotKey:当前模型在中间表的字段(当前模型类的外键) //【当前模型是粉丝】的外键id 97 'user_id'//relatedPivotKey:另一模型在中间表的字段(另一模型类的外键) 98 ) 99 ->withTimestamps(); 100 } 101 102 103 /** 104 * @return IlluminateDatabaseEloquentRelationsBelongsToMany 105 */ 106 public function votes() 107 { 108 return $this->belongsToMany(Answer::class, 'votes')->withTimestamps(); 109 } 110 111 112 /** 113 * @param $answer_id 114 * @return array 115 */ 116 public function voteAnswer($answer_id) 117 { 118 return $this->votes()->toggle($answer_id); 119 } 120 121 122 public function messages() 123 { 124 return $this->hasMany(Message::class, 'to_user_id'); 125 } 126 127 public function comments() 128 { 129 return $this->hasMany(Comment::class); 130 } 131 132 } 133 134
8.Repository 模式重构评论 :省略