Feel like clearing out your spam users? With the snippet below we can make your job much easier!
/**
* Adds a new column to the user management screen for displaying the number of comments.
*
* @param array $columns The existing columns in the user management screen.
*
* @return array The modified columns array with the new 'comments_count' column added.
*/functionemrikol_add_comments_column( array $columns ): array{
$columns['comments_count'] = esc_html__( text: 'Comments', domain: 'default' );
return $columns;
}
add_filter( 'manage_users_columns', 'emrikol_add_comments_column' );
/**
* Displays the number of comments for a user in the custom column.
*
* @param string $output The value to be displayed in the column.
* @param string $column_name The name of the custom column.
* @param int $user_id The ID of the user.
*
* @return string The updated value to be displayed in the column.
*/functionemrikol_show_comments_count( string $output, string $column_name, int $user_id ): string{
if ( 'comments_count' == $column_name ) {
$args = array(
'user_id' => $user_id,
'count' => true,
);
$comments_count = get_comments( args: $args );
return number_format_i18n( number: $comments_count );
}
return $output;
}
add_action( 'manage_users_custom_column', 'emrikol_show_comments_count', 10, 3 );
Code language:PHP(php)
This will add a “Comments” count to the WordPress user list so you can easily determine which users you can delete:
If you google debugging WordPress hooks you’ll find a lot of information.
About 1,180,000 results
Let’s add another one.
WordPress hooks are powerful, but also complex under the hood. There’s plenty of topics I could talk about here, but right now I’m only going to talk about speed. How long does it take for a hook to fire and return?
Some cool work in this area has already been done thanks to Debug Bar Slow Actions and Query Monitor, but outside of something like Xdebug or New Relic, you’ll have a hard time figuring out how long each individual hook callback takes without modifying either WordPress core or each hook call.
… or maybe not …
While doing some client debugging for my job at WordPress VIP (did I mention we’re hiring?) I came across the need to do this exact thing. I’ve just finished the code that will make this happen, and I’m releasing it into the wild for everyone to benefit.
What’s the problem we’re trying to solve? Well, this client has a sporadic issue where posts saving in the admin time out and sometimes fail. We’ve ruled out some potential issues, and are looking at a save_post hook going haywire.
Now I can capture every single save_post action, what the callback was, and how long it took. Here’s an example for this exact post:
We add an all action that will fire for every other action.
In the all callback, we make sure we’re looking for the correct hook.
We then build an array to store some data, use the $wp_filter global to fill out information such as the priority and the callback, and store the start time.
Next we have to add a new action to run for our hook right before the callback we want to time. We use the fact that, even though add_action() is supposed to use an int for the priority, it will also accept a string. We add new hooks, and re-prioritze all of the existing hooks with floats that are stringified.
This allows us to capture the start time and end time of each individual callback, instead of the priority group as a whole.
Of course, this does add a tiny bit of overhead, and could cause some problems if any other plugins use stringified hook priorities, or other odd issues–so be careful 🙂
Finally, here’s the code:
classVIP_Hook_Timeline{
public $hook;
public $callbacks = [];
public $callback_mod = 0.0001;
public $callback_mods = [];
publicfunction__construct( $hook ){
$this->hook = $hook;
add_action( 'all', array( $this, 'start' ) );
}
publicfunctionstart(){
// We only want to get a timeline for one hook.if ( $this->hook !== current_filter() ) {
return;
}
global $wp_filter;
// Iterate over each priority level and set up array.foreach( $wp_filter[ $this->hook ] as $priority => $callback ) {
// Make the mod counter if not exists.if ( ! isset( $this->callback_mods[ $priority ] ) ) {
$this->callback_mods[ $priority ] = $priority - $this->callback_mod;
}
// Make the array if not exists.if ( ! is_array( $this->callbacks[ $priority ] ) ) {
$this->callbacks[ $priority ] = [];
}
// Iterate over each callback and set up array.foreach( array_keys( $callback ) as $callback_func ) {
if ( ! is_array( $this->callbacks[ $priority ][ $callback_func ] ) ) {
$this->callbacks[ $priority ][ $callback_func ] = [ 'start' => 0, 'stop' => 0 ];
}
}
}
foreach( $this->callbacks as $priority => $callback ) {
foreach ( array_keys( $callback ) as $callback_func ) {
// Get data befmore we move things around.
$human_callback = $this->get_human_callback( $wp_filter[ $this->hook ][ $priority ][$callback_func] );
// Modify the priorities.
$pre_callback_priority = $this->callback_mods[ $priority ];
$this->callback_mods[ $priority ] = $this->callback_mods[ $priority ] + $this->callback_mod;
$new_callback_priority = $this->callback_mods[ $priority ];
$this->callback_mods[ $priority ] = $this->callback_mods[ $priority ] + $this->callback_mod;
$post_callback_priority = $this->callback_mods[ $priority ];
$this->callback_mods[ $priority ] = $this->callback_mods[ $priority ] + $this->callback_mod;
// Move the callback to our "new" priority.if ( $new_callback_priority != $priority ) {
$wp_filter[ $this->hook ]->callbacks[ strval( $new_callback_priority ) ][ $callback_func ] = $wp_filter[ $this->hook ]->callbacks[ $priority ][ $callback_func ];
unset( $wp_filter[ $this->hook ]->callbacks[ $priority ][ $callback_func ] );
if ( empty( $wp_filter[ $this->hook ]->callbacks[ $priority ] ) ) {
unset( $wp_filter[ $this->hook ]->callbacks[ $priority ] );
}
}
// Add a new action right before the one we want to debug to capture start time.
add_action( $this->hook, function( $value = null )use( $callback_func, $priority, $human_callback ){
$this->callbacks[ $priority ][ $callback_func ]['start'] = microtime( true );
// Uncomment this if you just want to dump data to the PHP error log, otherwise add your own logic.//$message = 'START: %d:%s, (Callback: `%s`)';// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log//error_log( sprintf( $message,// $priority,// $this->hook,// $human_callback,//) );// Just in case it's a filter, return.return $value;
}, strval( $pre_callback_priority ) );
// Add a new action right after the one we want to debug to capture end time.
add_action( $this->hook, function( $value = null )use( $callback_func, $priority, $human_callback ){
$this->callbacks[ $priority ][ $callback_func ]['stop'] = microtime( true );
// Uncomment this if you just want to dump data to the PHP error log, otherwise add your own logic.//$message = 'STOP: %d:%s, Taken %s (Callback: `%s`)';// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log//error_log( sprintf( $message,// $priority,// $this->hook,// $this->get_human_diff( $priority, $callback_func ),// $human_callback,//) );// Just in case it's a filter, return.return $value;
}, strval( $post_callback_priority ) );
}
}
}
publicfunctionget_human_callback( $callback ){
$human_callback = '[UNKNOWN HOOK]';
if ( is_array( $callback['function'] ) && count( $callback['function'] ) == 2 ) {
list( $object_or_class, $method ) = $callback['function'];
if ( is_object( $object_or_class ) ) {
$object_or_class = get_class( $object_or_class );
}
$human_callback = sprintf( '%s::%s', $object_or_class, $method );
} elseif ( is_object( $callback['function'] ) ) {
// Probably an anonymous function.
$human_callback = get_class( $callback['function'] );
} else {
$human_callback = $callback['function'];
}
return $human_callback;
}
publicfunctionget_start( $priority, $callback_func ){
return (float) $this->callbacks[ $priority ][ $callback_func ]['start'];
}
publicfunctionget_stop( $priority, $callback_func ){
return (float) $this->callbacks[ $priority ][ $callback_func ]['stop'];
}
publicfunctionget_diff( $priority, $callback_func ){
return (float) ( $this->get_stop( $priority, $callback_func ) - $this->get_start( $priority, $callback_func ) );
}
publicfunctionget_human_diff( $priority, $callback_func ){
$seconds = $this->get_diff( $priority, $callback_func );
// Seconds.if ( $seconds >= 1 || $seconds == 0 ) {
return number_format( $seconds, 3 ) . 's';
}
// Milliseconds.if ( $seconds >= .001 ) {
return number_format( $seconds * 1000, 3 ) . 'ms';
}
// Microseconds.if ( $seconds >= .000001 ) {
return number_format( $seconds * 1000000, 3 ) . 'μs';
}
// Nanoseconds.if ( $seconds >= .000000001 ) {
// WOW THAT'S FAST!return number_format( $seconds * 1000000000, 3 ) . 'ns';
}
return $seconds . 's?';
}
}
new VIP_Hook_Timeline( 'save_post' );Code language:PHP(php)
Of course, you could go through each post and check the meta, check for duplicates, and delete any of them but…
I think I’ve figured out a beast of an SQL query that will give you the Meta IDs of any duplicate post meta so that you can delete them with delete_metadata_by_mid().
SELECT * FROM (
SELECT meta_id FROM (
SELECT meta_table_1.* FROM wp_postmeta meta_table_1, wp_postmeta meta_table_2 WHERE (
meta_table_1.post_id = meta_table_2.post_id AND meta_table_1.meta_value = meta_table_2.meta_value AND meta_table_1.meta_key = meta_table_2.meta_key
) ORDER BY meta_table_2.post_id
) Table_All_Duplicates GROUP BY meta_id HAVING COUNT(*)>1
) Unique_Dupes WHERE meta_id NOT IN (
SELECT min(meta_id) AS min_meta_id FROM wp_postmeta GROUP BY post_id,meta_key,meta_value HAVING COUNT(post_id) > 1AND COUNT(meta_key) > 1AND COUNT(meta_value) > 1AND COUNT(meta_key >= 1)
);Code language:PHP(php)
An example:
mysql> SELECT * FROM wp_postmeta WHERE post_id = 919 AND meta_key = '_totally_duped';
+---------+---------+----------------+------------+
| meta_id | post_id | meta_key | meta_value |
+---------+---------+----------------+------------+
| 2123 | 919 | _totally_duped | 1 |
| 2124 | 919 | _totally_duped | 1 |
| 2125 | 919 | _totally_duped | 1 |
+---------+---------+----------------+------------+
3 rows inset (0.01 sec)
mysql> SELECT * FROM (SELECT meta_id FROM (SELECT meta_table_1.* FROM wp_postmeta meta_table_1, wp_postmeta meta_table_2 WHERE ( meta_table_1.post_id = meta_table_2.post_id AND meta_table_1.meta_value = meta_table_2.meta_value AND meta_table_1.meta_key = meta_table_2.meta_key ) ORDER BY meta_table_2.post_id) Table_All_Duplicates GROUP BY meta_id HAVING COUNT(*)>1) Unique_Dupes WHERE meta_id NOT IN (SELECT min(meta_id) AS min_meta_id FROM wp_postmeta GROUP BY post_id,meta_key,meta_value HAVING COUNT(post_id) > 1 AND COUNT(meta_key) > 1 AND COUNT(meta_value) > 1 AND COUNT(meta_key >= 1));
+---------+
| meta_id |
+---------+
| 2124 |
| 2125 |
+---------+
2 rows in set (0.03 sec)Code language:JavaScript(javascript)
I just want to note though, this is VERY slow. I did this because I’m currently working on trying to clean up a site that has about 24 million rows of postmeta, and I really hope that there’s some duplicates to delete. This one query has been running for about 12 hours now, and I have no idea when it will end.
So I guess I do have time for that after all ¯\_(ツ)_/¯
UPDATE:
I’ve discovered that it might be good to have more data for forensic investigation… so I’ve adjusted the query to also give me the post id, meta key, meta value length, and the original meta id:
mysql> SELECT post_id,group_concat(distinct meta_id) as meta_ids,min( distinct meta_id) as primary_meta_id,meta_key,char_length(meta_value) as meta_length,count( distinct meta_id ) * char_length(meta_value) as total_size FROM ( SELECT meta_table_1.* FROM wp_postmeta meta_table_1, wp_postmeta meta_table_2 WHERE (
-> meta_table_1.post_id = meta_table_2.post_id AND meta_table_1.meta_value = meta_table_2.meta_value AND meta_table_1.meta_key = meta_table_2.meta_key ) ORDER BY meta_table_2.post_id ) Table_All_Duplicates GROUP BY post_id,meta_key HAVING COUNT(*)>1;
+---------+----------------+-----------------+------------------+-------------+------------+
| post_id | meta_ids | primary_meta_id | meta_key | meta_length | total_size |
+---------+----------------+-----------------+------------------+-------------+------------+
| 915 | 2176,2177,2178 | 2176 | _totally_duped_2 | 1 | 3 |
| 919 | 2123,2124,2125 | 2123 | _totally_duped | 1 | 3 |
+---------+----------------+-----------------+------------------+-------------+------------+
2 rows inset (0.03 sec)Code language:JavaScript(javascript)
Last week an interesting issue came up for a client. Somehow a few posts were moved from Published to Draft. Unfortunately, post status isn’t stored in revisions so it’s unlikely we’ll ever know who or how it happened. Luckily there’s a simple solution I found to log all post status transitions in post meta: