Essay
Using A Config To Write Reusable Code – Part 3
We examine how the example code from the previous article behaves if we use of a Config file to map data from reusable code to project-specific code.
This is the third part of a series of articles. ( Part 1, Part 2 )
So far, we’ve found out that we need to have a clear separation between reusable code and project-specific code, and we identified the Config file as being a promising tool to map data from one side of this separation to the other. In this third article, we’ll examine what our Settings page example looks like if we do indeed make use of such a Config file.
Code Against Interfaces
First, we’ll want an interface that we can code against. This is important, as we don’t want all of the rest of our code to be tightly coupled to one very specific implementation of the Config mechanism. Instead, we want to only couple our code to such an interface that provides the basic methods to access the data, and easily allow better and more feature-complete Config implementations to be injected later on.
interface ConfigInterface {
/**
* Check whether the Config has a specific key.
*
* @param string $key The key to check the existence for.
* @return bool Whether the specified key exists.
*/
public function has_key( $key );
/**
* Get the value of a specific key.
*
* @param string $key The key to get the value for.
* @return mixed Value of the requested key.
*/
public function get_key( $key );
/**
* Get an array with all the keys.
*
* @return array Array of config keys.
*/
public function get_keys();
}
This is a very basic interface and it will be flexible enough for everything we need to do in these examples.
Injecting The Config Object
The basic principle now is that each reusable class will need a Config file with the configuration data that contains the project-specific bits to be injected. Our Plugin class that is responsible for instantiating these objects will inject the matching Config into the reusable class’ constructor.
Here’s how that would look like for the method in our Plugin class that initializes the Settings page and registers it with WordPress:
public function init_settings_page() {
// Load configuration for the settings page.
$config = new Config( AS_BETTER_SETTINGS_1_DIR . 'config/settings-page.php' );
// Initialize settings page.
$settings_page = new SettingsPage( $config );
// Register the settings page with WordPress.
add_action( 'init', [ $settings_page, 'register' ] );
}
We instantiate our basic Config implementation (see it on GitHub) by telling it where to find the Config file to load, and the inject that Config into the SettingsPage class.
This is still a very naïve implementation, as we need to create a separate file for each object we want to configure, and we are still directly coupling our Plugin class to a specific Config implementation here. We’ll revisit this bit of code in a later article to further refine both the way we instantiate a Config file as well as the way we decide what subset of the configuration data to use.
Okay, that was all still very easy, now comes the hard part…
Structuring Our Config Data
Our goal should be to create a reusable class that can transform a properly laid-out Config file into a collection of admin pages and subpages of arbitrary complexity. We don’t want to have a separate class for each page or a separate class for each section. We want to be able to just tell our SettingsPage class: “Here, this is what I need, go build it!”.
The way we can achieve this is be destructuring the hierarchy that goes into building such a Settings page and allow our class to iterate over that hierarchy, looping over multiple elements wherever it makes sense. So, let’s stick with our example we had in the previous articles and build a single page with a single section with two fields. This is what our hierarchical structure will look like:
admin page
settings
\--section
|--field 1
\--field 2
The admin pages and the settings are only indirectly connected to each other, as you can have any number of pages without ever using settings. This is why we have these two distinct hierarchical roots.
At each level in this hierarchy, we need to decide whether it makes sense to loop over multiple elements, or whether there’s only ever one possible element at that level. And as it turns out, each level of this structure can indeed be used multiple times. We can have multiple (sub)pages, we can have multiple sets of settings, we can have multiple sections per set of settings and we can have multiple fields per section.
What we’ll do in our reusable class then is to have one method per level to add a single element for that level. This then allows us to iterate over our Config file and just loop through multiple elements at the same level and pass each individual item to one of these methods.
admin page => add_page()
settings => add_setting()
\--section => add_section()
|--field 1 => add_field()
\--field 2 > add_field()
As the entire structure then is recursive, we can simply iterate over our entire Config and pass the entire sub-array of the current level to one of these methods.
Here’s our Config file:
return [
// Admin Pages.
'pages' => [
'as-settings-better-v1' => [
'parent_slug' => MenuPageSlug::SETTINGS,
'page_title' => 'as-settings-better-v1',
'menu_title' => 'as-settings-better-v1',
'capability' => 'manage_options',
'view' => 'views/options-page.php',
],
],
// Settings API Settings Group.
'settings' => [
'assb1_settings' => [
// Settings API Sections.
'sections' => [
'assb1_settings_section' => [
'view' => 'views/section-description.php',
// Settings API Fields.
'fields' => [
'assb1_text_field_first_name' => [
'view' => 'views/first-name-field.php',
],
'assb1_text_field_last_name' => [
'view' => 'views/last-name-field.php',
],
],
],
],
],
],
];
This is a simplified version of the Config file to make the structure obvious. You can also take a look at the complete Config file with comments.
I added inline comments to show where our indentation levels map to our overall Settings API hierarchy.
It should be rather obvious that assembling a Settings page through such a Config file, where we have adapted the way we submit the data to best fit the conceptual structure, is much easier than fooling around with several different WordPress functions, each of which needs a different callback to be able to complete its work.
Implementing The Settings Page
To make this all work, we will parse this Config file from within our SettingsPage class and pass all the relevant bits to the corresponding WordPress functions.
WordPress normally gets a callback for each of its Settings API functions, which is responsible for rendering the corresponding element. However, as we don’t want to deal with random callbacks and markup inside of functions, we map each of these callbacks to a View using a closure.
Here’s the example of a field being registered with WordPress:
protected function add_field( $data, $name, $args ) {
// Prepare the rendering callback.
$render_callback = function () use ( $data, $args ) {
if ( ! array_key_exists( 'view', $data ) ) {
return;
}
// Fetch $options to pass into view.
$options = get_option( $args['setting_name'] );
$this->render_view( $data['view'], [ 'options' => $options ] );
};
add_settings_field(
$name,
$data['title'],
$render_callback,
$args['page'],
$args['section']
);
}
The $data and $name arguments are passed in by array_walk() as part of its traversal. We’ve also added a third argument $args which is an associative array with additional data we need to pass around from level to level.
We then use these arguments to build a closure called $render_callback. The closure basically a) makes sure the Config has provided a view to render, b) builds the contextual data for that view and then c) renders the view from within the correct context.
Then, we call the WordPress function add_settings_field() with the closure we just built as the callback.
The developer adding a new Settings page does not need to care about all of this. The only thing that (s)he needs to add is a path to a view (or an instance of one).
Yay, Free Code!
You can browse or download the source code for this iteration of the plugin from its GitHub repository. It should contain lots of comments or hopefully descriptive method names. But if all else fails, do not hesitate to ask questions in the comments section below! This refactoring round added an entire level of abstraction, so I’m not entirely sure my attempts at explaining these changes will be satisfying.
In the next article of this series (which will be out in the coming days or months), we will completely isolate the different components we’ve now refactored and put them in entirely separate packages.