WordPressプラグイン・Viteの開発環境・ショートコード

目的は、ショートコードごとに異なるエフェクトを提供するプラグインを作成することです

目次
  1. プラグイン
  2. PHPのデバック(MAMP)
  3. Viteでフロントの開発環境を作る
  4. ショートコード

プラグイン

  • プラグインディレクトリの作成
    WordPressのインストールディレクトリ内のwp-content/pluginsフォルダ内に新しいディレクトリを作成します
    このディレクトリ名がプラグイン名になります
  • メインプラグインファイルの作成:
    プラグインディレクトリ内にPHPファイルを作成します
    通常はプラグイン名と同じファイル名を使用します
    このファイルはプラグインのメインファイルとなり、プラグインのメタ情報と実行コードを含む必要があります
  • プラグインヘッダの追加
    メインファイルの先頭にはプラグインのメタ情報を含むヘッダを追加します
<?php
/*
Plugin Name: プラグイン名
Plugin URI: プラグインの詳細ページURL
Description: プラグインの簡単な説明
Version: 1.0
Author: 開発者名
Author URI: ウェブサイトURL
*/

//直接的なアクセスを防ぐ
if( ! defined( 'ABSPATH') ){
    exit;
}

*ABSPATHはWordPressのコアファイルのwp-config.phpにて定義される定数で、WordPressのルートディレクトリの絶対パスを保持している

管理画面「プラグイン」ページで、プラグインの「有効化」の隣に「削除」リンクが表示されるようにする
プラグインフォルダ内にuninstall.php ファイルを作成

<?php
// セキュリティチェック
if (!defined('WP_UNINSTALL_PLUGIN')) {
    die;
}

// プラグインフォルダのパスを取得
$plugin_dir = plugin_dir_path(__FILE__);

// プラグインフォルダを削除
function rrmdir($dir) {
    if (is_dir($dir)) {
        $objects = scandir($dir);
        foreach ($objects as $object) {
            if ($object != "." && $object != "..") {
                if (is_dir($dir . "/" . $object) && !is_link($dir . "/" . $object))
                    rrmdir($dir . "/" . $object);
                else
                    unlink($dir . "/" . $object);
            }
        }
        rmdir($dir);
    }
}

rrmdir($plugin_dir);

余談:プラグイン開発で使用されるフック関数

  • register_activation_hook():
    プラグインが有効化された時に実行する関数を登録
    *プラグインの初期設定やデータベーステーブルの作成など、有効化時に必要なセットアップを行う
  • register_deactivation_hook():
    プラグインが無効化された時に実行する関数を登録
    *プラグインが使用していたリソースの解放や一時的なデータのクリーンアップなど、無効化時に必要な処理を行う
  • register_uninstall_hook():
    プラグインがWordPressからアンインストールされた時に実行する関数を登録
    *プラグインによって作成されたデータベーステーブルの削除や、設定オプションのクリーンアップなど、アンインストール時に必要な完全なクリーンアップを行う
    注意点として、このフックは静的クラスメソッドまたは静的関数のみをコールバックとして使用することができます
register_activation_hook(__FILE__, function() {
    // アクティベーション時の処理
});

register_deactivation_hook(__FILE__, function() {
    // デアクティベーション時の処理
});

register_uninstall_hook(__FILE__, function() {
    // アンインストール時の処理
});

PHPのデバック(MAMP)

エラーログは通常php_error_logやerror_logという名前のファイルに出力されますが
WordPressのデバッグモードを有効にしてwp-contentフォルダ直下のdebug.logファイルで監視する

//動作を確認用
add_action('wp_head', function() {
    error_log('テスト');
});

WordPressのルートディレクトリにあるwp-config.phpファイルを編集

define('WP_DEBUG', true);
define('WP_DEBUG_LOG', true);  // デバッグログを有効にする
define('WP_DEBUG_DISPLAY', false);  // エラーメッセージを画面に表示しない
@ini_set('display_errors', 0);  // PHPエラーメッセージを画面に表示しない

php.iniの編集

log_errors = On
error_log = "/path/to/wordpress/wp-content/debug.log"

wp-contentフォルダ直下のdebug.logファイルがない場合は作成

cd /path/to/wordpress/wp-content
touch debug.log
chmod 666 debug.log

debug.logファイルの更新を監視する

tail -f /path/to/wordpress/wp-content/debug.log

Viteでフロントの開発環境を作る

解決したいこと

  • 各エフェクトに対応するCSSとJSファイルを個別のチャンクとしてビルド
  • エフェクトの開発時(npm run dev)はエフェクトごとに実行
  • ビルド後のファイル名は通常ハッシュを含むため、manifest.json を使用してビルド後のファイル名を取得しそれをエンキューする
  • 開発環境と本番環境で読み込むpublicフォルダ内のファイルのパスを解決する
    *本番環境ではWordPressプラグイン内で動的に生成されるプラグインURLをJavaScriptに渡すようにする

*RollupはJavaScriptのモジュールバンドラーで、特にESモジュールを使用したプロジェクトのために設計
tree shaking(使用されていないコードを自動的に削除)がデフォルトで有効
rollupOptionsでエフェクトのフォルダごとにチャンクを作成する

src/
  ├── utils/
  │   └── common.js
  ├── effect1/
  │   ├── effect1.js
  │   └── effect1.css
  ├── effect2/
  │   ├── effect2.js
  │   └── effect2.css

プラグインフォルダ/
├── my-effects.php
├── frontend/  
│   ├── src/
│   │   └── effect1/
│   │       ├── effect1.js
│   │       └── effect1.css
│   ├── package.json
│   └── vite.config.js
├── dist/

エフェクトごとに開発環境を立ち上げる、ビルド時は基本的に全てのエフェクトを一括でビルドする
*エフェクトを追加する時にvite.config.jsとpackage.jsonとHTMLファイルを編集する

  • input
    ビルドのエントリーポイント(入力ファイル)を指定します
    オブジェクトのキーが出力ファイルの名前になり、値が対応する入力ファイルのパス
  • output
    ビルドの出力に関する設定entryFileNames、chunkFileNames、およびassetFileNamesは、それぞれ出力されるファイル名のテンプレートを指定
    • entryFileNames
      エントリーポイントファイルの出力名を指定
      assets/[name].jsと設定されているので
      effect1.jsとeffect2.jsがそれぞれassets/effect1.js、assets/effect2.jsとして出力される
    • chunkFileNames
      コード分割やダイナミックインポートで生成されるチャンクファイルの出力名を指定
    • assetFileNames
      CSSやフォントなどのアセットファイルの出力名を指定
  • outDir
    ビルド出力のディレクトリを指定
import { defineConfig } from 'vite';
import { resolve } from 'path';

//環境変数EFFECTが設定されていない場合、'all'
const effect = process.env.EFFECT || 'all';

const input = effect === 'all'
  ? {
      effect1: resolve(__dirname, 'src/effect1/effect1.js'),
      // 他のエフェクトをここに追加
    }
  : {
      [effect]: resolve(__dirname, `src/${effect}/${effect}.js`)
    };

export default defineConfig({
  build: {
    manifest: true,
    rollupOptions: {
      input,
      output: {
        entryFileNames: 'assets/[name].[hash].js',
        chunkFileNames: 'assets/[name]-[hash].js',
        assetFileNames: 'assets/[name]-[hash][extname]'
      }
    },
    outDir: resolve(__dirname, '../dist'),
  }
});
{
  "scripts": {
    "dev:effect1": "EFFECT=effect1 vite",
    "build": "vite build",
    "build:effect1": "EFFECT=effect1 vite build"
    // 他のエフェクトのスクリプトも追加
  }
}
<script type="module" src="/src/effect1/effect1.js"></script>
//開発環境の立ち上げ
npm run dev:effect1
//特定のエフェクトをビルド
npm run build:effect1
//全てのエフェクトをビルド
npm run build

PHP側で、manifest.json を使用してビルド時のファイル名を取得

{
  "src/effect1/effect1.js": {
    "file": "assets/effect1.BoyTrGyr.js",
    "name": "effect1",
    "src": "src/effect1/effect1.js",
    "isEntry": true,
    "css": [
      "assets/effect1-BcNcH9bZ.css"
    ]
  }
}
/**
 * 指定されたエントリーポイントに関連するアセットファイルのパスを取得
 *
 * @param string $entry エントリーポイントのパス(例: 'src/effect1/effect1.js')
 * @return array|null アセットファイルのパスの配列(JSとCSSのパス)、またはnull
 */
function get_asset_paths($entry) {
    // マニフェストファイルのパスを定義
    $manifest_path = plugin_dir_path(__FILE__) . 'dist/.vite/manifest.json';

    // マニフェストファイルが存在するかをチェック
    if (file_exists($manifest_path)) {
        // マニフェストファイルの内容を読み込み、JSONとしてデコード
        $manifest = json_decode(file_get_contents($manifest_path), true);

        // 指定されたエントリーポイントがマニフェストに存在するかをチェック
        if (isset($manifest[$entry])) {
            // アセットファイルのパスを格納する配列を初期化
            $paths = array();

            // JSファイルのパスを配列に追加
            $paths['js'] = plugins_url('dist/' . $manifest[$entry]['file'], __FILE__);

            // CSSファイルが存在する場合、各CSSファイルのパスを配列に追加
            if (isset($manifest[$entry]['css'])) {
                $paths['css'] = array_map(function($css_file) {
                    return plugins_url('dist/' . $css_file, __FILE__);
                }, $manifest[$entry]['css']);
            }
            // アセットファイルのパスを返す
            return $paths;
        } else {
            // エントリーポイントがマニフェストに存在しない場合のエラーログ
            error_log('Entry point not found in manifest: ' . $entry);
        }
    } else {
        // マニフェストファイルが存在しない場合のエラーログ
        error_log('Manifest file does not exist at path: ' . $manifest_path);
    }
    // エントリーポイントが見つからないか、マニフェストファイルが存在しない場合はnullを返す
    return null;
}

取得したファイル名を使ってエントリーポイントのアセットをキューに登録する関数
*この時に開発環境と本番環境で読み込むpublicフォルダ内のファイルのパスを解決するために
wp_localize_scriptを使ってプラグインURLをJavaScriptに渡すようにする

/**
 * 任意のエントリーポイントのアセットをキューに登録する関数
 *
 * @param string $entry エントリーポイントのパス(例: 'src/effect1/effect1.js')
 * @param string $handle_prefix スクリプトやスタイルのハンドルのプレフィックス
 */
function enqueue_assets($entry, $handle_prefix) {
    $asset_paths = get_asset_paths($entry);

    if ($asset_paths) {
        if (isset($asset_paths['js'])) {
            wp_enqueue_script("{$handle_prefix}-js", $asset_paths['js'], array(), null, true);
         
           //wp_localize_scriptを使ってプラグインURLをJavaScriptに渡す
            wp_localize_script("{$handle_prefix}-js", 'myPlugin', array(
                'url' => plugin_dir_url(__FILE__) . 'dist/'
            ));
            error_log("Script localized with URL: " . plugin_dir_url(__FILE__));
        }

        if (isset($asset_paths['css'])) {
            foreach ($asset_paths['css'] as $css_path) {
                wp_enqueue_style("{$handle_prefix}-css", $css_path, array(), null);
            }
        }
    }
}

//エンキューする
add_action('wp_enqueue_scripts', function() {
    enqueue_assets('src/effect1/effect1.js', 'effect1');
});

*wp_localize_scriptが実際に渡っているかはviteの開発環境(npm run dev時)では確認できないので
エンキューして、ワードプレス側のコードに画像のようなスクリプトが追加されていれがOK

viteのpublicフォルダに配置されたファイルは、そのままの名前とパスでビルド出力ディレクトリ(通常はdist)にコピーされます
public/images/logo.pngというファイルがあれば
ビルド後も同じパスdist/images/logo.png
publicフォルダ内のファイルを参照する場合、ルート相対パスを使う
例えば、public/images/logo.pngを参照する場合、/images/logo.pngと記述

なので下記の方法で通常問題なくビルド後も対応できると思う

//jsでプラグインURLを取得
const pluginUrl = typeof myPlugin !== 'undefined' ? myPlugin.url : '/';

//テンプレートリテラルで対応
`${pluginUrl}images/logo.png`

import を使ってJavaScriptモジュールをロードする場合、スクリプトタグに type=”module” を指定する必要があります

//モジュールタイプをスクリプトタグに追加するフィルタフック
function add_module_type_to_script($tag, $handle, $src) {
    if (strpos($handle, '-js') !== false) {
        $tag = '<script type="module" src="' . esc_url($src) . '"></script>';
    }
    return $tag;
}

add_filter('script_loader_tag', 'add_module_type_to_script', 10, 3);

ショートコード

投稿やページに[my_shortcode]と記述すると
my_shortcode_function関数が呼び出され、その戻り値が表示されます

//ショートコードの機能を提供するためのPHP関数
function my_shortcode_function($atts) {
    return "Hello!";
}
//ショートコードの登録
add_shortcode('my_shortcode', 'my_shortcode_function');

ショートコードの属性
shortcode_atts関数を使用して、デフォルト値を設定しユーザーが指定した値とマージします
[my_shortcode attr1=”value1″ attr2=”value2″]

function my_shortcode_function($atts) {
    // デフォルト属性を定義
    $atts = shortcode_atts(array(
        'attr1' => 'default1',
        'attr2' => 'default2'
    ), $atts, 'my_shortcode');

    // ショートコードの動作を定義する
    return "Attribute 1: " . $atts['attr1'] . ", Attribute 2: " . $atts['attr2'];
}

add_shortcode('my_shortcode', 'my_shortcode_function');

コンテンツを囲むショートコード
[my_enclosing_shortcode]コンテンツ[/my_enclosing_shortcode]
コンテンツが特定のHTMLでラップされて表示されます

function my_enclosing_shortcode_function($atts, $content = null) {
    return '<div class="my-enclosing">' . do_shortcode($content) . '</div>';
}

add_shortcode('my_enclosing_shortcode', 'my_enclosing_shortcode_function');

カスタムコンテンツ表示ショートコードの例
*まとめて文字列にして一度に出力するために、ob_start()を呼び出して出力バッファリングを開始し
その後のHTML出力をバッファにキャプチャ
キャプチャが終了したら、ob_get_clean()を呼び出してバッファの内容を取得しクリア

function custom_content_shortcode($atts) {
    $atts = shortcode_atts(array(
        'title' => 'Default Title',
        'message' => 'Default Message'
    ), $atts, 'custom_content');

    ob_start();
    ?>
    <div class="custom-content">
        <h2><?php echo esc_html($atts['title']); ?></h2>
        <p><?php echo esc_html($atts['message']); ?></p>
    </div>
    <?php
    return ob_get_clean();
}

add_shortcode('custom_content', 'custom_content_shortcode');