基于 Laravel 5.7 开发博客应用系列(四) —— 在后台实现文章标签增删改查功能

我们在十分钟开发博客项目一节开发的博客应用只是一个基本的博客系统,还有许多地方需要进一步完善。对大多数博客平台而言,例如 Wordpress,都可以给博客文章添加分类或标签,本节我们就来为博客文章添加标签功能。

1、创建标签模型和迁移

首先需要创建 Tag 模型类:

php artisan make:model Models/Tag --migration

该命令会在 app/Models 目录下创建模型类文件 Tag.php,由于我们在 make:model 命令中使用了 --migration 选项,所以同时会创建 Tag 模型对应数据表 tags 的数据库迁移。

在标签(Tag)和文章(Post)之间存在多对多的关联关系,因此还要按照下面的命令创建存放文章和标签对应关系的数据表迁移:

php artisan make:migration create_post_tag_pivot --create=post_tag_pivot

编辑标签迁移文件

database/migrations 目录下编辑新创建的标签迁移文件内容如下:

<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateTagsTable extends Migration
{
    /**
     * Run the migrations.
     */
    public function up()
    {
        Schema::create('tags', function (Blueprint $table) {
            $table->increments('id');
            $table->string('tag')->unique();
            $table->string('title');
            $table->string('subtitle');
            $table->string('page_image');
            $table->string('meta_description');
            $table->string('layout')->default('blog.layouts.index');
            $table->boolean('reverse_direction');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down()
    {
        Schema::drop('tags');
    }
}

下面我们对上面迁移文件中新增的字段作简要说明:

  • page_image:标签图片
  • meta_description:标签介绍
  • layout:博客终归要使用布局
  • reverse_directions:在文章列表按时间升序排列博客文章(默认是降序)

编辑文章与标签对应关系迁移

编辑文章与标签对应关系表迁移文件内容如下:

<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreatePostTagPivot extends Migration
{
    /**
     * Run the migrations.
     */
    public function up()
    {
        Schema::create('post_tag_pivot', function (Blueprint $table) {
            $table->increments('id');
            $table->integer('post_id')->unsigned()->index();
            $table->integer('tag_id')->unsigned()->index();
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down()
    {
        Schema::drop('post_tag_pivot');
    }
}

运行迁移

登录到 Laradock,在项目根目录下通过运行如下 Artisan 命令以生成这两个数据表:

php artisan migrate

2、实现 admin.tag.index

在博客后台管理一节中我们已经创建了 TagController 并定义了相应的路由。实现 admin.tag.index 就跟玩儿似地,只需要更新 TagControllerindex 方法,然后为其提供一个渲染视图即可。

实现 TagController@index

app/Http/Controllers/Admin 目录下修改 TagControllerindex 方法如下:

// 在 TagController 顶部添加如下语句
use App\Models\Tag;

// 替换 index 方法如下
public function index()
{
    $tags = Tag::all();
    return view('admin.tag.index')->withTags($tags);
}

很简单吧,只需要返回一个传递 $tags 变量的视图。

创建 admin.tag.index 视图

接下来我们来渲染这个视图,首先在 resources/views/admin 目录下创建一个 tag 子目录,然后在该目录下创建 index.blade.php 并编辑其内容如下:

@extends('admin.layout')

@section('content')
    <div class="container">
        <div class="row page-title-row">
            <div class="col-md-6">
                <h3>标签
                    <small>» 列表</small>
                </h3>
            </div>
            <div class="col-md-6 text-right">
                <a href="/admin/tag/create" class="btn btn-success btn-md">
                    <i class="fa fa-plus-circle"></i> 新增标签
                </a>
            </div>
        </div>

        <div class="row">
            <div class="col-sm-12">

                @include('admin.partials.errors')
                @include('admin.partials.success')

                <table id="tags-table" class="table table-striped table-bordered">
                    <thead>
                    <tr>
                        <th>标签</th>
                        <th>标题</th>
                        <th class="hidden-sm">副标题</th>
                        <th class="hidden-md">页面图片</th>
                        <th class="hidden-md">描述信息</th>
                        <th class="hidden-md">布局</th>
                        <th class="hidden-sm">排序</th>
                        <th data-sortable="false">操作</th>
                    </tr>
                    </thead>
                    <tbody>
                    @foreach ($tags as $tag)
                        <tr>
                            <td>{{ $tag->tag }}</td>
                            <td>{{ $tag->title }}</td>
                            <td class="hidden-sm">{{ $tag->subtitle }}</td>
                            <td class="hidden-md">{{ $tag->page_image }}</td>
                            <td class="hidden-md">{{ $tag->meta_description }}</td>
                            <td class="hidden-md">{{ $tag->layout }}</td>
                            <td class="hidden-sm">
                                @if ($tag->reverse_direction)
                                    逆序
                                @else
                                    升序
                                @endif
                            </td>
                            <td>
                                <a href="/admin/tag/{{ $tag->id }}/edit" class="btn btn-xs btn-info">
                                    <i class="fa fa-edit"></i> 编辑
                                </a>
                            </td>
                        </tr>
                    @endforeach
                    </tbody>
                </table>
            </div>
        </div>
    </div>
@stop

@section('scripts')
    <script>
        $(function () {
            $("#tags-table").DataTable({});
        });
    </script>
@stop

这个模板很简单,我们使用了后台布局,在文件顶部是页面标题和创建新标签的按钮,然后用表格来显示所有标签,最后,在页面顶部我们引入了一些 JavaScript 脚本将表格转化为 DataTables。

还有两个被包含进来的局部视图:admin.partials.errorsadmin.partials.successadmin.partials.errors 已经在构建后台管理系统一节中创建了,但 admin.partials.success 还没有创建。下面我们就来创建这个局部视图。

创建返回成功信息局部视图

resources/views/admin/partials 目录下创建 success.blade.php 文件,编辑其内容如下:

@if (Session::has('success'))
    <div class="alert alert-success">
        <button type="button" class="close" data-dismiss="alert">×</button>
        <strong>
            <i class="fa fa-check-circle fa-lg fa-fw"></i> Success. 
        </strong>
        {{ Session::get('success') }}
    </div>
@endif

访问后台标签管理列表

在浏览器中访问 http://blog.app/admin/tag

现在还没有任何数据,所以是个空的列表。

3、实现 admin.tag.create

接下来我们将会实现添加标签功能,这样当你点击「新增标签」按钮时就会跳转到填写标签表单页面。

实现 TagController@create

修改 app/Http/Controllers/Admin 目录下的 TagController 如下:

// 在 TagController 控制器顶部添加 $fields 属性
class TagController extends Controller
{
    protected $fields = [
        'tag' => '',
        'title' => '',
        'subtitle' => '',
        'meta_description' => '',
        'page_image' => '',
        'layout' => 'blog.layouts.index',
        'reverse_direction' => 0,
    ];

    // 替换 create() 方法如下
    /**
     * Show form for creating new tag
     */
    public function create()
    {
        $data = [];
        foreach ($this->fields as $field => $default) {
            $data[$field] = old($field, $default);
        }

        return view('admin.tag.create', $data);
    }

该方法会在点击「添加新标签」或者表单被填充但是验证失败时执行,对于后者我们会将传过来的数据通过 old 方法回写到表单中。

创建 admin.tag.create 视图

resources/views/admin/tag 目录下新建 create.blade.php 文件,编辑其内容如下:

@extends('admin.layout')

@section('content')
    <div class="container">
        <div class="row page-title-row">
            <div class="col-md-12">
                <h3>标签
                    <small>» 新增标签表单</small>
                </h3>
            </div>
        </div>

        <div class="row">
            <div class="col-md-8 col-md-offset-2">
                <div class="card">
                    <div class="card-header">
                        <h3 class="card-title">新增标签表单</h3>
                    </div>
                    <div class="card-body">

                        @include('admin.partials.errors')

                        <form role="form" method="POST" action="/admin/tag">
                            <input type="hidden" name="_token" value="{{ csrf_token() }}">

                            <div class="form-group row">
                                <label for="tag" class="col-md-3 col-form-label">标签</label>
                                <div class="col-md-3">
                                    <input type="text" class="form-control" name="tag" id="tag" value="{{ $tag }}"
                                           autofocus>
                                </div>
                            </div>

                            @include('admin.tag._form')

                            <div class="form-group row">
                                <div class="col-md-7 col-md-offset-3">
                                    <button type="submit" class="btn btn-primary btn-md">
                                        <i class="fa fa-plus-circle"></i>
                                        添加新标签
                                    </button>
                                </div>
                            </div>

                        </form>

                    </div>
                </div>
            </div>
        </div>
    </div>

@stop

由于我们会在添加和编辑标签时共用同一个表单,所以我们将表单部分放到一个独立的局部视图 admin.tag._form 中,下面我们就来创建这个局部视图:

创建 admin.tag._form 局部视图

resources/views/admin/tag 目录下新建一个 _form.blade.php 文件,编辑其内容如下:

<div class="form-group row">
    <label for="title" class="col-md-3 col-form-label">
        标题
    </label>
    <div class="col-md-8">
        <input type="text" class="form-control" name="title" id="title" value="{{ $title }}">
    </div>
</div>

<div class="form-group row">
    <label for="subtitle" class="col-md-3 col-form-label">
        副标题
    </label>
    <div class="col-md-8">
        <input type="text" class="form-control" name="subtitle" id="subtitle" value="{{ $subtitle }}">
    </div>
</div>

<div class="form-group row">
    <label for="meta_description" class="col-md-3 col-form-label">
        描述信息
    </label>
    <div class="col-md-8">
        <textarea class="form-control" id="meta_description" name="meta_description" rows="3">
            {{ $meta_description }}
        </textarea>
    </div>
</div>

<div class="form-group row">
    <label for="page_image" class="col-md-3 col-form-label">
        图片
    </label>
    <div class="col-md-8">
        <input type="text" class="form-control" name="page_image" id="page_image" value="{{ $page_image }}">
    </div>
</div>

<div class="form-group row">
    <label for="layout" class="col-md-3 col-form-label">
        布局
    </label>
    <div class="col-md-4">
        <input type="text" class="form-control" name="layout" id="layout" value="{{ $layout }}">
    </div>
</div>

<div class="form-group row">
    <label for="reverse_direction" class="col-md-3 col-form-label">
        排序
    </label>
    <div class="col-md-7">
        <label class="radio-inline">
            <input type="radio" name="reverse_direction" id="reverse_direction"
                   @if (! $reverse_direction)
                   checked="checked"
                   @endif
                   value="0">
            升序
        </label>
        <label class="radio-inline">
            <input type="radio" name="reverse_direction"
                   @if ($reverse_direction)
                   checked="checked"
                   @endif
                   value="1">
            逆序
        </label>
    </div>
</div>

在后台标签列表中点击「新增标签」按钮访问刚刚创建的页面:

4、实现 admin.tag.store

现在我们已经有了创建标签表单,还需要编写表单被提交后保存标签的业务逻辑代码。

创建表单请求类 TagCreateRequest

Laravel 的一个优秀特性就是表单请求类,这些类可以对指定表单字段进行验证。

使用如下 Artisan 命令创建一个新的 TagCreateRequest

php artisan make:request TagCreateRequest

创建的类存放在 app/Http/Requests 目录下,编辑刚刚创建的 TagCreateRequest.php 文件内容如下:

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class TagCreateRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
            'tag' => 'bail|required|unique:tags,tag',
            'title' => 'required',
            'subtitle' => 'required',
            'layout' => 'required',
        ];
    }
}

该类继承自 Illuminate\Foundation\Http\FormRequest,当该类实例化的时候就会对请求进行验证,需要验证的项目有两个:

  • authorize() :验证用户是否经过登录认证
  • rules():返回验证规则数组

表单请求的神奇之处在于会在表单请求类实例化的时候对请求进行验证,如果验证失败,会直接返回表单提交页面并显示错误信息。这意味着如果将表单请求作为控制器方法参数,那么验证工作将会在执行对应方法第一行代码之前进行。接下来我们就来定义保存的控制器方法。

实现 TagController@store

编辑控制器 TagController.php 代码如下:

<?php

// 在TagController顶部添加这个use语句
use App\Http\Requests\TagCreateRequest;

// 修改 store() 方法代码如下
/**
 * Store the newly created tag in the database.
 *
 * @param TagCreateRequest $request
 * @return Response
 */
public function store(TagCreateRequest $request)
{
    $tag = new Tag();
    foreach (array_keys($this->fields) as $field) {
        $tag->$field = $request->get($field);
    }
    $tag->save();

    return redirect('/admin/tag')
                    ->with('success', '标签「' . $tag->tag . '」创建成功.');
}

现在,通过依赖注入,TagCreateRequest 被构造、表单被验证,只有验证通过后才会将请求参数传递到 store 方法。store 方法接下来才会创建并保存新的 Tag 实例。最后,页面重定向到后台标签列表,并带上保存成功消息:

点击「添加新标签」按钮提交表单,保存成功:

5、实现 admin.tag.edit

接下来,我们来编辑路由方法 admin.tag.edit 来显示标签编辑表单。

实现 TagController@edit

更新 TagController 控制器,编辑 edit 方法如下:

/**
 * Show the form for editing a tag
 *
 * @param int $id
 * @return Response
 */
public function edit($id)
{
    $tag = Tag::findOrFail($id);
    $data = ['id' => $id];
    foreach (array_keys($this->fields) as $field) {
        $data[$field] = old($field, $tag->$field);
    }

    return view('admin.tag.edit', $data);
}

创建 admin.tag.edit 视图

resources/views/admin/tag 目录下,创建 edit.blade.php 并编辑其内容如下:

@extends('admin.layout')

@section('content')
    <div class="container">
        <div class="row page-title-row">
            <div class="col-md-12">
                <h3>标签
                    <small>» 编辑标签</small>
                </h3>
            </div>
        </div>

        <div class="row">
            <div class="col-md-8 col-md-offset-2">
                <div class="card">
                    <div class="card-header">
                        <h3 class="card-title">标签编辑表单</h3>
                    </div>
                    <div class="card-body">

                        @include('admin.partials.errors')
                        @include('admin.partials.success')

                        <form role="form" method="POST" action="/admin/tag/{{ $id }}">
                            <input type="hidden" name="_token" value="{{ csrf_token() }}">
                            <input type="hidden" name="_method" value="PUT">
                            <input type="hidden" name="id" value="{{ $id }}">

                            <div class="form-group row">
                                <label for="tag" class="col-md-3 col-form-label">标签</label>
                                <div class="col-md-3">
                                    <p class="form-control-plaintext">{{ $tag }}</p>
                                </div>
                            </div>

                            @include('admin.tag._form')

                            <div class="form-group row">
                                <div class="col-md-7 col-md-offset-3">
                                    <button type="submit" class="btn btn-primary btn-md">
                                        <i class="fa fa-save"></i>
                                        保存修改
                                    </button>
                                    <button type="button" class="btn btn-danger btn-md" data-toggle="modal"
                                            data-target="#modal-delete">
                                        <i class="fa fa-times-circle"></i>
                                        删除
                                    </button>
                                </div>
                            </div>

                        </form>

                    </div>
                </div>
            </div>
        </div>
    </div>

    {{-- 确认删除 --}}
    <div class="modal fade" id="modal-delete" tabIndex="-1">
        <div class="modal-dialog">
            <div class="modal-content">
                <div class="modal-header">
                    <button type="button" class="close" data-dismiss="modal">
                        ×
                    </button>
                    <h4 class="modal-title">请确认</h4>
                </div>
                <div class="modal-body">
                    <p class="lead">
                        <i class="fa fa-question-circle fa-lg"></i>
                        确定要删除这个标签?
                    </p>
                </div>
                <div class="modal-footer">
                    <form method="POST" action="/admin/tag/{{ $id }}">
                        <input type="hidden" name="_token" value="{{ csrf_token() }}">
                        <input type="hidden" name="_method" value="DELETE">
                        <button type="button" class="btn btn-secondary" data-dismiss="modal">取消</button>
                        <button type="submit" class="btn btn-danger">
                            <i class="fa fa-times-circle"></i> 确认
                        </button>
                    </form>
                </div>
            </div>
        </div>
    </div>

@stop

在浏览器中访问编辑页面

现在如果在列表页点击标签的编辑按钮,就会看到编辑表单,如果点击删除按钮则会弹出确认按钮:

当然,标签现在还不能真的被删除或更新,接下来我们要在后台实现其业务逻辑。

6、实现 admin.tag.update

admin.tag.store 路由处理 admin.tag.create 表单请求一样,我们使用 admin.tag.update 路由处理 admin.tag.edit 表单请求。

创建表单请求类 TagUpdateRequest

我们使用如下 Artisan 命令生成处理更新表单请求类 TagUpdateRequest

php artisan make:request TagUpdateRequest

然后编辑刚刚生成的 TagUpdateRequest.php 文件(该文件位于 app/Http/Requests 目录下):

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class TagUpdateRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
            'title' => 'required',
            'subtitle' => 'required',
            'layout' => 'required',
        ];
    }
}

该请求处理类和 TagCreateRequest 基本一样。

实现 TagController@update

接下来我们更新 TagController.php 文件内容如下:

// 在TagController顶部添加新的use语句
use App\Http\Requests\TagUpdateRequest;

// 替换 update() 方法如下
/**
 * Update the tag in storage
 *
 * @param TagUpdateRequest $request
 * @param int $id
 * @return Response
 */
public function update(TagUpdateRequest $request, $id)
{
    $tag = Tag::findOrFail($id);

    foreach (array_keys(array_except($this->fields, ['tag'])) as $field) {
        $tag->$field = $request->get($field);
    }
    $tag->save();

    return redirect("/admin/tag/$id/edit")
        ->with('success', '修改已保存.');
}

现在我们就可以编辑标签并点击「保存修改」按钮实现标签修改操作。

7、实现标签删除功能

到目前为止,我们已经基本完成后台标签功能了(我们后续将会在博客文章创建及编辑中实现标签与文章的绑定),剩下来要做的就是实现标签删除功能了。

实现 TagController@destroy

要在编辑标签页面实现删除标签功能,首先要修改 TagController.phpdestroy 方法如下:

/**
 * Delete the tag
 *
 * @param int $id
 * @return Response
 */
public function destroy($id)
{
    $tag = Tag::findOrFail($id);
    $tag->delete();

    return redirect('/admin/tag')
        ->with('success', '标签「' . $tag->tag . '」已经被删除.');
}

没有任何神奇的地方:获取标签、删除标签,删除成功后重定向到标签列表页:

移除 admin.tag.show

你可能已经注意到还有一个路由到目前为止还没有被使用,admin.tag.show 用来显示标签详情,该路由通常用于给那些没有编辑权限的用户查看详情。

在本系统中我们不需要查看标签详情(实际上这个功能也没有什么意义),所以将其删除。

首先在 routes/web.php 中修改路由如下:

// 将如下这行代码
 resource('admin/tag', 'TagController');
// 修改成
 resource('admin/tag', 'TagController', ['except' => 'show']);

然后在控制器 TagController.php 中移除 show() 方法。

下一节我们将会为博客系统实现文件上传功能。

上一篇: 基于 Laravel 5.7 开发博客应用系列(三) —— 构建博客后台管理系统

下一篇: 基于 Laravel 5.7 开发博客应用系列(五) —— 在后台实现文件上传删除管理功能