laravel6开发论坛-新建话题

110 阅读2分钟

新建话题

接下来开发帖子发布功能,允许注册用户发布帖子,发布完成后,跳转到帖子详情页面。

新增入口

代码生成器已经为我们生成了新建话题的路由 topics.create ,我们需要在右边导航栏和顶部导航栏新增发帖入口:

resources/views/layouts/_header.blade.php

@guest
  <li class="nav-item"><a class="nav-link" href="{{ route('login') }}">登录</a></li>
  <li class="nav-item"><a class="nav-link" href="{{ route('register') }}">注册</a></li>
@else
    <li class="nav-item">
      <a class="nav-link mt-1 mr-3 font-weight-bold" href="{{ route('topics.create') }}">
        <i class="fa fa-plus"></i>
      </a>
    </li>
  <li class="nav-item dropdown">

resources/views/topics/_sidebar.blade.php

<div class="card ">
  <div class="card-body">
    <a href="{{ route('topics.create') }}" class="btn btn-success btn-block" aria-label="Left Align">
      <i class="fas fa-pencil-alt mr-2"></i> 新建帖子
    </a>
  </div>
</div>

效果如下:

image.png

数据模型

我们需要准备好话题数据模型 Topic 里的 $fillable 属性。代码生成器在生成模型时将所有的字段都罗列出

来,这是很危险的,因为 fillable属性允许用户直接对数据进行修改,在每一次开发数据模型的CRUD功能时,我们都要慎重地对fillable 属性允许用户直接对数据进行修改,在每一次开发数据模型的 CRUD 功能时,我们都要慎重地对 fillable 属性进行定制。给自己提一个问题:『哪些字段我们将不允许用户直接修改?』。在我们当前的情况下以下字段将禁止用户修改:

  • user_id —— 文章的作者,我们不希望文章的作者可以被随便指派;
  • last_reply_user_id —— 最后回复的用户 ID,将由程序来维护;
  • order —— 文章排序,将会是管理员专属的功能;
  • reply_count —— 回复数量,程序维护;
  • view_count —— 查看数量,程序维护;

我们把以上字段从 Topic 数据模型的 $fillable 属性中移除:

app/Models/Topic.php


//白名单
protected $fillable = [
    'title', 'body', 'category_id', 'excerpt', 'slug'
];

控制器

代码生成器已经帮我们生成了路由,请自行打开 routes/web.php 文件进行确认。我们将允许用户在发帖时选择分类,因此接下来我们将修改控制器的 create() 方法,将所有的分类读取赋值给变量 $categories ,并传入模板中:

app/Http/Controllers/TopicsController.php

use App\Models\Category;

// 发布帖子
public function create(Topic $topic)
{
    $categories = Category::all();
    return view('topics.create_and_edit', compact('topic', 'categories'));
}

接下来修改模板:

resources/views/topics/create_and_edit.blade.php

@extends('layouts.app')

@section('content')

  <div class="container">
    <div class="col-md-10 offset-md-1">
      <div class="card ">

        <div class="card-body">
          <h2 class="">
            <i class="far fa-edit"></i>
            @if($topic->id)
              编辑话题
            @else
              新建话题
            @endif
          </h2>

          <hr>

          @if($topic->id)
            <form action="{{ route('topics.update', $topic->id) }}" method="POST" accept-charset="UTF-8">
              <input type="hidden" name="_method" value="PUT">
              @else
                <form action="{{ route('topics.store') }}" method="POST" accept-charset="UTF-8">
                  @endif

                  <input type="hidden" name="_token" value="{{ csrf_token() }}">

                  @include('shared._error')

                  <div class="form-group">
                    <input class="form-control" type="text" name="title" value="{{ old('title', $topic->title ) }}" placeholder="请填写标题" required />
                  </div>

                  <div class="form-group">
                    <select class="form-control" name="category_id" required>
                      <option value="" hidden disabled {{ $topic->id ? '' : 'selected' }}>请选择分类</option>
                      @foreach ($categories as $value)
                        <option value="{{ $value->id }}" {{ $topic->category_id == $value->id ? 'selected' : '' }}>
                          {{ $value->name }}
                        </option>
                      @endforeach
                    </select>
                  </div>

                  <div class="form-group">
                    <textarea name="body" class="form-control" id="editor" rows="6" placeholder="请填入至少三个字符的内容。" required>{{ old('body', $topic->body ) }}</textarea>
                  </div>

                  <div class="well well-sm">
                    <button type="submit" class="btn btn-primary"><i class="far fa-save mr-2" aria-hidden="true"></i> 保存</button>
                  </div>
                </form>
        </div>
      </div>
    </div>
  </div>

@endsection

请注意此模板为创建和编辑帖子时共用的模板。

效果:

image.png

提交表单

因为我们入库的数据还未指定 user_id 字段值。 user_id 是用来记录话题作者对应的用户 ID,当用户提交文章时,我们将在控制器中,把当前登录的用户 ID 赋值给 user_id :

app/Http/Controllers/TopicsController.php

<?php
.
.
.
use Illuminate\Support\Facades\Auth;

class TopicsController extends Controller

{

.
.
.

// 新增帖子
public function store(TopicRequest $request, Topic $topic)

{

$topic->fill($request->all());

$topic->user_id = Auth::id();

$topic->save();

return redirect()->route('topics.show', $topic->id)->with('success', '帖子创建成功!');

}

.
.
.

}

代码解析:

  • 因为要使用到 Auth 类,所以需在文件顶部进行加载;
  • store() 方法的第二个参数,会创建一个空白的 $topic 实例;
  • $request->all() 获取所有用户的请求数据数组,如 ['title' => '标题', 'body' => '内容', ... ] ;
  • topic>fill(topic->fill(request->all()); fill 方法会将传参的键值数组填充到模型的属性中,如以上数组, $topic->title 的值为 标题 ;
  • Auth::id() 获取到的是当前登录的 ID;
  • $topic->save() 保存到数据库中。

提交即可看见创建成功的页面:

image.png

模型观察器

excerpt 字段存储的是话题的摘录,将作为文章页面的 description 元标签使用,有利于 SEO 搜索引擎优化。摘录由文章内容中自动生成,生成的时机是在话题数据存入数据库之前。我们将使用 Eloquent 的 来实现此功能。

Eloquent 模型会触发许多事件(Event),我们可以对模型的生命周期内多个时间点进行监控: creating, created,updating, updated, saving, saved, deleting, deleted, restoring, restored。事件让你每当有特定的模型类在数据库保存或更新时,执行代码。当一个新模型被初次保存将会触发 creating 以及 created 事件。如果一个模型已经存在于数据库且调用了 save 方法,将会触发 updating 和 updated 事件。在这两种情况下都会触发 saving 和saved 事件。

Eloquent 观察器允许我们对给定模型中进行事件监控,观察者类里的方法名对应 Eloquent 想监听的事件。每种方法接收model 作为其唯一的参数。代码生成器已经为我们生成了一个观察器文件,并在 AppServiceProvider 中注册。接下来我们要定制此观察器,在 Topic 模型保存时触发的 saving 事件中,对 excerpt 字段进行赋值:

app/Observers/TopicObserver.php

<?php

namespace App\Observers;

use App\Models\Project;
use App\Models\Topic;

// creating, created, updating, updated, saving,
// saved,  deleting, deleted, restoring, restored

class ProjectObserver
{
    public function saving(Topic $topic)
    {
        $topic->excerpt = make_excerpt($topic->body);
    }

    public function creating(Project $project)
    {
        //
    }

    public function updating(Project $project)
    {
        //
    }
}

make_excerpt() 是我们自定义的辅助方法,我们需要在 helpers.php 文件中添加:

<?php
use Illuminate\Support\Str;

function make_excerpt($value, $length = 200)
{
    $excerpt = trim(preg_replace('/\r\n|\r|\n+/', ' ', strip_tags($value)));
    return Str::limit($excerpt, $length);
}

表单验证类

接下来我们处理下表单验证规则,代码生成器已经为我们生成了 TopicRequest 表单验证类,并且自动在控制器方法中注入,我们不需要修改:

<?php

namespace App\Http\Controllers;

use App\Models\Category;
use App\Models\Topic;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Http\Requests\TopicRequest;
use Illuminate\Support\Facades\Auth;

class TopicsController extends Controller
{
  .
  .
  .
  
    // 执行新增帖子
    public function store(TopicRequest $request, Topic $topic)
    {
        $topic->fill($request->all());
        $topic->user_id = Auth::id();
        $topic->save();
        
        return redirect()->route('topics.show', $topic->id)->with('success', '帖子创建成功!');
    }
  .
  .
  .
  
}

表单验证类里也有提供了基本的代码,我们需修改下验证规则和错误提醒:

app/Http/Requests/TopicRequest.php

<?php
namespace App\Http\Requests;
class TopicRequest extends Request
{
    public function rules()
    {
        switch($this->method())
        {
            // CREATE
            case 'POST':
                // UPDATE
            case 'PUT':
            case 'PATCH':
            {
                return [
                    'title' => 'required|min:2',
                    'body' => 'required|min:3',
                    'category_id' => 'required|numeric',
                ];
            }
            case 'GET':
            case 'DELETE':
            default:
                {
                    return [];
                };
        }
    }
    public function messages()
    {
        return [
            'title.min' => '标题必须至少两个字符',
            'body.min' => '文章内容必须至少三个字符',
        ];
    }
}

在上面的写法中, 表单方法 POST , PUT , PATCH 使用的是相同的一套验证规则。

测试一下:

image.png

新建帖子权限

我们还需要限制未登录用户发帖,代码生成器已经为我们生成了这块逻辑,这里我们检查确认即可:

app/Http/Controllers/TopicsController.php

<?php

namespace App\Http\Controllers;

class TopicsController extends Controller
{
    // 限制未登录用户发帖
    public function __construct()
    {
        $this->middleware('auth', ['except' => ['index', 'show']]);
    }
}

'except' => ['index', 'show'] —— 对除了 index() 和 show() 以外的方法使用 auth 中间件进行认证。