九保すこひ@フリーランスエンジニア|累計300万PVのブログ運営中
さてさて、「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
はなかなか使い勝手が良かったので、ぜひ皆さんも使ってみてくださいね。
ではでは〜❗
「明晰夢が見れるよう、トレーニング中。
ゼンゼンできない(笑)」