WordPress のアクションフックに登録されている無名関数(クロージャー)を解除する方法

WordPress は、主にPHP言語で構築されていて、長い歴史と互換性を維持するためにグローバル関数が多く使われています

また、主要な機能としてアクションフック/フィルターフックを使ってカスタマイズを行いやすいようになっています

このとっても便利なフック系の処理は、add_action/add_filter 関数で登録して、remove_action/remove_filter 関数で解除するようになっています

ただ、このフック処理には、以前は使えなかった無名関数(クロージャー)が最近のPHPでは普通に多用されるようになってきており、グローバル関数の使用が盛んだった頃の解除関数では、無名関数が使われた場合に remove_action/remove_filter 関数では解除できないという問題があります

これだけ普通に無名関数が使われるようになっている今では、本来はWordPressの remove_action/remove_filter で何らかの対応がされるべきと思うのですが、私の知る限り対応はされていません

そこで無名関数を解除するためのプログラムを作成したので紹介します

hook-utils.php プログラム

この無名関数を解除するためのプログラムは、私が公開しているプラグインのために作成したものですが、プログラムは汎用的に使えるようになっていて、アクションフック/フィルターフックから指定したフィルターを解除するための機能を持っているので興味ある方は活用してください

下記コードがこのライブラリの全てで、150行程度なのでとてもコンパクトです (^^)

<?php
/**
 * Hook filter utility
 * 
 * Description: Utility to remove filters that cannot be removed with WP standard functions from action hooks/filter hooks
 * 
 * Version: 0.6.0 
 * Author: enomoto@celtislab
 * Author URI: https://celtislab.net/
 * License: GPLv2
 * 
 */

namespace celtislabv060;

defined( 'ABSPATH' ) || exit;

class Hook_util {

	function __construct() {}
    
    public static function is_enable() {
        return class_exists('ReflectionFunction');
    }

    //=============================================================
    //Get information about specified hook filter
    // $action_name : hook name
    // $target_priority : Hooked priority (targets all priorities if unspecified)
    //=============================================================
    public static function get_hook($action_name, $target_priority=null) {
        global $wp_filter;
        $hooks = array();
        if ( is_object( $wp_filter[$action_name] ) ) {
            foreach($wp_filter[$action_name]->callbacks as $priority => $callbacks ){
                if($target_priority !== null && $priority != absint($target_priority))
                    continue;
                foreach ($callbacks as $key => $filter) {
                    $type = $filter_id = '';
                    $hook = self::hook_inf($priority, $filter, $type, $filter_id);
                    if(!empty($hook)){
                        $hooks[$type][$filter_id] = $hook;
                    }
                }
            }
        }
        return $hooks;
    }  

    //=============================================================
    //Removes the hook with the specified filter identification ID
    // $action_name : hook name
    // $remove_ids : ID for identifying filters to be removed (separate with commas if multiple)
    // $target_priority : Hooked priority (targets all priorities if unspecified)
    //=============================================================
    public static function remove_hook($action_name, $remove_ids, $target_priority=null) {
        global $wp_filter;
        if ( is_object( $wp_filter[$action_name] ) && !empty($remove_ids) ) {
            foreach($wp_filter[$action_name]->callbacks as $priority => $callbacks ){
                if($target_priority !== null && $priority != absint($target_priority))
                    continue;
                foreach ($callbacks as $key => $filter) {
                    $type = $filter_id = '';
                    $hook = self::hook_inf($priority, $filter, $type, $filter_id);
                    if(!empty($hook) && false !== strpos($remove_ids, $filter_id)){
                        unset( $wp_filter[$action_name]->callbacks[$priority][$key] );
                    }
                }
            }
        }
    }     
    
    /*---------------------------------------------------------------
     * ID generation for hook filter identification
     * 
     * $file : PHP file name calling the target hook filter
     *          For Plugin, relative path from plugin slug.        eg: jetpack/class.jetpack-gutenberg.php
     *          For Theme, the relative path from theme slug name  eg: twentytwenty/functions.php
     *          Otherwise, the path is relative to ABSPATH.
     * $callback : Target hook filter callback function
     *          For global functions, specify the function name
     *          For anonymous functions (closures), specify 'closure'
     *          For class methods, specify 'class name::method name'
     * $priority : target hooked priority
     * $accepted_args : Number of arguments that the target hook filter function can take
     * $staticvarkeys: For closure, static variable name array.  eg: add_action('wp_head', function() use(&$svar1, &$svar2)... => array('svar1','svar2') 
     */    
    public static function filter_id( $file, $callback, $priority=10, $accepted_args=1, $staticvarkeys=array() ) {
        $strvarkeys = serialize($staticvarkeys);
        return( md5( "{$file}_{$callback}_{$priority}_{$accepted_args}_{$strvarkeys}" ) );
    }

    //Parsing hooked filter information
    private static function hook_inf($priority, $filter, &$type, &$filter_id) {
        $hook_inf = array();
        try {                
            $callback = '';
            $accepted_args = 1;                
            if ( isset( $filter['function'] ) ) {
                if ( isset( $filter['accepted_args'] ) ) {
                    $accepted_args = absint($filter['accepted_args']);                
                }
                $ref = null;
                $staticvarkeys = array();
                if (is_string( $filter['function'] )){
                    $callback = $filter['function'];    //global function
                    $ref = new ReflectionFunction( $filter['function'] );

                } elseif(is_object( $filter['function'] )){
                    $callback = 'closure';              //closure object
                    $ref = new ReflectionFunction( $filter['function'] );
                    //get closure use static variable 
                    $var = $ref->getStaticVariables();
                    if(is_array($var)){
                        $staticvarkeys = array_keys($var);
                    }

                } elseif(is_array( $filter['function'] )){
                    if (is_string( $filter['function'][0] )){   //static class
                        $class = $filter['function'][0];
                        $func = $filter['function'][1];
                        $callback = "$class::$func";
                        $ref = new ReflectionMethod( $class, $func);
                        
                    } elseif(is_object( $filter['function'][0] )){ //instance class
                        $class = get_class( $filter['function'][0] ); 
                        $func = $filter['function'][1];
                        $callback = "$class::$func";
                        $ref = new ReflectionMethod( $class, $func);
                    }                    
                } 
                if(is_object($ref)){
                    $file = wp_normalize_path( $ref->getFileName() );
                    $rootdir = wp_normalize_path( ABSPATH );
                    $plugin_root = wp_normalize_path( WP_PLUGIN_DIR ) . '/';
                    $theme_root  = wp_normalize_path( get_theme_root() ) . '/';
                    if ( strpos($file, $plugin_root) !== false) {
                        $type = 'plugins';
                        $file = str_ireplace( $plugin_root, '', $file);
                    } elseif ( strpos($file, $theme_root) !== false) {
                        $type = 'themes';
                        $file = str_ireplace( $theme_root, '', $file);
                    } else {
                        $type = 'core';
                        $file = str_ireplace( $rootdir, '', $file);
                    }
                    $filter_id = self::filter_id($file, $callback, $priority, $accepted_args, $staticvarkeys);
                    $hook_inf = array('file' => $file, 'callback' => $callback, 'priority' => $priority, 'args' => $accepted_args, 'staticvarkeys' => $staticvarkeys);
                }
            }
        } catch ( Exception $e ) {
            return null;
        }
        return $hook_inf;
    }    
}

上記コードを適当なファイル名(とりあえず hook-utilis.php )で保存します

このプログラムをご利用の場合は、名前空間(namespace)をあなたのプログラム用に適当に修正して使用してください

使用方法

WordPress のコアでは、フックにグローバル関数が使われているので、その解除では従来どおりの解除処理で特に問題なく実行できます

今まで使っていた remove_action を解除する場合
例えば、wp_head アクションフックにグローバル関数 my_action が登録された場合
add_action( 'wp_head', 'my_action' );
これを解除するには適切なタイミングで下記を実行していました」
remove_action( 'wp_head', 'my_action' );

ただ、プラグインやテーマではフックにグローバル関数が使われているとは限らず、無名関数等が使われている場合もあります。それを解除するにはどうにかしてその無名関数を特定させる必要があります

基本的な使い方を紹介します

テーマの function.php やプラグインのメイン処理でライブラリファイルを配置したパスを指定して読み込みます

require_once ( __DIR__ . '/hook-utility.php');

無名関数が使われている場合
例えば、wp_head に無名関数で登録されて場合
add_action( 'wp_head', function() { ..... });
これを解除するには下記のようにします(名前空間が celtislabv060 の場合)
$remove = celtislabv060Hook_util::filter_id( ファイル名, 'closure');
celtislabv060Hook_util::remove_hook( 'wp_head', $remove);

何をしているかというと、無名関数なので関数名がありません。
内部的にはクロージャーオブジェクトとして登録されているので、解除したいオブジェクトをなんらかの方法で指定してあげる必要があります

ライブラリでは、その代替手段として無名関数が書かれているファイル名を使用します

プログラムで言うと filter_id メソッドが該当します

static function filter_id( $file, $callback, $priority=10, $accepted_args=1, $staticvarkeys=array())

$file対象フックフィルターを呼び出しているPHPファイル名
Plugin の場合は プラグインスラッグからの相対パス
例: jetpack/class.jetpack-gutenberg.php
Theme の場合は テーマスラッグ名からの相対パス
例: twentytwenty/functions.php
$callback対象フックフィルターのコールバック関数
1. グローバル関数の場合は、その関数名を指定
2. 無名関数(クロージャ)の場合は、’closure’ と指定
3. クラスメソッドの場合は、’クラス名::メソッド名’ を指定
$priority対象フックフィルターの優先度
$accepted_args対象フックフィルターの関数が取ることのできる引数の数
$staticvarkeysクロージャ用: use で渡している変数があればその名を識別のために使用するオプション
例: add_action(‘wp_head’, function() use(&$svar1, &$svar2)… => array(‘svar1′,’svar2’)

この filter_id に 対象フックフィルターを呼び出している Plugin や theme スラッグからのPHPファイルの相対パス、コールバック関数(’closure’)、優先度、受け取る引数の数を指定して MD5 変換したユニークな識別コードを生成します

なぜファイル名で識別できるかというと、 ReflectionFunction という強力な関数を使うと関数が書かれているファイル名が取得できます。プログラムから直接クロージャーオブジェクトを指定するなんてことは出来ませんが、どのファイルから呼ばれているかわかれば無名関数を特定できるということです

優先度、引数の数は、フック関数で未指定の場合はデフォルト値が使われているので省略可能ですが、明示的に指定されている場合は解除する側でも同じ値を指定する必要があります(remove_action と同等)

remove_hook メソッドにアクション名とこのユニークな識別コードを指定して実行すれば、対象の無名関数をフックから解除することが出来ます

ここではアクションフックについて紹介しましたが、フィルターフックの無名関数に対しても同様の方法で解除することが出来ます

以上

アクションフックに登録されている無名関数の解除方法について紹介いたしました


まとめ記事紹介

go-to-top