If you saw my last post, you may have noticed some cool embeds. These are coming from the Embed Extended plugin. This plugin takes OpenGraph data and treats it more like oEmbed data for WordPress. It works great with the block editor as well!
Tag: wordpress
-

Quick Tip: Script Debugging in WordPress
If you’re debugging core WordPress scripts, one thing you might run into is dealing with cached copies of the script. Due to how
script-loader.phpenqueues the core files, their versions are “hard coded” and short of editingscript-loader.phpas well, there’s a way to fix this via a filter:add_filter( 'script_loader_src', function( $src, $handle ) { if ( false !== strpos( $src, 'ver=' ) ) { $src = remove_query_arg( 'ver', $src ); $src = add_query_arg( array( 'ver', rawurlencode( uniqid( $handle ) . '-' ) ), $src ); } <span style="background-color: inherit; color: rgb(248, 248, 242); font-size: inherit; letter-spacing: -0.015em;">return $src;</span> }, -1, 2 );Code language: PHP (php)This will apply a unique
verargument to the core scripts on each refresh, so no matter what you’re editing you should get the most recent version from both any page cache you may have and also the browser cache (🤞).Also, don’t forget to enable
SCRIPT_DEBUGif you’re hacking away at core scripts to debug issues.I couldn’t find a good related image, so enjoy this delicious toilet paper.
-

Quick Tip: Disable WordPress Block Editor Fullscreen Mode
I don’t know why, but any time I edit posts on this site, the block editor always goes into fullscreen mode. Even if I disable it, the next time I edit a post or refresh, it goes right back. My preferences aren’t being saved.
Oh well, we can fix that with some PHP!
if ( is_admin() ) { function jba_disable_editor_fullscreen_by_default() { $script = "jQuery( window ).load(function() { const isFullscreenMode = wp.data.select( 'core/edit-post' ).isFeatureActive( 'fullscreenMode' ); if ( isFullscreenMode ) { wp.data.dispatch( 'core/edit-post' ).toggleFeature( 'fullscreenMode' ); } });"; wp_add_inline_script( 'wp-blocks', $script ); } add_action( 'enqueue_block_editor_assets', 'jba_disable_editor_fullscreen_by_default' ); }Code language: PHP (php)Many thanks to Jean-Baptiste Audras for this snippet he shared on his site last year 🥳
-

Deleting Old Post Revisions in WordPress with WP-CLI
Recently I’ve been working with a client who’s site we’re going to soon be migrating. To help with any downtime, we’ve been looking at reducing their database size, which is something around 50-60 gigabytes. After looking through the database, one easy win would be to purge as many post revisions as possible, since their
wp_poststable was about 50% revisions 😱We couldn’t just mass delete anything with a
revisionpost type though, because the client had some specific needs:- Keep all revisions for posts from the last 12 months.
- Keep all revisions that were made after the post was published.
- Keep a backup of all revisions deleted.
To do this, I crafted a custom WP-CLI command to purge these revisions. I’ve accidentally deleted the final version of the script, since it was only a one-run thing, but here’s an earlier version that could be a good starting point for anyone else that has a similar need to prune revisions:
/** * WP-CLI command that deletes pre-publish post revisions for posts older than 12 months. * * @subcommand prune-revisions [--live] [--verbose] */ public function prune_revisions( $args, $assoc_args ) { global $wpdb; $live = (bool) $assoc_args[ 'live' ]; $verbose = (bool) $assoc_args[ 'verbose' ]; $offset = 0; $limit = 500; $count = 0; $deletes = 0; if ( $live ) { $output_file = sanitize_file_name( sprintf( 'prune-revisions-backup_%d_%d.csv', get_bloginfo( 'name' ), current_time( 'timestamp', true ) ) ); $handle = fopen( $output_file, 'wb' ); if ( false === $handle ) { WP_CLI::error( sprintf( 'Error opening %s for writing!', $output_file ) ); } // Headers. $csv_headers = array( 'ID', 'post_author', 'post_date', 'post_date_gmt', 'post_content', 'post_title', 'post_excerpt', 'post_status', 'comment_status', 'ping_status', 'post_password', 'post_name', 'to_ping', 'pinged', 'post_modified', 'post_modified_gmt', 'post_content_filtered', 'post_parent', 'guid', 'menu_order', 'post_type', 'post_mime_type', 'comment_count', 'filter', ); fputcsv( $handle, $csv_headers ); } $count_sql = 'SELECT COUNT(ID) FROM ' . $wpdb->posts . ' WHERE post_type = "revision"'; $revision_count = (int) $wpdb->get_row( $count_sql, ARRAY_N )[0]; $progress = \WP_CLI\Utils\make_progress_bar( sprintf( 'Checking %s revisions', number_format( $revision_count ) ), $revision_count ); do { $sql = $wpdb->prepare( 'SELECT ID FROM ' . $wpdb->posts . ' WHERE post_type = "revision" LIMIT %d,%d', $offset, $limit ); $revisions = $wpdb->get_results( $sql ); foreach ( $revisions as $revision ) { $count++; $post_parent_id = wp_get_post_parent_id( $revision->ID ); // Fail on either 0 or false. if ( false == $post_parent_id ) { WP_CLI::warning( sprintf( 'Revision %d does not have a parent! Skipping!', $revision->ID ) ); continue; } $revision_modified = get_post_modified_time( 'U', false, $revision->ID ); $parent_publish_time = get_post_time( 'U', false, $post_parent_id ); if ( $parent_publish_time < current_time( 'timestamp') - ( MONTH_IN_SECONDS * 12 ) ) { // Post is older than 12 months, safe to delete pre-publish revisions. if ( $revision_modified < $parent_publish_time ) { if ( $live ) { // We're doing it live! WP_CLI::log( sprintf( 'Deleting revision %d for post %d. (%d%% done)', $revision->ID, $post_parent_id, ( $count / $revision_count ) * 100 ) ); // Backup data! $output = []; $data = get_post( $revision->ID ); // Validate the field is set, just in case. IDK how it couldn't be. foreach ( $csv_headers as $field ) { $output = isset( $data->$field ) ? $data->$field : ''; } fputcsv( $handle, $output ); $did_delete = wp_delete_post_revision( $revision->ID ); // Something went wrong while deleting the revision? if ( false === $did_delete || is_wp_error( $did_delete ) ) { WP_CLI::warning( sprintf( 'Revision %d for post %d DID NOT DELETE! wp_delete_post_revision returned:', $revision->ID, $post_parent_id ) ); } $deletes++; // Pause after lots of db modifications. if ( 0 === $deletes % 50 ) { if ( $verbose ) { WP_CLI::log( sprintf( 'Current Deletes: %d', $deletes ) ); } sleep( 1 ); } } else { // Not live, just output info. WP_CLI::log( sprintf( 'Will delete revision %d for post %d.', $revision->ID, $post_parent_id ) ); } } else { // Revision is after the post has been published. if ( $verbose ) { WP_CLI::log( sprintf( 'Post-Publish: Will NOT delete Revision %d for post %d.', $revision->ID, $post_parent_id ) ); } } } else { // Post is too new to prune. if ( $verbose ) { WP_CLI::log( sprintf( 'Too-New: Will NOT delete Revision %d for post %d.', $revision->ID, $post_parent_id ) ); } } } // Pause after lots of db reads. if ( 0 === $count % 5000 ) { if ( $verbose ) { WP_CLI::log( sprintf( 'Current Count: %d', $count ) ); } sleep( 1 ); } // Free up memory. $this->stop_the_insanity(); // Paginate. if ( count( $revisions ) ) { $offset += $limit; $progress->tick( $limit ); } else { WP_CLI::warning( 'Possible MySQL Error, retrying in 10 seconds!' ); sleep( 10 ); } } while ( $count < $revision_count ); $progress->finish(); if ( $live ) { fclose( $handle ); WP_CLI::success( sprintf( 'Deleted %d revisions', $deleted ) ); } else { WP_CLI::success( sprintf( 'Processed %d revisions', $revision_count ) ); } }Code language: PHP (php) -

Debugging WordPress Hooks: Speed
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_posthook going haywire.Now I can capture every single
save_postaction, what the callback was, and how long it took. Here’s an example for this exact post:START: 10:save_post, (Callback: `delete_get_calendar_cache`) STOP: 10:save_post, Taken 132.084μs (Callback: `delete_get_calendar_cache`) START: 10:save_post, (Callback: `sharing_meta_box_save`) STOP: 10:save_post, Taken 15.974μs (Callback: `sharing_meta_box_save`) START: 10:save_post, (Callback: `Jetpack_Likes_Settings::meta_box_save`) STOP: 10:save_post, Taken 19.073μs (Callback: `Jetpack_Likes_Settings::meta_box_save`) START: 10:save_post, (Callback: `SyntaxHighlighter::mark_as_encoded`) STOP: 10:save_post, Taken 19.073μs (Callback: `SyntaxHighlighter::mark_as_encoded`) START: 10:save_post, (Callback: `AMP_Post_Meta_Box::save_amp_status`) STOP: 10:save_post, Taken 16.928μs (Callback: `AMP_Post_Meta_Box::save_amp_status`) START: 20:save_post, (Callback: `Publicize::save_meta`) STOP: 20:save_post, Taken 428.915μs (Callback: `Publicize::save_meta`) START: 9000:save_post, (Callback: `Debug_Bar_Slow_Actions::time_stop`) STOP: 9000:save_post, Taken 11.921μs (Callback: `Debug_Bar_Slow_Actions::time_stop`) START: 1:save_post, (Callback: `The_SEO_Framework\Load::_update_post_meta`) STOP: 1:save_post, Taken 6.016ms (Callback: `The_SEO_Framework\Load::_update_post_meta`) START: 1:save_post, (Callback: `The_SEO_Framework\Load::_save_inpost_primary_term`) STOP: 1:save_post, Taken 1.535ms (Callback: `The_SEO_Framework\Load::_save_inpost_primary_term`) START: 10:save_post, (Callback: `delete_get_calendar_cache`) STOP: 10:save_post, Taken 179.052μs (Callback: `delete_get_calendar_cache`) START: 10:save_post, (Callback: `sharing_meta_box_save`) STOP: 10:save_post, Taken 247.002μs (Callback: `sharing_meta_box_save`) START: 10:save_post, (Callback: `Jetpack_Likes_Settings::meta_box_save`) STOP: 10:save_post, Taken 25.988μs (Callback: `Jetpack_Likes_Settings::meta_box_save`) START: 10:save_post, (Callback: `The_SEO_Framework\Load::delete_excluded_ids_cache`) STOP: 10:save_post, Taken 185.966μs (Callback: `The_SEO_Framework\Load::delete_excluded_ids_cache`) START: 10:save_post, (Callback: `SyntaxHighlighter::mark_as_encoded`) STOP: 10:save_post, Taken 15.020μs (Callback: `SyntaxHighlighter::mark_as_encoded`) START: 10:save_post, (Callback: `AMP_Post_Meta_Box::save_amp_status`) STOP: 10:save_post, Taken 12.875μs (Callback: `AMP_Post_Meta_Box::save_amp_status`) START: 20:save_post, (Callback: `Publicize::save_meta`) STOP: 20:save_post, Taken 377.893μs (Callback: `Publicize::save_meta`) START: 9000:save_post, (Callback: `Debug_Bar_Slow_Actions::time_stop`) STOP: 9000:save_post, Taken 14.067μs (Callback: `Debug_Bar_Slow_Actions::time_stop`)Code language: CSS (css)So how does it work?
- We add an
allaction that will fire for every other action. - In the
allcallback, we make sure we’re looking for the correct hook. - We then build an
arrayto store some data, use the$wp_filterglobal 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 anintfor the priority, it will also accept astring. We add new hooks, and re-prioritze all of the existing hooks withfloatsthat 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:
class VIP_Hook_Timeline { public $hook; public $callbacks = []; public $callback_mod = 0.0001; public $callback_mods = []; public function __construct( $hook ) { $this->hook = $hook; add_action( 'all', array( $this, 'start' ) ); } public function start() { // 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 ) ); } } } public function get_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; } public function get_start( $priority, $callback_func ) { return (float) $this->callbacks[ $priority ][ $callback_func ]['start']; } public function get_stop( $priority, $callback_func ) { return (float) $this->callbacks[ $priority ][ $callback_func ]['stop']; } public function get_diff( $priority, $callback_func ) { return (float) ( $this->get_stop( $priority, $callback_func ) - $this->get_start( $priority, $callback_func ) ); } public function get_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) - We add an











