我们在十分钟开发博客项目一节开发的博客应用只是一个基本的博客系统,还有许多地方需要进一步完善。对大多数博客平台而言,例如 WordPress,都可以给博客文章添加分类或标签,本节我们就来为博客文章添加标签功能。
首先需要创建 Tag 模型类:
- php artisan make:model --migration Tag
该命令会在 app
目录下创建模型文件 Tag.php
,由于我们在 make:model
命令中使用了 --migration
选项,所以同时会创建 Tag
模型对应的数据表迁移。
在标签(Tag)和文章(Post)之间存在多对多的关联关系,因此还要按照下面的命令创建存放文章和标签对应关系的数据表迁移:
- php artisan make:migration --create=post_tag_pivot create_post_tag_pivot
在 database/migrations
目录下编辑新创建的标签迁移文件内容如下:
- <?php
- 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\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();
- });
- }
- /**
- * Reverse the migrations.
- */
- public function down()
- {
- Schema::drop('post_tag_pivot');
- }
- }
登录到 Homestead 虚拟机在项目根目录下通过运行如下 Artisan 命令以生成这两个数据表:
- php artisan migrate
在博客后台管理一节中我们已经创建了 TagController
并定义了相应的路由。实现 admin.tag.index
就跟玩儿似地,只需要更新 TagController
的 index
方法,然后为其提供一个渲染视图即可。
在 app/Http/Controllers/Admin
目录下修改 TagController
的 index
方法如下:
- // 在 TagController 顶部添加如下语句
- use App\Tag;
- // 替换 index 方法如下
- public function index()
- {
- $tags = Tag::all();
- return view('admin.tag.index')->withTags($tags);
- }
很简单吧,只需要返回一个传递 $tags
变量的视图。
接下来我们来渲染这个视图,首先在 resources/views/admin
目录下创建一个 tag
子目录,然后在该目录下创建 index.blade.php
并编辑其内容如下:
- @extends('admin.layout')
- @section('content')
- <div class="container-fluid">
- <div class="row page-title-row">
- <div class="col-md-6">
- <h3>Tags <small>» Listing</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> New Tag
- </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>Tag</th>
- <th>Title</th>
- <th class="hidden-sm">Subtitle</th>
- <th class="hidden-md">Page Image</th>
- <th class="hidden-md">Meta Description</th>
- <th class="hidden-md">Layout</th>
- <th class="hidden-sm">Direction</th>
- <th data-sortable="false">Actions</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)
- Reverse
- @else
- Normal
- @endif
- </td>
- <td>
- <a href="/admin/tag/{{ $tag->id }}/edit" class="btn btn-xs btn-info">
- <i class="fa fa-edit"></i> Edit
- </a>
- </td>
- </tr>
- @endforeach
- </tbody>
- </table>
- </div>
- </div>
- </div>
- @stop
- @section('scripts')
- <script>
- $(function() {
- $("#tags-table").DataTable({
- });
- });
- </script>
- @stop
这个模板很简单,我们使用了后台布局,在文件顶部是页面标题和创建新标签的按钮,然后用表格来显示所有标签,最后,在页面顶部我们引入了一些 JavaScript 脚本将表格转化为 DataTables。
还有两个被包含进来的局部视图:admin.partials.errors
和 admin.partials.success
。admin.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
:
现在还没有任何数据,所以是个空的列表。
接下来我们将会实现添加标签功能,这样当你点击“添加标签”按钮时就会跳转到填写标签表单页面。
修改 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
方法回写到表单中。
在 resources/views/admin/tag
目录下新建 create.blade.php
文件,编辑其内容如下:
- @extends('admin.layout')
- @section('content')
- <div class="container-fluid">
- <div class="row page-title-row">
- <div class="col-md-12">
- <h3>Tags <small>» Create New Tag</small></h3>
- </div>
- </div>
- <div class="row">
- <div class="col-md-8 col-md-offset-2">
- <div class="panel panel-default">
- <div class="panel-heading">
- <h3 class="panel-title">New Tag Form</h3>
- </div>
- <div class="panel-body">
- @include('admin.partials.errors')
- <form class="form-horizontal" role="form" method="POST" action="/admin/tag">
- <input type="hidden" name="_token" value="{{ csrf_token() }}">
- <div class="form-group">
- <label for="tag" class="col-md-3 control-label">Tag</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">
- <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>
- Add New Tag
- </button>
- </div>
- </div>
- </form>
- </div>
- </div>
- </div>
- </div>
- </div>
- @stop
由于我们会在添加和编辑标签时共用同一个表单,所以我们将表单部分放到一个独立的局部视图 admin.tag._form
中,下面我们就来创建这个局部视图:
在 resources/views/admin/tag
目录下新建一个 _form.blade.php
文件,编辑其内容如下:
- <div class="form-group">
- <label for="title" class="col-md-3 control-label">
- Title
- </label>
- <div class="col-md-8">
- <input type="text" class="form-control" name="title" id="title" value="{{ $title }}">
- </div>
- </div>
- <div class="form-group">
- <label for="subtitle" class="col-md-3 control-label">
- Subtitle
- </label>
- <div class="col-md-8">
- <input type="text" class="form-control" name="subtitle" id="subtitle" value="{{ $subtitle }}">
- </div>
- </div>
- <div class="form-group">
- <label for="meta_description" class="col-md-3 control-label">
- Meta Description
- </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">
- <label for="page_image" class="col-md-3 control-label">
- Page Image
- </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">
- <label for="layout" class="col-md-3 control-label">
- Layout
- </label>
- <div class="col-md-4">
- <input type="text" class="form-control" name="layout" id="layout" value="{{ $layout }}">
- </div>
- </div>
- <div class="form-group">
- <label for="reverse_direction" class="col-md-3 control-label">
- Direction
- </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">
- Normal
- </label>
- <label class="radio-inline">
- <input type="radio" name="reverse_direction"
- @if ($reverse_direction)
- checked="checked"
- @endif
- value="1">
- Reversed
- </label>
- </div>
- </div>
在后台标签列表中点击“新建标签”(New Tag)按钮访问刚刚创建的页面:
现在我们已经有了创建标签表单,还需要编写表单被提交后保存标签的业务逻辑代码。
Laravel 5.1 的一个优秀特性就是表单请求类,这些类可以对指定表单字段进行验证。
使用如下 Artisan 命令创建一个新的 TagCreateRequest
:
- php artisan make:request TagCreateRequest
创建的类存放在 app/Http/Requests
目录下,编辑刚刚创建的 TagCreateRequest.php
文件内容如下:
- <?php
- namespace App\Http\Requests;
- use App\Http\Requests\Request;
- class TagCreateRequest extends Request
- {
- /**
- * 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' => 'required|unique:tags,tag',
- 'title' => 'required',
- 'subtitle' => 'required',
- 'layout' => 'required',
- ];
- }
- }
该类继承自 Illuminate\Foundation\Http\FormRequest
,当该类实例化的时候就会对请求进行验证,需要验证的项目有两个:
authorize()
:验证用户是否经过登录认证rules()
:返回验证规则数组表单请求的神奇之处在于会在表单请求类实例化的时候对请求进行验证,如果验证失败,会直接返回表单提交页面并显示错误信息。这意味着如果将表单请求作为控制器方法参数,那么验证工作将会在执行对应方法第一行代码之前进行。接下来我们就来定义保存的控制器方法。
编辑控制器 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')
- ->withSuccess("The tag '$tag->tag' was created.");
- }
现在,通过依赖注入,TagCreateRequest
被构造、表单被验证,只有验证通过后才会将请求参数传递到 store
方法。store
方法接下来才会创建并保存新的 Tag
实例。最后,页面重定向到后台标签列表,并带上保存成功消息:
点击“Add New Tag”按钮提交表单,保存成功:
接下来,我们来编辑路由方法 admin.tag.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);
- }
在 resources/views/admin/tag
目录中,创建 edit.blade.php
并编辑其内容如下:
- @extends('admin.layout')
- @section('content')
- <div class="container-fluid">
- <div class="row page-title-row">
- <div class="col-md-12">
- <h3>Tags <small>» Edit Tag</small></h3>
- </div>
- </div>
- <div class="row">
- <div class="col-md-8 col-md-offset-2">
- <div class="panel panel-default">
- <div class="panel-heading">
- <h3 class="panel-title">Tag Edit Form</h3>
- </div>
- <div class="panel-body">
- @include('admin.partials.errors')
- @include('admin.partials.success')
- <form class="form-horizontal" 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">
- <label for="tag" class="col-md-3 control-label">Tag</label>
- <div class="col-md-3">
- <p class="form-control-static">{{ $tag }}</p>
- </div>
- </div>
- @include('admin.tag._form')
- <div class="form-group">
- <div class="col-md-7 col-md-offset-3">
- <button type="submit" class="btn btn-primary btn-md">
- <i class="fa fa-save"></i>
- Save Changes
- </button>
- <button type="button" class="btn btn-danger btn-md" data-toggle="modal" data-target="#modal-delete">
- <i class="fa fa-times-circle"></i>
- Delete
- </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">Please Confirm</h4>
- </div>
- <div class="modal-body">
- <p class="lead">
- <i class="fa fa-question-circle fa-lg"></i>
- Are you sure you want to delete this tag?
- </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-default" data-dismiss="modal">Close</button>
- <button type="submit" class="btn btn-danger">
- <i class="fa fa-times-circle"></i> Yes
- </button>
- </form>
- </div>
- </div>
- </div>
- </div>
- @stop
现在如果点击标签的编辑按钮,就会看到编辑表单,如果点击删除按钮则会弹出确认按钮:
当然,标签现在还不能真的被删除或更新,接下来我们要在后台实现其业务逻辑。
和 admin.tag.store
路由处理 admin.tag.create
表单一样,我们使用 admin.tag.update
路由处理 admin.tag.edit
表单。
我们使用如下 Artisan 命令生成处理更新表单请求类 TagUpdateRequest
:
- php artisan make:request TagUpdateRequest
然后编辑刚刚生成的 TagUpdateRequest.php
文件(该文件位于 app/Http/Requests
目录下):
- <?php
- namespace App\Http\Requests;
- use App\Http\Requests\Request;
- class TagUpdateRequest extends Request
- {
- /**
- * 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.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")
- ->withSuccess("Changes saved.");
- }
现在我们就可以编辑标签并点击“保存更改”(Save Changes)按钮实现标签修改操作。
到目前为止,我们已经基本完成后台标签功能了(我们后续将会在博客文章创建及编辑中实现标签与文章的绑定),剩下来要做的就是实现标签删除功能了。
要在编辑标签页面实现删除标签功能,首先要修改 TagController.php
的 destroy
方法如下:
- /**
- * Delete the tag
- *
- * @param int $id
- * @return Response
- */
- public function destroy($id)
- {
- $tag = Tag::findOrFail($id);
- $tag->delete();
- return redirect('/admin/tag')
- ->withSuccess("The '$tag->tag' tag has been deleted.");
- }
没有任何神奇的地方:获取标签、删除标签,删除成功后重定向到标签列表页:
你可能已经注意到还有一个路由到目前为止还没有被使用,admin.tag.show
用来显示标签详情,该路由通常用于给那些没有编辑权限的用户查看详情。
在本系统中我们不需要查看标签详情(实际上这个功能也没有什么意义),所以将其删除。
首先在 app/Http/routes.php
中修改路由如下:
- // 将如下这行代码
- resource('admin/tag', 'TagController');
- // 修改成
- resource('admin/tag', 'TagController', ['except' => 'show']);
然后在控制器 TagController.php
中移除 show()
方法。
下一节我们将会为博客系统实现文件上传功能。