(WordPress) 非公開記事も含めた記事数カウントを管理画面に表示するプラグイン

経緯

WordPress でブログを運用していると、カテゴリーやタグ、カスタムタクソノミーを整理したくなることがあります。

特にタグのように非階層型の入力形式のタクソノミーの場合は自由入力なので、スペースの有無や誤字等で似たようなタームを誤って生成しがちです。

  • Windows 11Windows11 (スペースの有無)
  • Google ChromeGooogle Chrome (誤字。 o が1つ多い)

しかも、異なるタグとして集計されるのでタグクラウドの中から絞り込むと思わぬところで必要な記事が漏れていて「あの記事絶対タグが付いているはずなのに……」と見付からずに右往左往したり。

基本的には管理画面の「投稿」→「タグ」に進んでタグの一覧からどちらか一方にタグを付け直して、もう片方のタグで「カウント」つまり記事数が0になったタグを削除すれば良いとは思います。

しかし、ここで一つ課題が。

技術記事等で自分用メモとして残すための非公開記事がいくつかあったりするのですが、先程のタグ編集画面の「カウント」は非公開記事は除外された数のようです。

そのため、例えタグ編集画面で「カウント」が0になっていても、実際は非公開記事で使用されていた……ということが考えられます。これでは迂闊にタグを削除することができません。

コード

そこで、タグ編集画面で非公開記事も含めたカウントを表示するプラグインを作りました。

class FrostColumns
{
    protected $taxName;
    protected $postTypeName;
    protected $countIncludePrivateID;
    protected $countIncludePrivateLabel;

    /**
     * mb_strlen() での空文字列判定のラッパー関数
     *
     * @param string $str        : 入力文字列
     *
     * @return boolean            : 出力文字列
     */
    public function boolStrLen( $str )
    {
        return mb_strlen(
            $str,
            'UTF-8'
        ) > 0;
    }
    /**
     * コンストラクタ
     *
     */
    public function __construct()
    {
        if(
            mb_strpos(
                parse_url(
                    $_SERVER['REQUEST_URI'],
                    PHP_URL_PATH
                ),
                'edit-tags.php',
                0,
                'UTF-8'
            ) !== false
        ) {
            // edit-tags.php のみで動作
            $tax = esc_html(
                filter_input(
                    INPUT_GET,
                    'taxonomy',
                    FILTER_SANITIZE_SPECIAL_CHARS
                )
            );
            // タクソノミーの名前を取得
            $this->taxName = $this->boolStrLen( $tax )
                ? $tax
                : '';
            $posttype = esc_html(
                filter_input(
                    INPUT_GET,
                    'post_type',
                    FILTER_SANITIZE_SPECIAL_CHARS
                )
            );
            // 投稿タイプの名前を取得
            $this->postTypeName = $this->boolStrLen( $posttype )
                ? $posttype
                : '';
            // ラベル
            $this->countIncludePrivateID = 'count_include_private';
            $this->countIncludePrivateLabel = 'カウント(非公開・予約含)';
        }
    }
    /**
     * SQLを直接発行して公開済み・非公開・予約投稿の記事の投稿IDのみの一覧を取得、その数をリンクと共に出力する
     *
     * @param int $termID : タームID
     *
     */
    public function countArticlesBySQL( $termID )
    {
        // $wpdb 読み込み (global で宣言した変数のスコープの兼ね合いでここで読み込み宣言する)
        require_once( ABSPATH . '/wp-load.php');
        global $wpdb;

        // 各タームのオブジェクトを取得
        $termObj = get_term( $termID, $this->taxName );
        // 投稿のカテゴリー、タグはキー名がタクソノミー編集ページと投稿一覧ページで異なるので変換する
        $taxNameParam = $this->taxName;
        switch ( $this->taxName ) {
            case 'category':
                $taxNameParam = 'category_name';
                break;
            case 'post_tag':
                $taxNameParam = 'tag';
                break;
            default:
                $taxNameParam = $this->taxName;
                break;
        }
        // カスタム投稿タイプならばリンクのGETパラメータに投稿タイプの名前を付与
        $postTypeParam = $this->boolStrLen( $this->postTypeName )
            ? '&post_type=' . $this->postTypeName
            : '';
        // SQLのクエリで指定する投稿タイプの名前の文字列をセット
        $postTypeDBParam = $this->boolStrLen( $this->postTypeName )
            ? $this->postTypeName
            : 'post';
        // WP_Query では結果の投稿オブジェクトが大き過ぎてメモリを食い潰すので直接SQLを発行してIDのみ結果として取得して省力化する
        $the_query = "SELECT " . $wpdb->prefix . "posts.ID FROM " . $wpdb->prefix . "posts LEFT JOIN " . $wpdb->prefix . "term_relationships ON (" . $wpdb->prefix . "posts.ID = " . $wpdb->prefix . "term_relationships.object_id) WHERE 1=1 AND ( " . $wpdb->prefix . "term_relationships.term_taxonomy_id IN ( %d ) ) AND " . $wpdb->prefix . "posts.post_type = %s AND ((" . $wpdb->prefix . "posts.post_status = 'publish' OR " . $wpdb->prefix . "posts.post_status = 'future' OR " . $wpdb->prefix . "posts.post_status = 'private')) GROUP BY " . $wpdb->prefix . "posts.ID ORDER BY " . $wpdb->prefix . "posts.post_date DESC";
        $results = $wpdb->get_results(
            $wpdb->prepare(
                $the_query,
                $termID,
                $postTypeDBParam
            )
        );
        $cnt = count($results);

        echo <<<CNT

<a href="edit.php?{$taxNameParam}={$termObj->slug}{$postTypeParam}">{$cnt}</a>

CNT;

    }
    /**
     * 欄としての列の出力
     *
     * @param array $columns : 列
     *
     * @return array $columns : 列
     *
     */
    function addCountIncludePrivateColumns( $columns )
    {
        echo <<<STL
<style>
    .taxonomy-{$this->taxName} .manage-column.num \{width: 90px;\}
    .taxonomy-{$this->taxName} .manage-column.column-id \{width: 60px;\}
</style>

STL;

        $columns[$this->countIncludePrivateID] = $this->countIncludePrivateLabel;
        return $columns;
    }
    /**
     * 追加した列に実際の値を表示させる
     *
     * @param string $content     : 出力するコンテンツ
     * @param string $column_name : 列名
     * @param int    $term_id     : タームID
     *
     */
    function customCountIncludePrivateColumns( $content, $column_name, $term_id )
    {
        if ( $column_name == $this->countIncludePrivateID ) {
            $this->countArticlesBySQL( $term_id );
        }
    }
    /**
     * 初期処理。アクションフック・フィルターフックを発動させる
     *
     */
    public function initialize()
    {
        // 列の追加
        add_filter(
            'manage_edit-' . $this->taxName . '_columns',
            [
                $this,
                'addCountIncludePrivateColumns'
            ]
        );
        // 追加した列に実際の値を表示させる
        add_action(
            'manage_' . $this->taxName . '_custom_column',
            [
                $this,
                'customCountIncludePrivateColumns'
            ],
            10,
            3
        );
    }
}

// 処理
$wp_ab_frostcolumns = new FrostColumns();

if( is_admin() ) {
    // 管理者画面を表示している場合のみ実行
    $wp_ab_frostcolumns->initialize();
}
  • コンストラクタ:
    • URLをパースして edit-tags.php のみで動作するように判定
    • GETパラメータからタクソノミーの名前と(必要ならば)投稿タイプの名前を取得
  • countArticlesBySQLメソッド:
    • 本プラグインの肝
    • 後で使用するため wp-load.php から $wpdb で WordPress のDBオブジェクトを読み込み
    • edit-tags.php ではカテゴリーは category、タグは post_tag だが投稿一覧ページではそれぞれ category_name, tag とキーとなる文字列が変わるため遷移時の GETパラメータ に付与するときのために変換をかけておく
    • カスタム投稿タイプの場合、 SQLクエリ や GETパラメータ への付与が必要になるため処理
    • タクソノミーと投稿タイプ、投稿ステータス( publish, private, future のいずれか)で絞り込むSQLクエリを発行して投稿IDのみのオブジェクトを取得する
      • WP_Query で投稿データ全体のオブジェクトを取得すると重過ぎて PHP のメモリバッファを食い潰してしまうため、最小限になるように調整
    • 記事数を count で取得して、 aタグ として出力
  • addCountIncludePrivateColumnsメソッド:
    • テーブル出力にオリジナルの列を出力する。フィルターフック
  • customCountIncludePrivateColumnsメソッド:
    • 追加した列に実際の値を表示させるアクションフック
    • テーブル出力のキーが予めセットされていた文字列 (今回のオリジナル列) と同じ場合、 countArticlesBySQLメソッド を呼ぶ

やっていることとしてはこのような感じ。

検証

それでは、デモで検証してみます。

デモ環境の9つの記事
デモ環境の9つの記事

このように9つの記事を適当に作ってみました。今回はプラグインの効果をタグで試すことにします。

「ヘイルストーム」で記事を検索すると公開記事2件、非公開記事2件と2件ずつ、合計4件がヒット
「ヘイルストーム」で記事を検索すると公開記事2件、非公開記事2件と2件ずつ、合計4件がヒット

例えば、「ヘイルストーム」で記事を検索すると公開記事2件、非公開記事2件と2件ずつ、合計4件がヒットします。

タグの編集画面。先の「ヘイルストーム」の記事数は2と表示されている
タグの編集画面。先の「ヘイルストーム」の記事数は2と表示されている

ところが、通常のタグの編集画面では先の「ヘイルストーム」の記事数は2と表示されています。実際は非公開記事も含めて4件あるはずで、今回は「4」と表示されてほしいわけです。

プラグインを有効化した際のタグの編集画面。先の「ヘイルストーム」の記事数は追加された列で4と表示されている
プラグインを有効化した際のタグの編集画面。先の「ヘイルストーム」の記事数は追加された列で4と表示されている

それじゃスペルカード発動。先の「ヘイルストーム」の記事数は追加された列で4と表示されるようになりました。意図通りです。

別のサンプル。伸ばし棒のない「コールドディヴィニティ」は非公開記事1件のみ
別のサンプル。伸ばし棒のない「コールドディヴィニティ」は非公開記事1件のみ

別のサンプル。誤字した例として伸ばし棒のない「コールドディヴィニティ」は上述のタグ編集画面だと通常0件、追加列では1件と表示されています。ページ遷移すると、非公開記事1件のみが一覧に表示されています。

こういうケースでは伸ばし棒のある「コールドディヴィニティー」に直して、プラグインで追加された列も含めて「0」表示となったことを確認した後に伸ばし棒のない「コールドディヴィニティ」タグを削除する、というフローで活用できる、という寸法です。

参考

一覧ページのテーブルに列を追加・カスタマイズ

(未使用だが参考にはなった) 表示処理

$wp_list_table->display(); でテーブルを出力している。

基底クラスのこの部分が該当処理。ただし汎用的な内容。

(未使用だが参考にはなった) WP_Terms_List_Table

使用しているクラス

column_posts()メソッドがそれっぽい。が、引数の $tag に既に情報が入っているようなのでこの中ではない。

display_rows()メソッドでもない。

ソート可能な列の出力

$wpdb

テーブルのプレフィックス

(未使用) WP_Query

(未使用) 件数取得

found_postsプロパティ

(未使用) wp_count_posts()

複雑な条件の件数となると WP_Query 一択。

タクソノミー→ターム

(未使用だが参考にはなった) WP_Term_Query

countプロパティ はDBの値を読むだけ。逆に言えばDBに値が保存されていることが判明。

(未使用だが参考にはなった) タクソノミー操作系の目星

(未使用だが参考にはなった) タクソノミーやタームに関係ありそうなクラス

get_term()

PHP

ヒアドキュメント

スコープ

GETパラメータ

filter_input(INPUT_GET, 'taxonomy', FILTER_SANITIZE_SPECIAL_CHARS) で取得できる。

この記事を書いた人

アルム=バンド

フロントエンド・バックエンド・サーバエンジニア。LAMPやNodeからWP、Gulpを使ってejs,Scss,JSのコーディングまで一通り。たまにRasPiで遊んだり、趣味で開発したり。