WordPress プラグイン開発の学習ノート

WordPress のマニュアルやユーザーのブログ記事はたくさんある一方で、フレームワークのような体系的なチュートリアルが意外とないので、これまで書きためたブログ記事を編集してまとめることにしました。

対象読者
-------

Ajax 通信による SPA (Single Page Application) の開発ができるようにすることを目標とします。WordPress 4.4 で REST API が導入されました。

多言語に対応させる
---------------

プラグインのシンプルな例として、管理画面にページを追加して、文字列を表示するものを試してみましょう。

```php
/**
 * @package My_Plugin
 * @version 0.1
 */
/*
Plugin Name: My Plugin
Plugin URI: http://blog.sarabande.jp/
Description: Hello World
Author: Masaki Kagaya
Version: 0.1
Author URI: http://blog.sarabande.jp/
*/

add_action( 'admin_menu', function() {
  add_options_page( '私のプラグイン', '私のプラグイン',
    'manage_options', get_slug(), 'my_options_page'
  );
  load_plugin_textdomain( get_slug(), false,
    dirname( plugin_basename( __FILE__ ) ) . '/languages'
  );
});

function get_slug()
{
  return 'my-plugin';
}

function my_options_page()
{
  _e( 'Hello!', get_slug() );
}
```

説明の翻訳文を追加する場合、`Text Domain` と `Domain Path` を追加します。

```bash
Description: My First Plugin
Text Domain: my-plugin
Domain Path: /languages
```

`makepot.php` を使って PHP のソースコードから po ファイルを生成します。

```php
cd plugins
svn co http://develop.svn.wordpress.org/trunk/tools/
cd my-plugin/languages
php ../../tools/makepot.php wp-plugin ../ my-plugin-ja.po 
```

生成された po ファイルに日本語訳を追加します。

```bash
"Content-Type: text/plain; charset=UTF-8\n"

msgid "Hello!"
msgstr "こんにちは"
```

今度は po から mo ファイルを生成します。これで日本語訳が表示されるようになります。

```bash
msgfmt my-plugin-ja.po -o my-plugin-ja.mo
```

JavaScript に翻訳文を渡したい場合、`wp_localize_script` を使います。

```php
$slug = 'my-plugin';
$script_name = 'my-plugin-script';
wp_register_script( $script_name, plugins_url( '/js/my-plugin.js', __FILE__ ) );
wp_localize_script( $script_name, 'object_name', ['msg' => __( 'こんにちは', $slug )] );
wp_enqueue_script( $script_name );

// console.log(object_name.msg);
```

`json_encode` を使って JavaScript のデータを生成することもできます。

```php
<?php
$slug = 'my-plugin';
$msg = __('こんにちは', $slug);
$options = JSON_HEX_QUOT|JSON_HEX_AMP|JSON_HEX_APOS|JSON_HEX_TAG;
?>
<script>
var a = <?= json_encode( $msg, $options ) ?>;
var b = <?= json_encode( ['msg' => $msg], $options ) ?>;

console.log(a);
console.log(b['msg']);
</script>
```

HTTP API
--------

### HTTP クライアントをつくる

WordPress を使って HTTP クライアントをつくってみましょう。最初に次のコードを読み込みます。

```php
define( 'WP_USE_THEMES', false );
require_once 'path/to/wp-blog-header.php';
```

GET リクエストを送信してみましょう。

```php
$ret = wp_remote_get( 'http://localhost:3000/server.php?foo=bar' );
$ret = wp_remote_retrieve_body( $ret );
var_dump( 'bar' === $ret );
```

次に POST リクエストを送信してみましょう。

```php
$ret = wp_remote_post( 'http://localhost:3000/server.php', ['body' => ['foo' => 'bar']] );
$ret = wp_remote_retrieve_body( $ret );
var_dump( 'bar' === $ret );
```

### WebPay でクレジットカードの決済

HTTP API の練習として、WebPay で決済してみましょう。

```php
function webpay_charges($key, $data) {
    return webpay_post( 'https://api.webpay.jp/v1/charges', $key, $data );
}

function webpay_tokens($key, $data) {
    return webpay_post( 'https://api.webpay.jp/v1/tokens', $key, $data );
}

function webpay_post($url, $key, $data) {
    $res = wp_remote_post( $url, [
        'headers' => ['Authorization' => 'Basic '.base64_encode( $key.':' )],
        'body' => $data
    ] );

    $code = wp_remote_retrieve_response_code( $res );
    $body = wp_remote_retrieve_body( $res );
    $ret = json_decode( $body, true );

    return array_merge( ['code' => $code], $ret );
}
```

データベース
----------
### Options API を使って設定値を保存する

プラグインの設定値などのこまごまとしたデータは Options API を使って保存することができます。

```php
$key = 'my_plugin_key';

add_option( $key, '設定値' );
update_option( $key, '新しい設定値' );
echo get_option( $key, 'キーが存在しません。' ), "";
delete_option( $key );
echo get_option( $key, 'キーが存在しません。' );
```

連想配列を保存することもできます。

```php
add_option( 'my_option', ['key1' => 'value1', 'key2' => 'value2'] );
echo get_option( 'my_option' )['key1'];
```

### テーブルを定義する

テーブルの作成には `dbDelta` 関数を使います。データベースへのアクセスには `$wpdb` を使います。

```php
global $wpdb;
$table_name = 'test_table';

$sql = "CREATE TABLE IF NOT EXISTS $table_name (
  id int NOT NULL AUTO_INCREMENT,
  text text NOT NULL,
  UNIQUE KEY id (id)
);";

require_once( ABSPATH . 'wp-admin/includes/upgrade.php' );
dbDelta( $sql );

$wpdb->insert( $table_name, ['text' => 'テスト']);

$ret = $wpdb->get_results( "SELECT text FROM $table_name", 'ARRAY_A' );
var_dump( $ret );
```

セキュリティ
----------

### JavaScript/JSON のエスケープ

PHP で生成したデータを JavaScript/JSON に渡す場合、データをエスケープするために`json_encode` を使うことができます。

```php
<?php
$msg = 'こんにちは';
$options = JSON_HEX_QUOT|JSON_HEX_AMP|JSON_HEX_APOS|JSON_HEX_TAG;
?>
<script>
var a = <?= json_encode( $msg, $options ) ?>;
var b = <?= json_encode( ['msg' => $msg], $options ) ?>;

console.log(a);
console.log(b['msg']);
</script>
```

### CSRF 対策の nonce を生成する

```php
$action = 'name_of_my_action';
$nonce = wp_create_nonce( $action );
echo wp_verify_nonce( $nonce, $action ) ? '検証が成功しました。' : '検証が失敗しました。';
```

フォームの隠しフィールドに埋め込む場合、`wp_nonce_field` を使います。次のコードは管理画面のページでフォームを投稿して nonce を検証します。管理画面でフォームをつくる場合、後述の Settings API を使うほうがよいでしょう。

```php
add_action( 'admin_menu', function() {
    add_plugins_page(
        'My Plugin ページのタイトル',
        'My Plugin',
        'read',
        'unique_id_for_your_plugin',
        'my_plugin_callback'
    );
} );

function my_plugin_callback() {
    $action = 'name_of_my_action';

    echo '<h1>nonce のテスト</h1>';

    echo '<form method="post" action="#result">';
    wp_nonce_field( $action, 'name_of_nonce_field' );
    echo '<input type="submit">', PHP_EOL,
    '</form>';

    if (isset( $_POST['name_of_nonce_field']) ) {
        echo wp_verify_nonce( $_POST['name_of_nonce_field'], $action ) ? '検証が成功しました。' : '検証が失敗しました。';
    }
}
```

管理画面
-------

### プラグインのページを追加する

```php
add_action( 'admin_menu', function() {
    add_plugins_page(
        '私のプラグインページのタイトル',
        '私のプラグイン',
        'read',
        'unique_id_for_your_plugin',
        function() {
            echo '<h1>プラグインページのテスト</h1>';
        }
  );
} );
```

### Settings API を使ってフォームをつくる


```php
register_deactivation_hook( __FILE__, function() {
    $settings = myplugin_get_settings();
    $option_name = $settings['option_name'];

    delete_option( $option_name );
} );

add_action( 'admin_menu', function() {
    $settings = myplugin_get_settings();
    $slug = $settings['slug'];

    add_options_page(__( 'My Plugin の設定', $slug ), 'My Plugin', 'manage_options', $slug, 'myplugin_settings_page' );
    add_action( 'admin_init', 'myplugin_admin_init' );
} );

function myplugin_get_settings() {
    return [
        'slug' => 'my-plugin',
        'group' => 'my-settings-group',
        'option_name' => 'my-settings'
    ];
}

function myplugin_admin_init() {
    $settings = myplugin_get_settings();
    $slug = $settings['slug'];
    $group = $settings['group'];
    $option_name = $settings['option_name'];

    $section =  'my-section';
    $field = 'field-one';

    register_setting( $group, $option_name, 'myplugin_validate' );
    add_settings_section( $section, __( 'セクション1です。', $slug ), 'myplugin_section_one', $slug );
    add_settings_field( $field, __( 'フィールド1です。', $slug ), 'myplugin_field_one', $slug, $section );
}

function myplugin_settings_page() {
    $settings = myplugin_get_settings();
    $slug = $settings['slug'];
    $group = $settings['group'];

    echo '<h2>'.__( 'My Plugin の設定', $slug ).'</h2>'
    .'<form action="options.php" method="POST">';
    do_settings_sections( $slug );
    settings_fields( $group );
    submit_button();
    echo '</form>';
}

function myplugin_validate($input) {
    $settings = myplugin_get_settings();
    $slug = $settings['slug'];

    if ( mb_strlen( $input['option1'], 'UTF-8' ) > 10 ) {

        add_settings_error(
            'my-settings-option1',
            'my-settings-texterror',
            __( '10文字以上は保存できません。', $slug ),
            'error'
        );

        $input['option1'] = '';

    }

    return $input;
}

function myplugin_section_one() {
    $settings = myplugin_get_settings();
    $slug = $settings['slug'];

    echo __( 'セクションの説明です。', $slug );
}

function myplugin_field_one() {
    $settings = myplugin_get_settings();
    $option_name = $settings['option_name'];

    $options = get_option( $option_name );
    $value = empty( $options['option1'] ) ? '' : esc_attr( $options['option1'] );

    echo '<input type="text" name="'.$option_name.'[option1]" value="'.$value.'" />';
}
```

### HTTPS 通信を必須にする

一般公開のドメインと管理画面のドメインが同じであれば、wp-config.php で FORCE_SSL_ADMIN を定義します。

```php
define('FORCE_SSL_ADMIN', true);
```

ショートコード API
----------------

ショートコード API を通じて個別の記事でプラグインの機能が使えるようになります。

### フォームを追加する

練習として個別の記事からフォームを使えるようにしてみましょう。コードは `[donation_form]` とします。

```php
add_shortcode( 'donation_form', 'donation_form_func' );

function donation_form_func($atts) {
    return '<form name="DonationForm" action="" method="">
  <fieldset>
    <legend>寄付のお願い</legend>
    <ul>
      <li>
        <label for="UserName">お名前: </label>
        <input id="UserName" name="UserName" type="text" />
      </li>
      <li>
        <label for="ammount">寄付額: </label>
        <select id="amount" name="amount">
          <option value="1000">1000円</option>
          <option value="2000">2000円</option>
          <option value="3000">3000円</option>
        </select>
      </li>
    </ul>
  </fieldset>
  <fieldset class="buttons">
    <input class="submit" type="submit" value="寄付する" />
  </fieldset>
</form>';
}
```

###  異なるアプリから利用する

```php
require_once 'wordpress/wp/wp-blog-header.php';

add_shortcode( 'donation_form', 'donation_form_func' );
echo do_shortcode( '[donation_form max=15000 diff=1500]' );
```

### ショートコードの存在のチェック

```php
var_dump(
    true === shortcode_exists( 'donation_form' ),
    true === has_shortcode( '[donation_form max=15000 diff=1500]', 'donation_form' )
);
```

フィルターとアクション
-------------------

フィルターとアクションを利用することで、WordPress 本体やプラグインのふるまいを変えることができます。フィルターとアクションの違いは戻り値があるかどうかです。フィルターが実行された後で戻り値がある一方で、アクションが実行された後では戻り値はありません。

### フィルターの例
```php
add_filter( 'add_number', function($value) {
    return $value + 2;
} );

add_shortcode( 'get_number', function($atts) {
    return apply_filters( 'add_number', 1 );
} );
```

### アクションの例

```php
$message = '';

add_action( 'add_message', function() use (&$message) {
    $message .= 'World';
} );

add_shortcode( 'show_message', function($atts) {
    global $message;
    do_action( 'add_message' );
    return 'Hello ' . $message;
} );
```

### メールアドレスでログインできるようにする

フィルター学習の題材として、メールアドレスによるログインに取り組んでみましょう。

```php
add_filter( 'authenticate', 'login_with_email_address',  20, 3 );

function login_with_email_address($user, $username, $password) {
    $user = get_user_by( 'email', $username );

    if ( !empty( $username ) ) {
        $user = get_user_by( 'email', $username );
    }

    if ( isset( $user->user_login, $user ) ) {
        $username = $user->user_login;
    }

    return wp_authenticate_username_password( NULL, $username, $password );
}

add_action( 'login_form_login', function() {
    add_filter('gettext', function($text) {
        if ('ユーザー名' === $text) {
            $text .= 'もしくはメールアドレス';
        }

        return $text;    
    }, 20);
});
```

Ajax リクエストに対応する
----------------------

```php
<?php

/*
Plugin Name: My Plugin
Plugin URI: http://blog.sarabande.jp/post/89406853188
Version: 0.1
Author: Masaki Kagaya
Description: My Plugin

License: GPLv2 or later
*/

add_action( 'admin_menu', function() {
    add_plugins_page(
        'My Plugin',
        'My Plugin',
        'read',
        'myplugin_id',
        'myplugin_page'
  );
} );

add_action( 'wp_ajax_'.myplugin_get_action_name(), 'myplugin_response' );
add_action( 'wp_ajax_nopriv_'.myplugin_get_action_name(), 'myplugin_response' );

function myplugin_get_action_name() {
    return 'myplugin_action';
}

function myplugin_get_nonce_name() {
    return 'myplugin-nonce';
}

function myplugin_page() {
    echo '<h2>My Plugin Page</h2>';

    $nonce_name = myplugin_get_nonce_name();
    $nonce = wp_create_nonce( $nonce_name );
    $action_name = myplugin_get_action_name();
    $url = admin_url( 'admin-ajax.php' );

    $json_options = JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT;
    $url = json_encode( $url,  $json_options );

    $json = json_encode(
    array(
      'action' => $action_name,
      'security' => $nonce,
      'foo' => 'bar'
    ), $json_options );
?>

<div id="result"></div>
<script type="text/javascript">
(function($) {

  var url = <?php echo $url ?>;
  var data = <?php echo $json ?>;

  $.post( url, data, function(res) {

     if ( 'success' === res['msg'] ) {
         $( '#result' ).html( 'リクエストが成功しました。' );
     } else {
         $( '#result' ).html( 'リクエストが失敗しました。' );
     }
  }, 'json' );

})(jQuery);
</script>

<?php
}

function myplugin_response() {

    $nonce_name = myplugin_get_nonce_name();
    check_ajax_referer( $nonce_name, 'security' );

    if ( isset( $_POST['foo'] ) && 'bar' === $_POST['foo'] ) {
        $ret = array( 'msg' => 'success' );
    } else {
        $ret = array( 'msg' => 'failure' );
    }

    wp_send_json( $ret );
}
```

外部のアプリケーションでテーマを利用する
----------------------------------

```php
<?php
include 'path/to/wordpress/wp/wp-blog-header.php';
get_header();
?>

<div id="main-content" class="main-content">
  <div id="primary" class="content-area">
    <div id="content" class="site-content" role="main">
      <article id="post-1" class="post-1 post type-post status-publish format-standard hentry category-1">
        <div class="entry-content">
          <p>テスト</p>
        </div>
      </article>
    </div>
  </div>
</div>

<?php
get_sidebar();
get_footer();
```

JavaScript や CSS のファイルを読み込む
-----------------------------------

`wp_enqueue_script` や `wp_enqueue_style` を使います。独自の JS ファイルや CSS ファイルを登録するには `wp_register_script` および `wp_register_style` を使います。

### テーマの最小構成

[The Loop in Action](http://codex.wordpress.org/The_Loop_in_Action) に掲載されています。

```php
<?php
get_header();
if (have_posts()) :
   while (have_posts()) :
      the_post();
         the_content();
   endwhile;
endif;
get_sidebar();
get_footer();
```