LoginSignup
20
18

More than 5 years have passed since last update.

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

Last updated at Posted at 2015-11-09

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

対象読者

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

多言語に対応させる

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

/**
 * @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 DomainDomain Path を追加します。

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

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

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 ファイルに日本語訳を追加します。

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

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

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

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

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

$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
$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 クライアントをつくってみましょう。最初に次のコードを読み込みます。

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

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

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

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

$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 で決済してみましょう。

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 を使って保存することができます。

$key = 'my_plugin_key';

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

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

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

テーブルを定義する

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

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
$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 を生成する

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

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

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 ) ? '検証が成功しました。' : '検証が失敗しました。';
    }
}

管理画面

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

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

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

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 を定義します。

define('FORCE_SSL_ADMIN', true);

ショートコード API

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

フォームを追加する

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

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>';
}

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

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

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

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

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

フィルターとアクション

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

フィルターの例

add_filter( 'add_number', function($value) {
    return $value + 2;
} );

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

アクションの例

$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;
} );

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

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

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

/*
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
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_scriptwp_enqueue_style を使います。独自の JS ファイルや CSS ファイルを登録するには wp_register_script および wp_register_style を使います。

テーマの最小構成

The Loop in Action に掲載されています。

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

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
20
18