教程源于:Laravel学院
继文件上传后呢,咱来搞一搞文章的事情。
1 更改数据表
我们需要改改数据表的结构 因为涉及到重命名列名 所以咱需要引入一个包:Doctrine:
composer require "doctrine/dbal"
1.1 新建迁移文件
php artisan make:migration restructure_posts_table --table=posts
1.2 编辑迁移文件
class RestructurePostsTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::table('posts', function (Blueprint $table) { // 在title字段后添加subtitle 文章的副标题 $table->string('subtitle')->after('title'); // 把content改为content_raw Markdown格式的文本 $table->renameColumn('content', 'content_raw'); // 在content字段后添加content_html 使用 Markdown 编辑内容但同时保存 HTML 版本 $table->text('content_html')->after('content'); // 在content_html字段后添加page_image 文章使用到缩略图 $table->string('page_image')->after('content_html'); // 在page_image字段后添加meta_description 文章说明 $table->string('meta_description')->after('page_image'); // 在meta_description字段后添加is_draft 是否是草稿 $table->boolean('is_draft')->after('meta_description'); // 在is_draft字段后添加layout 并设置默认值 使用的布局 $table->string('layout')->after('is_draft')->default('blog.layouts.post'); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::table('posts', function (Blueprint $table) { $table->dropColumn('subtitle'); $table->dropColumn('content_html'); $table->dropColumn('page_image'); $table->dropColumn('meta_description'); $table->dropColumn('is_draft'); $table->dropColumn('layout'); $table->renameColumn('content_raw', 'content'); }); } }
1.3 运行迁移
运行命令后看眼数据库是否已经修改成功:
php artisan migrate
2 和Tag关联
文章和标签是多对多的关系,所以先创建迁移文件,然后记得运行迁移:
class CreatePostTagPivot extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create('post_tag_pivot', function (Blueprint $table) { $table->increments('id'); $table->integer('tag_id')->unsigned()->index(); $table->integer('post_id')->unsigned()->index(); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::drop('post_tag_pivot'); } }
2.1 编辑Tag模型
class Tag extends Model { protected $fillable = ['tag', 'title', 'subtitle', 'page_image', 'meta_description', 'layout', 'reverse_direction']; // 定义关系 public function posts() { return $this->belongsToMany(Post::class, 'post_tag_pivot'); } /** * 批量创建需要的tag * * @param array $tags */ public static function addNeededTags(array $tags) { if (count($tags) === 0){ return; } // 通过tag字段在$tags数组中查找,把找到的模型通过lists来获取所有的tag字段 $found = static::WhereIn('tag', $tags)->lists('tag')->all(); foreach (array_diff($tags, $found) as $tag) { // 把不存在的tag进行创建 其他字段先自动填充 static::create([ 'tag' => $tag, 'title' => $tag, 'subtitle' => 'Subtitle for '.$tag, 'page_image' => '', 'meta_description' => '', 'reverse_direction' => false, ]); } } }
3 展示文章
我们一步步的来,首先先来展示我们的文章吧。
3.1 修改PostController的index方法
public function index() { return view('admin.post.index')->withPosts(Post::all()); }
3.2 修改post/index.blade.php
@extends('admin.layout') @section('content') <div class="container-fluid"> <div class="row page-title-row"> <div class="col-md-6"> <h3>Post <small> >> Listing</small></h3> </div> <div class="col-md-6 text-right"> <a href="/admin/post/create" class="btn btn-success btn-md"> <i class="fa fa-plus-circle"></i> New Post </a> </div> </div> <div class="row"> <div class="col-sm-12"> @include('admin.partials.error') @include('admin.partials.success') <table id="posts-table" class="table table-bordered table-striped"> <thead> <tr> <td>Published</td> <td>Title</td> <td>Subtitle</td> <td data-sortable="false">Published</td> </tr> </thead> <tbody> @foreach($posts as $post) <tr> <td data-order="{{ $post->published_at->timestamp }}"> {{ $post->published_at->format('j-M-y g:ia') }} </td> <td>{{ $post->title }}</td> <td>{{ $post->subtitle }}</td> <td> <a href="/admin/post/{{ $post->id }}/edit" class="btn btn-xs btn-info"> <i class="fa fa-edit"></i> Edit </a> <a href="/blog/{{ $post->slug }}" class="btn btn-xs btn-warning"> <i class="fa fa-eye"></i> View </a> </td> </tr> @endforeach </tbody> </table> </div> </div> </div> @endsection @section('scripts') <script> $(function () { $("#posts-table").DataTable({ order: [[0, "desc"]] }); }); </script> @endsection
4 创建文章
4.1 编辑create方法
public function create() { $data = $this->dispatch(new PostFormFields()); return view('admin.post.create', $data); }
在上面的代码中我们使用到了一个任务,这个任务就是返回每个字段的默认值
4.2 创建Job
创建create方法中用到的job:
php artisan make:job PostFormFields
在app/Jobs中找到刚刚创建的job编辑如下:
class PostFormFields extends Job implements SelfHandling { protected $id; protected $fieldList = [ 'title' => '', 'subtitle' => '', 'page_image' => '', 'content' => '', 'meta_description' => '', 'is_draft' => "0", 'publish_date' => '', 'publish_time' => '', 'layout' => 'blog.layouts.post', 'tags' => [], ]; /** * Create a new job instance. * * @return void */ public function __construct($id = null) { $this->id = $id; } /** * Execute the job. * * @return void */ public function handle() { $fields = $this->fieldList; if ($this->id){ $fields = $this->fieldsFromModel($this->id, $fields); } else { $when = Carbon::now()->addHour(); $fields['publish_date'] = $when->format('M-j-Y'); $fields['publish_time'] = $when->format('g:i A'); } foreach ($fields as $fieldName => $fieldValue) { $fields[$fieldName] = old($fieldName, $fieldValue); } return array_merge($fields, ['allTags' => Tag::lists('tag')->all()]); } /** * 取出模型中的数据 * * @param $id * @param array $fields * @return array */ protected function fieldsFromModel($id, array $fields) { $post = Post::findOrFail($id); $fieldNames = array_keys(array_except($fields, ['tags'])); $fields = ['id' => id]; foreach ($fieldNames as $field) { $fields[$field] = $post->$field; } $fields['tags'] = $post->tags()->lists('tag')->all(); return $fields; } }
4.3 添加方法到Post模型
在job中我们使用了publishe_date和time,我们来实现这些get:
/** * 设置ContentRaw的同时设置ContentHTML。 * * @param $value */ public function setContentRawAttribute($value) { $this->attributes['content_raw'] = $value; $this->attributes['content_html'] = Markdown::convertToHtml($value); } /** * 使用content快捷的返回content_raw * * @param $value * @return mixed */ public function getContentAttribute($value) { return $this->content_raw; } /** * 快捷返回publish_time * * @param $value * @return mixed */ public function getPublishTimeAttribute($value) { return $this->published_at->format('g:i A'); } /** * 快捷返回publish_date * * @param $value * @return mixed */ public function getPublishDateAttribute($value) { return $this->published_at->format('M-j-Y'); }
4.4 下载需要用到的两个前端资源
我们在create视图中需要用到两个前端资源:Selectize.js(下拉列表功能)和Pickadate.js(日期插件),我们来使用Bower下载:
bower install selectize --save
bower install pickadate --save
使用Gulp来整理前端资源:
var gulp = require('gulp'); var rename = require('gulp-rename'); var elixir = require('laravel-elixir'); /* |-------------------------------------------------------------------------- | Elixir Asset Management |-------------------------------------------------------------------------- | | Elixir provides a clean, fluent API for defining some basic Gulp tasks | for your Laravel application. By default, we are compiling the Less | file for our application, as well as publishing vendor resources. | */ /** * 拷贝操作 */ gulp.task("copyfiles", function(){ // js gulp.src("vendor/bower_dl/jquery/dist/jquery.js") .pipe(gulp.dest("resources/assets/js/")); // bootstrap gulp.src("vendor/bower_dl/bootstrap/less/**") .pipe(gulp.dest("resources/assets/less/bootstrap")); gulp.src("vendor/bower_dl/bootstrap/dist/js/bootstrap.js") .pipe(gulp.dest("resources/assets/js/")); // font 不用编译和合并 直接复制到public就可以 gulp.src("vendor/bower_dl/bootstrap/fonts/**") .pipe(gulp.dest("public/assets/fonts")); // awesome gulp.src("vendor/bower_dl/font-awesome/less/**") .pipe(gulp.dest("resources/assets/less/fontawesome")); gulp.src("vendor/bower_dl/font-awesome/fonts/**") .pipe(gulp.dest("public/assets/fonts")); // 拷贝 datatables var dtDir = 'vendor/bower_dl/datatables.net-plugins/integration/'; gulp.src("vendor/bower_dl/datatables/media/js/jquery.dataTables.js") .pipe(gulp.dest('resources/assets/js/')); gulp.src(dtDir + 'bootstrap/3/dataTables.bootstrap.css') .pipe(rename('dataTables.bootstrap.less')) .pipe(gulp.dest('resources/assets/less/others/')); gulp.src(dtDir + 'bootstrap/3/dataTables.bootstrap.js') .pipe(gulp.dest('resources/assets/js/')); // 拷贝selectize gulp.src("vendor/bower_dl/selectize/dist/css/**") .pipe(gulp.dest("public/assets/selectize/css")); gulp.src("vendor/bower_dl/selectize/dist/js/standalone/selectize.min.js") .pipe(gulp.dest("public/assets/selectize/")); // 拷贝 pickadate gulp.src("vendor/bower_dl/pickadate/lib/compressed/themes/**") .pipe(gulp.dest("public/assets/pickadate/themes/")); gulp.src("vendor/bower_dl/pickadate/lib/compressed/picker.js") .pipe(gulp.dest("public/assets/pickadate/")); gulp.src("vendor/bower_dl/pickadate/lib/compressed/picker.date.js") .pipe(gulp.dest("public/assets/pickadate/")); gulp.src("vendor/bower_dl/pickadate/lib/compressed/picker.time.js") .pipe(gulp.dest("public/assets/pickadate/")); }); elixir(function(mix) { // 合并脚本文件 mix.scripts([ 'js/jquery.js', 'js/bootstrap.js', 'js/jquery.dataTables.js', 'js/dataTables.bootstrap.js' ], 'public/assets/js/admin.js', 'resources/assets' ); // 编译 Less mix.less('admin.less', 'public/assets/css/admin.css'); });
运行Gulp:
gulp copyfiles
gulp
4.5 创建create视图
@extends("admin.layout") {{-- 样式表 --}} @section('styles') <link rel="stylesheet" href="/assets/selectize/css/selectize.css"> <link rel="stylesheet" href="/assets/selectize/css/selectize.bootstrap3.css"> <link rel="stylesheet" href="/assets/pickadate/themes/default.css"> <link rel="stylesheet" href="/assets/pickadate/themes/default.date.css"> <link rel="stylesheet" href="/assets/pickadate/themes/default.time.css"> @endsection {{--content--}} @section("content") <div class="container-fluid"> <div class="row page-title-row"> <div class="col-md-12"> <h3>Posts <small> >> Add New Post</small></h3> </div> </div> <div class="row"> <div class="col-sm-12"> <div class="panel panel-default"> <div class="panel-heading"> <h3 class="panel-title">New Post Form</h3> </div> <div class="panel-body"> @include('admin.partials.error') <form action="{{ route("admin.post.store") }}" method="POST" class="form-horizontal"> <input type="hidden" name="_token" value="{{ csrf_token() }}"> @include('admin.post._form') <div class="col-md-8"> <div class="form-group"> <div class="col-md-10 col-md-offset-2"> <button class="btn btn-primary btn-lg" type="submit"> <i class="fa fa-disk-o"></i> Save New Post </button> </div> </div> </div> </form> </div> </div> </div> </div> </div> @endsection {{--scripts--}} @section('scripts') <script src="/assets/pickadate/picker.js"></script> <script src="/assets/pickadate/picker.date.js"></script> <script src="/assets/pickadate/picker.time.js"></script> <script src="/assets/selectize/selectize.min.js"></script> <script> $(function() { $("#publish_date").pickadate({ format: "mmm-d-yyyy" }); $("#publish_time").pickatime({ format: "h:i A" }); $("#tags").selectize({ create: true }); }); </script> @endsection
创建我们在上面用到的_form.blade.php:
<div class="row"> <div class="col-md-8"> <div class="form-group"> <label for="title" class="col-md-2 control-label"> Title </label> <div class="col-md-10"> <input type="text" class="form-control" name="title" autofocus id="title" value="{{ $title }}"> </div> </div> <div class="form-group"> <label for="subtitle" class="col-md-2 control-label"> Subtitle </label> <div class="col-md-10"> <input type="text" class="form-control" name="subtitle" id="subtitle" value="{{ $subtitle }}"> </div> </div> <div class="form-group"> <label for="page_image" class="col-md-2 control-label"> Page Image </label> <div class="col-md-10"> <div class="row"> <div class="col-md-8"> <input type="text" class="form-control" name="page_image" id="page_image" onchange="handle_image_change()" alt="Image thumbnail" value="{{ $page_image }}"> </div> <script> function handle_image_change() { $("#page-image-preview").attr("src", function () { var value = $("#page_image").val(); if ( ! value) { value = {!! json_encode(config('blog.page_image')) !!}; if (value == null) { value = ''; } } if (value.substr(0, 4) != 'http' && value.substr(0, 1) != '/') { value = {!! json_encode(config('blog.uploads.webpath')) !!} + '/' + value; } return value; }); } </script> <div class="visible-sm space-10"></div> <div class="col-md-4 text-right"> <img src="{{ page_image($page_image) }}" class="img img_responsive" id="page-image-preview" style="max-height:40px"> </div> </div> </div> </div> <div class="form-group"> <label for="content" class="col-md-2 control-label"> Content </label> <div class="col-md-10"> <textarea class="form-control" name="content" rows="14" id="content">{{ $content }}</textarea> </div> </div> </div> <div class="col-md-4"> <div class="form-group"> <label for="publish_date" class="col-md-3 control-label"> Pub Date </label> <div class="col-md-8"> <input class="form-control" name="publish_date" id="publish_date" type="text" value="{{ $publish_date }}"> </div> </div> <div class="form-group"> <label for="publish_time" class="col-md-3 control-label"> Pub Time </label> <div class="col-md-8"> <input class="form-control" name="publish_time" id="publish_time" type="text" value="{{ $publish_time }}"> </div> </div> <div class="form-group"> <div class="col-md-8 col-md-offset-3"> <div class="checkbox"> <label> <input {{ checked($is_draft) }} type="checkbox" name="is_draft"> Draft? </label> </div> </div> </div> <div class="form-group"> <label for="tags" class="col-md-3 control-label"> Tags </label> <div class="col-md-8"> <select name="tags[]" id="tags" class="form-control" multiple> @foreach ($allTags as $tag) <option @if (in_array($tag, $tags)) selected @endif value="{{ $tag }}"> {{ $tag }} </option> @endforeach </select> </div> </div> <div class="form-group"> <label for="layout" class="col-md-3 control-label"> Layout </label> <div class="col-md-8"> <input type="text" class="form-control" name="layout" id="layout" value="{{ $layout }}"> </div> </div> <div class="form-group"> <label for="meta_description" class="col-md-3 control-label"> Meta </label> <div class="col-md-8"> <textarea class="form-control" name="meta_description" id="meta_description" rows="6">{{ $meta_description }}</textarea> </div> </div> </div> </div>
上面使用到了一个帮助函数,我们在helper.php中添加这个方法:
/** * 如果传进来的参数是true 则返回checked,false放回空字符串。 * * @param $value * @return string */ function checked($value) { return $value ? 'checked' : ''; } /** * Return img url for headers */ function page_image($value = null) { if (empty($value)) { $value = config('blog.page_image'); } if (! starts_with($value, 'http') && $value[0] !== '/') { $value = config('blog.uploads.webpath') . '/' . $value; } return $value; }
4.6 创建表单请求类
class PostCreateRequest 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', 'content' => 'required', 'publish_date' => 'required', 'publish_time' => 'required', 'layout' => 'required', ]; } /** * 返回创建模型所需要用的数据。 * * @return array */ public function postFillData() { $published_at = new Carbon( $this->publish_date.' '.$this->publish_time ); return [ 'title' => $this->title, 'subtitle' => $this->subtitle, 'page_image' => $this->page_image, 'content_raw' => $this->get('content'), 'meta_description' => $this->meta_description, 'is_draft' => (bool)$this->is_draft, 'published_at' => $published_at, 'layout' => $this->layout, ]; } }
4.7 实现store方法
public function store(RequestsPostCreateRequest $request) { $post = Post::create($request->postFillData()); $post->syncTags($request->get('tags', [])); return redirect() ->route('admin.post.index') ->withSuccess('New Post Successfully Created.'); }
4.8 实现syncTags方法
在store方法中使用了Post模型的syncTags方法 用于同步标签,在Post模型中创建这个方法:
public function syncTags(array $tags) { Tag::addNeededTags($tags); if (count($tags)){ $this->tags()->sync(Tag::whereIn('tag', $tags)->lists('id')->all()); return; } $this->tags()->detach(); }
4.9 实现store方法
现在我们可以实现PostController上的store方法了:
public function store(RequestsPostCreateRequest $request) { $post = Post::create($request->postFillData()); $post->syncTags($request->get('tags', [])); return redirect() ->route('admin.post.index') ->withSuccess('New Post Successfully Created.'); }
5 更新Post
5.1 创建edit视图
@extends('admin.layout') @section('styles') <link href="/assets/pickadate/themes/default.css" rel="stylesheet"> <link href="/assets/pickadate/themes/default.date.css" rel="stylesheet"> <link href="/assets/pickadate/themes/default.time.css" rel="stylesheet"> <link href="/assets/selectize/css/selectize.css" rel="stylesheet"> <link href="/assets/selectize/css/selectize.bootstrap3.css" rel="stylesheet"> @stop @section('content') <div class="container-fluid"> <div class="row page-title-row"> <div class="col-md-12"> <h3>Posts <small>» Edit Post</small></h3> </div> </div> <div class="row"> <div class="col-sm-12"> <div class="panel panel-default"> <div class="panel-heading"> <h3 class="panel-title">Post Edit Form</h3> </div> <div class="panel-body"> @include('admin.partials.error') @include('admin.partials.success') <form class="form-horizontal" role="form" method="POST" action="{{ route('admin.post.update', $id) }}"> <input type="hidden" name="_token" value="{{ csrf_token() }}"> <input type="hidden" name="_method" value="PUT"> @include('admin.post._form') <div class="col-md-8"> <div class="form-group"> <div class="col-md-10 col-md-offset-2"> <button type="submit" class="btn btn-primary btn-lg" name="action" value="continue"> <i class="fa fa-floppy-o"></i> Save - Continue </button> <button type="submit" class="btn btn-success btn-lg" name="action" value="finished"> <i class="fa fa-floppy-o"></i> Save - Finished </button> <button type="button" class="btn btn-danger btn-lg" data-toggle="modal" data-target="#modal-delete"> <i class="fa fa-times-circle"></i> Delete </button> </div> </div> </div> </form> </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 post? </p> </div> <div class="modal-footer"> <form method="POST" action="{{ route('admin.post.destroy', $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> </div> @stop @section('scripts') <script src="/assets/pickadate/picker.js"></script> <script src="/assets/pickadate/picker.date.js"></script> <script src="/assets/pickadate/picker.time.js"></script> <script src="/assets/selectize/selectize.min.js"></script> <script> $(function() { $("#publish_date").pickadate({ format: "mmm-d-yyyy" }); $("#publish_time").pickatime({ format: "h:i A" }); $("#tags").selectize({ create: true }); }); </script> @stop
5.2 生成edit方法
我们创建好edit视图后要生成对应的方法:PostController下的edit方法。
public function edit($id) { $data = $this->dispatch(new PostFormFields($id)); return view('admin.post.edit', $data); }
5.3 创建修改用的Request
php artisan make:request PostUpdateRequest
我们的修改用的Request和创建用的Request很相像,所以直接继承就好:
class PostUpdateRequest extends PostCreateRequest { }
5.4 编辑update方法
public function update(RequestsPostUpdateRequest $request, $id) { $data = $request->postFillData(); $post = Post::findOrFail($id); $post->fill($data); $post->save(); $post->syncTags($request->get('tags', [])); if ($request->get('action') === 'continue'){ return redirect()->back()->withSuccess('Post saved.'); } return redirect()->route('admin.post.index')->withSuccess('Post saved.'); }
6 删除Post
修改destroy方法:
public function destroy($id) { $post = Post::findOrFail($id); // 删除中间表的数据。 $post->tags()->detach(); $post->delete(); return redirect() ->route('admin.post.index') ->withSuccess('Post deleted.'); }