Episode 2 – WordPress Plugin Development with WooCommerce
The admin screen
Now we build the settings screen. At the end of this episode you get a real page in the admin, under WooCommerce.
We write no saving code yet. Only the page. One thing at a time.
Step 1. Delete the temporary box
In order-boost.php, find this part and delete it:
// TEMPORARY. We delete this in the next episode.
add_action( 'admin_notices', function () {
echo '<div class="notice notice-success"><p>Order Boost is awake.</p></div>';
} );
Your orb_init function is now nearly empty. That is correct. It looks like this:
function orb_init() {
if ( ! class_exists( 'WooCommerce' ) ) {
return;
}
}
Reload the admin. The green box is gone.
Step 2. Make a second file
Our main file is the front door. It must stay small and easy to read. The real work goes in other files.
Type this in your terminal:
cd "/Users/sejim/Local Sites/simple-boards/app/public/wp-content/plugins/order-boost"
mkdir includes
touch includes/class-orb-settings.php
open -a "Visual Studio Code" includes/class-orb-settings.php
The name class-orb-settings.php is the WordPress habit. A file that holds a class called ORB_Settings gets the name class-orb-settings.php. All lower case. Dashes, not underscores.
Why care about a habit? Because in six months, a customer sends you an error that says ORB_Settings. You must find that code in one second, on a site you have never seen. The habit is what makes that possible.
Step 3. Tell PHP the file exists
Go back to order-boost.php. Type this line under the three define lines:
require_once ORB_DIR . 'includes/class-orb-settings.php';
require_once means “read that file into memory now”. PHP stops with a fatal error if the file is missing. That is what we want. A missing file is not something to hide.
The _once part means “and never read it twice”. Read a class file twice and PHP dies with “cannot declare class twice”.
Remember ORB_DIR from Episode 1. It is the path on disk. PHP reads files from disk, so we use the path here, not the web address.
Important: reading a file is not the same as running the work. The file only teaches PHP the word ORB_Settings. Nothing happens yet. It is like putting a recipe book on the shelf. No food is cooked.
Step 4. Lock the new file
In includes/class-orb-settings.php, type this:
<?php
/**
* The Order Boost settings screen.
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
Same lock as Episode 1. Every file. No exceptions.
Step 5. Open the class
Type this under the lock:
class ORB_Settings {
}
A class is a box for related code. This box holds everything about the settings screen, and nothing else. The badge gets its own box later. The cart notice gets its own box after that.
Why bother? Because when a ticket says “the settings page is blank”, you must know where to look with no thought. One feature, one box, one file.
Step 6. Add the wiring
Type this inside the class:
public function __construct() {
add_action( 'admin_menu', array( $this, 'add_menu' ) );
}
This part looks strange the first time. Take it slowly.
__construct is a special name. PHP runs this function the moment you create the object. It is the “switch on” step. A machine plugs in its cables when you switch it on. This constructor plugs in our hooks.
public means other code can call it. Leave it as public for now.
Now look at the second part of the add_action line. In Episode 1 we passed a plain function name in quotes. Here we pass array( $this, 'add_menu' ).
$this means “this object, the one that exists right now”. So the array says: call the method add_menu, on this object. WordPress needs both halves. A method has no meaning without an object to run it on.
Read it out loud like this: “When WordPress rings the admin_menu bell, run my add_menu method.”
admin_menu is the bell that rings while WordPress builds the left sidebar of the admin. That is the only moment you can add a page. Too early and the menu system is not ready. Too late and the sidebar is already printed.
Step 7. Register the page
Type this under the constructor, still inside the class:
public function add_menu() {
add_submenu_page(
'woocommerce',
'Order Boost',
'Order Boost',
'manage_woocommerce',
'order-boost',
array( $this, 'render' )
);
}
Six values. Learn all six. Four of your future tickets hide in this list.
'woocommerce'is the parent. It says “put me under the WooCommerce menu”. This is a slug, not a name. If you type'WooCommerce'with a capital W, the page still exists, but the menu item never appears. WordPress silently puts it nowhere.'Order Boost'is the page title. Browsers show it in the tab.'Order Boost'again is the menu title. This is the text in the sidebar. They are often different. A page title can be “Order Boost Settings” while the menu says “Order Boost”, because the sidebar is narrow.'manage_woocommerce'is the capability. This is the key to the door. WordPress only shows this menu item to a user who holds that key. A shop manager holdsmanage_woocommerce. A customer does not. An editor does not. So they never see our page.'order-boost'is the page slug. It becomes the web address:/wp-admin/admin.php?page=order-boost. It must be unique on the whole site.array( $this, 'render' )is the function that prints the page. Same shape as before. Object plus method name.
Here is a picture for the capability. A hotel key card opens some doors and not others. Housekeeping opens the store room. Guests do not. WordPress checks the card at the door, and it does not even show a door the card cannot open.
Step 8. Print the page
Type this under add_menu, still inside the class:
public function render() {
?>
<div class="wrap">
<h1>Order Boost</h1>
<p>Settings go here.</p>
</div>
<?php
}
This method runs when a person opens the page. What it prints becomes the page.
The ?> and <?php pair looks odd. It means “stop PHP here, this next part is plain HTML, start PHP again after”. You could echo the HTML instead. This way is easier to read when the HTML gets long, and it gets long in the next episode.
<div class="wrap"> is not decoration. WordPress admin styles live on that class. Without it, your heading sits hard against the left edge and the spacing looks broken. This is the reason some plugin screens look wrong. The developer forgot the wrap.
Your class file is now complete. It ends with a closing brace for the class:
}
Step 9. Switch it on
Nothing has happened yet. PHP knows the word ORB_Settings, but no object exists. The recipe book is still shut.
Go to order-boost.php. Type this inside orb_init:
new ORB_Settings();
Your boot function now looks like this:
function orb_init() {
if ( ! class_exists( 'WooCommerce' ) ) {
return;
}
new ORB_Settings();
}
new builds the object. Building the object runs __construct. __construct calls add_action. Now WordPress is listening.
Follow the chain once more, in order. It is worth the minute:
- WordPress rings
plugins_loaded. - Our
orb_initruns. - WooCommerce is there, so we do not stop.
new ORB_Settings()builds the object.- The constructor books a seat at the
admin_menubell. - Later, WordPress rings
admin_menu, andadd_menuruns. - A person clicks the menu, and
renderprints the page.
Note step 3. Because the object is built inside orb_init, and orb_init stops when WooCommerce is missing, the whole settings screen disappears with WooCommerce. One guard protects everything below it.
Step 10. Test it
Go to your admin. Look at the WooCommerce menu in the sidebar. Hover over it.
You see Order Boost in the list. Click it.
You see the heading “Order Boost” and the line “Settings go here.”
That is your admin screen.
Both files, all together
order-boost.php
<?php
/**
* Plugin Name: Order Boost
* Description: Sale badges and a free shipping nudge for WooCommerce.
* Version: 1.0.0
* Author: Rafiz
* Text Domain: order-boost
* Requires at least: 6.0
* Requires PHP: 7.4
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
define( 'ORB_VERSION', '1.0.0' );
define( 'ORB_DIR', plugin_dir_path( __FILE__ ) );
define( 'ORB_URL', plugin_dir_url( __FILE__ ) );
require_once ORB_DIR . 'includes/class-orb-settings.php';
add_action( 'plugins_loaded', 'orb_init' );
function orb_init() {
if ( ! class_exists( 'WooCommerce' ) ) {
return;
}
new ORB_Settings();
}
includes/class-orb-settings.php
<?php
/**
* The Order Boost settings screen.
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class ORB_Settings {
public function __construct() {
add_action( 'admin_menu', array( $this, 'add_menu' ) );
}
public function add_menu() {
add_submenu_page(
'woocommerce',
'Order Boost',
'Order Boost',
'manage_woocommerce',
'order-boost',
array( $this, 'render' )
);
}
public function render() {
?>
<div class="wrap">
<h1>Order Boost</h1>
<p>Settings go here.</p>
</div>
<?php
}
}
Break it on purpose
Two breaks. Both are real tickets you will read one day.
Break 1. The wrong capability.
Change 'manage_woocommerce' to 'manage_martians':
'manage_martians',
Reload the admin. The Order Boost menu item is gone. No error. Nothing in the log.
Nobody on the site holds a key called manage_martians, so WordPress shows the door to nobody. When a customer writes “your menu is missing for my shop manager, but the owner can see it”, this is the first thing you check.
Break 2. The wrong parent.
Put the capability back. Now change 'woocommerce' to 'woocommerce-x':
'woocommerce-x',
Reload. The menu item is gone again. But the page still lives. Type the address by hand and it opens:
/wp-admin/admin.php?page=order-boost
That is the tell. If the page opens by address but no menu shows, the parent slug is wrong. If neither works, the capability is wrong.
Put 'woocommerce' back. The menu returns.
Where we are
You now have a plugin with a real admin page, and you know the seven steps from plugins_loaded to a printed screen.
The page is still a sign with no controls. Nothing is saved. Nothing is read.
Next episode: the form. We add the four settings, we save them to the database, and we meet the four security checks that every save must have. That episode is where most plugins go wrong, and where a lot of your future tickets come from.
Two questions first:
- We built the object inside
orb_init, not at the top of the file. What breaks if we movenew ORB_Settings();up beside therequire_onceline, above the WooCommerce check? - In
add_submenu_pagewe passedarray( $this, 'render' ). Why can we not pass just'render'in quotes, the way we passed'orb_init'in Episode 1?