Laravel5.6 博客搭建系列三--博客内容增删改查

08 May 2018 Category: PHP

本篇内容分享创建后台博客内容增删改查操作。

创建markdown转html Service

要想实现markdown 到html的转换,需要安装两个依赖库:

composer require michelf/php-markdown
composer require michelf/php-smartypants

在app下创建Services目录,存放相应的服务类文件。在app/Services创建Markdowner.php,实习markdown转html,文件内容如下:

<?php

namespace App\Services;

use Michelf\MarkdownExtra;
use Michelf\SmartyPants;

class Markdowner
{

    public function toHTML($text)
    {
        $text = $this->preTransformText($text);
        $text = MarkdownExtra::defaultTransform($text);
        $text = SmartyPants::defaultTransform($text);
        $text = $this->postTransformText($text);
        return $text;
    }

    protected function preTransformText($text)
    {
        return $text;
    }

    protected function postTransformText($text)
    {
        return $text;
    }
}

修改表结构

composer require "doctrine/dbal"
php artisan make:migration --table=posts restructure_posts_table


<?php

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

class RestructurePostsTable extends Migration
{
 /**
     * Run the migrations.
     */
    public function up()
    {
        Schema::table('posts', function (Blueprint $table) {
            $table->string('subtitle')->after('title'); 
            $table->renameColumn('content', 'content_raw'); 
            $table->text('content_html')->after('content');
            $table->string('meta_description')->after('content_html'); 
            $table->boolean('is_draft')->after('meta_description'); 
            $table->string('layout')->after('is_draft')->default('blog.layouts.post'); 
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down()
    {
        Schema::table('posts', function (Blueprint $table) {
            $table->dropColumn('layout');
            $table->dropColumn('is_draft');
            $table->dropColumn('meta_description');
            $table->dropColumn('content_html');
            $table->renameColumn('content_raw', 'content');
            $table->dropColumn('subtitle');
        });
    }
}

表字段说明:

-	subtitle:文章副标题
-	content_raw:Markdown格式文本
-	content_html:使用 Markdown 编辑内容但同时保存 HTML 版本
-	meta_description:文章备注说明
-	is_draft:该文章是否是草稿
-	layout:使用的布局
php artisan migrate

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;
use App\Services\Markdowner;
class Post extends Model
{
    protected $dates = ['published_at'];
    protected $fillable = [
        'title', 'subtitle', 'content_raw', 'meta_description','layout', 'is_draft', 'published_at'
    ];
    /**
     * Set the title attribute and automatically the slug
     *
     * @param string $value
     */
    public function setTitleAttribute($value)
    {
        $this->attributes['title'] = $value;

        if (! $this->exists) {
            $this->setUniqueSlug($value, '');
        }
    }

    /**
     * Recursive routine to set a unique slug
     *
     * @param string $title
     * @param mixed $extra
     */
    protected function setUniqueSlug($title, $extra)
    {
        $slug = str_slug($title.'-'.$extra);

        if (static::whereSlug($slug)->exists()) {
            $this->setUniqueSlug($title, $extra + 1);
            return;
        }

        $this->attributes['slug'] = $slug;
    }

    /**
     * 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);
    }

    public function getContentAttribute($value)
    {
        return $this->content_raw;
    }
}

创建路由

对后台路由进行修改

Route::namespace('Admin')->middleware(['auth'])->group(function () {
    Route::resource('admin/default', 'DefaultController');
    Route::resource('admin/post', 'PostController');
});

后台视图模板resources/admin/layouts/main.blade.php中设置文章练tab链接

<a class="nav-link" href=""></a>

修改控制器

使用表单请求类来验证文件创建及更新请求。首先,使用 Artisan 命令创建表单请求处理类,对应文件会生成在 app/Http/Requests 目录下:

php artisan make:request PostCreateRequest 
php artisan make:request PostUpdateRequest

编辑新创建的 PostCreateRequest.php 内容如下:

<?php

namespace App\Http\Requests;

use Carbon\Carbon;

class PostCreateRequest extends Request
{
    /**
     * Determine if the user is authorized to make this request.
     */
    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 the fields and values to create a new post from
     */
    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,
        ];
    }
}

PostUpdateRequest与PostCreateRequest类似,因此只需继承PostCreateRequest

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class PostUpdateRequest extends PostCreateRequest
{

}

创建一个公用的、可以从 PostController 中调用的任务类,我们将其称之为PostFormFields。该任务会在我们想要获取文章所有字段填充文章表单时被执行。

首先使用 Artisan 命令创建任务类模板:

php artisan make:job PostFormFields

创建的任务类位于 app/Jobs 目录下。编辑新生成的 PostFormFields.php 文件内容如下:

<?php

namespace App\Jobs;

use Illuminate\Bus\Queueable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use App\Post;
class PostFormFields implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    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 $fields;
    }

    protected function fieldsFromModel($id, array $fields)
    {
        $post = Post::findOrFail($id);

        $fieldNames = array_keys($fields);

        $fields = ['id' => $id];
        foreach ($fieldNames as $field) {
            $fields[$field] = $post->{$field};
        }

        return $fields;
    }
}

该任务最终返回文章字段和值的键值对数组,我们将使用其返回结果用来填充文章编辑表单。如果 Post 模型未被加载(比如创建文章时),那么就会返回默认值。如果 Post 被成功加载(文章更新),那么就会从数据库获取值。

<?php

namespace App\Http\Controllers\Admin;

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

class PostController extends Controller
{
	public function index()
	{
        $posts = Post::where('published_at', '<=', Carbon::now())
                ->orderBy('published_at', 'desc')
                ->paginate(config('blog.posts_per_page'));
        return view('admin.post.index', compact('posts'));
	}
    /**
     * Show the new post form
     */
    public function create()
    {
        $data = $this->dispatch(new PostFormFields());

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

    /**
     * Store a newly created Post
     *
     * @param PostCreateRequest $request
     */
    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 post edit form
     *
     * @param int $id
     * @return Response
     */
    public function edit($id)
    {
        $data = $this->dispatch(new PostFormFields($id));

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

    /**
     * Update the Post
     *
     * @param PostUpdateRequest $request
     * @param int $id
     */
    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 Response
     */
    public function destroy($id)
    {
        $post = Post::findOrFail($id);
        $post->tags()->detach();
        $post->delete();

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

创建视图文件

error.blade.php文件内容如下:

@if (count($errors) > 0)
    <div class="alert alert-danger">
        <strong>Whoops!</strong>
        There were some problems with your input.<br><br>
        <ul>
        @foreach ($errors->all() as $error)
            <li></li>
        @endforeach
        </ul>
    </div>
@endif

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>
        
    </div>
@endif

在 resources/views/admin/post 目录下创建 index.blade.php:

@extends('admin.layouts.main')
@section('content')
<div class="container">
    <div class="row page-title-row">
        <div class="col-md-6">
            
        </div>
        <div class="col-md-6 text-right">
            <a href="" class="btn btn-success btn-md">
                <i class="fa fa-plus-circle"></i> New Post
            </a>
        </div>
    </div>
    @include('admin.partials.error')
    @include('admin.partials.success')
    <div class="row">
        <div class="col-sm-12">
            <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="">
                        
                    </td>
                    <td></td>
                    <td></td>
                    <td>
                        <a href="/admin/post//edit" class="btn btn-xs btn-info">
                            <i class="fa fa-edit"></i> Edit
                        </a>
                        <a href="/blog/" class="btn btn-xs btn-warning">
                            <i class="fa fa-eye"></i> View
                        </a>
                    </td>
                </tr>
            @endforeach
            </tbody>
            </table>
        </div>
        
    </div>
</div>
@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="">
            </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="">
            </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"><h4 id="创建用户认证系统">创建用户认证系统</h4>

<p>本篇文章跟大家分享搭建后台管理认证系统以及创建后台视图模板</p>

<p>Laravel 中实现登录认证非常简单。实际上,几乎所有东西 Laravel 都已经为你配置好了。配置文件位于 config/auth.php,其中包含了用于调整认证服务行为的、文档友好的选项配置。</p>

<p>执行<code class="highlighter-rouge">php artisan make:auth</code><code class="highlighter-rouge">php artisan migrate</code> 创建控制器以及需要的数据表。脚本会在目录app/Http/Auth 下创建一下几个文件:</p>

<ul>
  <li>
    <p>创建必须的控制器</p>

    <ul>
      <li>
        <p>LoginController 登录退出操作,继承App\Http\Controllers\Controller,所有的业务逻辑在<code class="highlighter-rouge">trait AuthenticatesUsers</code>中,可以通过设置属性<code class="highlighter-rouge">$redirectTo</code>改变登录之后的跳转地址,设置<code class="highlighter-rouge">$redirectAfterLogout</code>改变退出之后的跳转地址;</p>
      </li>
      <li>
        <p>RegisterController 提供用户注册相关操作,所有业务逻辑在<code class="highlighter-rouge">trait RegistersUsers</code></p>
      </li>
      <li>
        <p>ForgotPasswordController 忘记密码,发送验证邮件相关操作</p>
      </li>
      <li>
        <p>ResetPasswordController 重置密码相关操作</p>
      </li>
    </ul>
  </li>
  <li>
    <p>添加路由:</p>
  </li>
</ul>

<p>routes/web.php文件中添加用户认证相关路由</p>

<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Auth::routes();

</code></pre></div></div>

<ul>
  <li>
    <p>创建视图模板文件</p>

    <p>-layouts 文件夹,创建app.blade.php 作为整个应用的视图模板文件</p>

    <p>-auth 文件夹,分别创建登录,注册,找回密码等视图文件</p>
  </li>
</ul>

<h4 id="创建后台管理首页">创建后台管理首页</h4>

<p>执行以下命令:</p>

<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>php artisan make:controller Admin\\DefaultController

</code></pre></div></div>
<p>脚本会在app\Http\Controlles下创建admin目录,并创建DefaultController文件,修改DefaultController文件,添加后台显示文章列表操作,添加以下代码:</p>

<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    public function index()
    {
        return view('admin.default.index');
    }

</code></pre></div></div>

<ul>
  <li>创建视图文件</li>
</ul>

<p>在resources下创建admin/post目录,并在该目录下创建admin/post/index.blade.php文件,文件内容如下:</p>

<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>@extends('layouts.app')

@section('content')
&lt;div class="container"&gt;
    welcome to Post Admin
&lt;/div&gt;
@endsection


</code></pre></div></div>

<ul>
  <li>修改路由route/web.php,限制后台必须登录</li>
</ul>

<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
Route::get('admin', function () {
    return redirect('/admin/default');
});
Route::namespace('Admin')-&gt;middleware(['auth'])-&gt;group(function () {
    Route::resource('admin/default', 'DefaultController');
});

</code></pre></div></div>

<h4 id="创建后台模板">创建后台模板</h4>

<p>很多情况下前后台使用的模板不同,因此需要给后台自定义视图模板。复制一份resources/layouts/app.blade.php 到resources/admin/layouts/main.blade.php</p>

<p></p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;a</span> <span class="na">class=</span><span class="s">"navbar-brand"</span> <span class="na">href=</span><span class="s">""</span><span class="nt">&gt;</span>
    
<span class="nt">&lt;/a&gt;</span>

</code></pre></div></div>

<p>后面加入以下内容,给后台添加导航栏</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="nt">&lt;ul</span> <span class="na">class=</span><span class="s">"nav"</span><span class="nt">&gt;</span>
  <span class="nt">&lt;li</span> <span class="na">class=</span><span class="s">"nav-item"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;a</span> <span class="na">class=</span><span class="s">"nav-link active"</span> <span class="na">href=</span><span class="s">""</span><span class="nt">&gt;&lt;/a&gt;</span>
  <span class="nt">&lt;/li&gt;</span>
  <span class="nt">&lt;li</span> <span class="na">class=</span><span class="s">"nav-item"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;a</span> <span class="na">class=</span><span class="s">"nav-link"</span> <span class="na">href=</span><span class="s">"#"</span><span class="nt">&gt;&lt;/a&gt;</span>
  <span class="nt">&lt;/li&gt;</span>
  <span class="nt">&lt;li</span> <span class="na">class=</span><span class="s">"nav-item"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;a</span> <span class="na">class=</span><span class="s">"nav-link"</span> <span class="na">href=</span><span class="s">"#"</span><span class="nt">&gt;&lt;/a&gt;</span>
  <span class="nt">&lt;/li&gt;</span>
  <span class="nt">&lt;li</span> <span class="na">class=</span><span class="s">"nav-item"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;a</span> <span class="na">class=</span><span class="s">"nav-link "</span> <span class="na">href=</span><span class="s">"#"</span><span class="nt">&gt;&lt;/a&gt;</span>
  <span class="nt">&lt;/li&gt;</span>
<span class="nt">&lt;/ul&gt;</span>

</code></pre></div></div>

<p><code class="highlighter-rouge">&lt;/head&gt;</code>前添加@yield(‘styles’),在<code class="highlighter-rouge">&lt;/body&gt;</code>前添加@yield(‘scripts’),创建样式以及脚本模块,后续在视图文件中添加样式和js脚本</p>

<h4 id="效果">效果</h4>

<p>访问 <code class="highlighter-rouge">http://127.0.0.1:8000/admin/default</code> 可以看到以下内容</p>

<p><img src="http://blog.static.aiaiaini.com/blog2.png" alt="image" /></p>

<p>本教程代码<a href="http://blog.static.aiaiaini.com/blog2.zip">下载</a></p>

</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="">
            </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="">
            </div>
        </div>
        <div class="form-group">
            <div class="col-md-8 col-md-offset-3">
                <div class="checkbox">
                    <label>
                        <input  type="checkbox" name="is_draft">
                        Draft?
                    </label>
                 </div>
            </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="{"theme"=>{"name"=>"zichen"}}">
            </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"></textarea>
            </div>
        </div>
    </div>
</div>
@extends('admin.layouts.main')
@section('content')
<div class="container">
    <div class="row">
        <div class="col-sm-12">
            <div class="panel panel-default">
                <div class="panel-body">
                    @include('admin.partials.error')
                    @include('admin.partials.success')
                    <form class="form-horizontal" role="form" method="POST" action="">
                        <input type="hidden" name="_token" value="">
                        <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" name="action" value="continue">
                                        <i class="fa fa-floppy-o"></i>
                                            Save - Continue
                                    </button>
                                    <button type="submit" class="btn btn-success" name="action" value="finished">
                                        <i class="fa fa-floppy-o"></i>
                                            Save - Finished
                                    </button>
                                </div>
                            </div>
                        </div>
                    </form>
                </div>
            </div>
        </div>
    </div>
</div>
@stop

@extends('admin.layouts.main')
@section('content')
<div class="container">
    <div class="row">
        <div class="col-sm-12">
            <div class="panel panel-default">
                <div class="panel-body">
                    @include('admin.partials.error')
                    @include('admin.partials.success')
                    <form class="form-horizontal" role="form" method="POST" action="">
                        <input type="hidden" name="_token" value="">
                        @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

resource路由删除操作需要的请求方式是delete,form 没办法发起delete请求,Laravel 在表单中添加_method=”delete”模拟。 创建js文件,实现公共的删除操作,在resouces/assets/js/deletemoda.js,在js中,给每个页面填充一个隐藏表单,点击删除的时候根据删除内容发起删除请求。

效果如下: image

本教程代码下载