如何通过自定义元字段过滤“管理用户”页面上的用户?


9

问题

WP似乎已在将其用于过滤用户列表之前删除了我的查询变量的值。

我的密码

此函数将自定义列添加到我的Users表上/wp-admin/users.php

function add_course_section_to_user_meta( $columns ) {
    $columns['course_section'] = 'Section';
    return $columns;
}
add_filter( 'manage_users_columns', 'add_course_section_to_user_meta' );

此函数告诉WP如何填充列中的值:

function manage_users_course_section( $val, $col, $uid ) {
    if ( 'course_section' === $col )
        return get_the_author_meta( 'course_section', $uid );
}
add_filter( 'manage_users_custom_column', 'manage_users_course_section' );

这将Filter在Users表上方添加一个下拉列表和按钮:

function add_course_section_filter() {
    echo '<select name="course_section" style="float:none;">';
    echo '<option value="">Course Section...</option>';
    for ( $i = 1; $i <= 3; ++$i ) {
        if ( $i == $_GET[ 'course_section' ] ) {
            echo '<option value="'.$i.'" selected="selected">Section '.$i.'</option>';
        } else {
            echo '<option value="'.$i.'">Section '.$i.'</option>';
        }
    }
    echo '<input id="post-query-submit" type="submit" class="button" value="Filter" name="">';
}
add_action( 'restrict_manage_users', 'add_course_section_filter' );

此函数更改用户查询以添加my meta_query

function filter_users_by_course_section( $query ) {
    global $pagenow;

    if ( is_admin() && 
         'users.php' == $pagenow && 
         isset( $_GET[ 'course_section' ] ) && 
         !empty( $_GET[ 'course_section' ] ) 
       ) {
        $section = $_GET[ 'course_section' ];
        $meta_query = array(
            array(
                'key'   => 'course_section',
                'value' => $section
            )
        );
        $query->set( 'meta_key', 'course_section' );
        $query->set( 'meta_query', $meta_query );
    }
}
add_filter( 'pre_get_users', 'filter_users_by_course_section' );

其他资讯

它会正确创建我的下拉菜单。当我选择一个课程部分并单击时Filter,页面会刷新并course_section显示在URL中,但它没有任何关联的值。如果我检查HTTP请求,则表明它使用正确的变量值提交了,但是302 Redirect似乎有一个剥离了我选择的值。

如果我course_section通过直接在URL中键入变量来提交变量,则过滤器将按预期工作。

我的代码大致基于Dave Court的代码

我也尝试使用此代码将我的查询变量列入白名单,但没有运气:

function add_course_section_query_var( $qvars ) {
    $qvars[] = 'course_section';
    return $qvars;
}
add_filter( 'query_vars', 'add_course_section_query_var' );

我正在使用WP 4.4。有什么想法为什么我的过滤器不起作用?


仅供参考,我在WP Trac网站上添加了一张票证,该票证将阻止开发人员跳过以下所述的任何麻烦。
morphatic

Answers:


6

更新2018-06-28

尽管下面的代码通常可以正常工作,但这是WP> = 4.6.0的代码的重写(使用PHP 7):

function add_course_section_filter( $which ) {

    // create sprintf templates for <select> and <option>s
    $st = '<select name="course_section_%s" style="float:none;"><option value="">%s</option>%s</select>';
    $ot = '<option value="%s" %s>Section %s</option>';

    // determine which filter button was clicked, if any and set section
    $button = key( array_filter( $_GET, function($v) { return __( 'Filter' ) === $v; } ) );
    $section = $_GET[ 'course_section_' . $button ] ?? -1;

    // generate <option> and <select> code
    $options = implode( '', array_map( function($i) use ( $ot, $section ) {
        return sprintf( $ot, $i, selected( $i, $section, false ), $i );
    }, range( 1, 3 ) ));
    $select = sprintf( $st, $which, __( 'Course Section...' ), $options );

    // output <select> and submit button
    echo $select;
    submit_button(__( 'Filter' ), null, $which, false);
}
add_action('restrict_manage_users', 'add_course_section_filter');

function filter_users_by_course_section($query)
{
    global $pagenow;
    if (is_admin() && 'users.php' == $pagenow) {
        $button = key( array_filter( $_GET, function($v) { return __( 'Filter' ) === $v; } ) );
        if ($section = $_GET[ 'course_section_' . $button ]) {
            $meta_query = [['key' => 'courses','value' => $section, 'compare' => 'LIKE']];
            $query->set('meta_key', 'courses');
            $query->set('meta_query', $meta_query);
        }
    }
}
add_filter('pre_get_users', 'filter_users_by_course_section');

我结合了@birgire和@cale_b的一些想法,这些想法还提供了以下值得阅读的解决方案。具体来说,我:

  1. 使用了$which添加到的变量v4.6.0
  2. 通过使用可翻译字符串(例如,i18n)使用了最佳实践 __( 'Filter' )
  3. 交换循环的(更时尚呢?) array_map()array_filter()range()
  4. 用于sprintf()生成标记模板
  5. 使用方括号数组表示法代替 array()

最后,我在较早的解决方案中发现了一个错误。这些解决方案总是青睐顶部<select>在底部<select>。因此,如果您从顶部下拉菜单中选择了一个过滤器选项,然后又从底部下拉菜单中选择了一个,则过滤器仍将仅使用顶部的任何值(如果不为空)。此新版本纠正了该错误。

更新2018-02-14

自WP 4.6.0起已修复问题更改已记录在正式文档中。不过,下面的解决方案仍然有效。

导致问题的原因(WP <4.6.0)

问题在于该restrict_manage_users动作被调用了两次:一次在“用户”表上方,一次在“表”下方。这意味着将select创建两个具有相同名称的下拉菜单。Filter单击该按钮时,第二个select元素中的任何值(即表下方的值)都会覆盖第一个元素中的值,即表上方的值。

如果您想深入了解WP源,restrict_manage_users则从内部触发该操作WP_Users_List_Table::extra_tablenav($which),该功能可创建本机下拉菜单来更改用户角色。该函数借助$which变量的帮助,该变量告诉它是创建select表单的上方还是下方,并允许其为两个下拉菜单提供不同的name属性。不幸的是,$which变量没有传递给restrict_manage_users操作,因此我们必须想出另一种方式来区分我们自己的自定义元素。

@Linnea所建议的,执行此操作的一种方法是添加一些JavaScript来捕获Filter单击并同步两个下拉列表的值。我选择了现在将要描述的纯PHP解决方案。

如何修复

您可以利用将HTML输入转换为值数组,然后过滤该数组以除去所有未定义值的功能。这是代码:

    function add_course_section_filter() {
        if ( isset( $_GET[ 'course_section' ]) ) {
            $section = $_GET[ 'course_section' ];
            $section = !empty( $section[ 0 ] ) ? $section[ 0 ] : $section[ 1 ];
        } else {
            $section = -1;
        }
        echo ' <select name="course_section[]" style="float:none;"><option value="">Course Section...</option>';
        for ( $i = 1; $i <= 3; ++$i ) {
            $selected = $i == $section ? ' selected="selected"' : '';
            echo '<option value="' . $i . '"' . $selected . '>Section ' . $i . '</option>';
        }
        echo '</select>';
        echo '<input type="submit" class="button" value="Filter">';
    }
    add_action( 'restrict_manage_users', 'add_course_section_filter' );

    function filter_users_by_course_section( $query ) {
        global $pagenow;

        if ( is_admin() && 
             'users.php' == $pagenow && 
             isset( $_GET[ 'course_section' ] ) && 
             is_array( $_GET[ 'course_section' ] )
            ) {
            $section = $_GET[ 'course_section' ];
            $section = !empty( $section[ 0 ] ) ? $section[ 0 ] : $section[ 1 ];
            $meta_query = array(
                array(
                    'key' => 'course_section',
                    'value' => $section
                )
            );
            $query->set( 'meta_key', 'course_section' );
            $query->set( 'meta_query', $meta_query );
        }
    }
    add_filter( 'pre_get_users', 'filter_users_by_course_section' );

奖励:PHP 7重构

由于我对PHP 7感到很兴奋,以防万一您在PHP 7服务器上运行WP,因此这是一个使用null合并运算符??的简短而性感的版本:

function add_course_section_filter() {
    $section = $_GET[ 'course_section' ][ 0 ] ?? $_GET[ 'course_section' ][ 1 ] ?? -1;
    echo ' <select name="course_section[]" style="float:none;"><option value="">Course Section...</option>';
    for ( $i = 1; $i <= 3; ++$i ) {
        $selected = $i == $section ? ' selected="selected"' : '';
        echo '<option value="' . $i . '"' . $selected . '>Section ' . $i . '</option>';
    }
    echo '</select>';
    echo '<input type="submit" class="button" value="Filter">';
}
add_action( 'restrict_manage_users', 'add_course_section_filter' );

function filter_users_by_course_section( $query ) {
    global $pagenow;

    if ( is_admin() && 'users.php' == $pagenow) {
        $section = $_GET[ 'course_section' ][ 0 ] ?? $_GET[ 'course_section' ][ 1 ] ?? null;
        if ( null !== $section ) {
            $meta_query = array(
                array(
                    'key' => 'course_section',
                    'value' => $section
                )
            );
            $query->set( 'meta_key', 'course_section' );
            $query->set( 'meta_query', $meta_query );
        }
    }
}
add_filter( 'pre_get_users', 'filter_users_by_course_section' );

请享用!


那么您的解决方案在4.6.0之后仍然可以正常工作吗?使用最新版本的wordpress有没有更简单的方法?我似乎找不到像今年这样的指南
Jeremy Muckel

1
@JeremyMuckel您问题的简短答案是“是”。我的旧解决方案仍然有效。我几个月来一直在生产中定期使用它,并且我的大多数站点都更新到了最新的稳定WP版本(当前为4.9.6)。话虽如此,我提供了一个使用新补丁的更新解决方案,并且还修复了我先前解决方案中的一个细微错误。
形态

这很有用,但是您在“如何修复”和“奖金:PHP 7重构”下的表单代码丢失了,</select>我还发现要使其正常工作,我必须将其放在<form method="get">选择菜单之前,并</form>在过滤器按钮之后。
cogdog19年

@cogdog很好地抓住了丢失的</select>标签!我加了 奇怪的是,您需要将其包装为一个,<form>因为整个页面都以一种大格式包装,并且此代码被注入到其中部。很高兴您能正常使用。:)
morphistic

4

在核心中,底部的输入名称标有实例编号,例如new_role(顶部)和new_role2(底部)。这是用于类似命名约定的两种方法,即course_section1(top)和course_section2(bottom):

方法1

由于$which变量(topbottom)没有传递给该restrict_manage_users挂钩,我们可以通过创建自己的挂钩版本来解决该问题:

让我们创建wpse_restrict_manage_users可以访问$which变量的动作挂钩:

add_action( 'restrict_manage_users', function() 
{
    static $instance = 0;   
    do_action( 'wpse_restrict_manage_users', 1 === ++$instance ? 'top' : 'bottom'  );

} );

然后,我们可以使用以下方法将其挂接:

add_action( 'wpse_restrict_manage_users', function( $which )
{
    $name = 'top' === $which ? 'course_section1' : 'course_section2';

    // your stuff here
} );

我们现在必须$namecourse_section1顶部,并course_section2底部

方法#2

让我们连接到restrict_manage_users,以显示下拉列表,每个实例使用不同的名称:

function add_course_section_filter() 
{
    static $instance= 0;    

    // Dropdown options         
    $options = '';
    foreach( range( 1, 3 ) as $rng )
    {
        $options = sprintf( 
            '<option value="%1$d" %2$s>Section %1$d</option>',
            $rng,
            selected( $rng, get_selected_course_section(), 0 )
        );
    }

    // Display dropdown with a different name for each instance
    printf( 
        '<select name="%s" style="float:none;"><option value="0">%s</option>%s</select>', 
        'course_section' . ++$instance,
        __( 'Course Section...' ),
        $options 
    );


    // Button
    printf (
        '<input id="post-query-submit" type="submit" class="button" value="%s" name="">',
        __( 'Filter' )
    );
}
add_action( 'restrict_manage_users', 'add_course_section_filter' );

我们在其中使用了核心功能selected()和辅助功能:

/**
 * Get the selected course section 
 * @return int $course_section
 */
function get_selected_course_section()
{
    foreach( range( 1, 2) as $rng )
        $course_section = ! empty( $_GET[ 'course_section' . $rng ] )
            ? $_GET[ 'course_section' . $rng ]
            : -1; // default

    return (int) $course_section;
}

然后,当我们在pre_get_users操作回调中检查选定的课程部分时,也可以使用它。


这是一种令人着迷的方法。我从来没有这样使用过static关键字(仅在类中)。$instance执行此操作时是否会成为全局变量?您是否需要担心变量名冲突?我还喜欢创建一个新动作的技术,该动作可以背负现有动作。谢谢!
morphatic

这种方法有时可能很方便,并且在核心中用于例如计算短码(画廊,播放列表,音频)实例。此处的静态变量范围不会与全局变量范围混淆。静态变量的值将在这些函数调用之间保留,而局部变量则不会。我搜索并发现了这个不错的教程,其中包含更多详细信息。@morphatic
birgire

4

我在Wordpress 4.4和Wordpress 4.3.1中都测试了您的代码。在4.4版中,我遇到与您完全相同的问题。但是,您的代码在4.3.1版本中可以正常工作!

我认为这是一个Wordpress错误。我不知道是否有报道。我认为该错误背后的原因可能是“提交”按钮发送了两次查询变量。如果查看查询变量,您将看到列出了两次course_section,一次具有正确的值,一次为空。

编辑:这是JavaScript解决方案

只需将其添加到主题的functions.php文件中,然后将NAME_OF_YOUR_INPUT_FIELD更改为输入字段的名称即可!由于WordPress自动在管理端加载jQuery,因此您不必排队任何脚本。此代码段只是将更改侦听器添加到下拉列表输入中,然后自动更新另一个下拉列表以匹配相同的值。这里有更多解释。

add_action( 'in_admin_footer', function() {
?>
<script type="text/javascript">
    var el = jQuery("[name='NAME_OF_YOUR_INPUT_FIELD']");
    el.change(function() {
        el.val(jQuery(this).val());
    });
</script>
<?php
} );

希望这可以帮助!


感谢Linnea。是的,我发现了同样的事情,当您单击Filter它时,它会提交正确的值,但是随后再次重定向回页面,这次删除了该值。我的猜测是,它是某种安全“功能”,可以防止提交随机的,可能是恶意的值,但我不知道如何解决。叹。
morphatic

哦!我弄清楚了为什么var出现两次。因为存在一个下拉列表,所以users表的上方和下方都具有相同的name属性。如果我使用表格下面的下拉列表进行过滤,则可以正常工作。由于该字段位于其上方的字段之后,因此它的null值将覆盖较早的字段。嗯....
形态

好发现!我试图找出重复的来源。我认为可能需要一些JavaScript才能解决此问题。在提交表单之前,将另一个下拉菜单设置为相同的值。
Linnea Huxford

1

这是一个不同的Javascript解决方案,可能对某些人有帮助。就我而言,我只是完全删除了第二个(底部)选择列表。我发现我从不使用底部输入...

add_action( 'in_admin_footer', function() {
    ?>
    <script type="text/javascript">
        jQuery(".tablenav.bottom select[name='course_section']").remove();
        jQuery(".tablenav.bottom input#post-query-submit").remove();
    </script>
    <?php
} );

1

非JavaScript解决方案

为选择提供一个“数组样式”的名称,如下所示:

echo '<select name="course_section[]" style="float:none;">';

然后,传递两个参数(从表的顶部和底部开始),现在采用已知的数组格式。

然后,可以在pre_get_users函数中像这样使用该值:

function filter_users_by_course_section( $query ) {
    global $pagenow;

    // if not on users page in admin, get out
    if ( ! is_admin() || 'users.php' != $pagenow ) {
        return;
    } 

    // if no section selected, get out
    if ( empty( $_GET['course_section'] ) ) {
        return;
    }

    // course_section is known to be set now, so load it
    $section = $_GET['course_section'];

    // the value is an array, and one of the two select boxes was likely
    // not set to anything, so use array_filter to eliminate empty elements
    $section = array_filter( $section );

    // the value is still an array, so get the first value
    $section = reset( $section );

    // now the value is a single value, such as 1
    $meta_query = array(
        array(
            'key' => 'course_section',
            'value' => $section
        )
    );

    $query->set( 'meta_key', 'course_section' );
    $query->set( 'meta_query', $meta_query );
}

0

另一种解决方案

您可以将过滤器选择框放在单独的文件中,例如 user_list_filter.php

require_once 'user_list_filter.php'在您的动作回调函数中使用

user_list_filter.php 文件:

<select name="course_section" style="float:none;">
    <option value="">Course Section...</option>
    <?php for ( $i = 1; $i <= 3; ++$i ) {
        if ( $i == $_GET[ 'course_section' ] ) { ?>
        <option value="<?=$i?>" selected="selected">Section <?=$i?></option>
        <?php } else { ?>
        <option value="<?=$i?>">Section <?=$i?></option>
        <?php }
     }?>
</select>
<input id="post-query-submit" type="submit" class="button" value="Filter" name="">

并在您的操作回调中:

function add_course_section_filter() {
    require_once 'user_list_filter.php';
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.