BLOGブログ

サマーノート(Symfony5でsummernoteを使う)

「WebアプリにWYSIWYGエディタを組み込みたいなー。」と思った場合、皆さんは何を使っていますか?
「WYSIWYGエディタ」とは、ブログ感覚で、入力ができるエディタの総称で、WordPressなどでお馴染みですよね。

WYSIWYGプロジェクトは数多くあり、どれを使うか迷うところですよね。

  • 「このエディタは、多機能だけど、ライセンスの問題があるなぁ。」
  • 「手軽に組み込めそうだが、ファイルアップロードとの連携が大変そうだなぁ」など

今回は、そんな中で、目つけた「summernote(サマーノート)」をご紹介したいと思います。

summernote(サマーノート)

https://summernote.org/
summernote(サマーノート)の良いところは次のとおりです。

  1. 「MITライセンス」で商用利用しやすい。
  2. 画像ファイルをWYSIWYがBase64エンコードしてくれるので、画像のアップロード処理の実装が不要。
  3. 「Bootstrap」上で使える(Bootstrapのバージョンは、4でも3でもOK)。
  4. GitHubスター数も多く、メンテナスもされている。

「summernote」が必要とする、依存ファイルは「Bootstrap」と「jquery」ですが、これら依存ファイルも含め「Symfony5」の「Encore(Webpack)」を使って「yarn」経由で設置をしてみます。過去に「Encore(Webpack)」と「yarn」についての記事を書いているので、こちらも目を通していただけると幸いです。

Yarn(や~ん!)【nvm、node、npm、yarnについて】

https://www.maruzen-p.jp/blog/700/

Symfony5でWebpackを使う

https://www.maruzen-p.jp/blog/656/

クライアントサイドがメインになりますので、サーバサイド(コントローラとかエンティティ)の話は簡単に流したいと思います。まずは、最低限必要な、Entity、FormType、Controller、Viewを用意します。 DBは用意されている前提です。
では、始めましょう、オーケイ、レッツ、ビギン!

エンティティとテーブルの用意

まず、Blogの記事を格納するための、「Blog」エンティティをSymfonyの「make」コマンドを使って、対話的に作っていきます。

$ bin/console make:entity

「Blog」エンティティが持つフィールドは「タイトル(title)」と「本文(body)」のみにします。

 New property name (press <return> to stop adding fields):
 > title

 Field type (enter ? to see all types) [string]:
 >

 Field length [255]:
 >

 Can this field be null in the database (nullable) (yes/no) [no]:
 > yes

 updated: src/Entity/Blog.php

 Add another property? Enter the property name (or press <return> to stop adding fields):
 > body

 Field type (enter ? to see all types) [string]:
 > text

 Can this field be null in the database (nullable) (yes/no) [no]:
 > yes

 updated: src/Entity/Blog.php

これで、「Blog」エンティティができました。 このまま、「Blog」エンティティと紐づくテーブルを「maker」コマンドでDB上に作成します。

マイグレーションを生成

$ bin/console make:migration

マイグレーションを実行してテーブルが作成されます。

$ bin/console doctrine:migrations:migrate

これで、必要なテーブルができました。

mysql> desc blog;
+-------+--------------+------+-----+---------+----------------+
| Field | Type         | Null | Key | Default | Extra          |
+-------+--------------+------+-----+---------+----------------+
| id    | int(11)      | NO   | PRI | NULL    | auto_increment |
| title | varchar(255) | YES  |     | NULL    |                |
| body  | longtext     | YES  |     | NULL    |                |
+-------+--------------+------+-----+---------+----------------+
3 rows in set (0.00 sec)

フォームタイプを作成

次にフォーム要素を作るためのフォームタイプを用意します。 これも、「maker」コマンドを使って、「Blogエンティティ」と紐づけする形で生成します。

$ bin/console make:form

 The name of the form class (e.g. TinyKangarooType):
 > BlogType

 The name of Entity or fully qualified model class name that the new form will be bound to (empty for none):
 > Blog

 created: src/Form/BlogType.php

生成されたフォームタイプ「BlogType」に保存ボタンを加えて、次のようしました。

src/Form/BlogType.php

<?php

namespace App\Form;

use App\Entity\Blog;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

class BlogType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('title')
            ->add('body')
            ->add('save', SubmitType::class,[
                'label' => '保存',
            ])
        ;
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults([
            'data_class' => Blog::class,
        ]);
    }
}

コントローラーとテンプレートの用意

コントローラと、テンプレートも「maker」コマンドを使って生成します。

$ bin/console make:controller

 Choose a name for your controller class (e.g. FiercePizzaController):
 > BlogController

 created: src/Controller/BlogController.php
 created: templates/blog/index.html.twig

コントローラーでは、フォームを表示し、POSTされた場合は、そのPOSTをDBに保存するようにします。

src/Controller/BlogController.php

<?php

namespace App\Controller;

use App\Entity\Blog;
use App\Form\BlogType;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

class BlogController extends AbstractController
{
    /**
     * @Route("/blog", name="blog")
     */
    public function index(Request $request): Response
    {
        $entityManager = $this->getDoctrine()->getManager();
        $form = $this->createForm(BlogType::class);

        $form->handleRequest($request);
        if($form->isSubmitted()){
            $blog = $form->getData();
            $entityManager->persist($blog);
            $entityManager->flush();
        }
        return $this->render('blog/index.html.twig', [
            'controller_name' => 'ブログ記事の入力フォーム',
            'form' => $form->createView(),
        ]);
    }
}

ビューは渡されたフォームオブジェクトをそのまま表示しているだけです。

templates/blog/index.html.twig

{% extends 'base.html.twig' %}

{% block title %}Hello BlogController!{% endblock %}

{% block body %}
<style>
    .example-wrapper { margin: 1em auto; max-width: 800px; width: 95%; font: 18px/1.5 sans-serif; }
    .example-wrapper code { background: #F5F5F5; padding: 2px 6px; }
</style>

<div class="example-wrapper">
    <h1>{{ controller_name }}! ✅</h1>
    {{ form(form) }}
</div>
{% endblock %}

ここで一旦コミット

サーバサイドの処理はこれでよいので、ここで一旦コミットします。

$ git status
On branch master
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        new file:   migrations/Version20201130120559.php
        new file:   src/Controller/BlogController.php
        new file:   src/Entity/Blog.php
        new file:   src/Form/BlogType.php
        new file:   src/Repository/BlogRepository.php
        new file:   templates/blog/index.html.twig

git commit -m "サーバサイドの処理はこれでOK"
[master 485d137] サーバサイドの処理はこれでOK
 6 files changed, 217 insertions(+)
 create mode 100644 migrations/Version20201130120559.php
 create mode 100644 src/Controller/BlogController.php
 create mode 100644 src/Entity/Blog.php
 create mode 100644 src/Form/BlogType.php
 create mode 100644 src/Repository/BlogRepository.php
 create mode 100644 templates/blog/index.html.twig

ブラウザでアクセス

「http://localhost/blog」にアクセスをするとフォームが表示されます。 「maker」コマンドで自動生成したコードを利用しているので、見栄えが悪く、本文を入力するためのWYSIWYGエディタもありません。 このあとBootstrapとsummernoteでWYSIWYGエディタを入れて見た目を整えていきます。

summernote、Bootstrapを「Webpack Encore」で追加

さて、ここからが本題です。「Webpack Encore」を使ってyarn経由でsummernote、Bootstrapを追加します。依存ファイルは「jquery」と「popper.js」になります。

「Webpack Encore」を使えるようにします。

Composerで「Webpack Encore」をインストール

$ composer require symfony/webpack-encore-bundle

yarnをインストール(グローバルとローカルの双方に)

$ npm install yarn
$ npm install -g yarn

yarnで「symfony/webpack-encore」の依存ファイルを追加

$ yarn install

コミット

$ git status
On branch master
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        modified:   .gitignore
        new file:   assets/app.js
        new file:   assets/styles/app.css
        modified:   composer.json
        modified:   composer.lock
        modified:   config/bundles.php
        new file:   config/packages/assets.yaml
        new file:   config/packages/prod/webpack_encore.yaml
        new file:   config/packages/test/webpack_encore.yaml
        new file:   config/packages/webpack_encore.yaml
        new file:   package-lock.json
        new file:   package.json
        modified:   symfony.lock
        new file:   webpack.config.js
        new file:   yarn.lock

$ git commit -m "webpack-encore-bundleを追加"

Bootstrapとjqueryの設定

「summernote」が依存する「jquery」「popper.js」「bootstrap」を先に追加しておきます。

$ yarn add jquery popper.js bootstrap --dev

フォームでbootstrap4テーマを使うように設定

config/packages/twig.yaml

twig:
    default_path: '%kernel.project_dir%/templates'
+   form_themes: ['bootstrap_4_layout.html.twig']

ベーステンプレートで「webpack-encore」が生成するエントリーポイントを読み込むようにします。

templates/base.html.twig

<!DOCTYPE html>
<html>
     <head>
         <meta charset="UTF-8">
         <title>{% block title %}Welcome!{% endblock %}</title>
-        {% block stylesheets %}{% endblock %}
+        {% block stylesheets %}
+            {{ encore_entry_link_tags('app') }}
+        {% endblock %}
     </head>
     <body>
         {% block body %}{% endblock %}
-        {% block javascripts %}{% endblock %}
+        {% block javascripts %}
+            {{ encore_entry_script_tags('app') }}
+        {% endblock %}
     </body>
</html>

BootstrapのCSSを読み込み

assets/styles/app.css

+ @import "~bootstrap/dist/css/bootstrap.min.css";
  body {
      background-color: lightgray;
  }

BootstrapのJavaScriptと、jqueryを読み込み

assets/app.js

+ const $ = require('jquery');
+ require('bootstrap');
  // any CSS you import will output into a single css file (app.css in this case)
  import './styles/app.css';

  // Need jQuery? Install it with "yarn add jquery", then uncomment to import it.
  // import $ from 'jquery';

+ $(document).ready(function (){
+     console.log("jquery is now available!");
+ });

コンパイルをしてエントリーポイントの生成

$ yarn encore dev

ブラウザでアクセス

「http://localhost/blog」にアクセスをするとフォームにBootstrapが適用されています。この後は、summernoteを適用していきます。

summernoteを利用

yarnでsummernoteを追加

$ yarn add summernote

summernoteのCSSを読み込み

assets/styles/app.css

@import "~bootstrap/dist/css/bootstrap.min.css";
+ @import "~summernote/dist/summernote-bs4.min.css";
body {
    background-color: lightgray;
}

summernoteのJSを読み込み、テキストエリアで利用するように指定。

assets/app.js

const $ = require('jquery');
require('bootstrap');
+ require('summernote');
// any CSS you import will output into a single css file (app.css in this case)
import './styles/app.css';

// Need jQuery? Install it with "yarn add jquery", then uncomment to import it.
// import $ from 'jquery';

$(document).ready(function (){
    console.log("jquery is now available!");
+   $("textarea").summernote();
});

再度コンパイル

$ yarn encore dev

ブラウザでアクセス

「http://localhost/blog」にアクセスをするとテキストエリアにWYSIWYGが適用されているのが確認できます。

画像をアップロードすると、

Base64に変換されます

これをそのままDBに保存をすれば、画像アップロードが簡単に実現できます。

mysql> select * from blog \G;
*************************** 1. row ***************************
   id: 1
title: テスト記事
 body: <p><img src="data:image/jpeg;base64,/9j/4QAYRXhpZgAASUkqAAgAAAAAAAAAAAAAAP/sABFEdWNreQABAAQAAAA8AAD/4QMvaHR0cDovL25zLmFkb2JlLmNvbS....

感想

サマーノートはとても手軽に実装ができます。ただ、画像を多用するような記事の場合、動作がのっそりしてしまいます。また、PDFファイルなど画像以外のファイルをアップロードができません。長所と短所を理解した上で使うには良さそうです。今日はサマーノートを試してみましたが、後日、他のWYSIWYGエディタも試してみたいと思います。

この記事を書いたブログライター:ヒロシ

ヒロシです。
丸善印刷でWebのお仕事をしています。
趣味は英会話です!

この記事を書いたブログライター:ヒロシ

ヒロシです。
丸善印刷でWebのお仕事をしています。
趣味は英会話です!