WordPressプラグイン作成の勉強をしてみた

**wordpress初心者です。サンプルは調べながら作成しました、間違いなどありましたら指摘お願いします**:bow:

この記事の編集履歴

- [直接アクセスされたかどうかのチェックを追加](https://qiita.com/okumurakengo/items/d94e71fb85a55613c8da/revisions/3)
- [エスケープ処理を追加](https://qiita.com/okumurakengo/items/d94e71fb85a55613c8da/revisions/4)
- [CSRF対策をするように変更](https://qiita.com/okumurakengo/items/d94e71fb85a55613c8da/revisions/5)
- [アンインストールの処理追加](https://qiita.com/okumurakengo/items/d94e71fb85a55613c8da/revisions/6)

# 参考

- [0からわかるWordPressプラグイン開発（スッテプ・バイ・ステップ） - Qiita](https://qiita.com/TanakanoAnchan/items/f1191c4c008f0a2b7c2e)
- [WordPress Plugin Development - YouTube](https://www.youtube.com/playlist?list=PLz-tsD7hAvuuvi9K6ukLdaSp9-T2I7xeV)
- [WordPress で Ajaxを利用する | バシャログ。](http://bashalog.c-brains.jp/19/02/27-193024.php)
- [jsファイルでWordPressのテンプレートパスを使えるようにする | TECH | 孤独村](https://kodoku-village.com/tech/94)
- [plugins - why does the add_action(&#39;the_content) overwrite my page - WordPress Development Stack Exchange](https://wordpress.stackexchange.com/questions/247535/why-does-the-add-actionthe-content-overwrite-my-page)

# サンプル

[Server-Sent Events(SSE)](https://developer.mozilla.org/ja/docs/Web/API/Server-sent_events/Using_server-sent_events)を使用した簡易チャットプラグインを作成してみました

### 確認バージョン

- PHP 7.2
- wordpress 5.2.1
- Google Chrome 74.0.3729.169

【[ソースはこちらです](https://github.com/okumurakengo/simple-sse-chat)】

![EIrvCHs1Ic.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142910/dff64c7e-09ff-eb77-060a-21e234e657d3.gif)

# この記事の概要

wordpressの基本的な機能を勉強しながら、簡易チャットプラグインを作成していきます

---

**最終的なコードを見たい方は[こちら](https://github.com/okumurakengo/simple-sse-chat)を見て、本記事は読まなくて良いです。**

私が調べながら作っていった手順を一歩ずつ書いていく内容となっています。
**間違いやより良いやり方がある場合は教えてください。**

### 対象読者

- wordpressのplugin作成に壁を感じてる方 
    - 初心者の私でもやればできたというのが伝わればと、

- wordpress初心者 (wordpressを画面から操作したことがあり、PHPの基礎的な知識がある)
    - plugin作成はそこまで難しくなかったので、初心者がいきなりplugin作成してみる、もありだと思いました

# 1 プラグインをwordpressに認識させる

### 1-1 phpファイル作成

`{root}/wp-content/plugins/simple-sse-chat/simple-sse-chat.php`

`plugins`ディレクトリに作成したいプラグイン用のフォルダと、それと同じ名前のphpファイルを作成します

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142910/845c0b43-476e-3b82-cd72-c0053f0a59ec.png)

### 1-2 プラグイン名と説明を設定

```/wp-content/plugins/simple-sse-chat/simple-sse-chat.php
<?php
/*
    Plugin Name: Simple SSE Chat
    Description: Server Snet Eventsを使用した簡易チャット
*/

// ↓このファイルに直接アクセスされた場合のために必ず処理の先頭につけましょう
if ( ! defined( 'ABSPATH' ) ) {
   exit;
}
```

この状態で管理画面のプラグインのページをみると、プラグインを確認することができます

<img width="1091" alt="Screen Shot 2019-05-26 at 15.16.12.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142910/f0764b79-4d98-0c37-2755-ddb3cd5fbccb.png">

### 1-3 有効化する

`有効化`を押すと`/wp-content/plugins/simple-sse-chat/simple-sse-chat.php`で書いた内容が実行されることになります。
現時点ではまだ何もおきません。

![ZeOrdi3X3l.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142910/ccfad74e-87df-c8f1-c881-e60d8b6f0ea8.gif)

# 2 管理画面のメニューに追加

### 2-1 `add_action('admin_menu', $callback)`

`add_action`または、`add_filter`を使って、フックを設定します。
ここでは、管理メニューが読み込まれる前に実行される `admin_menu`を指定します。

フックについて参考：
[【WordPress入門】アクションフックとフィルターフックを使いこなそう | 滋賀／京都／大阪でホームページ制作ならYUKiYURi WEB](https://yukiyuriweb.com/introduction-to-wordpress-action-and-filter-hooks/)

```/wp-content/plugins/simple-sse-chat/simple-sse-chat.php
<?php

// ...

add_action('admin_menu', function () {
    add_menu_page(
        'Simple Chat Settings', // <title>タグの内容を設定
        'Simple Chat', // 左メニューに表示される名前を設定
        'manage_options', // 権限
        'simple-sse-chat', // スラッグ
        'admin_menu_simple_sse_chat', // メニューを開いたときに実行される関数名
        'dashicons-admin-comments', // アイコン
        200 // メニューの表示順、200と大きい数字にしたので、メニューの一番下に表示される
    );
});
function admin_menu_simple_sse_chat() {
    ?>
        <div class="wrap">
            <h2>Simple Chat Settings</h2>
        </div>
    <?php
}
```

<img width="963" alt="Screen Shot 2019-05-26 at 15.32.02.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142910/9f8d8117-4023-be04-eae1-870736735f43.png">

メニューに追加され、ページを表示することができました。

### 2-2 `get_option`、`update_option`で入力値を保存する

```/wp-content/plugins/simple-sse-chat/simple-sse-chat.php
<?php

// ...

add_action('admin_menu', function () {
    add_menu_page(
        'Simple Chat Settings', // <title>タグの内容を設定
        'Simple Chat', // 左メニューに表示される名前を設定
        'manage_options', // 権限
        'simple-sse-chat', // スラッグ
        'admin_menu_simple_sse_chat', // メニューを開いたときに実行される関数名
        'dashicons-admin-comments', // アイコン
        200 // メニューの表示順、200と大きい数字にしたので、メニューの一番下に表示
    );
});
function admin_menu_simple_sse_chat() {
    if ($_SERVER['REQUEST_METHOD'] === 'POST') {
        check_admin_referer('hoge-fuga-piyo'); // CSRF対策
        update_option('simple_sse_chat_header', $_POST['chat_heder']);
        ?>
        <div id="setting-error-settings-update" class="update-settings-error notice is-dismissible"><strong>Settings have been saved.</strong></div>
        <?php
    }
    $chat_heder = get_option('simple_sse_chat_header', 'Simple Chat'); // 第２引数は指定のoption_nameがない場合の初期値
    ?>
        <div class="wrap">
            <h2>Simple Chat Settings</h2>
            <form method="POST" action="">
                <?php wp_nonce_field( 'hoge-fuga-piyo'); ?>
                <label for="chat_heder">チャットタイトル</label>
                <textarea name="chat_heder" class="large-text"><?= esc_textarea($chat_heder) ?></textarea>
                <input type="submit" name="submit_scripts_update" class="button button-primary" value="UPDATE">
            </form>
        </div>
    <?php
}
```

![sGFRYQa8ze.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142910/eb8bb64d-53fc-2842-18df-eb969d5a4f2d.gif)

`wp_options`テーブルに入力値が保存されていることが確認できました。

![Screen Shot 2019-05-26 at 16 (2).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142910/4e7c1612-93b4-2cda-2239-121241e32443.png)

# 3 ショートコードでチャットのHTMLを表示する

### 3-1 まず、簡単なHTMLだけを表示

```/wp-content/plugins/simple-sse-chat/simple-sse-chat.php
<?php

// ...

add_shortcode('simple_sse_chat', function () {
    return '<span style="color:blue;">ショートコードのテスト</span>';
});
```

※wordpress 5からの投稿エディタのGutenberg（グーテンベルグ）でのショートコード設定方法↓

![lGhEyE8N28.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142910/0ce8e80b-3a5a-d71f-502c-518b408d8eaa.gif)

ショートコードがページに表示できたのを確認できました。

### 3-2 チャット用のHTMLを表示

HTMLを見やすくするためにバッファリングしてHTMLを表示するようにしてみました。
`get_option('simple_sse_chat_header', 'Simple Chat');`として管理画面から登録した値を出力します。

```/wp-content/plugins/simple-sse-chat/simple-sse-chat.php
<?php

// ...

add_shortcode('simple_sse_chat', function () {
    $header = get_option('simple_sse_chat_header', 'Simple Chat');
    ob_start(); // バッファリング開始
    ?>
    <form id="js-simple-sse-chat-form">
        <h2><?= esc_html($header) ?></h2>
        <div class="simple-sse-chat-container">
            <table id="js-simple-sse-chat-body">
                <tbody></tbody>
            </table>
        </div>
        <input type="text" name="chat-content" id="js-simple-sse-chat-input">
        <input type="submit" value="送信">
    </form>
    <style>
        .simple-sse-chat-container {
            height: 200px;
            overflow: scroll;
        }
    </style>
    <?php
    return ob_get_clean(); // バッファからデータを取得してクリア
});
```

<img width="907" alt="Screen Shot 2019-05-26 at 16.32.53.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142910/b6557f5f-97cc-378b-d573-83399f6f0ec7.png">

# 4 チャットデータ登録用のテーブルをDBに作成する

### 4-1 `register_activation_hook()`でプラグインがアクティブ化されたときに実行される関数を定義

```/wp-content/plugins/simple-sse-chat/simple-sse-chat.php
<?php

// ...

register_activation_hook(__FILE__, function () {
    // プラグインがアクティブ化されたときに実行される
});
```

### 4-2 `$wpdb`を使用してDBを操作する

```/wp-content/plugins/simple-sse-chat/simple-sse-chat.php
<?php

// ...

register_activation_hook(__FILE__, function () {
    global $wpdb;
    $sql = sprintf('CREATE TABLE %ssimple_sse_chat (
        `id` INT NOT NULL AUTO_INCREMENT,
        `user_id` INT NOT NULL,
        `content` TEXT NOT NULL,
        PRIMARY KEY (`id`)
    ) %s;', $wpdb->prefix, $wpdb->get_charset_collate());
    require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
    dbDelta($sql);
});
```

一度停止して、最有効化しても実行してくれます

![RPH5RSjPOU.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142910/c5d5c704-90f3-afd6-a44d-d884d8261c49.gif)

プラグイン有効化時にテーブルを作成してくれました。

![Screen Shot 2019-05-26 at 16.44.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142910/c9c902b5-9fd7-1527-bdb6-d39e0dfbfb35.png)

# 5 チャットの入力値を登録する

### 5-1 javascriptを読み込む

jsファイルを作成します。

```/wp-content/plugins/simple-sse-chat/script.js
console.log(simple_sse_chat_data) // <- PHPから渡される変数
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142910/3bbc44b1-c750-0d97-0286-b671eb3d0d1e.png)

今回はショートコードのHTMLをjavascriptで操作したいので、

1. `add_action('the_content', $callback)`で、投稿や固定ページの内容が表示されたときに、
1. `has_shortcode($content, 'simple_sse_chat')` で指定のショートコードが使われていた場合のみ、jsファイルを読み込みます。

```/wp-content/plugins/simple-sse-chat/simple-sse-chat.php
<?php

// ...

add_action('the_content', function ($content) {
    // ショートコードが使われているページのみjsを読み込む
    if (has_shortcode($content, 'simple_sse_chat')) {
        // script.jsにsimple_sse_chat_dataという名前のオブジュエクトを定義し、home_urlというプロパティを定義
        wp_enqueue_script('simple_sse_chat', plugin_dir_url(__FILE__) . 'script.js');
        wp_localize_script('simple_sse_chat', 'simple_sse_chat_data', [
            'home_url' => home_url(),
            'nonce' => wp_create_nonce('hoge-fuga-piyo'), // CSRF対策
        ]);
    }
    return $content;
});
```

指定のjavascriptが読み込まれ、PHPからjavascriptに`simple_sse_chat_data`という変数名で値を渡せていることが確認できました。

<img width="957" alt="Screen Shot 2019-05-28 at 1.41.34.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142910/33005391-c9ae-c53e-2b38-5c9d3064faf9.png">

### 5-2 `wp_ajax_{action_name}`でチャットの内容を登録する用のurlを設定

```/wp-content/plugins/simple-sse-chat/simple-sse-chat.php
<?php

// ...

add_action('wp_ajax_chat_post', function () {
    check_ajax_referer('hoge-fuga-piyo', 'security'); // CSRF対策

    global $wpdb;
    $wpdb->insert($wpdb->prefix.'simple_sse_chat', [
        'user_id' => get_current_user_id(),
        'content' => $_POST['chat-content'],
    ]);
});
```

↑のような書き方ですと`/wp-admin/admin-ajax.php?action=chat_post`でリクエストすると、コールバック関数を実行してくれます。

### 5-3 ajaxで登録処理をする

```/wp-content/plugins/simple-sse-chat/script.js
const { home_url, nonce } = simple_sse_chat_data;

// 登録
document.getElementById("js-simple-sse-chat-form").addEventListener("submit", async e => {
    e.preventDefault();
    formData = new FormData(e.target)

    // 入力値が空の場合は何もしない
    if (formData.get("chat-content") === "") return;

    formData.append('security', nonce);

    // 登録のリクエスト
    const res = await fetch(`${home_url}/wp-admin/admin-ajax.php?action=chat_post`, {
        method: "POST",
        body: formData,
    })
    
    // 入力欄を空にしからにしておく
    document.getElementById("js-simple-sse-chat-input").value = ""
});
```

![6i9KVEcoqd.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142910/504ffeb2-3038-9423-a592-bc6b84758f5d.gif)

チャットからの入力値をDBに保存することができました。

![Screen Shot 2019-05-26 at 17.57.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142910/34db26c0-9780-2b79-dc17-f3cf7168272c.png)

# 6 Server-Sent Eventsでチャットの内容を表示する

### 6-1 Server-Sent Eventsを実行するurlを定義する

```/wp-content/plugins/simple-sse-chat/simple-sse-chat.php
<?php

// ...

// ajaxのhookだが、SSEも問題なかったのでwp_ajax_{action_name}を使用しました
add_action('wp_ajax_event_streame', function () {
    if (!wp_verify_nonce($_GET['_wpnonce'], 'hoge-fuga-piyo')) {
      exit; // CSRF対策
    }

    global $wpdb;
    header('Content-Type: text/event-stream');
    header('Cache-Control: no-store');
    while(true) {
        printf("data: %s\n\n", json_encode([
            'chat_data' => $wpdb->get_results(
                "SELECT s.id, s.content, u.user_login
                 FROM {$wpdb->prefix}simple_sse_chat s
                 LEFT JOIN {$wpdb->prefix}users u ON s.user_id = u.id
                 ORDER BY id DESC
                 LIMIT 10"
            ),
        ]));
        ob_end_flush();
        flush();
        sleep(1);
    }
});
```

### 6-2 javascriptで表示処理をする

```/wp-content/plugins/simple-sse-chat/script.js
const { home_url, nonce } = simple_sse_chat_data;

// ...

const es = new EventSource(`${home_url}/wp-admin/admin-ajax.php?action=event_streame&_wpnonce=${nonce}`);

// 表示
let lastId = 0;
es.addEventListener("message", e => {
    const { chat_data } = JSON.parse(e.data);

    // 更新がなければ何もしない
    if (lastId === (chat_data[0] ? chat_data[0].id : 0)) return;

    // jsonを受け取ってHTMLにして表示
    targetElement = document.getElementById("js-simple-sse-chat-body").querySelector("tbody");
    targetElement.innerHTML = ""
    chat_data.forEach(data => {
        const { user_login, content } = data
        targetElement.insertAdjacentHTML("afterbegin", `
            <tr>
                <td>
                    <small>${user_login}</small>
                    <br>
                    <strong>${content}</strong>
                </td>
            </tr>
        `)
    })

    // 新着があったら下にスクロール
    const scrollElement = document.querySelector(".simple-sse-chat-container")
    scrollElement.scrollTop = scrollElement.scrollHeight

    lastId = (chat_data[0] ? chat_data[0].id : 0);
});
```

Server-Sent Eventsでチャットの内容を表示することができました。

![xMzAQP4EHQ.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142910/970f2893-b2ed-cf59-362d-6e718fb3ec75.gif)

# 7 プラグイン削除時に不要なデータを削除する

### 7-1 `register_uninstall_hook()`でプラグインが削除された場合に実行される関数を定義

今回の場合だと、`wp_simple_sse_chat`テーブルの削除、`wp_options`テーブルの`option_name`：`simple_sse_chat_header`の削除が必要です。

```/wp-content/plugins/simple-sse-chat/simple-sse-chat.php
<?php

// ...

// プラグインが削除されたときに実行される
function simple_sse_chat_uninstall () {
    // TODO マルチサイトの考慮をするとより良いものとなる
    //  see also https://github.com/okumurakengo/simple-sse-chat/pull/7

    global $wpdb;
    $wpdb->query("DROP TABLE IF EXISTS {$wpdb->prefix}simple_sse_chat"); // テーブル削除
    delete_option('simple_sse_chat_header'); // wp_optionsテーブルの値を削除
}
register_uninstall_hook(__FILE__, 'simple_sse_chat_uninstall');
```

管理画面から削除ボタンを押すと、こちらの関数が実行してくれます

![Screen Shot 2019-05-28 at 2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142910/226738c8-56d7-e0a3-12a4-89a81e7e01fe.png)


--- 

最後まで読んでいただいてありがとうございました。m(_ _)m