WordPressのプラグインでPHP Namespacesを使って他のプラグインとの衝突を防ぐ

ハイサイ！オースティンやいびーん。んな、ガンじゅー やみ？

# 概要

WordPressのカスタムプラグインのPHPロジックを、namespaceを使ってカプセル化して外部からの影響を遮断し、外部への影響を防ぐ方法を紹介します。

## 背景

WordPressのプラグイン開発において危険な落とし穴が大量にあります。

その一つは、対策をしないと**関数名などがグローバルスコープに定義されること**です。

グローバルで定義してしまうと、他のプラグインと被った時に思わぬエラーが起きます。

WP開発者なら、ただでさえ思わぬエラーが大量に起きている現状を味わっているはずだから、無駄に増やしたくない気持ちは理解していただけるかと思います。:sweat_smile:

そこで、PHPのネイティブ機能、[Namespaces（名前空間）](https://www.php.net/manual/ja/language.namespaces.php)を使うと他と遮断した関数名とクラス名の空間を作ることができます。

その技術を使ってプラグインのカプセル化を図りましょう！

## 何をするか

1. プラグインを作る
1. Namespaceを追加する
1. Todo APIの実装を追加する

# プラグインを追加する

まず、読者のWordPressのプラグインのダイレクトリをIDEで開きましょう。

通常だと、`/var/www/html/wp-content/plugins`になるかと思います。

ちなみに筆者は、Docker Composeで`plugins`と`themes`を`src/plugins`、`src/themes`の[バインドマウント](https://docs.docker.com/storage/bind-mounts/)をしてGitにコミットしているのでそこを編集します。

## PHPのメインファイルを作成する

今回追加するプラグインのフォルダーを作ります。

名前は任意で問題ありませんが、自作のプラグインであることがわかりやすいように頭文字をダイレクトリー名の頭に追加するといいと思います。また、`kebab-case`が一般的です。

```
mkdir src/plugins/ajm-todo-plugin
```

上記のように名前をつけるといいでしょう。そこに同名の`.php`ファイルを入れます。

```
touch src/plugins/ajm-todo-plugin/ajm-todo-block.php
```

## メインファイルのヘッダーを追加する

WPプラグインのメインファイルにヘッダーを含める必要があります。

ヘッダーを含めないとプラグイン一覧に表示されません。

```php:src/plugins/ajm-todo-plugin/ajm-todo-block.php
<?php
/**
 * Plugin Name:       Todo Api Block
 * Description:       A simple Todo API coupled with a web component block component
 * Requires at least: 6.1
 * Requires PHP:      7.0
 * Version:           0.1.0
 * Author:            Austin J. Mayer
 * License:           MIT
 * Text Domain:       todo-api-block
 */
```

`Plugin Name`さえあれば大丈夫です。ヘッダーについてはWPドキュメントを参考にしていただければと思います。

https://developer.wordpress.org/plugins/plugin-basics/header-requirements/

ここまで追加して保存すると、プラグイン一覧に表示されます。

![Screenshot 2023-04-14 at 10.43.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2344956/465d7277-4471-0053-00b3-9295e60af382.png)

有効にしても何も起きないのですが、有効・無効化ができることをまず確認しましょう。

# Namespaceを追加する

次に、上記作った`ajm-todo-block.php`のメインファイルのヘッダーコメントのあとに`namespace`を追加します。

```php:src/plugins/ajm-todo-plugin/ajm-todo-block.php

<?php
/**
 * Plugin Name:       Todo Api Block
 */

namespace Todo;
```

これだけ追加すればこのファイルで定義するすべての関数とクラスは`Todo`名前空間に所属するようになります。

# 有効化時と無効化時にデータベースを構築および削除するフックを追加する

Todo REST APIを実装するので、WPのデータベースにテーブルを追加する必要があります。

## データベース操作のロジックを管理するクラスを作る

ファイルを分けてデータベース操作のロジックを分けておきましょう。教育目的で[インタフェース](https://www.php.net/manual/ja/language.oop5.interfaces.php)と実装も分けておきましょう。

インタフェースを最初に定義しておけば、後でいくら実装を変えてもインタフェースの約束を満たしていればエラーなどが起きません。OOP開発においてこれはとても大切な概念で、一般的には「[契約プログラミング](https://ja.wikipedia.org/wiki/%E5%A5%91%E7%B4%84%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0)」と呼ばれています。

このファイルの頭にも`namespace Todo;`を追加することに注意してください。

```php:src/plugins/ajm-todo-plugin/worker.abstract.php
<?php

namespace Todo;

interface WorkerAbstract
{
  /**
   * テーブルを作成する
   */
  public function setup_table(): void;
  /**
   * テーブルを削除する
   */
  public function destroy_table(): void;

  /**
   * データを追加する
   * @example
   *  $worker->create(['text' => $text]);
   */
  public function create(array $data): void;

  /**
   * IDで指定したTodoを削除する
   */
  public function destroy(int $id): void;

  /**
   * IDで指定したTodoを取得してオブジェクトとして返す
   */
  public function get(int $id): object | null;

  /**
   * ページネーション用でTodoを取得する
   */
  public function all(int $page, int $limit, string $order): array;
}
```
これでどのようなWorkerを作ればいいかわかりますので実装しましょう。

```php:src/plugins/ajm-todo-plugin/worker.php
<?php

namespace Todo;

include('worker.abstract.php');

class Worker implements WorkerAbstract
{
  private $wpdb;
  private $table_name = '';
  private $db_version = '1.0';

  function __construct()
  {
    global $wpdb;
    $this->wpdb = $wpdb;
    $this->table_name = $wpdb->prefix . 'todos';
  }

  public function setup_table(): void
  {
    $charset_collate = $this->wpdb->get_charset_collate();
    $sql = "CREATE TABLE $this->table_name (
    id INT(11) NOT NULL AUTO_INCREMENT,
    created_at TIMESTAMP NOT NULL DEFAULT (UTC_TIMESTAMP),
    text VARCHAR(255) NOT NULL,
    url varchar(55) DEFAULT '' NOT NULL,
    PRIMARY KEY  (id)
    ) $charset_collate;";
    require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
    dbDelta($sql);
    add_option("todo_db_version", $this->db_version);
  }

  public function destroy_table(): void
  {
    $sql = "DROP TABLE IF EXISTS $this->table_name";
    $this->wpdb->query($sql);
  }

  public function all($page = 1, $limit = 10, $order = 'DESC'): array
  {
    $offset = ($page - 1) * $limit;
    $sql = "SELECT * FROM $this->table_name
    ORDER BY created_at $order
    LIMIT $limit OFFSET $offset;";
    return $this->wpdb->get_results($sql);
  }

  public function get(int $id): object | null
  {
    $sql = "SELECT * FROM $this->table_name
    WHERE id = $id";
    return $this->wpdb->get_row($sql, 'OBJECT');
  }

  public function create(array $data): void
  {
    $this->wpdb->insert($this->table_name, $data);
  }

  public function destroy(int $id): void
  {
    $this->wpdb->delete($this->table_name, ['id' => $id]);
  }
}
```

プラグインのテーブル作成についてあれこれ落とし穴があるので、困ったら以下の細かいドキュメントを参照していただければと思います。

https://codex.wordpress.org/Creating_Tables_with_Plugins

上記の`Worker`をメインファイルで使ってみましょう！

## メインファイルでテーブル作成と削除の実行をプラグインのフックに追加する

WPにはフック制度によって様々なタイミングに独自のロジックを実行するように指示できる機能を保有しており、このフック機能で拡張性を実現しています。

プラグインを有効にした時にとある関数を実行するために以下のように実装します。

```php:src/plugins/ajm-todo-plugin/ajm-todo-block.php
<?php
/**
 * Plugin Name:       Todo Api Block
 */

namespace Todo;
include('worker.php');
use Todo\Worker;

function setup_todo_table()
{
	$worker = new Worker();
	$worker->setup_table();
}
register_activation_hook(__FILE__, __NAMESPACE__ . '\setup_todo_table');
```

上記の`register_activation_hook`でプラグインを有効にした時にコールバック関数を実行するようにしていますが、ここで`namespace`の魔法が入っています。

`register_activation_hook`は、WPがまた違うところで実際に実行するので、このファイルの中で指定している`namespace`の外にコールバックが実行されてしまいます。

そうすると、`setup_todo_table知らんど！見つからんど！`とエラーをはかれます。

WP側でもコールバックが見つかるように`namespaceを上記のようにコールバック関数名に追加する必要があります。

他にも以下のように指定できます。

```php
register_activation_hook(__FILE__, '\Todo\setup_todo_table');
```

どちらでも大丈夫です。

テーブル削除も同様に追加します。

```php
<?php
namespace Todo;

...

function destroy_todo_table()
{
	$worker = new Worker();
	$worker->destroy_table();
}
register_deactivation_hook(__FILE__, __NAMESPACE__ . '\destroy_todo_table');
```

# APIのパスのロジックをコントローラで実装する

上記のWorkerと同様にインタフェースと実装で分けましょう。

## インタフェース

グローバルのWP_REST_Request等を使用して型推測をよくしましょう。ここも同じ`Todo`名前空間を共有します。

```php:src/plugins/todo-api-block/controller.abstract.php
<?php

namespace Todo;

use WP_REST_Request;
use WP_REST_Response;

interface TodoControllerAbstract {
  /**
   * WPにパスを登録する
   */
  static function init(): void;

  /**
   * ページネーションでTodoをJSON配列で返す
   */
  static function all(WP_REST_Request $request): WP_REST_Response;

  /**
   * IDで指定したTodoをJSONオブジェクトで返す
   */
  static function get(WP_REST_Request $request): WP_REST_Response;

  /**
   * フォームデータを受け取ってTodoを保存する
   * 成功時に200を返すのみ
   */
  static function post(WP_REST_Request $request): WP_REST_Response;

  /**
   * IDで指定したTodoを削除する
   * 成功時に200を返すのみ
   */
  static function delete(WP_REST_Request $request): WP_REST_Response;
}
```

## 実装

```php:src/plugins/todo-api-block/controller.php
<?php

namespace Todo;
include('controller.abstract.php');

use WP_REST_Request;
use WP_REST_Response;

class TodoController implements TodoControllerAbstract
{
  static function init(): void
  {
    register_rest_route('todo/v1', '/todos', [
      'methods' => 'GET',
      'callback' => 'Todo\TodoController::all',
      'permission_callback' => function () {
        return true;
      }
    ]);
    register_rest_route('todo/v1', '/todos/(?P<id>\d+)', [
      'methods' => 'GET',
      'callback' => 'Todo\TodoController::get',
      'permission_callback' => function () {
        return true;
      }
    ]);
    register_rest_route('todo/v1', '/todos', [
      'methods' => 'POST',
      'callback' => 'Todo\TodoController::post',
      'permission_callback' => function () {
        return true;
      }
    ]);
    register_rest_route('todo/v1', '/todos/(?P<id>[\d]+)', [
      'methods' => 'DELETE',
      'callback' => 'Todo\TodoController::delete',
      'permission_callback' => function () {
        return true;
      }
    ]);
  }

  static function all(WP_REST_Request $request): WP_REST_Response
  {
    $qp = $request->get_query_params();
    $page = array_key_exists('page', $qp) && is_numeric($qp['page']) ? intval($qp['page']) : 1;
    if ($page < 1) {
      return TodoController::make400Response('InvalidPage');
    }

    $worker = new Worker();
    $data = $worker->all($page);

    return TodoController::make200Response($data);
  }

  static function get(WP_REST_Request $request): WP_REST_Response
  {
    $params = $request->get_params();
    $id = intval($params['id']);

    $worker = new Worker();
    $data = $worker->get($id);
    if (!$data) return TodoController::make400Response('NotFound');

    return TodoController::make200Response($data);
  }

  static function post(WP_REST_Request $request): WP_REST_Response
  {
    $body = $request->get_body_params();
    $text = $body['text'];
    if (!$text) {
      return TodoController::make400Response('InvalidBody');
    }

    $worker = new Worker();
    $worker->create(['text' => $text]);

    return TodoController::make200Response();
  }

  static function delete(WP_REST_Request $request): WP_REST_Response
  {
    $params = $request->get_params();
    $id = intval($params['id']);

    $worker = new Worker();
    $worker->destroy($id);

    return TodoController::make200Response();
  }

  static private function make400Response($message = ''): WP_REST_Response
  {
    $response = new WP_REST_Response($message);
    $response->set_status(400);
    return $response;
  }

  static private function make200Response($data = 200): WP_REST_Response
  {
    $response = new WP_REST_Response($data);
    $response->set_status(200);
    return $response;
  }
}
```

## メインファイルで登録する

WPのフックで上記のREST APIのエンドポイントを登録するロジックを実行させるようにする必要がありますので、もう一度メインファイルで手を加えましょう！

```php:src/plugins/ajm-todo-plugin/ajm-todo-block.php
<?php
/**
 * Plugin Name:       Todo Api Block
 */

namespace Todo;

include('worker.php');
include('controller.php');

use Todo\Worker;

...

add_action('rest_api_init', __NAMESPACE__ . '\TodoController::init');
```

これでREST APIの実装はすべて終わりました！試してみましょう。

# プラグインを有効化し、REST APIのエンドポイントを試す

もう一度プラグイン一覧でこのプラグインを有効化してみます。

データベースを確認すると、`wp_todos`というテーブルが作成されているかと思います。

`http://localhost:8080/wp-json/todo/v1/todos/`を開くと、空の配列が返されるはずです。以下のJavaScriptをコンソールで実行すると追加できます。

```javascript
const formData = new FormData();
formData.set('text', 'Todo API todo test!')
fetch('http://localhost:8080/wp-json/todo/v1/todos', {
    method: 'POST',
    body: formData
});
```

試してみると正しく実行されているようです！

![Screenshot 2023-04-14 at 11.50.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2344956/d0064fe4-617c-b57a-ba84-6ab63d3f04d4.png)


# まとめ

以上、namespaceでカプセル化したWordPressのプラグインを作成する方法を紹介してきましたが、いかがでしょうか？

インタフェースを使って契約プログラミングも紹介しました。本記事のTodo例だと過剰でしょうが、どんな開発者でもこういった習慣を身につけるべきだと思っています。
WordPressのソースコードがよく批判されるのですが、古いコードベースで契約プログラミングのようなモダンな開発概念が取り入れていないせいで、なかなかリファクタリングできないらしいです。

Namespaceでカプセル化を実現するのもそうですが、契約プログラミングを意識して開発をすると、部分的に実装を変えてもインタフェースの契約を満たしていればOKなので技術負債を解消する時に役立ちます。WPを使わないで同じAPIを実装することになったら、インタフェースはすべて役に立ちますね。

WordPressでもなんでも、自分が関わっている開発に責任感と誇りを持って、いいコードを残していきましょう！そしてそのいいコードが古くなった時に、簡単にゴミ箱に捨てられるようにしてあげましょう！:joy: