1 路由
outesweb.php
//首页重定义 Route::get('/','QuestionsController@index'); Route::resource('questions','QuestionsController',[ 'names'=>[ 'create' =>'question.create', 'show' =>'question.show' ] ]);
outesapi.php
Route::get('/topics',function (Request $request){ $topics = AppTopic::select(['id','name']) ->where('name','like','%'.$request->query('q').'%') ->get(); return $topics; });
2 控制器
appHttpControllersQuestionsController.php
<?php namespace AppHttpControllers; use AppHttpRequestsStoreQuestionRequest; //use AppQuestion; //use AppTopic; use IlluminateHttpRequest; use IlluminateSupportFacadesAuth; use AppRepositoriesQuestionRepository; class QuestionsController extends Controller { protected $questionRepository; /** * 依赖注入 * QuestionsController constructor. * @param AppRepositoriesQuestionRepository $questionRepository */ public function __construct(QuestionRepository $questionRepository) { $this->middleware('auth')->except(['index','show']); $this->questionRepository = $questionRepository; } /** * Display a listing of the resource. * * @return IlluminateHttpResponse */ public function index() { $questions = $this->questionRepository->getQuestionsFeed(); return view('questions.index',compact('questions')); } /** * Show the form for creating a new resource. * * @return IlluminateHttpResponse */ public function create() { // $oldTopicList = $this->questionRepository->getOldTopicList(old('topics')); return view('questions.create',compact('oldTopicList')); } /** * Store a newly created resource in storage. * * @param IlluminateHttpRequest $request * @return IlluminateHttpResponse */ //public function store(Request $request) public function store(StoreQuestionRequest $request) { /*$rules = [ 'title' =>'required|min:6|max:196', 'body' =>'required|min:20', ];*/ /*$message = [ 'body.required'=>'内容不得为空' , 'body.min'=>'内容不得少于20个字符' , ];*/ //自动过滤掉token值 //$this->validate($request,$rules,$message); $topics = $this->questionRepository->normalizeTopic($request->get('topics')); $data = $request->all(); $save = [ 'title' =>$data['title'], 'body' =>$data['body'], 'user_id' =>Auth::id() ]; //$rs = Question::create($save); $rs = $this->questionRepository->create($save); $rs->topics()->attach($topics); return redirect()->route('question.show',[$rs->id]); } /** * Display the specified resource. * * @param int $id * @return IlluminateHttpResponse */ public function show($id) { //$question = Question::where('id',$id)->with('topics')->first(); $question = $this->questionRepository->byIdWithTopics($id); if(!$question){ abort('404','你可能来到了没有知识的荒漠'); } return view('questions.show',compact('question')); } /** * Show the form for editing the specified resource. * * @param int $id * @return IlluminateHttpResponse */ public function edit($id) { $question = $this->questionRepository->byId($id); if (Auth::user()->owns($question)){ return view('questions.edit',compact('question')); } return back(); } /** * Update the specified resource in storage. * * @param IlluminateHttpRequest $request * @param int $id * @return IlluminateHttpResponse */ public function update(StoreQuestionRequest $request, $id) { $question = $this->questionRepository->byId($id); $topics = $this->questionRepository->normalizeTopic($request->get('topics')); $question->update([ 'title'=>$request->get('title'), 'body' =>$request->get('body') ]); //sync 同步 $question->topics()->sync($topics); return redirect()->route('question.show',[$question->id]); } /** * Remove the specified resource from storage. * * @param int $id * @return IlluminateHttpResponse */ public function destroy($id) { DB::beginTransaction(); $question = $this->questionRepository->byId($id); if(Auth::user()->owns($question)){ $question = $this->questionRepository->byIdWithTopics($id); $rs1 = $question->delete(); $topicsIds = $question->topics->keyBy('id')->toArray(); if ($topicsIds){ $rs2 = $topics = $this->questionRepository->normalize2Topic($topicsIds); }else{ $rs2 = true; } if($rs1 && $rs2 ){ DB::commit(); } else{ DB::rollBack(); } //sync 同步 if ($topicsIds){ //移除问题的某些的标签 //$question->topics()->detach($topics); //移除问题的所有的标签 $question->topics()->detach(); } return redirect('/'); } abort('403','你没有权限'); } }
3 验证
appHttpRequestsStoreQuestionRequest.php
<?php namespace AppHttpRequests; use IlluminateFoundationHttpFormRequest; class StoreQuestionRequest extends FormRequest { /** * Determine if the user is authorized to make this request. * * @return bool */ public function authorize() { //是否需要验证 //return false; return true; } /** * Get the validation rules that apply to the request. * * @return array */ public function rules() { return [ 'title' =>'required|min:6|max:196', 'body' =>'required|min:20', ]; } public function messages() { return [ 'body.required'=>'内容不得为空' , 'body.min'=>'内容不得少于20个字符' , ]; } }
4 数据仓库层
appRepositoriesQuestionRepository.php
<?php /** * Created by PhpStorm. * User: SUN * Date: 2021/6/29 * Time: 17:49 */ namespace AppRepositories; use AppQuestion; use AppTopic; class QuestionRepository { /** * 根据question的id查找topic 列表 * @param $id */ public function byIdWithTopics($id) { $question = Question::where('id',$id)->with('topics')->first(); return $question; } public function create(array $attributes) { return Question::create($attributes); } /** * @param array $topics * * @return array * */ public function normalizeTopic(array $topics) { //collect 遍历方法 return collect($topics)->map(function ($topic){ //如果是数字 if(is_numeric($topic)){ Topic::find($topic)->increment('questions_count'); return (int)$topic; }else{ $oldTopic = Topic::where('name','=',$topic)->first(); if ($oldTopic){ //查得到 Topic::find($oldTopic->id)->increment('questions_count'); return (int)$oldTopic->id; }else{ //查不到 $newTopic = Topic::create(['name'=>$topic,'questions_count'=>1]); return $newTopic->id; } } })->toArray(); } public function normalize2Topic(array $topics) { //collect 遍历方法 return collect($topics)->map(function ($topic){ if($topic['questions_count']>1){ //dd($topic['questions_count']); $topic = Topic::find($topic['id']); $topic->decrement('questions_count'); return $topic['id']; } Topic::destroy($topic); return $topic['id']; }); } public function byId($id) { return Question::find($id); } public function getOldTopicList($oldTopics) { if($oldTopics){ foreach ( $oldTopics as $v){ if(is_numeric($v)){ $topics[] = $v; }else{ $oldTopicsList[$v]= $v ; } } if (isset($topics)){ $oldTopicsList = array_merge(Topic::find($topics)->pluck('name','id')->toArray(),$oldTopicsList); } } else{ $oldTopicsList = []; } return $oldTopicsList; } /** * 问题列表 */ public function getQuestionsFeed() { return Question::published() ->latest('updated_at') ->with('user') ->get(); } }
5 模型
5.1 app/Question.php
<?php namespace App; use IlluminateDatabaseEloquentModel; class Question extends Model { //fillable为白名单,表示该字段可被批量赋值;guarded为黑名单,表示该字段不可被批量赋值。 protected $fillable = ['title','body','user_id']; public function isHidden() { return $this->is_hidden === 'T'; } /** * laravel自带的功能 使用的时候要把 scope 去掉,并转化为小写 * @param $query * * @return mixed */ public function scopePublished($query) { return $query->where('is_hidden','F'); } public function topics() { //多对多的关系 //belongsToMany如果第二个参数不是question_topic的话 可以通过第二个参数传递自定义表名 return $this->belongsToMany(Topic::class,'question_topic') ->withTimestamps(); } public function user() { return $this->belongsTo(User::class); } }
5.2 appTopic.php
<?php namespace App; use IlluminateDatabaseEloquentModel; class Topic extends Model { // protected $fillable = ['name','questions_count']; public function questions() { return $this->belongsToMany(Question::class) ->withTimestamps(); } }
6 模板
6.1 resourcesviewsquestionscreate.blade.php
@extends('layouts.app') @section('content') @include('vendor.ueditor.assets') <div class="container"> <div class="row justify-content-center"> <div class="col-md-8 col-md-offset-2"> <div class="card"> <div class="card-header">发布问题</div> <div class="card-body"> <form action="/questions" method="post"> {!! csrf_field() !!} <div class="form-group"> <label for="title"><h5>标题 </h5> </label> <input id="title" type="text" name="title" class="form-control @error('title') is-invalid @enderror" placeholder="标题" value="{{ old('title') }}"> @if($errors->has('title')) <div class="alert alert-danger"> <ul> <li>{{ $errors->first('title') }}</li> </ul> </div> @endif @if ($errors->any()) <div class="alert alert-danger"> <ul> @foreach ($errors->all() as $error) <li>{{ $error }}</li> @endforeach </ul> </div> @endif </div> <div class="form-group"> <label for="topic"><h5>话题</h5></label> <select class="js-example-basic-multiple js-example-data-ajax form-control" name="topics[]" multiple="multiple"> @foreach ($oldTopicList as $k=>$topic) <option value="{{is_numeric($k)?:$topic}}" selected="selected"> {{ $topic }} </option> @endforeach </select> {{--<select name="topics[]" class="js-example-basic-multiple js-example-data-ajax form-control" multiple> @foreach($settings->includes->get('topics') as $option) <option value="{{ $option->id }}" {{ (collect(old('topics'))->contains($option->id)) ? 'selected':'' }}>{{ $option->name }}</option> @endforeach </select> <select id="forWhom" name="topics[]" multiple class="form-control chosen"> <option value="">--- Select ---</option> @foreach ($desgInfo as $key => $value) <option value="{{ $key }}" {{ (collect(old('forWhom'))->contains($key)) ? 'selected':'' }} /> {{ $value }} </option> @endforeach </select>--}} </div> <div class="form-group"> <label for="container"><h5>描述 </h5> </label> <!--text/plain的意思是将文件设置为纯文本的形式,浏览器在获取到这种文件时并不会对其进行处理--> <!-- 转义 {{ old('body') }} --> <!-- 非转义 {!! old('body') !!} --> <!-- 编辑器容器 --> <script id="container" name="body" type="text/plain" style = "height: 200px;">{!! old('body') !!}</script> <button class="btn btn-success pull-right" type="submit">发布问题</button> </div> </form> </div> </div> </div> </div> </div> @section('my-js') <script type="text/javascript"> $(function(){ $(".js-example-data-ajax").select2({ tags: true, placeholder: '请选择相关的话题', minimumInputLength: 1, ajax: { url: "/api/topics", dataType: 'json', delay: 250, data: function (params) { return { q: params.term, // search term page: params.page }; }, processResults: function (data, params) { // parse the results into the format expected by Select2 // since we are using custom formatting functions we do not need to // alter the remote JSON data, except to indicate that infinite // scrolling can be used params.page = params.page || 1; return { results: data, pagination: { more: (params.page * 30) < data.total_count } }; }, cache: true }, templateResult: formatRepo, templateSelection: formatRepoSelection }); function formatRepo (repo) { if (repo.loading) { return repo.text; } return "<div class='select2-result-repository clearfix'>"+ "<div class='select2-result-repository__meta'>" + "<div class='select2-result-repository__title'>" + repo.name?repo.name:"laravel" + "</div></div></div>"; } function formatRepoSelection (repo) { return repo.name || repo.text; } }); </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() }}'); // 设置 CSRF token. }); </script> @endsection @endsection
6.2 resourcesviewsquestionsedit.blade.php
@extends('layouts.app') @section('content') @include('vendor.ueditor.assets') <div class="container"> <div class="row justify-content-center"> <div class="col-md-8 col-md-offset-2"> <div class="card"> <div class="card-header">发布问题</div> <div class="card-body"> <form action="/questions/{{$question->id}}" method="post"> {{ method_field('PATCH') }} {!! csrf_field() !!} <div class="form-group"> <label for="title"><h5>标题 </h5> </label> <input id="title" value="{{ $question->title }}" type="text" name="title" class="form-control @error('title') is-invalid @enderror" placeholder="标题"> @if($errors->has('title')) <div class="alert alert-danger"> <ul> <li>{{ $errors->first('title') }}</li> </ul> </div> @endif @if ($errors->any()) <div class="alert alert-danger"> <ul> @foreach ($errors->all() as $error) <li>{{ $error }}</li> @endforeach </ul> </div> @endif </div> <div class="form-group"> <label for="topic"><h5>话题</h5></label>/ <select class="js-example-basic-multiple js-example-data-ajax form-control" name="topics[]" multiple="multiple"> @foreach($question->topics as $topic) <option value="{{$topic->id}}" selected="selected"> {{ $topic->name }} </option> @endforeach </select> </div> <div class="form-group"> <label for="container"><h5>描述 </h5> </label> <!--text/plain的意思是将文件设置为纯文本的形式,浏览器在获取到这种文件时并不会对其进行处理--> <!-- 转义 {{ old('body') }} --> <!-- 非转义 {!! old('body') !!} --> <!-- 编辑器容器 --> <script id="container" name="body" type="text/plain" style = "height: 200px;">{!! $question->body !!}</script> <button class="btn btn-success pull-right" type="submit">发布问题</button> </div> </form> </div> </div> </div> </div> </div> @section('my-js') <script type="text/javascript"> $(function(){ $(".js-example-data-ajax").select2({ tags: true, placeholder: '请选择相关的话题', minimumInputLength: 1, ajax: { url: "/api/topics", dataType: 'json', delay: 250, data: function (params) { return { q: params.term, // search term page: params.page }; }, processResults: function (data, params) { // parse the results into the format expected by Select2 // since we are using custom formatting functions we do not need to // alter the remote JSON data, except to indicate that infinite // scrolling can be used params.page = params.page || 1; return { results: data, pagination: { more: (params.page * 30) < data.total_count } }; }, cache: true }, templateResult: formatRepo, templateSelection: formatRepoSelection }); function formatRepo (repo) { if (repo.loading) { return repo.text; } return "<div class='select2-result-repository clearfix'>"+ "<div class='select2-result-repository__meta'>" + "<div class='select2-result-repository__title'>" + repo.name?repo.name:"laravel" + "</div></div></div>"; } function formatRepoSelection (repo) { return repo.name || repo.text; } }); </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() }}'); // 设置 CSRF token. }); </script> @endsection @endsection
6.3 resourcesviewsquestionsindex.blade.php
@extends('layouts.app') @section('content') <div class="container"> <div class="row justify-content-center"> <div class="col-md-8 col-md-offset-2"> @foreach($questions as $question) <div class="media-left"> <a href=""> <img width="50px" src="{{$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> @endforeach </div> </div> </div> @endsection
6.4 resourcesviewsquestionsshow.blade.php
@extends('layouts.app') @section('content') <div class="container"> <div class="row justify-content-center"> <div class="col-md-8"> <div class="card"> <div class="card-header">{{ $question->title }}</div> @foreach($question->topics as $topic) <span > <a class="topic" href="/topic/{{ $topic->id }}">{{$topic->name}}</a></span> @endforeach <div class="card-body card-img"> {!! $question->body !!} </div> <div class="action"> @if(Auth::check() && Auth::user()->owns($question)) <span class="edit" style="display: inline"> <a href="/questions/{{$question->id}}/edit">编辑</a> <form action="/questions/{{$question->id}}" method="POST" class="delete-form"> {{method_field("DELETE")}} {{csrf_field()}} <button class="button is-naked delete-button">删除</button> </form> </span> @endif </div> </div> </div> </div> </div> @endsection <style> .card-img p img { max-width: 100% !important; } </style>