So far we only needed to store user-defined options, so we utilized the Settings API. However, our plugin has to be able to read/write options itself to “remember” how many times an IP address has attempted to login unsuccessfully, if it’s currently locked out, etc.
We need an object-oriented way to store and retrieve options. During the “Design” phase, we briefly discussed this, but abstracted away some of the implementation details, focusing solely on the actions we’d like to be able to perform—getting, setting, and removing an option.
We’ll also sort of “group” options together based on their section to keep them organized. That’s purely based on personal preference.
Let’s turn this into an interface:
interface Options {
/**
* Return the option value based on the given option name.
*
* @param string $name Option name.
* @return mixed
*/
public function get( $name );
/**
* Store the given value to an option with the given name.
*
* @param string $name Option name.
* @param mixed $value Option value.
* @param string $section_id Section ID.
* @return bool Whether the option was added.
*/
public function set( $name, $value, $section_id );
/**
* Remove the option with the given name.
*
* @param string $name Option name.
* @param string $section_id Section ID.
*/
public function remove( $name, $section_id );
}
Ideally, we’d be able to interact with the WordPress Options API, by doing something like this:
$options = new WP_Options();
$options->get( 'retries' );
At this point, you might be wondering why we don’t just use the get_option()
WordPress function, instead of going into the trouble of creating our own interface and class. While using WordPress functions directly would be a perfectly acceptable way of developing our plugin, by going a step further and creating an interface to depend on, we stay flexible.
Our WP_Options
class is going to implement our Options
interface. That way, we’ll be ready if our needs change in the future. For instance, we might need to store our options in a custom table, in an external database, in memory (e.g. Redis), you name it. By depending on an abstraction (i.e. interface), changing something in the implementation, is as simple as creating a new class implementing the same interface.
WP_Options
Let’s start writing our WP_Options
class, by retrieving all options using the get_option()
WordPress function in its constructor.
class WP_Options {
/**
* @var array Stored options.
*/
private $options;
/**
* WP_Options constructor.
*/
public function __construct() {
$this->options = get_option( Plugin::PREFIX );
}
}
Since the $options
property will be used internally, we’ll declare it private
so it may only be accessed by the class that defined it, the WP_Options
class.
Now, let’s implement our Options
interface by using the implements
operator.
class WP_Options implements Options {
// ...
Our IDE is yelling at us to either declare our class abstract or implement the get()
, set()
, and remove()
methods, defined in the interface.
So, let’s start implementing these methods!
Getting an option
We’ll start with the get()
method, which is going to look for the specified option name in our $options
property, and either return its value or false
if it doesn’t exist.
class WP_Options implements Options {
private $options;
public function __construct() {
$this->options = get_option( Plugin::PREFIX );
}
/**
* Return the option value based on the given option name.
*
* @return mixed
*/
public function get( $option_name ) {
if ( ! isset( $this->options[ $option_name ] ) ) {
return false;
}
return $this->options[ $option_name ];
}
}
Now it’s a good time to think about default options.
Default options
As mentioned previously, we’d like to group options together, based on their section. So, we’ll probably split the options into a couple of sections. The “General Options” section and another one for the data we need to keep track of. Lockouts, retries, lockout logs, and total number of lockouts—we’ll arbitrarily call this state.
We’ll use a constant to store our default options. The value of a constant can’t be changed while our code is executing, which makes it ideal for something like our default options. Class constants are allocated once per class, and not for each class instance.
NOTE: The name of a constant is in all uppercase by convention.
const DEFAULT_OPTIONS = array(
'general_options' => array(
'allowed_retries' => 4,
'normal_lockout_time' => 1200, // 20 minutes
'max_lockouts' => 4,
'long_lockout_time' => 86400, // 24 hours
'hours_until_retries_reset' => 43200, // 12 hours
'site_connection' => 'direct',
'handle_cookie_login' => 'yes',
'notify_on_lockout_log_ip' => true,
'notify_on_lockout_email_to_admin' => false,
'notify_after_lockouts' => 4
),
'state' => array(
'lockouts' => array(),
'retries' => array(),
'lockout_logs' => array(),
'total_lockouts' => 0
)
);
In the DEFAULT_OPTIONS
nested array, we’ve set a default value for all of our options.
What we’d like to do next, is store the default option values in the database once the plugin is initialized, by using the add_option()
WordPress function.
class WP_Options {
public function __construct() {
$all_options = array();
foreach ( self::DEFAULT_OPTIONS as $section_id => $section_default_options ) {
$db_option_name = Plugin::PREFIX . '_' . $section_id;
$section_options = get_option( $db_option_name );
if ( $section_options === false ) {
add_option( $db_option_name, $section_default_options );
$section_options = $section_default_options;
}
$all_options = array_merge( $all_options, $section_options );
}
$this->options = $all_options;
}
}
Let’s take a closer look at this snippet. First, we iterate the default options array and retrieve the options using the get_option()
WordPress function.
foreach ( self::default_options as $section_id => $section_default_options ) {
$db_option_name = Plugin::PREFIX . '_' . $section_id;
$section_options = get_option( $db_option_name );
// ...
Then, we check whether each option already exists in the database, and if not, we store its default option.
if ( $section_options === false ) {
add_option( $db_option_name, $section_default_options );
$section_options = $section_default_options;
}
Finally, we collect the options of all sections.
$all_options = array_merge( $all_options, $section_options );
And store them in the $options
property so we’ll be able to access them later on.
$this->options = $all_options;
The WordPress options table in the database is going to have a couple of rows, where the option_name
consists of the plugin’s prefix concatenated to the section name.
Let’s move on now to the rest of the methods we need to implement.
Storing an option
Similarly, we’d like to easily store a new option in the database, and overwrite any previous value, like this:
$options = new Options();
$options->set( 'retries', 4 );
So, let’s implement the set()
method, which is going to use the update_option()
WordPress function.
/**
* Store the given value to an option with the given name.
*
* @param string $name Option name.
* @param mixed $value Option value.
* @param string $section_id Section id. Defaults to 'state'.
* @return bool Whether the option was added.
*/
public function set( $name, $value, $section_id = 'state' ) {
$db_option_name = Plugin::PREFIX . '_' . $section_id;
$stored_option = get_option( $db_option_name );
$stored_option[ $name ] = $value;
return update_option( $db_option_name, $stored_option );
}
Removing an option
Lastly, we’ll implement the remove()
method, which is going to set the option to its initial value:
/**
* Remove the option with the given name.
*
* @param string $name Option name.
* @param string $section_id Section id. Defaults to 'state'.
* @return bool Whether the option was removed.
*/
public function remove( $name, $section_id = 'state' ) {
$initial_value = array();
if ( isset( self::DEFAULT_OPTIONS[ $section_id ][ $name ] ) ) {
$initial_value = self::DEFAULT_OPTIONS[ $section_id ][ $name ];
}
return $this->set( $name, $initial_value, $section_id );
}
We’ve bundled everything together in a single class. All options-related data (i.e. our properties) and the implementation details (i.e. the methods we just implemented) are encapsulated in the WP_Options
class.
Encapsulation/Abstraction
Wrapping everything in a single class, enclosing the internals (as if in a capsule), essentially “hiding” them from the outside world, is what we call encapsulation. Encapsulation is another core concept of object-oriented programming.
Using the Options
interface, we focused on what we do with our options instead of how we do it, abstracting the idea of options, simplifying things conceptually. This is what we call abstraction, another core concept of object-oriented programming.
Try our Award-Winning WordPress Hosting today!
Encapsulation and abstraction are completely different concepts, but clearly, as you can see, highly-related. Their main difference is that encapsulation exists in the implementation level, while abstraction exists in the design level.
Dependencies
Let’s consider the following scenario:
There’s a Lockouts
class, responsible for determining whether an IP address should get locked out, what should be the duration of that lockout, if an active lockout is still valid or has expired etc. That class contains a should_get_locked_out()
method, responsible for determining whether an IP address should get locked out. That method would need to read the maximum number of allowed retries before an IP address gets locked out, which is a configurable value, meaning it’s stored as an option.
So, the code we just described would look similar to this:
class Lockouts {
// ...
/**
* @var WP_Options An instance of `WP_Options`.
*/
private $options;
/**
* Lockouts constructor
*/
public function __construct() {
$this->options = new WP_Options();
}
/**
* Return the number of retries.
*
* @return int
*/
private function get_number_of_retries() {
// ...
}
/**
* Check whether this IP address should get locked out.
*
* @return bool
*/
public function should_get_locked_out() {
$retries = $this->get_number_of_retries();
$allowed_retries = $this->options->get( 'allowed_retries' );
return $retries % $allowed_retries === 0;
}
// ...
}
Basically, we’re creating a new instance of WP_Options
in the constructor, and then use that instance to retrieve the value of the allowed_retries
option.
That’s absolutely fine, but we have to keep in mind that our Lockouts
class now depends on WP_Options
. We call WP_Options a dependency.
If our needs change in the future, for example, we need to read/write options on an external database, we’d need to replace the WP_Options
with a DB_Options
class. That doesn’t seem so bad, if we need to retrieve options in only one class. However, it may get a bit tricky when there are many classes with multiple dependencies. Any changes to a single dependency will likely ripple across the codebase, forcing us to modify a class if one of its dependencies changes.
We can eliminate this issue by rewriting our code to follow the Dependency Inversion Principle.
Decoupling
The Dependency Inversion Principle (DIP), the “D” in S.O.L.I.D., states:
- High-level modules should not import anything from low-level modules. Both should depend on abstractions.
- Abstractions should not depend on details. Details (concrete implementations) should depend on abstractions.
In our case, the Lockouts
class is the “high-level module” and it depends on a “low-level module”, the WP_Options
class.
We’ll change that, using Dependency Injection, which is easier than it may sound. Our Lockouts
class will receive the objects it depends on, instead of creating them.
class Lockouts {
// ...
/**
* Lockouts constructor.
*
* @param WP_Options $options
*/
public function __construct( WP_Options $options ) {
$this->options = $options;
}
// ...
}
So, we inject a dependency:
$options = new WP_Options();
$lockouts = new Lockouts( $options );
We just made our Lockouts
class easier to maintain since it’s now loosely coupled with its WP_Options
dependency. Additionally, we’ll be able to mock the dependencies, making our code easier to test. Replacing the WP_Options
with an object that mimics its behavior will allow us to test our code without actually executing any queries on a database.
/**
* Lockouts constructor.
*
* @param WP_Options $options
*/
public function __construct( WP_Options $options ) {
$this->options = $options;
}
Even though we have given the control of Lockouts
’ dependencies to another class (as opposed to Lockouts
controlling the dependencies itself), Lockouts
still expects a WP_Options
object. Meaning, that it still depends on the concrete WP_Options
class, instead of an abstraction. As previously mentioned, both modules should depend on abstractions.
Let’s fix that!
/**
* Lockouts constructor.
*
* @param Options $options
*/
public function __construct( Options $options ) {
$this->options = $options;
}
And by simply changing the type of the $options
argument from the WP_Options
class to the Options
interface, our Lockouts
class depends on an abstraction and we’re free to pass a DB_Options
object, or an instance of any class that implements the same interface, to its constructor.
Single Responsibility
It’s worth noting that we used a method called should_get_locked_out()
to check whether the IP address should get locked out or not.
/**
* Check whether this IP address should get locked out.
*
* @return bool
*/
public function should_get_locked_out() {
$retries = $this->get_number_of_retries();
$allowed_retries = $this->options->get( 'allowed_retries' );
return $retries % $allowed_retries === 0;
}
We could easily write a one-liner like this:
if ( $this->get_number_of_retries() % $this->options->get( 'allowed_retries' ) === 0 ) {
However, moving that piece of logic into its own little method, has a lot of benefits.
- If the condition to determine whether an IP address should get locked out ever changes, we’ll only have to modify this method (instead of searching for all occurrences of our if statement)
- Writing unit tests becomes easier when each “unit” is smaller
- Improves the readability of our code a lot
Reading this:
if ( $this->should_get_locked_out() ) {
// ...
seems to us way easier than reading that:
if ( $this->get_number_of_retries() % $this->options->get( 'allowed_retries' ) === 0 ) {
// ...
We’ve done this for pretty much every method of our plugin. Extracting methods out of longer ones till there’s nothing else to extract. The same goes for classes, each class and method should have a single responsibility.
The Single Responsibility Principle (SRP), the “S” in S.O.L.I.D., states:
“Every module, class, or function in a computer program should have responsibility over a single part of that program’s functionality, and it should encapsulate that part.”
Or, as Robert C. Martin (“Uncle Bob”) says:
“A class should have one, and only one, reason to change.”
Revisiting the main plugin file
At the moment, our main plugin file contains only this:
/**
* Plugin Name: PRSDM Limit Login Attempts
* Plugin URI: https://pressidium.com
* Description: Limit rate of login attempts, including by way of cookies, for each IP.
* Author: Pressidium
* Author URI: https://pressidium.com
* Text Domain: prsdm-limit-login-attempts
* License: GPL-2.0+
* Version: 1.0.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
Once again, we’ll wrap everything in a Plugin class, this time just to avoid naming collisions.
namespace Pressidium\Limit_Login_Attempts;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class Plugin {
/**
* Plugin constructor.
*/
public function __construct() {
// ...
}
}
We’ll instantiate this Plugin
class at the end of the file, which is going to execute the code in its constructor.
new Plugin();
In the constructor we’ll hook into the plugins_loaded action, which fires once activated plugins have loaded.
public function __construct() {
add_action( 'plugins_loaded', array( $this, 'init' ) );
}
public function init() {
// Initialization
}
We’ll also call a require_files()
method to load all of our PHP files.
public function __construct() {
$this->require_files();
add_action( 'plugins_loaded', array( $this, 'init' ) );
}
private function require_files() {
require_once __DIR__ . '/includes/Sections/Section.php';
require_once __DIR__ . '/includes/Pages/Admin_Page.php';
require_once __DIR__ . '/includes/Pages/Settings_Page.php';
// ...
}
Finally, we’ll initialize our plugin by creating some objects in our init()
method.
NOTE: The following snippet contains only a small part of the main plugin file. You can read the actual file at the plugin’s GitHub repository.
public function init() {
$options = new Options();
$hooks_manager = new Hooks_Manager();
$settings_page = new Settings_Page( $options );
$hooks_manager->register( $settings_page );
// ...
}
Organizing the files
Keeping your files organized is vital, especially when working on large plugins with lots of code. Your folder structure should group similar files together, helping you and your teammates stay organized.
We’ve already defined a namespace (Pressidium\Limit_Login_Attempts
), containing several sub-namespaces for Pages
, Sections
, Fields
, Elements
, etc. Following that hierarchy to organize our directories and files, we ended up with a structure similar to this:
.
├── includes
│ ├── Hooks
│ │ ├── Actions.php
│ │ ├── Filters.php
│ │ └── Hooks_Manager.php
│ ├── Pages
│ │ ├── Admin_Page.php
│ │ └── Settings_Page.php
│ ├── Sections
│ │ ├── Fields
│ │ │ ├── Elements
│ │ │ │ ├── Checkbox_Element.php
│ │ │ │ ├── Custom_Element.php
│ │ │ │ ├── Element.php
│ │ │ │ ├── Number_Element.php
│ │ │ │ └── Radio_Element.php
│ │ │ └── Field.php
│ │ └── Section.php
│ └── WP_Options.php
├── prsdm-limit-login-attempts.php
└── uninstall.php
Each file contains a single class. Files are named after the classes they contain, and directories and subdirectories are named after the (sub-)namespaces.
There are multiple architecture patterns and naming schemes you may use. It’s up to you to pick one that makes sense to you and suits the needs of your project. When it comes to structuring your project, the important thing is to be consistent.
Conclusion
Congratulations! You’ve completed our article series about WordPress and object-oriented programming.
Hopefully you learned a few things and are excited to start applying what you learned on your own projects!
Here’s a quick recap of what we covered in this series:
- Requirements gathering: We decided on what the plugin should do.
- Design: We thought about how the plugin will be structured, the relationships between our potential classes, and a high-level overview of our abstractions.
- Implementation: We wrote the actual code of some key parts of the plugin. While doing that, we introduced you to several concepts and principles.
However, we barely scratched the surface of what OOP is and has to offer. Getting good at a new skill takes practice, so go ahead and start building your own object-oriented WordPress plugins.
Happy coding!
See Also
- Summary of the series
- Part 1 – WordPress and Object-Oriented Programming – An Overview
- Part 2 – WordPress and Object Oriented Programming: A Real World Example
- Part 3 – WordPress and Object Oriented Programming: Α WordPress Example – Defining the Scope
- Part 4 – WordPress and Object Oriented Programming: A WordPress Example – Design
- Part 5 – WordPress and Object Oriented Programming: A WordPress Example – Implementation: The Administration Menu
- Part 6 – WordPress and Object Oriented Programming: A WordPress Example – Implementation: Registering the Sections
- Part 7 – WordPress and Object Oriented Programming: A WordPress Example – Implementation: Managing WordPress Hooks
Start Your 14 Day Free Trial
Try our award winning WordPress Hosting!