Blurbette Plugin: Admin Control Panel
In this chapter I’ll proceed a bit differently: I’ll make a few changes to WPCX_Blurbette_Def and WPCX_Blurbette_Registry first, then define the new class below.
The sole aim of this admin control panel is to update a list of options. WordPress provides a Settings API that provides output helpers and manages groups of individual settings; but for no particular reason, I prefer to assign them all to a single option as an array.
Here is how the control panel will look:
Notice most of the options are checkbox on/off values. This means the form simply doesn’t submit them if unchecked, and upon processing they won’t show up in our options array. So a simple empty() or isset() serves to check their values. I’ll decide on some key names now, which I can use in building the form and processing the data:
- use_shortcode
- use_widget
- use_copy_metabox
- copied_everywhere (can have 3 values: ‘y’, ‘n’ or undefined)
- clear_on_deactivate
Definition / Registry Updates
I’ll start by adding a new constant to WPCX_Blurbette_Def:
class WPCX_Blurbette_Def { const POST_TYPE = 'wpcx_blurbette'; const TEXT_DOMAIN = 'wpcx_blurbette'; const OPTION_METAKEY = 'wpcx_blurbette_options';
Then, a new options property in WPCX_Blurbette_Registry:
class WPCX_Blurbette_Registry { public $objects = array(); public $options = array();
… and a simple addition to its __construct() method:
function __construct() { spl_autoload_register( array( $this, 'class_autoloader' ) ); $this->options = get_option( WPCX_Blurbette_Def::OPTION_METAKEY ); WPCX_Blurbette_Def::do_all_hooks(); WPCX_Blurbette_Shortcode::do_all_hooks(); add_action( 'widgets_init', array( $this, 'register_widget' ) ); add_action( 'plugins_loaded', array( $this, 'instantiate_the_rest' ) ); }
Now, the Registry fetches and stores all the settings once, and all the classes can look to its options array for values.
While I’m editing WPCX_Blurbette_Registry, I’ll add a few conditionals. First, in the __construct() method:
function __construct() { spl_autoload_register( array( $this, 'class_autoloader' ) ); $this->options = get_option( WPCX_Blurbette_Def::OPTION_METAKEY ); WPCX_Blurbette_Def::do_all_hooks(); if ( ! empty( $this->options['use_shortcode'] ) ) : WPCX_Blurbette_Shortcode::do_all_hooks(); endif; if ( ! empty( $this->options['use_widget'] ) ) : add_action( 'widgets_init', array( $this, 'register_widget' ) ); endif; add_action( 'plugins_loaded', array( $this, 'instantiate_the_rest' ) ); }
Now, the shortcode and widget methods are only hooked if the appropriate settings are enabled. This is a clear benefit to using a Registry coding pattern and autonomous classes — it’s easy enable or disable whole sections on a single conditional.
One more change — if shortcodes are disabled, then it doesn’t make sense to provide the TinyMCE button which outputs them:
function instantiate_the_rest() { if ( is_admin() ) : if ( ! empty( $this->options['use_shortcode'] ) ) : $this->register( 'WPCX_Blurbette_MCE_WithDialog', array( 'registry' => $this, 'name' => 'WPCXBlurbette', 'row' => 2, 'js' => plugin_dir_url( __FILE__ ) . 'mce/mce_blurbette.js' ) ); endif; endif; }
Immediately after that, I’ll instantiate the new class, which I’ll build below. I’ll place it within the is_admin() conditional too:
function instantiate_the_rest() { if ( is_admin() ) : if ( ! empty( $this->options['use_shortcode'] ) ) : $this->register( 'WPCX_Blurbette_MCE_WithDialog', array( 'registry' => $this, 'name' => 'WPCXBlurbette', 'row' => 2, 'js' => plugin_dir_url( __FILE__ ) . 'mce/mce_blurbette.js' ) ); endif; $this->register( 'WPCX_Blurbette_AdminPanel', array( 'registry'=>$this ) ); endif; }
I’ve decided to pass a registry arg, pointing back to this WPCX_Blurbette_Registry instance, for ease of access.
The Admin Panel Class
This class is little more than a standard web form. Taking it in chunks:
<?php class WPCX_Blurbette_AdminPanel { private $registry; const CAPABILITY = 'manage_options'; const NONCE_NAME = 'wpcx_blurbette_admin_nonce'; const NONCE_VALUE = __FILE__; function __construct( $options ) { $this->registry = $options['registry']; add_action( 'admin_menu', array( $this, 'register_submenu' ) ); } function register_submenu() { add_submenu_page( 'options-general.php', __( 'Blurbette Settings', WPCX_Blurbette_Def::TEXT_DOMAIN ), __( 'Blurbettes', WPCX_Blurbette_Def::TEXT_DOMAIN ), self::CAPABILITY, WPCX_Blurbette_Def::POST_TYPE . '_mgmt', array( $this, 'control_panel' ) ); }
As mentioned above, the $registry property points back to the main WPCX_Blurbette_Registry instance that registered this object. With this property defined, all the settings can be accessed herein as $this->registry->options.
Also, I’ve decided that only users with manage_options capability will have access to this control panel.
Then, I’ve defined a nonce name and value for use below. Notice I’ve set the nonce value to __FILE__; internally WordPress adds the logged-in user id, so this nonce value is completely unique to this class (file) and user.
The add_submenu_page() function (documented here) is purely standard. Next is its referenced callback method, which outputs the page — starting with the first half:
function control_panel() { if ( isset( $_POST[self::NONCE_NAME] ) ) : $update_message = $this->save_posted_settings(); endif; $settings = $this->registry->options; ?><div class="wrap"> <h2><?php echo $GLOBALS['title'] ?></h2> <?php if ( isset( $update_message ) ) : ?> <div id="setting-error-settings_updated" class="<?php echo ( stripos( $update_message, 'saved' ) === false )? 'error' : 'updated'; ?> settings-error"> <p><strong><?php echo $update_message ?></strong></p></div> <?php endif; ?> <form name="wpcx_blurbette_optsform" method="post" action="<?php echo add_query_arg( array( 'page' => $GLOBALS['plugin_page'] ), admin_url( $GLOBALS['pagenow'] ) ); ?>"> <?php wp_nonce_field( self::NONCE_VALUE, self::NONCE_NAME ); ?>
This first checks if there is $_POSTed data by looking for the presence of a defined field (I’ve chosen the nonce field), and if so calls save_posted_settings() (below), which returns a message. If that message is defined, then present a standardized <div> element containing it — HTML classname “saved” if successful, “error” if not.
Then, the form tag, which defines this page as its action. This page’s URL is constructed by adding a ‘page’ query arg (value defined in add_submenu_page(), above) to the global variable $GLOBALS['pagenow']. Followed by the the all-important wp_nonce_field().
The second half of the method is simply the HTML table that presents the information, styled to match other WordPress panels, and the closing tags:
<table class="form-table"> <tr valign="top"> <th scope="row"><?php _e( 'Shortcodes', WPCX_Blurbette_Def::TEXT_DOMAIN ) ?></th> <td><label><input type="checkbox" name="blurbette_opt[use_shortcode]" value="1" <?php checked( $settings['use_shortcode'], 1 ); ?> /> <?php _e( 'Use Shortcodes', WPCX_Blurbette_Def::TEXT_DOMAIN ) ?></label> </td> </tr> <tr valign="top"> <th scope="row"><?php _e( 'Widgets', WPCX_Blurbette_Def::TEXT_DOMAIN ) ?></th> <td><label><input type="checkbox" name="blurbette_opt[use_widget]" value="1" <?php checked( $settings['use_widget'], 1 ); ?> /> <?php _e( 'Use Widget', WPCX_Blurbette_Def::TEXT_DOMAIN ) ?></label> </td> </tr> <tr valign="top"> <th scope="row"><?php _e( 'Copy', WPCX_Blurbette_Def::TEXT_DOMAIN ) ?></th> <td><label><input type="checkbox" name="blurbette_opt[use_copy_metabox]" value="1" <?php checked( $settings['use_copy_metabox'], 1 ); ?> /> <?php _e( 'Enable "Copy" button in post panels', WPCX_Blurbette_Def::TEXT_DOMAIN ) ?></label> <blockquote><?php _e( 'New Blurbettes are available:', WPCX_Blurbette_Def::TEXT_DOMAIN ) ?> <br /> <label><input type="radio" name="blurbette_opt[copied_everywhere]" value="y" <?php checked( $settings['copied_everywhere'], 'y' ); ?> /> <?php _e( 'Everywhere', WPCX_Blurbette_Def::TEXT_DOMAIN ) ?></label> <label><input type="radio" name="blurbette_opt[copied_everywhere]" value="n" <?php checked( $settings['copied_everywhere'], 'n' ); ?> /> <?php _e( 'Nowhere', WPCX_Blurbette_Def::TEXT_DOMAIN ) ?></label> </blockquote> </td> </tr> <tr valign="top"> <th scope="row"><?php _e( 'Deactivation', WPCX_Blurbette_Def::TEXT_DOMAIN ) ?></th> <td><label><input type="checkbox" name="blurbette_opt[clear_on_deactivate]" value="1" <?php checked( $settings['clear_on_deactivate'] , 1 ); ?> /> <?php _e( 'Clear everything when deactivating this plugin', WPCX_Blurbette_Def::TEXT_DOMAIN ) ?></label> </td> </tr> </table> <?php submit_button(); ?> </form> </div> <?php }
Lastly, the save_posted_settings() method, followed by a filter callback:
protected function save_posted_settings() { if ( ! wp_verify_nonce( $_POST[self::NONCE_NAME], self::NONCE_VALUE ) ) : return __( 'Sorry, there was an error on this form page.', WPCX_Blurbette_Def::TEXT_DOMAIN ); endif; if ( ! current_user_can( self::CAPABILITY ) ) : return __( 'Unauthorized.', WPCX_Blurbette_Def::TEXT_DOMAIN ); endif; $filtered_input = filter_var( $_POST['blurbette_opt'], FILTER_CALLBACK, array( 'options' => array( $this, 'single_wordchar' ) ) ); $settings_changed = false; foreach( $filtered_input as $key=>$value ) : if ( $value != $this->registry->options[$key] ) : $settings_changed = true; break; endif; endforeach; $success = update_option( WPCX_Blurbette_Def::OPTION_METAKEY, $filtered_input ); if ( $success || ! $settings_changed ) : $this->registry->options = $filtered_input; return __( 'Settings saved.', WPCX_Blurbette_Def::TEXT_DOMAIN ); else: return __( 'Sorry, settings were not updated.', WPCX_Blurbette_Def::TEXT_DOMAIN ); endif; } protected function single_wordchar( $val ) { $filtered = preg_replace( '/[^\w]/', '', $val ); return $filtered[0]; } } // end class WPCX_Blurbette_AdminPanel
As with all form-processing routines, input must be checked and filtered. So first this method bails if wp_nonce_verify() fails, and if the user lacks proper capability.
Then, input is filtered. For variety, I’ve chosen the PHP filter_var() function with the callback single_wordchar(). Every input value should be either a 1 or a single letter, so the callback gets to be very strict, stripping out all non-word characters and returning only the first character.
I’ve also defined a variable $settings_changed for a specific reason: if update_option() fails for some reason, it returns false — however it also returns false if the updated option is unchanged (new value matches the database value). By checking if settings have changed, I can combine it with update_options()‘s returned value to determine success or failure.
Recapping the code
Here is the entire WPCX_Blurbette_AdminPanel class:
<?php class WPCX_Blurbette_AdminPanel { private $registry; const CAPABILITY = 'manage_options'; const NONCE_NAME = 'wpcx_blurbette_admin_nonce'; const NONCE_VALUE = __FILE__; function __construct( $options ) { $this->registry = $options['registry']; add_action( 'admin_menu', array( $this, 'register_submenu' ) ); } function register_submenu() { add_submenu_page( 'options-general.php', __( 'Blurbette Settings', WPCX_Blurbette_Def::TEXT_DOMAIN ), __( 'Blurbettes', WPCX_Blurbette_Def::TEXT_DOMAIN ), self::CAPABILITY, WPCX_Blurbette_Def::POST_TYPE . '_mgmt', array( $this, 'control_panel' ) ); } function control_panel() { if ( isset( $_POST[self::NONCE_NAME] ) ) : $update_message = $this->save_posted_settings(); endif; $settings = $this->registry->options; ?><div class="wrap"> <h2><?php echo $GLOBALS['title'] ?></h2> <?php if ( isset( $update_message ) ) : ?> <div id="setting-error-settings_updated" class="<?php echo ( stripos( $update_message, 'saved' ) === false )? 'error' : 'updated'; ?> settings-error"> <p><strong><?php echo $update_message ?></strong></p></div> <?php endif; ?> <form name="wpcx_blurbette_optsform" method="post" action="<?php echo add_query_arg( array( 'page' => $GLOBALS['plugin_page'] ), admin_url( $GLOBALS['pagenow'] ) ); ?>"> <?php wp_nonce_field( self::NONCE_VALUE, self::NONCE_NAME ); ?> <table class="form-table"> <tr valign="top"> <th scope="row"><?php _e( 'Shortcodes', WPCX_Blurbette_Def::TEXT_DOMAIN ) ?></th> <td><label><input type="checkbox" name="blurbette_opt[use_shortcode]" value="1" <?php checked( $settings['use_shortcode'], 1 ); ?> /> <?php _e( 'Use Shortcodes', WPCX_Blurbette_Def::TEXT_DOMAIN ) ?></label> </td> </tr> <tr valign="top"> <th scope="row"><?php _e( 'Widgets', WPCX_Blurbette_Def::TEXT_DOMAIN ) ?></th> <td><label><input type="checkbox" name="blurbette_opt[use_widget]" value="1" <?php checked( $settings['use_widget'], 1 ); ?> /> <?php _e( 'Use Widget', WPCX_Blurbette_Def::TEXT_DOMAIN ) ?></label> </td> </tr> <tr valign="top"> <th scope="row"><?php _e( 'Copy', WPCX_Blurbette_Def::TEXT_DOMAIN ) ?></th> <td><label><input type="checkbox" name="blurbette_opt[use_copy_metabox]" value="1" <?php checked( $settings['use_copy_metabox'], 1 ); ?> /> <?php _e( 'Enable "Copy" button in post panels', WPCX_Blurbette_Def::TEXT_DOMAIN ) ?></label> <blockquote><?php _e( 'New Blurbettes are available:', WPCX_Blurbette_Def::TEXT_DOMAIN ) ?> <br /> <label><input type="radio" name="blurbette_opt[copied_everywhere]" value="y" <?php checked( $settings['copied_everywhere'], 'y' ); ?> /> <?php _e( 'Everywhere', WPCX_Blurbette_Def::TEXT_DOMAIN ) ?></label> <label><input type="radio" name="blurbette_opt[copied_everywhere]" value="n" <?php checked( $settings['copied_everywhere'], 'n' ); ?> /> <?php _e( 'Nowhere', WPCX_Blurbette_Def::TEXT_DOMAIN ) ?></label> </blockquote> </td> </tr> <tr valign="top"> <th scope="row"><?php _e( 'Deactivation', WPCX_Blurbette_Def::TEXT_DOMAIN ) ?></th> <td><label><input type="checkbox" name="blurbette_opt[clear_on_deactivate]" value="1" <?php checked( $settings['clear_on_deactivate'] , 1 ); ?> /> <?php _e( 'Clear everything when deactivating this plugin', WPCX_Blurbette_Def::TEXT_DOMAIN ) ?></label> </td> </tr> </table> <?php submit_button(); ?> </form> </div> <?php } protected function save_posted_settings() { if ( ! wp_verify_nonce( $_POST[self::NONCE_NAME], self::NONCE_VALUE ) ) : return __( 'Sorry, there was an error on this form page.', WPCX_Blurbette_Def::TEXT_DOMAIN ); endif; if ( ! current_user_can( self::CAPABILITY ) ) : return __( 'Unauthorized.', WPCX_Blurbette_Def::TEXT_DOMAIN ); endif; $filtered_input = filter_var( $_POST['blurbette_opt'], FILTER_CALLBACK, array( 'options' => array( $this, 'single_wordchar' ) ) ); $settings_changed = false; foreach( $filtered_input as $key=>$value ) : if ( $value != $this->registry->options[$key] ) : $settings_changed = true; break; endif; endforeach; $success = update_option( WPCX_Blurbette_Def::OPTION_METAKEY, $filtered_input ); if ( $success || ! $settings_changed ) : $this->registry->options = $filtered_input; return __( 'Settings saved.', WPCX_Blurbette_Def::TEXT_DOMAIN ); else: return __( 'Sorry, settings were not updated.', WPCX_Blurbette_Def::TEXT_DOMAIN ); endif; } protected function single_wordchar( $val ) { $filtered = preg_replace( '/[^\w]/', '', $val ); return $filtered[0]; } } // end class WPCX_Blurbette_AdminPanel