We’re going to cover creating a working WordPress options page, without using the Settings API.
I don’t like the WordPress Settings API. And every time we talk about writing about it, I start whining like the most annoying child you can imagine. I whine because the Settings API is the most official way to make admin options pages in WordPress, which is an important thing to do for both public and private plugins. But to put it kindly, I find the WordPress Settings API baffling and more than a little bit quirky.
But there are parts to making options pages that don’t really involve the Settings API. And in fact, you don’t ever need to use it at all. Today we’re going to describe how to create an admin options page, and how to save a site-wide option—but without using the Settings API.
One more thing before we get started. If you’re looking to learn WordPress development, we’ve written the best guide to it out there:
The Best Way to Learn WordPress Development
Get Up and Running Today!
Up and Running is our complete “learn WordPress development” course. Now in its updated and expanded Third Edition, it’s helped hundreds of happy buyers learn WordPress development the fast, smart, and thorough way.Here’s what they have to say:
“Other courses I’ve tried nearly always lack clear explanations for why WordPress does things a certain way, or how things work together. Up and Running does all of this, and everything is explained clearly and in easy-to-understand language.” -Caroline, WordPress freelancer
We’ll talk first about how to make a page that just shows up in the WordPress admin area, and then we’ll go into more detail about how to save and load options safely on a page.
A High-Level Overview of a WordPress Options Page
In WordPress, an admin options page is a page that the site administrator accesses to change something about his or her site. Any page listed under “Tools” or “Settings,” for example, is an admin options page. And a large plugin like WordPress SEO by Yoast might register an entire admin submenu full of admin options pages:
All admin menu pages have markup that describes to the user what the page allows him or her to change. And the heart of every admin menu page is one or more settings, outputted to the user by a web <form>
element, that changes something about your website.
Basic Steps to Create an Admin Options Page
Creating an admin menu page with working settings has three main steps:
- Creating a page in the admin area.
- Writing the page contents.
- Within the page contents, creating a working form that will save the settings you wish to change.
Where We’re Dodging the Settings API
Step 3 is the one that usually requires the Settings API. However, you can accomplish the same goal by creating a simple, old-school-PHP-style Options Page in the WordPress backend, and avoid using the Settings API altogether.
And while the community, the Codex, and most of your peers will tell you you’re doing it wrong, this does work, and it cuts down on the incidental complexity (that is, the poor API design) that travels along with the Settings API.
So that’s a high-level introduction. We’ll be walking you through each step below.
1. Creating a Page in the Admin Area
Your first step is to make a page in the admin area of your site.
Your first step is to make a page in the administration area of your site, to contain your page content and <form>
. To do this, you’ll use one of a list of similar functions which all have the same basic naming pattern: add_*_page()
. With the right one of those guys, you can create a new administration page pretty much wherever you want.
The most common and easy of these is the add_menu_page()
function. Each of these functions has a pretty long list of parameters, and unfortunately, these parameters aren’t in a very clear order. (This is the heart of my whole complaint about this area of WordPress code.) From the Codex:
add_menu_page( $page_title, $menu_title, $capability, $menu_slug, $function, $icon_url, $position );
I’ll quickly explain each parameter:
$page_title
is the thing that WordPress might actually display on the page. (It turns out in most code, including ours, people end up discarding the value.)$menu_title
is how your page will appear in the sidebar.$capability
is the string representation of the WordPress user capability system that you’ll want someone who can interact with this page to have.$menu_slug
is the url-safe representation that your page will use.$function
is the callback for the function you want to render your actual page.$icon_url
is the URL (or dashicon signifier) that you want your page to have.$position
is an integer that determines where (height-wise) in the left sidebar your option should appear, 1 being the top, 99 being the bottom. (See the annotated guide below to get a sense of it in more detail.)
Only the first four parameters (to $menu_slug)
are mandatory, but I’d include the fifth as well, because the behavior without it is kind of confusing.
Managing the Complexity and Illegibility of the add_*_page()
functions
One of the big drawback to that fact that this function, and most others similar to it, takes a very large number of parameters is that you’ll forget what each does have how it works pretty quickly.
Because PHP doesn’t support named parameters, I’d recommend keeping your parameters very close and easy to refer to. What that means is that instead of doing this:
add_action('admin_menu', 'awesome_page_create');
function awesome_page_create() {
add_menu_page( 'My Awesome Admin Page', 'Awesome Admin Page', 'edit_posts', 'awesome_page', 'my_awesome_page_display', '', 24);
}
You can do this:
add_action('admin_menu', 'awesome_page_create');
function awesome_page_create() {
$page_title = 'My Awesome Admin Page';
$menu_title = 'Awesome Admin Page';
$capability = 'edit_posts';
$menu_slug = 'awesome_page';
$function = 'my_awesome_page_display';
$icon_url = '';
$position = 24;
add_menu_page( $page_title, $menu_title, $capability, $menu_slug, $function, $icon_url, $position );
}
One of my sources of frustration with these functions (and the Settings API as a whole) is that I’ve historically favored the first form over the second. It has the advantage of being slightly more concise; but when I come back later to try to look at or change the code, I’m always confused.
This interface has too many arguments for you to favor conciseness. The second example makes up for PHP’s lack of named parameters by naming by proxy, and it creates far more sustainable and legible code in the long term.
The Other add_*_page()
Functions
As I said, all add_*_page()
functions are similar. Rather than exhaustively list them and highlight how modestly different each of them is, I’ll point you to the Codex page that shows them all off: Administration Menus API. A very short summary is that if you don’t want to create a top-level menu (one as big and prominent as “Dashboard”, “Posts” or “Plugins”), you want to create a submenu. And there’s one main function you use for this, and a whole lot of shortcuts with slightly better naming.
Basically, if you want to create a new page that’s under the “Posts” sidebar menu, your options are these two:
$parent_slug = 'edit.php';
add_submenu_page( $parent_slug, $page_title, $menu_title, $capability, $menu_slug, $function );
// or
add_posts_page( $page_title, $menu_title, $capability, $menu_slug, $function );
As I’ve already mentioned my personal complaint about opaque and confusing long lists of parameters, you can probably guess my preference. The main reason that you’d use add_submenu_page()
vs. something like add_posts_page()
, to my mind, is when you’ve created both the parent and the children, so you’re unable to use the shortcut function because it doesn’t exist.
2. Using the Callback Function to Make Your Page
You’ll need to use a callback function to output content to your page.
The heart of making your administration page is using the $function
parameter to actually output your page. Remember that one of the arguments we passed into add_menu_page()
using our my_awesome_page_display()
function above is as follows:
$function = 'my_awesome_page_display';
'my_awesome_page_display'
is a callback function name that you’re expected to define, and which will contain the markup that displays on your page.
To start, you can just use something pretty simple like:
function my_awesome_page_display() {
echo '<h1>My Page!!!</h1>';
}
And with that, you’ve created a really really useless admin page. It provides no options and doesn’t display any data. It just says “My Page!!!” at the top.
What you probably want to do is create a <form>
on that page so people can give your plugin or theme useful data, and which’ll probably end up being stored in the database as WordPress options. This is where the WordPress Settings API can be really helpful, and justify all its complexity.
Consider Breaking Your Form Out Into a Separate File
This is something you should consider whether or not you use the Settings API to build your form: rather than making your callback function 100 lines long, include a separate template file. You can simplify things really nicely by not filling your callback function with a bunch of HTML echo
s.
Creating your form and the rest of your markup in a separate file that you include looks something like this:
// in main file
function my_awesome_page_display() {
include 'form-file.php';
}
// form-file.php
<h1>My Awesome Settings Page</h1>
This has the big advantage that, in our main settings page file, we’re not juggling a bunch of echo
es or a bunch of open-and-closing PHP tags.
3. Creating a Working Form and Saving Your Settings
At the moment, our page remains useless. Here’s some actually working code that’ll give you a working admin options page, without using the Settings API. It also begins to highlight some of the reasons that you’ll want to use the Settings API.
// in main file
function my_awesome_page_display() {
if (isset($_POST['awesome_text'])) {
$value = $_POST['awesome_text'];
update_option('awesome_text', $value);
}
$value = get_option('awesome_text', 'hey-ho');
include 'form-file.php';
}
// form-file.php
<h1>My Awesome Settings Page</h1>
<form method="POST">
<label for="awesome_text">Awesome Text</label>
<input type="text" name="awesome_text" id="awesome_text" value="<?php echo $value; ?>">
<input type="submit" value="Save" class="button button-primary button-large">
</form>
What does this do? As we just covered, form-file.php
is what’s displaying the form, which we’ve just added—it’s all the markup inside <form>
.
Note that we haven’t set an action
value on our form. That will make the form use its method
—which we did specify, as POST
—back to the same page it was on.
This form has only one field and doesn’t do much. Its only field is named awesome_text
, and it does get a dynamically filled value of $value
from PHP. That’ll be supplied from the location of our function call which include
s the template.
Speaking of that function, at the bottom it’s assigning a value to the variable $value.
PHP makes variable values available to include
d files, which is why our form knows about $value.
Note that getting $value
is done using a get_option()
call. If you’ve read about the Options API, you may remember that the first parameter is the name of your option and the second is the (optional) value you want back if your option isn’t set. So you could rewrite our get_option()
call above in two ways:
$value = get_option('awesome_text', 'hey-ho');
// or
$value = get_option('awesome_text');
if (FALSE === $value) {
$value = 'hey-ho';
}
They work the same, but because I think the signature and meaning of the get_option
call is readable and obvious enough, I prefer the shortness of the first.
The logic at the top of the function uses the PHP $_POST
superglobal array to get the submitted value of the form. If you’re not familiar with $_POST
, the very basic thing you need to understand is that PHP pulls out values from submitted forms POSTed to a script, and puts them in the $_POST
array for you, at the index that was the form elements’s name
field. In our case, the form element was named awesome_text
, and we get that value by accessing $_POST['awesome_text']
.
Finally, the if-statement at the beginning of my_awesome_page_display()
is again using the Options API, this time to save $_POST['awesome_text']
, if it’s set, as the new value for awesome_text
in the database.
Security Considerations of Saving Options
The above code works and is valid, but it has three causes for caution from a security perspective:
- We’re not checking that the submitted value came from a user who has permissions to change the option
- We’re not making sure that after such a check, the user intended to change the specified form (via a nonce)
- We’re not making sure that the submitted data has the right shape, and doesn’t contain something malicious
The details of why these are important can be understood a little better in our primer on WordPress security, but the basic code that would improve these first two problems are:
// form-file.php
<h1>My Awesome Settings Page</h1>
<form method="POST">
<label for="awesome_text">Awesome Text</label>
<input type="text" name="awesome_text" id="awesome_text" value="<?php echo $value; ?>">
<?php wp_nonce_field( 'wpshout_option_page_example_action' ); ?>
<input type="submit" value="Save" class="button button-primary button-large">
</form>
And for the saving:
// in main file
function my_awesome_page_display() {
if (!current_user_can('manage_options')) {
wp_die('Unauthorized user');
}
check_admin_referrer( 'wpshout_option_page_example_action' );
if (isset($_POST['awesome_text'])) {
update_option('awesome_text', $_POST['awesome_text']);
}
$value = get_option('awesome_text', 'hey-ho');
include 'form-file.php';
}
What we’ve changed in the above code is make changes so that, first, the user is checked. A user could potentially forge a request to our form and have non-admins edit the options. We don’t want that to happen, so we’re first making sure before we save submitted data that the user has the right permissions.
After that, we’re making sure that the nonce
we added to the form matches. This makes sure that a user who has the right permissions can’t be maliciously tricked into changing our option by accident. In both cases, you may want to consider a better and more elegant handling of a guard-cluase failure than my wp_die
s, but these will work to keep your users safe. (You may well frustrate them though.)
A last security concern worth noting is the possibility of invalid or malicious data. If our option is, for example, supposed to be an integer, it is a good idea to make sure that the value is an integer. register_setting
is one of the ways to accomplish this sanitization, but you could also just do it here. The whole topic of sanitization and validation is too big for this article, but definitely keep it in mind.
Drawbacks of Skipping the Settings API
Playing with the
$_POST
variable isn’t inherently a problem, but it sure isn’t pretty.
One of the biggest drawbacks to what we’ve done here — just set our options ourselves using the Options API — is that we’ve had to write some pretty boilerplate stuff ourselves. Playing with the $_POST
variable isn’t inherently a problem, but it sure isn’t pretty.
A more practical problem is one veterans of the internet may guess. Because of the way this form was built, if a user (or their browser) reloads the page, you’ll get a prompting about “Do you want to resubmit form data?” This is one of those little things that’s navigable with your own PHP—you’ll want to use the Post/Redirect/Get pattern—but it’s not something you’re faced with when using the Settings API. Our friend Carl published a great piece about that:
Preventing Form Resubmission Warnings in WordPress with the Post/Redirect/Get Pattern
What We’ve Learned
The biggest and most generalizable lesson we’ve covered is how you create new admin pages for your plugin or theme. In either situation, you want to use those add_*_page()
functions, picking the one most appropriate for those situations.
In each of those functions, you’ll need to passing in the name of a callback function which you’ll use to actually create your page and its form. In our example today, we used some old-school PHP, and WordPress’s Options API, to process that form submission. But if you do choose to use the Settings API, you’d still register that page the same way.
There are arguments worth having about whether or not the Settings API is appropriate and useful. But whether you end up using it or not, you now know the very essentials of creating a WordPress admin page—and that’s nothing to sneeze at. Happy hacking!
I did it that way
first i check if post is not empty.
if (!empty($_POST))
{
if (! isset( $_POST[‘name_of_nonce_field’] ) || ! wp_verify_nonce( $_POST[‘name_of_nonce_field’], ‘name_of_my_action’ ) )
{
print ‘Sorry, your nonce did not verify.’;
exit;
}
}
….form….
echo wp_nonce_field( ‘name_of_my_action’, ‘name_of_nonce_field’ );
…/form….
Hey Jason!
ACF is great, but we have to be careful.
Using ACF you can…
– Increase by 173% to render page
– Increase by 14% your number of queries
– Increase by 26% memory usage
You really should consider the advantages and disadvantages of using ACF.
See ya!
Hi there!
You are executing
check_admin_referer
without knowing that the request was a POST. This if-logic will effectively prevent all users from ever reaching the option page.Sorry if I have made a mistake and my point is incorrect.
Nice article on a different approach although!
No offense! Settings API is not pretty even tedious, but your way is worse. The Options page creating with Settings API is pluginable, extensible and flexiable, imagine that you created a plugin and an options page for this plugin, then other users or developers want to add extra settings fields or sections to the options page, it’s impossible by your way unless modify your code directly, but with Settings API, we can add/remove/modify a settings sections or fields without modify original file/codes.
I wish I saw this article before…
I am using the setting API hoping that it will keep my admin updated with WP versions
BUT, its complex, simply the wrong way to go.
Soon I will replace using this article to hold my hand…
BIG LIKE and thanks
Thank you for this tut man …very helpful
It is a useful article. How to add the tabbed setting pages for settings panel built with wordpress settings API?
thank you!. i’d rather sanitize teh data myself then having to comprehend the settings api. i have 8 yrs of php and for some reason i cant wrap my head around learning the settings api.
I’m not a huge fan of doing settings pages “The WordPress Way” either. That said, skipping register_setting(), and any sanitization of POST data at all, is a security risk, and is not a step worth skipping.
Nice intro article.
With WP4 however, it’s time to switch to Customizer API, it’s way simplier and allows preview changes while making them.
The customizer’s great for visual things. But it’s hardly a complete one-for-one replacement with other ways to give a plugin feedback about how it should behave.