プラグインなしでWordPressにカスタムフィールドを実装したい（単一フィールド編）

wordpressはあまり触らないので、誤り、不足、余計な記述などあるかもしれません。

## 目的
単一のテキストを入力するカスタムフィールドを実装することを目的とする。シンプルにするため、本記事では文字列以外の型は扱わない。

## 前提
[プラグインなしでWordPressにカスタム投稿タイプを実装したい](https://qiita.com/ka2goro/items/f7dfe58a1514a3993d6f)で実装したカスタム投稿タイプ（info）にフィールドを追加するかたちで進めていく。

## 構成
今回触れるファイルは以下のように管理する。（それ以外は省略）
```bash
./wp-content/themes/mytheme
├── includes
│   └── info
│       ├── helper.php
│       ├── custom-post-type.php
│       └── single-custom-field.php
├── archive-info.php
├── single-info.php
└── functions.php
```
前回の記事から追加されたのは`single-custom-field.php`と`helper.php`。

## コード全体

<details><summary>single-custom-field.php</summary>

```php:single-custom-field.php
<?php 
defined( 'ABSPATH' ) || exit;

require_once plugin_dir_path( __FILE__ ) . 'helper.php';

function register_info_single_custom_field() {
    $c = new InfoSingleCustomField();
    $key = $c->key;
    $post_type = $c->type;
    $default = $c->default;
    
    // フィールドの振る舞いを定義する
    $args = array(
        'type' => 'string', // 値の型
        'single' => true, // 単一の値を扱う
        'default' => $default, // フィールドのデフォルト値
        'show_in_rest' => true, // REST APIに含める
    );
    
    // カスタムフィールドを登録する
    register_post_meta( $post_type, $key, $args );
}   
add_action( 'init', 'register_info_single_custom_field' );

function add_info_single_custom_field_meta_box( $post_type ) {
    $c = new InfoSingleCustomField();
    $id = $c->id;
    $type = $c->type;

    // info以外の投稿タイプでは何もしない
    // add_meta_box()でタイプを指定してるので不要かもしれない
    if ( $post_type !== $type ) {
        return;
    }

    add_meta_box(
        $id, // フィールドのdiv要素に付与されるid
        'カスタムテキストフィールド', // フィールドのラベル
        'render_info_single_custom_field_callback', // フィールドのdomをレンダリングするコールバック
        $type, // 対象の投稿タイプ
        'normal', // コンテキスト
        'default' // 表示優先度
    );
}
add_action( 'add_meta_boxes', 'add_info_single_custom_field_meta_box' );

function render_info_single_custom_field_callback( $post ) {
    $c = new InfoSingleCustomField();
    $key = $c->key;
    $nonce_name = $c->nonce_name;
    $nonce_action = $c->nonce_action;

    wp_nonce_field( $nonce_action, $nonce_name );

    // trueを渡すと単一の値を取得する
    $value = get_post_meta( $post->ID, $key, true );
    ?>

    <div>
        <label for="<?php echo( esc_attr( $key ) ); ?>">
            テキスト
        </label>
        <input type="text" 
            id="<?php echo( esc_attr( $key ) ); ?>"
            name="<?php echo( esc_attr( $key ) ); ?>"
            value="<?php echo( esc_attr( $value ) ); ?>">
    </div>

<?php
}

function save_info_single_custom_field_meta_box_data( $post_id ) {
    $c = new InfoSingleCustomField();
    $key = $c->key;
    $post_type = $c->type;
    $nonce_name = $c->nonce_name;
    $nonce_action = $c->nonce_action;

    // 投稿タイプをチェック
    if ( get_post_type( $post_id ) !== $post_type ) {
        return;
    }

    // ノンスチェック（ノンスが存在し、それが正しいか）
    if ( !isset( $_POST[$nonce_name] ) || !wp_verify_nonce( $_POST[$nonce_name], $nonce_action ) ) {
        return;
    }

    // 自動保存で更新を行わない
    if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
        return;
    }

    // 権限チェック
    if ( !current_user_can( 'edit_post', $post_id ) ) {
        return;
    }

    // データを取得
    $new_value = isset( $_POST[$key] ) ? sanitize_text_field( $_POST[$key] ) : '';
    // データを保存
    update_post_meta( $post_id, $key, $new_value );
}
add_action( 'save_post', 'save_info_single_custom_field_meta_box_data' );
?>
```
</details>

<details><summary>helper.php</summary>

```php:helper.php
<?php 
// スクリプトの不正な直接アクセスを防ぐ
defined( 'ABSPATH' ) || exit;

class InfoSingleCustomField {
    // フィールドがレンダリングされるdiv要素のid
    public $id = 'info_single_custom_field_container'; 
    // フィールドを識別するキー
    public $key = 'info_single_custom_field'; 
    // フィールドを追加する投稿タイプの名前
    public $type = 'info'; 
    // フィールドに表示されるデフォルトの値
    public $default = 'default value'; 
    // ノンスの名前
    public $nonce_name = 'info_single_custom_field_meta_box_nonce'; 
    // ノンスのハッシュを生成するためのユニークな値
    public $nonce_action;

    function __construct() {
        $this->nonce_action = basename( __FILE__ );
    }
}
?>
```
</details>

<details><summary>single-info.php</summary>

```php:single-info.php
<?php get_header(); ?>

<h1>info</h1>

<div>
    <?php 
    global $post;
    
    if ( !empty($post) ) :
    ?>
        <div>
            <h2><?php echo( $post->post_title ); ?></h2>
            <small><?php echo( $post->post_date ); ?></small>

            <div>
                <small>
                    <?php 
                    // メタデータの取得する
                    $post_meta = get_post_meta( $post->ID, 'info_single_custom_field', true );
                    echo( 'カスタムフィールド: ' . $post_meta );
                    ?>
                </small>
            </div>

            <div>
                <?php print_r( $post->post_content ); ?>
            </div>
        </div>
    <?php 
    endif;
    ?>
</div>

<?php get_footer(); ?>
```
</details>

## 実装
### ヘルパー
保全性と安全性のため、多用する値は一箇所で定数として管理しておく。また、これらの値はグローバルな値として扱いたくないので、クラスのプロパティとして管理することにする。
もっと良い実装あったら知りたい。

それぞれの値の役割の詳細については都度触れる。

```helper.php
<?php 
// スクリプトの不正な直接アクセスを防ぐ
defined( 'ABSPATH' ) || exit;

class InfoSingleCustomField {
    // フィールドがレンダリングされるdiv要素のid
    public $id = 'info_single_custom_field_container'; 
    // フィールドを識別するキー
    public $key = 'info_single_custom_field'; 
    // フィールドを追加する投稿タイプの名前
    public $type = 'info'; 
    // フィールドに表示されるデフォルトの値
    public $default = 'default value'; 
    // ノンスの名前
    public $nonce_name = 'info_single_custom_field_meta_box_nonce'; 
    // ノンスのハッシュを生成するためのユニークな値
    public function nonce_action() { 
        return basename(__FILE__);
    }
?>
```

不正な実行を防ぐため、プロジェクト外からアクセスした場合は処理を終了する。
```php:helper.php
// スクリプトの不正な直接アクセスを防ぐ
defined( 'ABSPATH' ) || exit;
```


### フィールドの本体実装
`single-custom-field.php`に本体の実装を書いていく。

:::note warn
複数のカスタムフィールドを実装する場合、ここに書いていく関数名は簡単すぎると他の箇所と競合してしまうので、ユニークになるようにあえて冗長な名前にしてる。
:::


<br>

`helper.php`同様外部からのアクセスは処理を止める。
```php:single-custom-field.php
<?php 
defined( 'ABSPATH' ) || exit;
```

`helper.php`を読み込む。
```diff_php:single-custom-field.php
<?php 
defined( 'ABSPATH' ) || exit;

+ require_once plugin_dir_path( __FILE__ ) . 'helper.php';
```

**メタデータを追加**
`register_post_meta`を`init`に追加して、カスタムフィールドのメタデータを投稿に登録する。

```single-custom-field.php
// つづき
function register_info_single_custom_field() {
    $c = new InfoSingleCustomField();
    $key = $c->key;
    $post_type = $c->type;
    $default = $c->default;
    
    // フィールドの振る舞いを定義する
    $args = array(
        'type' => 'string', // 値の型
        'single' => true, // 単一の値を扱う
        'default' => $default, // フィールドのデフォルト値
        'show_in_rest' => true, // REST APIに含める
    );
    
    // カスタムフィールドを登録する
    register_post_meta( $post_type, $key, $args );
}
add_action( 'init', 'register_info_single_custom_field' );
```

<details><summary><var>register_post_meta()</var>の引数の説明</summary>

1\. **`$post_type`（必須）**
投稿タイプの名前を渡す。投稿タイプ以外では'post', 'comment', 'term', 'user'などを渡す。空文字を渡した場合、全ての投稿タイプに対して適応される。

2\. **`$key`（必須）**
フィールドを識別するキー。

3\. **`$args`（必須）**
<ins>`type: string`</ins>
保存されるデータの型を指定。'string'、'boolean'、'integer'、'number'、'array'、'object'を使うことができる。デフォルトでは'string'。
<ins>`single: bool`</ins>
単一の値を扱う場合はtrue、複数の場合はfalseを渡す。デフォルトではfalse。
<ins>`defaul: mixed`</ins>
コントロールパネルで表示されるデフォルトの値。メタデータに何も保存されていない場合この値が返される。
<ins>`show_in_rest: bool`</ins>
REST APIで使用するためにはtrueを指定する。今回はブロックエディタに対応したいのでtrue。デフォルトではfalseなので注意。

:::note warn
`default`に何かしらの値を指定しても、初期状態でメタデータには何も保存されない。
:::

:::note info
`$args`に指定するパラメーターの詳細は`register_meta()`のドキュメントで確認する。
- [WordPress私的マニュアル（日本語）](https://elearn.jp/wpman/function/register_meta.html)
- [Developer.WordPress.org（英語）](https://developer.wordpress.org/reference/functions/register_meta/)
:::

</details>
<br>

**メタボックスを追加**
投稿の編集ページにフィールドを表示するメタボックスを追加する。
```php:single-custom-field.php
// つづき
function add_info_single_custom_field_meta_box( $post_type ) {
    $c = new InfoSingleCustomField();
    $id = $c->id;
    $type = $c->type;

    // info以外の投稿タイプでは何もしない
    // add_meta_box()でタイプを指定してるので不要かもしれない
    if ( $post_type !== $type ) {
        return;
    }

    add_meta_box(
        $id, // フィールドのdiv要素に付与されるid
        'カスタムテキストフィールド', // フィールドのラベル
        'render_info_single_custom_field_callback', // フィールドのdomをレンダリングするコールバック
        $type, // 対象の投稿タイプ
        'normal', // コンテキスト
        'default' // 表示優先度
    );
}
add_action( 'add_meta_boxes', 'add_info_single_custom_field_meta_box' );
```

**<var>add_meta_box()</var>のドキュメント**
- [WordPress私的マニュアル（日本語）](https://elearn.jp/wpman/function/add_meta_box.html)
- [Developer.WordPress.org（英語）](https://developer.wordpress.org/reference/functions/add_meta_box/)

<br>

**フィールドをレンダリング**
`input`タグを使って、編集画面のメタボックス内にフィールドをレンダリングする。この関数をコールバックとして`add_meta_box()`に渡す。

nonceの説明は↓の記事がわかりやすかった。

https://webutubutu.com/webdesign/4076

```php:single-custom-field.php
// つづき
function render_info_single_custom_field_callback( $post ) {
    $c = new InfoSingleCustomField();
    $key = $c->key;
    $nonce_name = $c->nonce_name;
    $nonce_action = $c->nonce_action;

    wp_nonce_field( $nonce_action, $nonce_name );

    // trueを渡すと単一の値を取得する
    $value = get_post_meta( $post->ID, $key, true );
    ?>

    <div>
        <label for="<?php echo( esc_attr( $key ) ); ?>">
            テキスト
        </label>
        <input type="text" 
            id="<?php echo( esc_attr( $key ) ); ?>"
            name="<?php echo( esc_attr( $key ) ); ?>"
            value="<?php echo( esc_attr( $value ) ); ?>">
    </div>
<?php
}
```

`wp_nonce_field()`を実行して、メタデータと正しく紐づけるための隠れたinput要素を生成する。
```php
wp_nonce_field( $nonce_action, $nonce_name )
```

`label`タグの`for`属性と`input`タグの`id`属性には、フィールドを識別するためのキーを指定する。
また、タグの属性に値を挿入する場合は、不正な文字列の挿入を防ぐために`esc_attr()`関数でエスケープする。
```php
    <div>
        <label for="<?php echo( esc_attr( $key ) ); ?>">
            テキスト
        </label>
        <input type="text" 
            id="<?php echo( esc_attr( $key ) ); ?>"
            name="<?php echo( esc_attr( $key ) ); ?>"
            value="<?php echo( esc_attr( $value ) ); ?>">
    </div>
```
<br>

**表示の確認**
`functions.php`で`single-custom-field.php`を読み込む。

```diff_php:functions.php
<?php
// 省略
require_once plugin_dir_path(__FILE__) . '/includes/info/single-custom-field.php';
```
ここまでの状態で、編集画面にメタボックスを表示できる。
ただし値の保存はまだできない。

![スクリーンショット 2024-12-22 20.45.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3776694/0abc15f9-d0e7-70c4-2769-9dfe220e3245.png)

<br>

**データを保存**
投稿の保存にフックしてメタデータの値を更新する。
```single-custom-field.php
// つづき
function save_info_single_custom_field_meta_box_data( $post_id ) {
    $c = new InfoSingleCustomField();
    $key = $c->key;
    $post_type = $c->type;
    $nonce_name = $c->nonce_name;
    $nonce_action = $c->nonce_action;

    // 投稿タイプをチェック
    if ( get_post_type( $post_id ) !== $post_type ) {
        return;
    }

    // ノンスチェック（ノンスが存在し、それが正しいか）
    if ( !isset( $_POST[$nonce_name] ) || !wp_verify_nonce( $_POST[$nonce_name], $nonce_action ) ) {
        return;
    }

    // 自動保存で更新を行わない
    if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
        return;
    }

    // 権限チェック
    if ( !current_user_can( 'edit_post', $post_id ) ) {
        return;
    }

    // データを取得
    $new_value = isset( $_POST[$key] ) ? sanitize_text_field( $_POST[$key] ) : '';
    // データを保存
    update_post_meta( $post_id, $key, $new_value );
}
add_action( 'save_post', 'save_info_single_custom_field_meta_box_data' );
?>
```

`$_POST[$key]`でnonceに入力された値を取得し、データ型が正しいことを保証するために`sanitize_text_field()`でサニタイズする。
```php
$new_value = isset( $_POST[$key] ) ? sanitize_text_field( $_POST[$key] ) : '';
```

編集内容の保存をした時にメタデータを更新し保存する。
```php
update_post_meta( $post_id, $key, $new_value );
```

ただし、↓の処理で編集の機能の自動保存時には更新しないようにする。
```php
    if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
        return;
    }
```
:::note warn
特に、'show_in_rest'にtrueを指定（ブロックエディタでの利用を有効化）してる時は自動保存機能がオンになるので注意。
:::


## 投稿詳細ページで表示を確認
実際にフィールドに入力したテキストを投稿詳細ページに表示して確認してみる。

簡易的に詳細ページを作る。
```php:single-info.php
<?php get_header(); ?>

<h1>info</h1>

<div>
    <?php 
    global $post;
    
    if ( !empty($post) ) :
    ?>
        <div>
            <h2><?php echo( $post->post_title ); ?></h2>
            <small><?php echo( $post->post_date ); ?></small>

            <div>
                <small>
                    <?php 
                    // メタデータの取得する
                    $post_meta = get_post_meta( $post->ID, 'info_single_custom_field', true );
                    echo( 'カスタムフィールド: ' . $post_meta );
                    ?>
                </small>
            </div>

            <div>
                <?php print_r( $post->post_content ); ?>
            </div>
        </div>
    <?php 
    endif;
    ?>
</div>

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

'https://{domain}/wp/info/{slug}'にアクセスするとフィールドに入力した値が期待通りに保存・取得できているのがわかる。
![スクリーンショット 2024-12-22 20.53.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3776694/8df82fb1-b2cc-4ca8-7129-7fd27fae0330.png)


## プラグインとして実装（おまけ）

プラグインだとコントロールパネルで有効/無効を切り替えらるから便利かも。

wp/wp-contentにpluginsディレクトリを作ってinfoディレクトリをそのままぶちこむ。

ファイルの先頭に、wordpressがプラグインを識別するための記述をする。

```php:wp/wp-content/plugins/info/single-custom-field.php
<?php
/**
 * Plugin Name: Custom Single Field
 * Description: 特定の投稿タイプにカスタムフィールドを追加するオリジナルのプラグイン
 */
```

コントロールパネルの"プラグイン > インストール済みプラグイン"に↓のようにプラグインが追加されるので有効化すると使える。
![スクリーンショット 2024-12-22 20.56.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3776694/278e3491-5d5a-ed57-0169-541ebf731ae4.png)


## あとがき
そのうち繰り返しフィールド編を投稿します。