Adding Screen Options To WordPress Admin Pages

Many of the built-in WordPress admin pages have a Screen Options drop-down tab on the top right of the page.   This is used to set how many items are shown in the default WordPress tables, such as the list of pages or posts.   Some of the WordPress admin interfaces also allow you to set which columns to show or hide on the page.

As a plugin or theme author it provides a consistent interface for your users if you utilize this built-in feature as a way to control the admin user experience for your users.   I’ve been busy adding this to the Store Locator Plus Manage Locations interface for version 4.8 and found the documentation to be lacking at best.   Here are my notes on how I hacked this into existence.

The Steps

There are a few basic steps to the process;  Tell WordPress you have screen options you want to track, Save those options when they are changed, Implement those options while rendering the page.

Seems easy enough, but they are all wired together in what is a little less elegant that some of the other components of WordPress Core.  Hopefully these notes will help you get starts on how to do this properly.

Tell WordPress You Have Options

This is done by adding options using the WordPress add_screen_option() method.   It should be called via the WordPress load-<hook> action hook to ensure the proper WP_Screen environment is active when this function is called.

load-{$page_hook} is called from within the base WordPress admin.php call.    This comes AFTER the admin_init hook. Which means your instructions to add screen options to an admin page can come much later than the “Save The Options” code below.   However, you will need to know the page you are hooked into.    Since most admin pages are accessed by adding an entry to the WordPress admin menu, you should be able to store the page hook when calling add_menu_page() or add_submenu_page() to attach your custom admin page to the WordPress admin interface.   This is also a good place to add your hook to setup the screen options.

In this example code I use a named array in $this->menu_items to build a list of several pages I want to add to the admin interface.   It has several properties including the name of the method that will render my page and the name of the method that will add my screen options.    It will also store the hook.     This SLP_AdminUI class is a simple “traffic cop” that loads other classes that do the work so we can isolate code to the specific page being processed.

class SLP_AdminUI extends SLP_BaseClass_Admin {

/**
 * Create the admin menu.
 *
 * Roles and Caps
 * manage_slp_admin
 * manage_slp_user
 *
 * WordPress Store Locator Plus Menu Roles & Caps
 *
 * Info : manage_slp_admin
 * Locations: manage_slp_user
 * Settings: manage_slp_admin
 * General: manage_slp_admin
 */
public function create_admin_menu() {

$this->menu_items['slp_manage_locations'] =
   array(
      'label'          => __( 'Locations', 'store-locator-le' ),
      'slug'           => 'slp_manage_locations',
      'class'          => $this,
      'function'       => 'renderPage_Locations',
       'screen_options' => 'slp_manage_locations_screen_options' ,
   );


// Attach Menu Items To Sidebar and Top Nav
//
foreach ( $this->menu_items as $slug => $menu_item ) {

  $this->menu_items[ $slug ][ 'hook' ] = add_submenu_page(
   SLPLUS_PREFIX,
   $menu_item['label'],
   $menu_item['label'],
   $slpCapability,
   $menu_item['slug'],
   array( $menu_item['class'], $menu_item['function'] )
  );
  add_action( 'load-' . $this->menu_items[ $slug ][ 'hook' ] , array( $this , $this->menu_items[ $slug ][ 'screen_options' ] ) );
}

}



/**
 * Render the Locations admin page.
 */
function renderPage_Locations() {
   $this->slplus->Admin_Locations->render_adminpage();
}

/**
 * Add Manage Locations Screen Options
 */
public function slp_manage_locations_screen_options() {
   $this->slplus->Admin_Locations->add_screen_options();
}



}

 

And the page-specific class that does the per-page magic…

class SLP_Admin_Locations extends WP_List_Table {

/**
 * Add screen options.
 */
 public function add_screen_options() {
 add_screen_option( 'per_page', array( 'option' => 'admin_locations_per_page' , 'label' => __( 'Locations Per Page' , 'store-locator-le' ) , 'default' => 50 ) );
 }
}

Save The Options

Options are saved via the generic ‘set-screen-option’ filter in WordPress.   The option is called via the set_screen_options() function as part of every single WordPress admin.php call.   At least there is top-of-function short circuits that will drop out of the process for a variety of reasons which means the majority of admin pages that do not have screen options will not process much code before moving on.   For those pages that do have screen options, here are the important notes.

You must add the filter to set the screen options by employing the WordPress set-screen-option filter your code.   Since this is fired off very early in the process, basically at the start of WordPress when the admin page is processed, you need to add the filter early in the WordPress feature stack.   Calling it too late, for example inside your WordPress ‘admin_menu’ action hook functions, will not help you.  I’ve hooked my set-screen-options to the WordPress init action.

set-screen-option filter call stack 2017-06-15_09-20-20.png

The call stack an variable references from WordPress Core 4.8 when the set-screen-option filter if applied.

Note: Finding the right point of where to hook your code into WordPress is a key to leveling up your WordPress Foo.   You can find the general order of action hooks on the old-school WordPress Codex.

WordPress Admin Hooks 2017-06-15_09-54-46.png

The filter passes in 3 parameters and you really should be using object-oriented programming techniques so the full call should look something like the code below.  The SLP_Actions init() method is called by the main plugin code via the WordPress init action hook: add_action( ‘init’ , array( $this->Actions , ‘init’ ) ).  The save_screen_options() method in this action class is only a traffic cop; it will load up the proper code module based on what page is being processed by WordPress and call the save_screen_options() method for that specific page.  In this example it is the Manage Locations page.

class SLP_Actions extends SLPlus_BaseClass_Object {

/**
 * Called when the WordPress init action is processed.
 *
 * Current user is authenticated by this time.
 */
public function init() {
 add_filter( 'set-screen-option' , array( $this , 'save_screen_options' ) , 10 , 3 );
}


/**
 * Save screen options.
 *
 * @param boolean $status false by default, return this to not save options
 * @param string  $option the option name, a key in user_meta
 * @param mixed   $value  the option value for user_meta
 * @return mixed
 */
public function save_screen_options( $status, $option, $value) {
   $this_page = isset( $_REQUEST['page'] ) ? $_REQUEST['page'] : '';
   switch ( $this_page ) {
      case 'slp_manage_locations':
         require_once( SLPLUS_PLUGINDIR . 'include/module/admin_tabs/SLP_Admin_Locations.php' );
         return $this->slplus->Admin_Locations->save_screen_options( $status, $option, $value );
         break;
   }
   return $status;
}

}

This SLP_Admin_Locations class is only loaded when the Locations tab is active in the Store Locator Plus plugin.    This ensures less memory is consumed when other WordPress admin pages are loaded and keeps our screen options isolated to just those we are interested in for this page.

class SLP_Admin_Locations extends WP_List_Table {
/**
 * Save screen options.
 *
 * @param $status
 * @param $option
 * @param $value
 * @return mixedf
 */
public function save_screen_options( $status, $option, $value) {
 $valid_options = array( 'admin_locations_per_page' );
 if ( in_array( $option , $valid_options ) ) return $value;
 return $status;
}
}

If the filter, save_screen_options() of SLP_Admin_Locations in this example, returns a value other than false then the set_screen_options() of WordPress will call

update_user_meta($user->ID, $option, $value);

Implement The Options

Implementing the options will depend on  your specific use case.   In my case I’m using the per_page setting to determine how many locations to show in a locations table.  It is a modified version of WP_List_Table so the typical prepare_items() examples do not apply.   In my class I’ve also decided to implement the WP_Screen per_page standard of fetching the user_meta with get_user_option() and fall back to the WP_Screen _options property fallbacks for default values.

Here is the function I call in various places to limit how many locations are shown in my table.

/**
 * Get the screen option per_page.
 * @return int
 */
private function get_screen_option_per_page() {
   $this->get_wp_screen();
 $option = $this->wp_screen->get_option( 'per_page', 'option' );
 if ( ! $option ) {
  $option = str_replace( '-', '_', "{$this->wp_screen->id}_per_page" );
 }

 $per_page = (int) get_user_option( $option );
 if ( empty( $per_page ) || $per_page < 1 ) {
  $per_page = $this->wp_screen->get_option( 'per_page', 'default' );
  if ( ! $per_page ) {
   $per_page = 20;
  }
 }
 return $per_page;
}

private function get_my_data() {
  $max_to_show = min( $this->get_screen_option_per_page() , $this->slplus->Location_Manager->location_limit );
}
Manage Locations Per Page Option 2017-06-15_15-27-19.png

The Manage Locations per-page screen option in SLP 4.8

Related Notes

Some interesting things I found along the way that may be worth noting…

The default per_page up/down toggle will fire the page update/save (form submit) every time someone clicks the toggle.  Ouch.   Typing a number an clicking the Apply button is a lot less server overhead.

The set-screen-option is a per-user setting.   It is stored in user_meta.    The $option value is the user meta key , and value is the value.     Using serialized options may be a good idea here but it will complicate code.  I’m a big fan of less data I/O, because that is a costly performance hit, over multiple get_option() or user_meta() requests that hammer the database.

The option name ‘per_page’ is super-special.   This single screen option name will alone trigger the Screen Options drop down to be active.   Sadly this is a standalone option in user_meta which means it will force at least TWO data I/O operations if you use this AND any other options (including a serialized option).  Ugggh, WordPress…

The Screen Options drop down will only render if you add a screen option with the name ‘per_page’, the page has meta boxes attached, or you have defined layout columns for the page. From the WP_Screen::show_screen_options() method:

$show_screen = ! empty( $wp_meta_boxes[ $this->id ] ) || $columns || $this->get_option( 'per_page' );

Browsers that disable JavaScript will not see the Screen Options drop-down.  WordPress will apply the hide-if-no-js class which hides elements that depend on JS to work properly.

WordPress Plugin Overhead

I recently wrote an article on why you should bury the bulk of your plugin PHP code at least TWO levels deep from the top-level directory.  The short version: WordPress builds a list of every single PHP file in the plugin directory and first level subdirectories then opens every single one and reads the first 8K scanning for the /* Name: string to build a list of plugins and their meta.  This is triggered by the get_plugins() function in WordPress Core.

More investigation of WP Core 4.7.3 get_plugins and how moving your php files at least 2 levels deep can impact performance is warranted.  Understanding when WordPress is going to run this function will help you determine what the overall performance impact will be and whether or not it is worth modifying your plugins.

My old-school computer background tells me that file opens and string regex comparisons are costly even on today’s fastest solid state drive servers.    The operating system overhead of opening and closing files tends to be costly especially when servers now have millions or billions of file address pointers to  sift through to find the proper memory or disk offsets.  If you are running on one of the millions of old-school spinning magnetic disk servers that are still out there the performance hit will be even more significant.

What Calls get_plugins()

wp-admin

import.php

Import WordPress Administration Screen

Direct load, no function/method.

$plugins = get_plugins( ‘/’ . $plugin_slug );

plugin-editor.php

Edit plugin editor administration panel.

Direct load, no function/method.

$plugins = get_plugins();

plugins.php

If $_REQUEST verify-delete is set.

if ( $folder_plugins = get_plugins( ‘/’ . $plugin_slug ) ) {

wp-admin/includes

ajax-actions.php

Administration API: Core Ajax handlers

function wp_ajax_update_plugin()

Ajax handler for updating a plugin.

$plugin_data = get_plugins( ‘/’ . $result[ $plugin ][‘destination_name’] );

class-language-pack-upgrader.php

Core class used for updating/installing language packs (translations)

public function get_name_for_update

Get the name of an item being updated.

$plugin_data = get_plugins( ‘/’ . $update->slug );

class-plugin-upgrader.php

Upgrade API: Plugin_Upgrader class

public function plugin_info

This isn’t used internally in the class, but is called by the skins.

Needs further investigation.   If I recall this method is widely used.

$plugin = get_plugins(‘/’ . $this->result[‘destination_name’]); //Ensure to pass with leading slash

Called by:
  • class-plugin-installer-skin.php : $plugin_file = $this->upgrader->plugin_info();
  • class-plugin-upgrader-skin.php : $this->plugin = $this->upgrader->plugin_info();
  • class-plugin-upgrader.php:  $this->skin->plugin_info = get_plugin_data( WP_PLUGIN_DIR . ‘/’ . $plugin, false, true);

class-wp-plugins-list-table.php

List Table API: WP_Plugins_List_Table class

public function prepare_items

Undocumented but in WP_List_Table this is what sets the list of items to show.

Looks like this will be called every single time you go to your plugins tab on WP Admin.

$all_plugins = apply_filters( ‘all_plugins’, get_plugins() );

dashboard.php

WordPress Dashboard Widget Administration Screen API

function wp_dashboard_plugins_output

If you have plugins setup on your WP Admin Dashboard and the transient is expired….

$plugin_slugs = array_keys( get_plugins() );

plugin-install.php

WordPress Plugin Install Administration API

function install_plugin_install_status

Determine the status we can perform on a plugin.

If the status is install.   Minimal impact as it only will happen during a plugin install.

$installed_plugin = get_plugins(‘/’ . $api->slug);

plugin.php

WordPress Plugin Administration API

function validate_plugin

Validate the plugin path. Checks that the file exists and is a valid file. See validate_file().

Happens during plugin activation.

$installed_plugins = get_plugins();

Called By
  • plugin.php activate_plugin()
  • plugin.php validate_active_plugins()

update.php

WordPress Administration Update API

function get_plugin_updates

Undocumented

This is likely called from the update plugins routine that runs every 12 hours by default. This runs every time the function is called.

$all_plugins = get_plugins();

Called by
  • update-core.php :list_plugin_updates()
  • class-wp-automatic-updater.php : send_email() when success

wp-includes

update.php

A simple set of functions to check our version 1.0 update service.

function wp_update_plugins

Check plugin versions against the latest versions hosted on WordPress.org.

$plugins = get_plugins();

Why Your WordPress Plugin Should Have Almost Nothing In The Main Folder

As we continue to roll out our Store Locator Plus SaaS service built on top of WordPress as our application foundation we continually refine our plugin, theme, and API architecture.    One of the issues I noticed while testing performance and stability is how WordPress Core handles plugins.    Though WordPress caches plugin file headers there are a lot of cases where it re-reads the plugin directories.

What do I mean by “read the plugin directories”?

WordPress has a function named get_plugin_data().   Its purpose is simple.  Read the metadata for a plugin and return it in an array.   This is where things like a plugin name, version, an author come from when you look at the plugins page.

However that “simple” function does some notable things when it comes to file I/O.   For those of you that are not into mid-level computer operations, file I/O is one of the most time consuming operations you can perform in a server based application.   It tends to have minimal caching and is slow even on an SSD drive.   On old-school rotating disks the performance impact can be notable.

So what are those notable things?

It is best described by outlining the process it goes through when called from the get_plugins() function in WordPress Core.

  • Find the WordPress plugins directory (easy and fast)
  • Get the meta for every single file in that directory using PHP readdir and then…
    • skip over any hidden files
    • skip over any files that do not end with .php
    • store every single file name in that directory in an array
  • Now take that list of every single file and do this…
    • if it is not readable, skip it (most will be on most servers so no saving time here)
    • call the WP Core get_plugin_data() method above and store the “answers” in an array , to do THAT, we need to do THIS for all of those files
      • call WP Core get_file_data() which does this..
        • OPEN the file with PHP fopen
        • Read the first 8192 characters
        • CLOSE the file
        • Translate all newline and carriage returns
        • Run WordPress Core apply_filters()
        • Do some array manipulation
        • Do a bunch of regex stuff to match the strings WordPress likes to see in headers like “Plugin Name:” or “Version:” and store the matching strings in an array.
        • Return that array which is the “answers” (plugin metadata) that WordPress is interested in.
    • take that array and store it in the global $wp_plugins variable with the plugin base name as the key to the named array.

In other words it incurs a LOT of overhead for every file that exists in your plugin root directory.

Cache or No Cache

Thankfully viewing a plugin page tends to fetch that data from a cache.   The cache is a string stored in the WP database so a single data fetch and a quick parsing of what is likely a JSON string and you get your plugins page listing fairly quickly.   However caches do expire.

More important to this discussion is the fact that there are a LOT functions in the WordPress admin panel and cron jobs that explicitly skip the cache and update the plugin data.  This runs the entire routine noted above to do that.

Designing Better Plugins

If you care about the performance impact of your plugins on the entire WordPress environment in which it lives, and you SHOULD, then you may want to consider a “minimalist top directory approach” to designing your plugins.

Best Practices on the Plugin Developer Handbook mentions “Folder Structure” and shows an example of having something like this as your plugin file setup:

However they don’t get into the performance details of WHY you should have an includes directory and what goes in there.

In my opinion, EVERYTHING that is not the main plugin-name.php or uninstall.php file should go in the ./includes directory.  Preferably in class files named after the class, but that is a discussion for another blog post.

If possible you may even want to try making plugin-name.php as minimalist as possible with almost no code. Even though the fread in WordPress Core get_file_data() only grabs the first 8192 characters, most of that content is “garbage” that it will not process because it is not part of the /* … */ commentary it is interested in.   If you can get your main plugin-name.php file to be something like 4K because it only includes the header plus a require_once( ‘./includes/main-code-loader.php’); or something similar the memory consumption, regular expression parser and other elements used by get_file_data() will have less work to do.

No matter what your code design, it is going to have some performance impact on WordPress.   My guess is it will be especially notable on sites that have 3,987 plugins installed and are running an “inline” WordPress update.   Ever wonder why that latest version of your premium (not hosted in the WordPress Plugin Directory) plugins don’t show up?   It could be because WordPress spent all the time granted to a single PHP process reading the first 8K of 39,870 files because all those plugins had a dozen-or-so files in the root directory.

Help yourself and help others.  Put the bulk of your plugin code in the includes folder.  The WordPress community will thank you.

 

Hacking WordPress – Articles About Coding Plugins and Themes

Recently I’ve begun building a music directory site.    I purchased a premium theme and the accompanying premium plugins.    Sadly, premium does not necessarily mean well-written.   Turns out this theme and the associate plugins do a few good things, look great on the surface and do a lot of really horrid things in the background.   If I were launching a personal site, like this bio blog, I’d not worry about it.   However the long term goal is to build a music directory that handles hundreds-of-thousands of page loads every month.   It needs to be fast without having to run it on IBM’s Watson to do so.

I’ve decided to fork the theme and the plugins and start hacking them into shape.    Coming soon will be the first in a series of “hacking WordPress” articles.   The first article will be about how to now load your plugin code on every single admin page on the site.

If you are writing WordPress Themes or plugins and have a question or an idea for a post, please let me know.   If I can’t answer the question myself I will try to find a guru that can.

 

Stop Loading Your Code On Every Admin Page

I see it every day.   It comes with the territory of supporting thousands of Store Locator Plus customers around the world.   We login to a customer’s site to find the reason our plugin is not working is because some other plugin is pissing their code all over our admin pages.

There is seldom, if ever, a reason that your plugin should be loading your code on my pages.   But it is far easier to load your code everywhere on every page.    It is far, far easier to write something like “if this is an admin page, load my code” than “if this is one of MY admin pages, load my code”.    Sadly it is really not much more work to do the second option and every single WordPress site your plugin runs on will be that  much faster.    Now if the other dozen plugins and the theme itself would be that smart WordPress sites everywhere would be a lot happier.

This video blog discusses how the plugin “HMLive Extensions” that I’ve forked and renamed to protect the guilty is “doing it wrong”.    It is loading a huge block of HTML code on EVERY SINGLE ADMIN PAGE then hiding it when it can with CSS trickery.  That only makes it slower and more bloated.

The take-aways from this video should be:

  • Use classes and object oriented programming techniques.
  • Do not use is_admin() by itself to decide whether or not to load your huge chunks of code.
  • Use class_exists() to test a class exists before defining it.
  • Use a simple array with slugs , in this case $_REQUEST[‘post_type’] variable values, to determine which admin pages to run your code on.

No this is not the “be-all-end-all” fix for this type of issue.    The posts and videos in this series are meant to be a general guide to shed some light on ways to improve code implementation and hopefully make your plugins and themes better citizens within WordPress.   Incur overhead only when needed and don’t stomp on other people’s code.

%d bloggers like this: