経緯
WordPress でブログを運用していると、カテゴリーやタグ、カスタムタクソノミーを整理したくなることがあります。
特にタグのように非階層型の入力形式のタクソノミーの場合は自由入力なので、スペースの有無や誤字等で似たようなタームを誤って生成しがちです。
- Windows 11と- Windows11(スペースの有無)
- Google Chromeと- Gooogle 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パラメータからタクソノミーの名前と(必要ならば)投稿タイプの名前を取得
 
- URLをパースして 
- 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つの記事を適当に作ってみました。今回はプラグインの効果をタグで試すことにします。

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

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

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

別のサンプル。誤字した例として伸ばし棒のない「コールドディヴィニティ」は上述のタグ編集画面だと通常0件、追加列では1件と表示されています。ページ遷移すると、非公開記事1件のみが一覧に表示されています。
こういうケースでは伸ばし棒のある「コールドディヴィニティー」に直して、プラグインで追加された列も含めて「0」表示となったことを確認した後に伸ばし棒のない「コールドディヴィニティ」タグを削除する、というフローで活用できる、という寸法です。
参考
一覧ページのテーブルに列を追加・カスタマイズ
- WordPress の管理画面でターム一覧をカスタマイズする方法 | Source Code EX
- WordPress:管理画面のカテゴリーやタクソノミー一覧ページにID(タームID)項目を追加する方法 – NxWorld
- WordPress管理画面のカテゴリー・タグの一覧テーブルにIDを表示する方法 | WEMO
(未使用だが参考にはなった) 表示処理
$wp_list_table->display(); でテーブルを出力している。
基底クラスのこの部分が該当処理。ただし汎用的な内容。
(未使用だが参考にはなった) WP_Terms_List_Table
使用しているクラス
- WordPress\/class-wp-terms-list-table.php at master ・ WordPress\/WordPress ・ GitHub
- WP_Terms_List_Table::column_posts() | Method | WordPress Developer Resources
column_posts()メソッドがそれっぽい。が、引数の $tag に既に情報が入っているようなのでこの中ではない。
display_rows()メソッドでもない。
ソート可能な列の出力
$wpdb
テーブルのプレフィックス
(未使用) WP_Query
(未使用) 件数取得
found_postsプロパティ
(未使用) wp_count_posts()
複雑な条件の件数となると WP_Query 一択。
タクソノミー→ターム
(未使用だが参考にはなった) WP_Term_Query
- WordPressでカテゴリー・タグ・タクソノミーのタームを全取得して一覧表示する方法【WP_Term_Query \/ get_terms()】 | WEMO
- WP_Term_Query | Class | WordPress Developer Resources
- WordPress\/class-wp-term-query.php at master · WordPress\/WordPress · GitHub
countプロパティ はDBの値を読むだけ。逆に言えばDBに値が保存されていることが判明。
(未使用だが参考にはなった) タクソノミー操作系の目星
- get_tax_sql()
- 実際にテーブルを作っているクラス
(未使用だが参考にはなった) タクソノミーやタームに関係ありそうなクラス
- WordPress\/class-wp-tax-query.php at master · WordPress\/WordPress · GitHub
- WordPress\/class-wp-taxonomy.php at master · WordPress\/WordPress · GitHub
- WP_Term | Class | WordPress Developer Resources- WordPress\/class-wp-term.php at master · WordPress\/WordPress · GitHub- Termのオブジェクト(ハコ)という感じ
 
 
- WordPress\/class-wp-term.php at master · WordPress\/WordPress · GitHub
get_term()
PHP
ヒアドキュメント
スコープ
GETパラメータ
filter_input(INPUT_GET, 'taxonomy', FILTER_SANITIZE_SPECIAL_CHARS) で取得できる。
 アルム=バンド
		アルム=バンド