
九保すこひです(フリーランスのITコンサルタント、エンジニア)
さてさて、「PHPでサイトをつくる」といえば、近年ではLaravel
、そしてwordpress
がメインの選択肢になるのではないでしょうか。
(2020.12.26現在では)なんとウェブサイトの39%がwordpress
で構築されているということでPHP業界
だけでなく大きな影響を持っています。
そして、そんな人気からたまに直面する課題が・・・・・・
Laravelでwordpressみたいな「CMS」が使いたい
というものです。(CMS = コンテンツ・マネジメント・
以前このブログで、ログイン中のLaravelユーザーでwordpressへ自動ログインするという記事を公開しましたが、「正直言うとwordpressほど高機能でなくてもよくて、シンプルにいきたい!」という場面もあるかもしれません。
そこで
今回は、Laravel
に「シンプルなCMS」をつくる方法をご紹介します。
利用するのは、CKEditor 5
です。
CKEditor
は、簡単にいうと「画像やHTMLタグが使える入力ボックス」で、いわゆる「リッチ・テキストエディタ」とか「wysiwyg(うぃじうぃぐ)エディタ」と呼ばれるものです。
ぜひ皆さんのお役にたてると嬉しいです
(最後に、今回実際に開発したソースコード一式をダウンロードできますよ)
【追記:2021.01.14】コードが古いまま残っている部分がありましたので修正しました。ご不便をおかけしてすみません。m(_ _)m
「着るコタツがリコール回収されることに。
気に入ってたのに・・・」
開発環境: Laravel 8.x、Vue 3、axios 0.19、CKEditor 5
ルートをつくる
まずはルートをつくります。
以下の内容を追加してください。
routes/web.php
Route::prefix('post')->group(function(){
Route::get('/', [PostController::class, 'index']);
Route::get('/list', [PostController::class, 'list']);
Route::get('/{post}', [PostController::class, 'show']);
Route::post('/', [PostController::class, 'store']);
Route::put('/{post}', [PostController::class, 'update']);
Route::delete('/{post}', [PostController::class, 'destroy']);
});
なお、今回はテストのため省略していますが、2つ目のルート
show()
以外は管理者ログインせずに表示されるべきではありません。以下を参考にしてauth
ミドルウェアなどをつけて対応してください。
【Laravel 5.8 】「こんなとき」のミドルウェア全7実例
目次 [非表示]
コントローラーをつくる
続いてコントローラーです。
まず以下のコマンドを実行してください。
php artisan make:controller PostController
すると、ファイルが作成されるので、中身を以下のように変更してください。
app/Http/Controllers/PostController.php
<?php
namespace App\Http\Controllers;
use App\Models\Post;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
class PostController extends Controller
{
public function index() {
return view('post.index');
}
public function list() {
return Post::get();
}
public function show(Post $post) {
return view('post.show')->with([
'post' => $post
]);
}
public function store(Request $request) {
// バリデーションは省略してます
$post = new Post();
$post->title = $request->title;
$post->description = $request->description;
$result = $post->save();
return ['result' => $result];
}
public function update(Request $request, Post $post) {
// バリデーションは省略してます
$post->title = $request->title;
$post->description = $request->description;
$result = $post->save();
return ['result' => $result];
}
public function destroy(Post $post) {
return [
'result' => $post->delete()
];
}
}
今回、この中のindex()
とshow()
が直接ブラウザでアクセスされるメソッドで、それ以外は全てAjax
でのアクセスを想定しています。
モデル&マイグレーションをつくる
次に、モデルとマイグレーションをつくります。
以下のコマンドを実行してください。
php artisan make:model Post -m
すると、モデルとマイグレーションのファイルが作成されるので、マイグレーションの中身を以下のように変更します。
database/migrations/****_**_**_******_create_posts_table.php
// 省略
public function up()
{
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->string('title')->comment('タイトル');
$table->text('description')->comment('内容');
$table->timestamps();
});
}
// 省略
なお、記事の「表示 or 非表示」機能をつけたい場合は、wordpress
のようにstatus
のような項目を追加しておくといいでしょう。
テストデータをつくる
では、作業がしやすいようにテストデータをつくります。
以下のコマンドを実行してください。
php artisan make:seed PostsTableSeeder
すると、ファイルが作成されるので中身を以下のように変更してください。
<?php
namespace Database\Seeders;
use App\Models\Post;
use Illuminate\Database\Seeder;
class PostsTableSeeder extends Seeder
{
// 省略
public function run()
{
for($i = 0; $i < 25 ; $i++) {
$post = new Post();
$post->title = 'テストタイトル - '. $i;
$post->description = "テスト内容\nテスト内容\nテスト内容\nテスト内容 - ". $i;
$post->save();
}
}
}
続いて、このテストデータが有効になるように、Laravel
側へ登録します。
database/seeders/DatabaseSeeder.php
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
class DatabaseSeeder extends Seeder
{
/**
* Seed the application's database.
*
* @return void
*/
public function run()
{
// User::factory(10)->create();
$this->call(PostsTableSeeder::class); //
ここを追加しました
}
}
では、この状態でマイグレーションを実行します。
以下のコマンドを実行してください。
php artisan migrate:fresh --seed
実行が完了するとデータベースは以下のようになります。
ビューをつくる
では、ブラウザで実際に表示される「ビュー」を作っていきましょう。
今回は以下の2つを用意します。
- 記事を管理するページ
- 投稿した個別記事を表示するページ
まずは、記事を管理するページです。
resources/views/post/index.blade.php
<html>
<head>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css">
</head>
<body>
<div id="app" class="container p-3">
<h1 class="mb-4">シンプルなCMSのサンプル</h1>
<!-- 一覧表示部分 -->
<div v-if="isStatusIndex">
<div class="text-right pb-4">
<button type="button" class="btn btn-success" @click="changeStatus('create')">追加</button>
</div>
<table class="table">
<tr v-for="post in posts">
<td v-text="post.title"></td>
<td class="text-right">
<a :href="'/post/'+ post.id" class="btn btn-light mr-2" target="_blank">確認</a>
<button type="button" class="btn btn-warning mr-2" @click="setCurrentPost(post)">変更</button>
<button type="button" class="btn btn-danger" @click="onDelete(post)">削除</button>
</td>
</tr>
</table>
</div>
<!-- エディタ表示部分 -->
<div v-if="isStatusCreate || isStatusEdit">
<input class="form-control mb-3" type="text" placeholder="タイトル" v-model="postTitle">
<!-- ここにリッチテキスト・エディタが表示されます -->
<div id="editor"></div>
<div class="text-right pt-4">
<button type="button" class="btn btn-secondary mr-2" @click="changeStatus('index')">キャンセル</button>
<button type="button" class="btn btn-primary" @click="onSave">保存する</button>
</div>
</div>
</div>
<script src="https://unpkg.com/vue@3.0.2/dist/vue.global.prod.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.19.2/axios.min.js"></script>
<script src="https://cdn.ckeditor.com/ckeditor5/24.0.0/classic/ckeditor.js"></script>
<script>
Vue.createApp({
data() {
return {
status: 'index', //
ここの内容で表示切り替え
posts: [],
currentPost: {},
postTitle: '', // タイトル
richEditor: null // CKEditorのインスタンス
}
},
methods: {
initRichEditor(defaultDescription) {
const target = document.querySelector('#editor');
ClassicEditor.create(target)
.then(editor => {
this.postTitle = this.currentPost.title || '';
this.richEditor = editor;
this.richEditor.setData(defaultDescription);
});
},
getPosts() {
const url = '/post/list';
axios.get(url)
.then(response => {
this.posts = response.data;
});
},
setCurrentPost(post) {
this.currentPost = post;
this.status = 'edit';
},
changeStatus(status) {
this.status = status;
},
onSave() {
if(confirm('保存します。よろしいですか?')) {
let url = '';
let method = '';
if(this.isStatusCreate) {
url = '/post';
method = 'POST';
} else if(this.isStatusEdit) {
url = `/post/${this.currentPost.id}`;
method = 'PUT';
}
const params = {
_method: method,
title: this.postTitle,
description: this.richEditor.getData()
};
axios.post(url, params)
.then(response => {
if(response.data.result === true) {
this.getPosts();
this.changeStatus('index');
}
})
.catch(error => {
console.log(error); // エラーの場合
});
}
},
onDelete(post) {
if(confirm('削除します。よろしいですか?')) {
const url = `/post/${post.id}`;
axios.delete(url)
.then(response => {
if(response.data.result === true) {
this.getPosts();
}
});
}
}
},
computed: {
isStatusIndex() {
return (this.status === 'index');
},
isStatusCreate() {
return (this.status === 'create');
},
isStatusEdit() {
return (this.status === 'edit');
}
},
watch: {
status(value) {
if(value === 'create') {
this.currentPost = {};
}
const editorKeys = ['create', 'edit'];
const defaultDescription = (value === 'edit') ? this.currentPost.description : '';
if(editorKeys.includes(value)) { //
`create` か `edit` の場合だけ CKEditor を起動
Vue.nextTick(() => {
this.initRichEditor(defaultDescription);
});
}
}
},
setup() {
return {
richEditor: Vue.reactive({}) //
reactive変数をつくる
}
},
mounted() {
this.getPosts();
}
}).mount('#app');
</script>
</body>
</html>
この中で重要なのが、CKEditor
を起動している部分です。
CKEditor
を起動したら以下のようにVue
の変数に格納していますが、これは通常の変数ではありません。
this.richEditor = editor;
これは、setup()
内でセットしたリアクティブな変数です。
setup() {
return {
richEditor: Vue.reactive({}) //
これです
}
}
また、変数status
を変更することでVue
が表示切り替えをしていることにも注目してください。
切り替え内容は次のとおりです。
- index: 一覧表示
- create: タイトル、CKEditor を表示(中身は空)
- edit: タイトル、CKEditor を表示(中身は該当データ)
次に、個別詳細ページのビューです。
resources/views/post/show.blade.php
<html>
<head>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css">
</head>
<body>
<div id="app" class="container p-3">
<h1 class="mb-4">{{ $post->title }}</h1>
{!! $post->description !!}
</div>
</body>
</html>
こちらは、$post->description
の部分がエスケープ処理をしない、{!! ... !!}
になっていることに注意してください。
なぜなら、CKEditor
で登録された内容はHTMLタグ
が含まれているからです。
おまけ:画像のアップロード機能をつける
CKEditor
で画像アップロード機能を使えるようにするためには、有料のプラグインを使うか、独自にアダプターをつくる必要があります。
もちろん有料プラグインは多機能でいいのですが、今回は「シンプルなCMS」がテーマなので独自アダプターで実装してみましょう。
専用のアダプター・クラスをつくる
まず、JavaScript
のクラスをつくります。(これがアダプターになります)
resources/views/post/index.blade.php
<!-- 省略 -->
<script>
class UploadAdapter {
constructor(loader) {
this.loader = loader;
}
upload() {
return this.loader.file
.then(file => {
return new Promise((resolve, reject) => {
const url = '/post/upload_image';
let formData = new FormData();
formData.append('image', file);
axios.post(url, formData)
.then(response => {
if(response.data.result === true) {
const imageUrl = response.data.image_url;
resolve({ default: imageUrl });
} else {
reject();
}
})
.catch(error => {
reject();
});
});
});
}
}
// 省略
中身としては、以下のことを実行しています。
- 画像が選択されたら、axios でファイルを Laravel へ送信
- Laravel で画像を保存
- 返された画像URLが <img> タグに使われるように resolve() にセット
そして、このアダプターをVue
内につくったinitRichEditor()
でセットします。
// 省略
initRichEditor(defaultDescription) {
const target = document.querySelector('#editor');
ClassicEditor.create(target)
.then(editor => {
this.postTitle = this.currentPost.title || '';
this.richEditor = editor;
//
ここでアダプターをセットします
this.richEditor.plugins
.get('FileRepository')
.createUploadAdapter = loader => {
return new UploadAdapter(loader);
};
this.richEditor.setData(defaultDescription);
});
}
ファイルアップロード部分をつくる
画像のアップロード機能をつくります。
まず、アップロード用のルートを追加してください。
routes/web.php
Route::prefix('post')->group(function(){
// 省略
Route::post('/', [PostController::class, 'store']);
Route::post('/upload_image', [PostController::class, 'upload_image']); //
ここを追加しました
// 省略
});
そして、コントローラーにupload_image()
メソッドを追加します。
app/Http/Controllers/PostController.php
<?php
// 省略
class PostController extends Controller
{
// 省略
public function upload_image(Request $request) {
// バリデーションは省略してます
$file = $request->file('image');
$extension = $file->extension();
$path = 'public/cms_images';
$name = date('Ymd-His') .'_'. Str::random(5) .'.'. $extension;
$request->file('image')->storeAs($path, $name);
return [
'result' => true,
'image_url' => url('/storage/cms_images/'. $name)
];
}
}
最後に忘れてはいけないのが、以下のコマンドです。
php artisan storage:link
このコマンドを実行することで、/public
フォルダ内に/storage/app/public
へのシンボリックリンクが貼られることになり、ブラウザから直接アクセスができるようになります。
※なお、もし画像の管理をしたいということであればcms_images
のようなDBテーブルを用意し、そこにパスなどを保存しておくといいでしょう。
テストしてみる
では、実際にテストしてみましょう
まずは、「http://******/post」にブラウザでアクセスします。
テストデータが一覧表示されるので、まずは、新規追加です。
「追加」ボタンをクリックしてみましょう。
すると・・・・・・
タイトルとCKEditor
が表示されました。
では、以下のように入力して保存してみましょう
すると・・・・・・
画面が切り替わり、データが追加されました。
では、「確認」ボタンをクリックしてみましょう。
はい
先ほど投稿した内容が表示されています。
成功です
では、次に「変更」ボタンをクリックします。
すると、初期状態でタイトル、CKEditor
にテキストが表示されています。
これを、変更して保存して確認してみると・・・・・・
はい
こちらもうまく変更できました。
成功です
ダウンロードする
以下から、今回実際に開発したソースコード一式をダウンロードできます。
【Laravel】シンプルにCMSを実装する※ ただしマイグレーションなどはご自身で実行してください。
CKEditor 5 を使うときの注意点
CKEditor 5
のライセンスは「GPL 2+」です。
MITライセンスと違い、CKEditor
を含む「派生アプリケーション」を配布する場合、ソースコードの開示が義務付けられています。
そのため、CKEditorを含むアプリケーションを配布する場合は注意が必要です。
おわりに
ということで、今回はLaravel
+ CKEditor 5
でシンプルなCMS
を作ってみました。
wordpress
ほど高機能じゃなくていいけど、ちょっとしたことは自由に投稿できるようにしたい、という場合にはちょうどいいんじゃないでしょうか。
なお、DBテーブルのフィールド名なのですが、当初はwordpress
を参考にしてdescription
ではなく、content
としていました。(実際の wordpress
は post_content
です)
しかし、途中でIDEにエラーが表示されることになったので、content
という名前は変更することにしました。(エラー内容を読む限り、すでに別の変数としてModel
が使っているようです)
まだまだ知らないことは多いですね
とにもかくにも、CKEditor
はなかなか使い勝手が良かったので、ぜひ皆さんも使ってみてくださいね。
ではでは〜
「明晰夢が見れるよう、トレーニング中。
ゼンゼンできない(笑)」