While trying to write an options page and leverage persistent, admin-configurable settings for “JP User Registration Blacklist“, I found a maze of conflicting standards and options.
This is my attempt to provide a simple, straightforward, and comprehensive example, with source code that you can copy and implement with your own plugin.
Table of Contents
Special Thanks!
I pulled a great deal from Otto’s tutorial, so, THANKS, Otto!
http://ottopress.com/2009/wordpress-settings-api-tutorial/
Objective
I wrote a simple plugin, JP User Registration Blacklist, and there are a few static options, such as error messages, that I wanted to give the administrator the ability to change.
I also wanted the ability to have and modify persistent settings, where previous versions had been based on hard-coded values. Without an admin options page, the only option available would be to make database updates – not very user-friendly!
Some additional considerations:
- I wanted this to ‘look and feel’ like other plugins
- I wanted a ‘settings’ link, directly from the plugins page
- I wanted the plugin to automatically create and store a default set of persistent options (no configuration required)
- I wanted the ability to specify an extensible list of fields, and create generic, reusable code that I can use for other projects (or share for others)
Obviously, there is always more than one way to accomplish the objective, and I’m relatively new to PHP / WordPress, so there may be room for improvement or optimization. I’m always open to suggestions and polite feedback!
Also, I don’t want to present the impression that this is all copy and paste from other sources. I wrote a lot of my own code that I’m sharing, and I’ve put dozens of hours in to wrestling this thing in to submission.
Overview — How the heck does this thing work?
WordPress uses “hook” functions (callbacks) for everything. Building an admin options page consists of four main parts:
- Create a class that can be instantiated, that essentially encapsulates the admin options page, and all of its functions
- Create a hook. Some plugins seem to be able to directly call a config page by it’s URL (call the PHP file directly), while others hook in to random places within the WordPress menu structure.
- Once the hook is in place, the admin options page can be called via its slug. An additional hook creates the “Settings” link, that many plugins have on the Plugins page.
- Hook the “Activation” event, to create and store default options in the database, if they don’t already exist.
Create a Class
The class is a maze of callbacks. Everything is a callback. Rather than get in to the guts of what is calling what else, I’ve made this as simple as defining all the field properties in an array, and the class takes care of all the registration, formatting, and updating.
Currently, default values are handled as discreet functions for each field. Again, there are multiple ways to do this, including ‘switch’, or include the default values as part of the field definition itself. If there is a better way to do it, from a performance or efficiency standpoint, I’m open to suggestions.
Create a Hook
I could not get the “direct PHP call” approach to work. Again, I may just not know the secret recipe, or may be overlooking something simple.
I settled for creating a WordPress menu hook, which results in a “page slug” that can be referenced from anywhere.
I’ve seen plugins that extend the ‘Settings’ menu, or even add their own top-level menu. I’m a purist, and I’d prefer not to screw up the WordPress menu interface with a bunch of random junk (even my own random junk), so I opted to extend the ‘Plugins’ menu, which logically makes sense, anyway.
Create the “Plugins” Link
By default, you get nothing. Many plugins add a “Settings” link, so that you can conveniently activate, and then configure your plugin, without having to hunt around for where someone hid the options menu.
Additionally, some plugins have “Settings” on the left (as the first option), and others add it to the end, on the right.
I opted for the right, but I left the code remarked out, to add it to the left.
Registering the menu hook allows you to specify a slug for the admin options page itself, which you can then point to from the link.
Store Defaults Upon Activation
Unless your plugin executes as a persistent object, it won’t have the ability to create and store options, unless the admin configures them in advance.
Hooking the “Activation” event allows your plugin to store a basic set of default options in the database, without instantiating anything, or having to create a special class.
Your defaults should allow the plugin to work immediately, without admin-intervention. Remember that this is a live site, and users may be hitting it as you are messing around with the plugin.
Persistent options can be stored as individual entries, or you can create an array and store the entire array as one entry in the database. I opted for the latter, which is cleaner and easier to clean up, but can be a bit harder to tweak or debug. Doing it this way allows the plugin to be reset, worst case, by deleting one row from the database.
Tip: Deactivate Plugin Before Editing!
I ran in to a couple of situations where, through syntax error, or simply not knowing what I was doing, I couldn’t save, couldn’t go back to the plugins page, and couldn’t access the dashboard.
I ended up having to connect to the host and edit the file directly in order to recover.
To save yourself the time and hassle, just be sure to disable the plugin each time, before you edit one of the plugin files.
Structure
I attempted to put most of the code for the admin options in a separate file, which will provide some level of isolation for future updates, simplifies troubleshooting, and keeps the main file as clean as possible.
For this example, let’s assume your plugin is called “My Cool Plugin”:
- Name: “My Cool Plugin” – displayed on the Plugins page
- Path: “wp-content/plugins/mycoolplugin”
- Main file: “mycoolplugin.php”
- Short name: “MCP” (My Cool Plugin), will be used to ensure that all variables and functions are globally-unique.
- Admin page slug: “MCP-settings” is the “slug” (shortcut URL) for the new Admin options page
Let’s assume your admin options code is then stored in:
- Settings file: “mycoolplugin_settings.php” (same path)
The rest of this article is a walkthrough for modifying your plugin (with code samples included).
At the end, a recap is provided, with actual complete skeleton files that you can copy and customize for your next plugin project.
Modify your Plugin
A few simple things need to be added to the main (“mycoolplugin.php”) file:
- At the top of the file:
- Include the settings file (one-liner , using the PHP require_once function)
- Read the global options array from the database (another one-liner, using the WordPress get_option function)
- At the bottom of the file:
- Add the callback hook for the settings link (WordPress plugin_action_links_$plugin — this syntax is fairly standard, and calls your specified function when the “Plugin Action Links” are being generated)
- Instantiate the admin options page (instantiate the class)
- Create the “Activation” event hook, to store default options in the database
Include Settings File
This is a simple one-liner, that should be the first actual code line in your file, after the Plugin header.
require_once basename( __FILE__,'.php').'_settings.php';
Explanation:
- require_once – This is a PHP function that includes another file, but ONLY loads it, if it’s NOT already loaded. From a code standpoint, the code in the second file appears virtually inline, from the point where “require_once” appears in the main file — which is why it needs to appear at the top of the file. Functions and the admin class that are referenced in the main file have to be declared before they are used, and thus they have to appear before they are called. If we put require_once at the end of the file, we’d get a bunch of errors, because the references would not have been declared yet, relative to the point where they’re called in the main file.
- basename() – PHP function that returns just the filename portion of a /path/filename.ext, and can additionally trim off some portion of the end of the file. In this case, we’re trimming off the “.php” extension.
- __FILE__ – PHP macro that expands to the path and filename.
- For our example, __FILE__ equates to /wp-content/plugins/mycoolplugin/mycoolplugin.php
- basename() by itself then returns “mycoolplugin.php”, but we’ve added ,”.php” as the second option, telling basename to trim “.php” from the end.
- All of this returns “mycoolplugin”
- Dot (“.”) concatenates strings in PHP. Here, we use “dot” to add “_settings.php” to the end, resulting in “mycoolplugin_settings.php”, which is the file name included by require_once.
This executes everything in “mycoolplugin_settings.php”, before anything else executes. This ensures functions and our admin class are declared early, and available when they are called elsewhere.
Read Global Options Array
$MCP_opts=get_option( MCP_option_name(), MCP_default_all() );
Explanation:
- $MCP_opts – This variable will hold the array of options
- get_option() – This is a WordPress function, that reads an option (setting) from the database.
- MCP_option_name() – This is a function that we will build later, in the mycoolplugin_settings.php file. Its job is to return the name of the option to read from the database.
- MCP_default_all() – This is a function that we will build later, in the mycoolplugin_settings.php file. Its job is to return an array containing the default values for all fields.
This results in an array, $MCP_opts, that has fieldname => value pairs for each setting.
Callback Hook for “Settings” Link
$plugin = plugin_basename(__FILE__); if ( is_admin() ) add_filter("plugin_action_links_$plugin", 'MCP_settingslink' );
Explanation:
- $plugin – Stores a string containing the name of the action that will be triggered
- plugin_basename() – WordPress function that returns ONLY the plugin folder and file name. For our example, this is mycoolplugin/mycoolplugin.php
- is_admin() – WordPress function, that returns true if the user has administrative rights.
- if() – PHP function. If the “if” criteria is true, PHP executes the next line of code (add_filter).
- The purpose of if( is_admin() ) is to ONLY display the “Settings” link for administrative users. Normal users will see the “Deactivate” and “Edit” links, assuming they have the proper permissions.
- add_filter() – WordPress generic event hook. A filter is code that is added to the end of existing code, triggered for a particular action. In this case, when the “Plugin Action Links” are being generated, we want to attach a function callback, “MCP_settingslink” to the end of the existing process. When the “normal” process completes, it will now additionally execute the code inside the MCP_settingslink function.
- MCP_settingslink() – This is a function that we will build later, in mycoolplugin_settings.php.
- Using macros, such as __FILE__, to build the event handler means that the code is portable. To use this in another plugin, only the callback function name (MCP_settingslink) needs to be modified.
- The structure of the link is:
http://whateversite.com/wp-admin/plugins.php?page=MCP-settings
- MCP-settings is the Admin options page slug, created when we hook the menu (later)
If the current user is an administrator, they will see a “Settings” link on the Plugins page, beneath the plugin.
Instantiate the Admin Options Class
if( is_admin() ) $my_settings_page = new MyCoolSettingsPage();
Explanation:
- if( is_admin() ) – As with the “Settings” link, the admin page will ONLY be built and instantiated for an administrative user, including the appearance of the Plugin manu hook.
- $my_settings_page – Variable holds the instance of the Admin Options class
- MyCoolSettingsPage() – class defined later in mycoolplugin_settings.php
- new – keyword that specifies a new instance of the class.
Once instantiated, the “Plugins” menu hook will be in place. Administrators will be able to navigate to “Plugins..My Cool Plugin Settings”. The actual menu hooks are located in the code for the class — we will address that later. The manu hook also allows the code to specify a slug for the admin page: “MCP-settings” (this is covered later).
Create the Activation Event Hook
function MCP_activate() { $MCP_opts=get_option(MCP_option_name(), MCP_default_all() ); update_option(MCP_option_name(),$MCP_opts); } register_activation_hook( __FILE__, 'MCP_activate' )
Explanation:
- function – declare a PHP function, that we call later. In this case, this will be our callback function that gets executed when the administrator clicks “Activate” to activate the plugin (or any time the plugin is activated)
- MCP_activate() – our function name, referenced by the activation hook
- $MCP_opts – this line is identical to the second line we added, whose purpose is to read the settings out of the database, optionally supplying proper default values. This ensures that $MCP_opts contains an array of valid settings — either we read them from the database because they already exist, or we created a set of valid defaults.
- update_option() – A WordPress function to update an existing option, or create it if it doesn’t exist. The entire array is stored as one row in the database, regardless of the number of fields in the array.
- register_activation_hook() – “hooks” (chains) additional code, to execute when the plugin is activated. Note the similar syntax to the Plugin Action Links hook, and also the MCP_activate function name that we declared previously.
The purpose of this section is to ensure that one good, clean copy of the options gets stored in the database, allowing the plugin to start work immediately, without requiring admin configuration.
Sample Plugin File
wp-content/plugins/mycoolplugin/mycoolplugin.php
View or Download the file here: mycoolplugin.txt
Customize the Settings File
The settings file in this example is mycoolplugin_settings.php
Keeping the Admin Options Page class and supporting declarations in a separate file makes the code more modular, and reduces clutter in the main plugin file.
The following must be customized in the settings file:
- Customize the fields (field name, label, help text, and input type)
- Customize the default value functions
- Customize the page (friendly) title function
- Customize the input validation
- Customize the “Settings” link
The rest of the file should be OK, left as default. Don’t worry about the inner workings — just take it as a “black box” for now, and if you want to delve in to it further, check out Otto’s excellent tutorial (see above).
Customize the Fields
I set this up so that the field definitions are array-driven
This is probably the most complicated step.
There is a function called MCP_field_list() that returns an array containing all field names and parameters.
The list of fields is stored in an array, each element of which is another array containing the form label text, help text, and the type of input.
Let’s assume we want the following three options on the admin options page (we’ll review the purpose of “label”, “help”, and “type” in a minute):
- age
- label = Your Age
- help = Type in your age, in years
- type = text
- bio
- label = Short Biography
- help = Type in a little about yourself
- type = textarea
- title
- label = Job Title
- help = Type in your job title
- type = text
We need to construct a function that produces a 2D array structure:
$fields = MCP_field_list(); --- RESULT --- $fields['age'] => 'label' => 'Your Age' 'help' => 'Type in your age, in years' 'type' => 'text' $fields['age']['type'] == 'text' $fields['bio']['label'] == 'Short Biography' Etc...
Here is the function that produces the structure above:
function MCP_field_list() { // *** Field name and label *** $fields = array( 'age' => array( 'label' => 'Your Age', 'help' => 'Type in your age, in years', 'type' => 'text' ), 'bio' => array( 'label' => 'Short Biography', 'help' => 'Type in a little about yourself', 'type' => 'textarea' ), 'title' => array( 'label' => 'Job Title', 'help' => 'Type in your job title', 'type' => 'text' ) ); return $fields; }
You are basically building an array of arrays (a list of lists). Watch the punctuation! A comma (orange) separates each array element, but also separates each array from the next field. Don’t use a trailing comma after the last item, or you’ll get a syntax error. If you get frustrated, or the above is too complex, try this simplified syntax:
function MCP_field_list() { $agefields = array( 'label' => 'Your Age', 'help' => 'Type in your age, in years', 'type' => 'text' ); $biofields = array( 'label' => 'Short Biography', 'help' => 'Type in a little about yourself', 'type' => 'textarea' ); $titlefields = array( 'label' => 'Job Title', 'help' => 'Type in your job title', 'type' => 'text' ); $fields = array( 'age' => $agefields, 'bio' => $biofields, 'title' => $titlefields ); return $fields; }
In either case, to add a field, just copy and paste one section at a time.
Explanation:
- Field Name (yellow) – This is the key name that’s stored in the database
- Label – This value is displayed to the left of the input area, prompting the user for what type of information is expected
- Help – Help text appears below the input area, providing additional information to the user
- Type – “text” makes a standard single-line text input field. “textarea” makes a multiline input box.
Save and test. Once you have the fields displayed properly, move on to the next section.
Customize Default Values
There are multiple ways to accomplish creating an array of default values, including simply having a single array() constructor.
I chose to use individual functions for each default value, which provides maximum flexibility. For example, let’s say you build a default value function that produces a specific pattern or type of data — most simply, let’s say you need a random number between 1 and 10:
function default_random() { return mt_rand(1,10); }
If you have multiple fields that all require the same TYPE of default value, but not necessarily the SAME value, you can invoke the one function multiple times.
If you have a better / preferred method, please feel free to do it differently.
The way I have this organized, there is a default value function for each field, and then a default_all function that builds an array from the individual default return values.
function MCP_default_age() { return(mt_rand(10,50)); } function MCP_default_bio() { return "I grew up in Awesometown, USA, lived an awesome life, and my interests include being awesome"; } function MCP_default_title() { return "Sr. Awesome Guy"; } function MCP_default_all() { $opt=array( 'age' => MCP_default_age(), 'bio' => MCP_default_bio(), 'title' => MCP_default_title() ); return $opt; }
Tasks:
- Build a default value function for each field
- Add all fields to the array in the MCP_default_all() function
Customize the Page (Friendly) Title Function
The return value from this function appears in the title bar, the page header, the menu option name, and several other places. This should be the “friendly” name, that you want the users to see.
For our example above, this should be “My Cool Plugin”
function MCP_page_title()
{
return 'My Cool Plugin';
}
Customize the Input Validation
For most purposes, the standard validation process will be sufficient. The standard validation process ensures that each field is defined, and is not blank. If either of these is false, it assigns the default value (from the default_all function)
In this example, we want to make sure ‘age’ is non-zero, so we’ve added the following validation code:
// *** AGE, NUMERIC *** $k='age' // * Convert to numeric * $new_input[$k] = absint( $new_input[$k] ); // * Correct zero value * if ( $new_input[$k]==0 ) $new_input[$k]=$defaults[$k];
Explanation:
- $k=’age’ – For simplicity, use a variable for the field name. This keeps the code modular, and avoids copy-and-paste errors, if you copy your validation code for other fields.
- $new_input – this array holds the “sanitized” input values
- absint() – PHP function to convert a string to an integer. If the string is a number, the resulting integer will be that number. If the string is a string, the resulting integer will be 0.
- if() – Checks for a 0 value, and assigns the default if so. The default_age function produces a random number between 10 and 60 (see above), so the result will be that Age is assigned a new random number between 10 and 60.
Customize the Settings Link
By default, the only links you get on the Plugins page underneath your plugin are “Deactivate” and “Edit”. This function, called by our hook in the main file, adds a “Settings” link underneath your plugin.
The Settings link should redirect to the slug for your admin options page:
function MCP_settingslink($links) { $settings_link = '<a href="plugins.php?page='.MCP_page_name().'">Settings</a>'; //array_unshift($links, $settings_link); $links[]=$settings_link; return $links; }
This function is the callback when we hook the Plugin Action Link event. Since we are using a function, MCP_page_name() to generate the link, this code is very portable. We don’t actually have to change anything, but the Settings link can be customized as you desire.
There is no real standard, but most plugins that I’ve seen add the “Settings” link to the right of the other links. However, if you uncomment the “array_unshift” line, and comment the “$links[]” line, then your “Settings” link will appear on the left — unshift prepends a value at the beginning of the array.
If you have multiple option pages, you can add more links, or links to external resources.
You can also change the format of the Settings link – for example, you could change the color using standard HTML.
Sample Settings File
wp-content/plugins/mycoolplugin/mycoolplugin_settings.php
View or Download the file here: mycoolplugin_settings
OK, So What Now?
Now, you have the ability to create persistent option values that you can use to build your own awesome plugins, and an admin options page, through which administrators can edit them.
Assuming our samples above, here is a simple shortcode function that you could drop directly in to your main plugin file:
function bio_func( $atts ){ global $MCP_opts; $bio = 'Age: '.$MCP_opts['age'].', '.$MCP_opts['title']; $bio = $bio.'<BR>'.$MCP_opts['bio'].'<BR>'; return $bio; } add_shortcode( 'bio', 'bio_func' );
Explanation:
- global $MCP_opts – pulls in the global variable, MCP_opts, that we used at the beginning of our plugin file to store options read from the database
- add_shortcode – WordPress function to create shortcodes. Adding [bio] to your post will now print out a short biography
Sample:
Age: 99, Zoo Keeper
I grew up near zoos, and I love being in zoos, so I became a zookeeper. I’ve been a zookeeper at 5 different zoos over the past 20 years.
Conclusion
I pulled together bits and pieces from several sources, and wrote about 150 lines of my own code, to produce an extremely simple, plug-and-play, admin options page that you can use for your plugin.
If you find this useful, or have a better way of doing something I’ve described above, please feel free to leave me a comment!