一步一步用laravel开发回风博客系统-08-文章的增删改查

原作者网站:http://laravelcoding.com/blog?tag=L5+Beauty
可以参考中文站点:http://laravelacademy.org/resources/blog

我是基于5.2的,而且有些东西我觉得有必要有的没必要,所以思路是跟着以上两个参考着搞,具体还是有区别的,我最终代码放在了Github:https://github.com/wedojava/hfblog.dev

由于正在补英语,所以有些我能看懂的就没从原作者那里翻译。

上节,我们已经实现了后台标签的增删改查,本节我们实现文章的增删改查。由于本人最近学英语,所以我觉得好理解的就直接上英文原文了。

修改相关模型

tags 和 posts 表之间的关系是多对多的,我们现在编辑 Tag 模型和 Post 模型。

app\Tag.php

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Tag extends Model
{

protected $fillable = [
'tag',
'title',
'subtitle',
'page_image',
'meta_description',
'reverse_directions',
];

/**
* 定义文章与标签的多对多关系
* @return BelongsToMany
*/

public function posts()
{

return $this->belongsToMany('App\Post', 'post_tag_pivot');
}

/**
* Add any tags needed from the list in view.
* @param array $tags List of tags to check/add
*/

public static function addNeededTags(array $tags)
{

if (count($tags) === 0) {
return;
}
//
// $storedTags = 找到tags表里`tag`值是数组$tags的值的行,取出tag列的值作为数组返回。
// 如果 tags 表的 `tag` 列里有 `$tags` 这个数组里的值,则用 `lists()`
// 方法将查询结果生成到名为 `tag` 的数组,`->all()` 获取全部数据。
// 5.2里,已经不在推荐用lists,而更推荐用 `pluck()` 方法:
// https://laravel.com/docs/5.2/collections#method-pluck
// 5.1:
// $storedTags = static::whereIn('tag', $tags)->lists('tag')->all();
// 5.2:
$storedTags = static::whereIn('tag', $tags)->pluck('tag')->all();
// array array_diff ( array $array1 , array $array2 [, array $... ] )
// returns the values in array1 that are not present in any of the other arrays.
//
// 下面的循环筛选出数据库里没有的tags,逐个循环create到数据库里去
foreach (array_diff($tags, $storedTags) as $tag) {
static::create([
'tag' => $tag,
'title' => $tag,
'subtitle' => 'Subtitle for '.$tag,
'page_image' => '',
'meta_description' => '',
'reverse_direction' => false,
]);
}
}
}

  • $fillable

Here we set the name of the columns that can be filled with an array. The addNeededTags() method will use this.

  • posts()

The many-to-many relationship between posts and tags.

  • addNeededTags()

A static function to add tags that aren’t in the database already.

In the posts() method we’re only passing two arguments to belongsToMany().
The first argument is the name of the model class.
The second argument is the name of the table to use.
The next two arguments are the foreignKey and the otherKey, but since we’re using post_id and tag_id, the additional arguments to belongsToMany() can be omitted. (Laravel is smart enough to figure them out.)

Laravel 非常聪明,如果关系表的字段设置类似 Post 表的 id 对应于关系表的post_idTag表的 id 对于关系表的 tag_id,则外键和其他键值可以省略不写,laravel自会识别,当然,参数不仅名字要合适,位置也不能错:
第一个参数指的是 belongsToMany()方法的第一个参数是对面表(或模型)的名称,所以 Tag模型里的 belongsToMany()方法的第一参数当然应该是 App\Post
第二个参数是关系表的名称,描述和对面表(或模型)之间关系的表名,所以当然应该是 post_tag_pivot

Mass Assignment Protection(质量分配保护)
Laravel’s models are built with Mass Assignment Protection. This means the model’s create() method takes an array of column names and values, but only allows assignment of those columns that are in a white list. (The white list is the $fillable attribute).

别管这个功能的名字是多么的神奇高档,实际上就是为了大规模写入数据库更方便罢了。通过设定参数 $fillable$guarded 参数分别表示 这些字段可以放心填充这些字段不能自动填充 。和 save() 方法相比,更推荐这个方式,毕竟,如果未来的字段逐渐扩充的时候,这个方式更好扩展,当然了,字段少的情况下, save() 还是蛮方便的。另一方面,为了安全,防止恶意注入,务必设定好 $fillable$guarded 参数。

app\Post.php

<?php

namespace App;

use App\Services\Markdowner;
use Illuminate\Database\Eloquent\Model;

class Post extends Model
{

protected $dates = ['published_at'];

/**
* The many-to-many relationship between posts and tags.
* @return BelongsToMany
*/

public function tags()
{

return $this->belongsToMany('App\Tag', 'post_tag_pivot');
}

/**
* Set the HTML content automatically when the raw content is set
*
* @param string $value
*/

public function setContentRawAttribute($value)
{

$markdown = new Markdowner();
$this->attributes['content_raw'] = $value;
$this->attributes['content_html'] = $markdown->toHTML($value);
}

/**
* Sync tag relation adding new tags as needed
* @param array $tags
*/

public function syncTags(array $tags)
{

Tag::addNeededTags($tags);

if (count($tags)) {
// After method sync() is complete, only the IDs in the array $tags will exist
// in the intermediate table:post_tag_pivot
$this->tags()->sync(
Tag::whereIn('tag', $tags)->pluck('id')->all()
);
return;
}
$this->tags()->detach();
}
}

  • tags()

    • Similar to the posts() method in the Tag model, but here we’re going the other way.
    • 这个方法就好比Tag模型里的posts()方法,用于建立表之间的关系。
  • setContentRawAttribute()

    • Now when the content_raw value is set in the model, it will be automatically be converted to HTML and assigned to the content_html attribute.
    • 这个方法可是让 markdown 格式的 content_raw 值转换成 Html 格式并分配到 content_html 的属性。
  • syncTags()

    • Synchronizes the tags with the post.
    • 通过 post 来同步对应的tags,这部分是否是必须的,存在的理由是什么呢?

添加 Selectize.js 和 Pickadate.js

Let’s add two additional assets to our system. We’ll use bower to pull the resources in and gulp to put them where we want them.

Pulling them in with Bower

The first is Selectize.js. 它用来配置并实现标签到每个post的分配。
下面我们通过 bower 来 pull 到本地。

Adding Selectize.js with Bower:

bower install selectize --save

输出:

bower not-cached    git://github.com/brianreavis/selectize.js.git#*
bower resolve git://github.com/brianreavis/selectize.js.git#*
bower download https://github.com/brianreavis/selectize.js/archive/v0.12.1.tar.gz
bower extract selectize#* archive.tar.gz
bower resolved git://github.com/brianreavis/selectize.js.git#0.12.1
bower not-cached git://github.com/brianreavis/sifter.js.git#0.4.x
bower resolve git://github.com/brianreavis/sifter.js.git#0.4.x
bower not-cached git://github.com/brianreavis/microplugin.js.git#0.0.x
bower resolve git://github.com/brianreavis/microplugin.js.git#0.0.x
bower download https://github.com/brianreavis/microplugin.js/archive/v0.0.3.tar.gz
bower extract microplugin#0.0.x archive.tar.gz
bower resolved git://github.com/brianreavis/microplugin.js.git#0.0.3
bower download https://github.com/brianreavis/sifter.js/archive/v0.4.5.tar.gz
bower extract sifter#0.4.x archive.tar.gz
bower resolved git://github.com/brianreavis/sifter.js.git#0.4.5
bower install selectize#0.12.1
bower install microplugin#0.0.3
bower install sifter#0.4.5

selectize#0.12.1 vendor/bower_dl/selectize
├── jquery#2.1.4
├── microplugin#0.0.3
└── sifter#0.4.5

microplugin#0.0.3 vendor/bower_dl/microplugin

sifter#0.4.5 vendor/bower_dl/sifter

Then let’s pull in Pickadate.js.There’s quite a few libraries for picking dates and times available, but I wanted to use this one because it works slick on small devices. Follow the instructions below to pull in Pickadate.js.
这个是用来炫酷的方便的显示日期的,可以显示日期和时间,这类插件很多,Chuck Heintzelman 选择它是因为对于小型设备而言,它的工作很平滑。

Adding Pickadate.js with Bower

bower install pickadate --save

输出:

bower not-cached    git://github.com/amsul/pickadate.js.git#*
bower resolve git://github.com/amsul/pickadate.js.git#*
bower download https://github.com/amsul/pickadate.js/archive/3.5.6.tar.gz
bower extract pickadate#* archive.tar.gz
bower resolved git://github.com/amsul/pickadate.js.git#3.5.6
bower install pickadate#3.5.6

pickadate#3.5.6 vendor/bower_dl/pickadate
└── jquery#2.1.4

Managing them with Gulp

Now that these libraries are downloaded, update the gulpfile.js to match what’s below.

Updated gulpfile.js,Add sth in the task of copyfiles

// Copy 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/"));

// Copy 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/"));

This configuration is basically the same as before, but with the needed files from selectize and pickadate copied into the public asset directory.

Run gulp copyfiles to do the copying.

Creating the Request Classes

Just like we did in before with the Tags, we’ll use Request classes to validate the create and update Post request.

First, create the requests using artisan in the Homestead VM. This will create skeletons for each of these classes in the app/Http/Request directory.

Creating the Request Class Skeletons

php artisan make:request PostCreateRequest

php artisan make:request PostUpdateRequest

Update the newly created PostCreateRequest.php to match what’s below.

Content of PostCreateRequest.php

<?php

namespace App\Http\Requests;

use App\Http\Requests\Request;
use Carbon\Carbon;

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',
];
}

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,
];
}
}

This is a standard request with the authorize() and rules() methods, but we’ar also adding the postFillData() method to make it easy to pull the data from the request to fill a new Post model with.

And update the PostUpdateRequest.php to match the following:

Content of PostUpdateRequest.php

<?php
namespace App\Http\Requests;

class PostUpdateRequest extends PostCreateRequest
{

//
}

NOTE: We’re just inheriting the authorize() and rules() methods from the PostCreateRequest class.Yes, we could get by with a single class to handle both but I don’t know how things may change in the future and like the idea of separate classes.
注意: 我们继承了 PostCreateRequest 里的 authorize()rules() 方法,我们当然可以将这两个方法抽象到单独的方法中去实现,但是,我不知道未来是否会有什么变化,并且,也不喜欢单独出一个类来做这些事情。

Creating the PostFormFields Job

Let’s create a utility job we can call from the PostController. It’ll be called the PostFormFields job. This job will get excuted when want to get a list of all the fields to populate post form.
我们来创建一个可以在 PostController 里调用名为 PostFormFields 的 job,这个 job 将在我们获取表单内容并将之填充到 form 的时候起作用。

Laravel Job Classes are Useful
Whenever you want to encapsulate a bit of action into its own class, a job class is one way to go. You dispatch to the job and don’t have to worry about the details. You can even queue jobs to occur later.

Just like helpers.php contain one-off functions, I like to think of job classes as one-off action classes.

每当你想封装一些 action 到你的 class 里,用 job 是个办法。你可以调用 job 无需考虑具体的详细情况。你甚至可以为 job 安排队列。
就像 helpers.php 包含了一些一次性功能,我更喜欢理解 job 类为一次性功能类。

First create the job skeleton with artisan.

Createing PostFormFields Job Skeleton

php artisan make:job PostFormFields

This creates the file in app/Jobs.

Edit the PostFormFields job to match the following.

Content of PostFormFields job:

<?php

namespace App\Jobs;

use App\Jobs\Job;
use App\Post;
use App\Tag;
use Carbon\Carbon;
use Illuminate\Contracts\Bus\SelfHandling;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;

// implements 和 use 在作者源码里是不一样的,我试试看有什么区别
class PostFormFields extends Job implements SelfHandling, ShouldQueue
{

use InteractsWithQueue, SerializesModels;

/**
* The id(if any) of the Post row.
* @var integer
*/

protected $id;

/**
* List of fields and default value for each field
* @var array
*/

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 array of fieldnames => values
*/

public function handle()
{

$fields = $this->fieldList;

// 如果 id 不是 null,通过Post模型返回 id 是 $id 的字段内容的 array。
if ($this->id) {
$fields = $this->fieldsFromModel($this->id, $fields);
} else { // 否则设定时间字段内容,其他字段默认是空。
// Add an hour to the instance
$when = Carbon::now()->addHour();
$fields['publish_date'] = $when->format('M-j-Y');
$fields['publish_time'] = $when->format('g:i A');
}

// 遍历 fieldList 内容并设定对应的old的值。
foreach ($fields as $fieldName => $fieldValue) {
$fields[$fieldName] = old($fieldName, $fieldValue);
}

return array_merge(
$fields,
['allTags' => Tag::pluck('tag')->all()]
);
}

/**
* Return the field values from the model
* @param integer $id Post ID
* @param array $fields postModelFieldList
* @return array $fieldsList
*/

public 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()->pluck('tag')->all();

return $fields;
}
}

The point of this job is to return an array of fields and values to use to populate a form.

If a Post isn’t loaded(as will be the case in a create), then default values will be returned.If a Post is loaded(for update), then the values will be pulled from the database.当Post没有被加载(创建post时),默认值将被返回,当Post被加载时(更新时),values 将从数据库获取。

Also, there is two extra fields returned,tagsallTags.
同时,会返回两个额外的字段:tags和allTags。

  1. tags : an array of all the tags associated with the Post.
  2. allTags : an array of all tags on file.

Adding to helpers.php

We’ll need a couple one-off functions so edit the app/helpers.php file and add the two function below.

Additions to helpers.php

<?php
/**
* Return "checked" if true.
* @param string $value
* @return string
*/
function checked($value)
{
return $value ? 'checked' : '';
}

/**
* Return image url for headers.
* @param string $value
* @return string url
*/
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;
}

  • checked()
    • This helper function will be used in views to output the checked attribute in check boxes and redio buttons.
  • page_image()
    • This function returns the full path to an image in the uploadeded area using the value from the configuration.If a value isn’t specified then it pulls a default image from the blog config(which you’ll need to set up yourself in as ‘page_image’ if you wish to use.)

Updating the Post Model

You may have noticed how we’re breaking apart the publish_at into publish_date and publish_time.Let’s add a couple fields to the Post model to make this easy. Update app/Post.php as specified below.

Updates to Post Model

// Add the following near the top of the class, after $dates
protected $fillable = [
'title',
'subtitle',
'content_raw',
'page_image',
'meta_description',
'layout',
'is_draft',
'published_at',
];
// Add the following three methods
/**
* Return the date portion of published_at
*/
public function getPublishDateAttribute($value)
{
return $this->published_at->format('M-j-Y');
}

/**
* Return the time portion of publish_at
*/
public function getPublishTimeAttribute($value)
{
return $this->published_at->format('g:i A');
}

/**
* Alias for content_raw
*/
public function getContentAttribute($value)
{
return $this->content_raw;
}

The $fillable property will let us fill the data during creating.

We also added getContentAttribute() as an accessor that returns $this->content_raw.Now when you use $post->content it’ll execute this function.

Updating the Controller

Now we’ll update all needed functionality in the PostController class.(Remember the file for this class is in the app/Http/Controllers/Admin directory.)

Because of the Request classes and PostFormFields class create earlier, the size of the controller will stay relatively small.

Update the PostController class to match what’s blow.

Content of PostController.php

<?php

namespace App\Http\Controllers\Admin;

use App\Http\Controllers\Controller;
use App\Http\Requests;
use App\Http\Requests\PostCreateRequest;
use App\Http\Requests\PostUpdateRequest;
use App\Jobs\PostFormFields;
use App\Post;
use Illuminate\Http\Request;

class PostController extends Controller
{

/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/

public function index()
{

return view('admin.post.index')
->withPosts(Post::all());
}

/**
* Show the form for creating a new resource.
*
* @return \Illuminate\Http\Response
*/

public function create()
{

$data = $this->dispatch(new PostFormFields());
return view('admin.post.create', $data);
}

/**
* Store a newly created resource in storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/

public function store(PostCreateRequest $request)
{

$post = Post::create($request->postFillData());
$post->syncTags($request->get('tags', []));

return redirect()
->route('admin.post.index')
->withSuccess('New Post Successfully Created.');
}

/**
* Show the form for editing the specified resource.
*
* @param int $id
* @return \Illuminate\Http\Response
*/

public function edit($id)
{

$data = $this->dispatch(new PostFormFields($id));

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

/**
* Update the specified resource in storage.
*
* @param \Illuminate\Http\Request $request
* @param int $id
* @return \Illuminate\Http\Response
*/

public function update(PostUpdateRequest $request, $id)
{

$post = Post::findOrFail($id);
$post->fill($request->postFillData());
$post->save();
$post->syncTags($request->get('tags', []));

if ($request->action === 'continue') {
return redirect()
->back()
->withSuccess('Post saved.');
}

return redirect()
->route('admin.post.index')
->withSuccess('Post saved.');
}

/**
* Remove the specified resource from storage.
*
* @param int $id
* @return \Illuminate\Http\Response
*/

public function destroy($id)
{

$post = Post::findOrFail($id);
$post->tags()->detach();
$post->delete();

return redirect()
->route('admin.post.index')
->withSuccess('Post deleted.');
}
}

  • index()
    • Pass index the view $posts will all the posts and file and return it.
  • create()
    • Use the PostFormFields job to return all the field values.Return the create view with these values passed in.
  • store()
    • Create the post with the fillable data from the request.Attach any tags and return to the index route with a success message.
  • edit()
    • Use the PostFormFields job to return all the field values for the post being edited.Return the edit view with these values passed in.
  • update()
    • Load the post.Update all the fillable fields.Save any changes and keep the tags in sync. Then return either back to the edit form or to the index list with a success message.
  • destroy()
    • Load the post.Unlink any associated tags.Delete the post and return to the index route with a success message.

A pretty slim controller really.About the only thing left to do are the views.

The Post Views

Now we’ll create all the views the PostController referenced earlier.

First update the existing index.blade.php in the resources/views/admin/post directory to match what’s below.

Content of admin.post.index view

@extends('admin.layout')

@section('content')
<div class="container">
<div class="row page-title-row">
<div class="col-md-6">
<h3>Posts <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.errors')
@include('admin.partials.success')

<table id="posts-table" class="table table-striped table-bordered">
<thead>
<tr>
<th>Published</th>
<th>Title</th>
<th>Subtitle</th>
<th data-sortable="false">Actions</th>
</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->id }}"
class="btn btn-xs btn-warning">

<i class="fa fa-eye"></i> View
</a>
<a href="/admin/post/{{ $post->id }}" class="btn btn-xs btn-danger" data-method="delete" data-token="{{csrf_token()}}" data-confirm="Are you sure?">
<i class="fa fa-times-circle"></i> Del
</a>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>

</div>
@stop

@section('scripts')
<script src="/assets/js/laravelDeleteHandler.js"></script>
<script>
$(function() {
$("#posts-table").DataTable({
order: [[0, "desc"]]
});
});
</script>

@stop

这里我们需要注意,删除按钮是需要脚本支持的,这个脚本不是每一页都需要加载,所以应该在需要的时候在加载,修改之前的 glupfile.js ,注释掉这一行:

'js/laravelDeleteHandler.js'

然后重新执行下 gulp ,成功后,修改 /tag/index.blade.php ,类似/post/index.blade.php,修改最后的脚本段为:

@section('scripts')
<script src="/assets/js/laravelDeleteHandler.js"></script>
<script>
$(function() {
$("#posts-table").DataTable({
order: [[0, "desc"]]
});
});
</script>

@stop

This is a pretty simple view that sets up the table with all the posts and then initializes the table as a DataTable in the script section.

Next, create the view create.blade.php file in the resources/views/admin/post directory,with the following content.

@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">
<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.errors')

<form class="form-horizontal" role="form" method="POST"
action="
{{ route('admin.post.store') }}">
<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 type="submit" class="btn btn-primary btn-lg">
<i class="fa fa-disk-o"></i>
Save New Post
</button>
</div>
</div>
</div>

</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

Here we add in the Selectize and Pickadate libraries.You’ll also note that we’re referencing an admin.post._form partial which isn’t yet created.

Let’s create that partial. In the resources/views/admin/post directory create the _form.blade.php file with the following content.

Content of the admin.post._form partial

<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>

This is a partial because both the create and edit view will share it.

Create edit.blade.php in the same directory with the content below.

Content of the admin.post.edit view

@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">
<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.errors')
@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>

{{-- Confirm Delete --}}
<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

抽象脚本段

Something small but in my opinion fundamental about code repeat “Don’t repeat yourself” (DRY)
At resources/views/admin/post we see create.blade.php and edit.blade.php both have the exact same Javascript code at the end. If in the future we would need to change that JS code, we’ll need to change it for both files. To avoid this, we can create a partial file for just the JS code. So, we create a file under resources/views/admin/post with the name _js.blade.php with all the section scripts inside it:

@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 type="text/javascript">
$(function() {
$("#publish_date").pickadate({
format: "mmm-d-yyyy"
});
$("#publish_time").pickatime({
format: "h:i A"
});
$("#tags").selectize({
create: true
});
});
</script>

@stop

Then we include this partial at the end of both create and edit view files with:

@include('admin.post._js')

同理,css引用也可以这么做。

需要注意 helpers.php 文件的引用

And with that, all the views for the posts management are done.

NOTE: It’s all right,but a warning when create or edit page :

Call to undefined function page_image()

Yes, edit composer.json file, take autoload fragment to the below:

"autoload": {
"classmap": [
"database"
],
"files":[
"app/helpers.php"
],
"psr-4": {
"App\\": "app/"
}
},

Recap

This was a fairly long chapter but a huge amount was accomplished. We created a migration to modify the posts table and then updated the Tag and Post models. Then bower was used to pull in the Selectize.js and Pickadate.js libraries which, of course, we managed using gulp.

Request classes (to handle form input) were created. As was a Laravel Job class to return the post form data. Finally, the controller and views were wrapped up.

All this work resulted in a clean and easy way to administer posts.