Bypass the Settings API

The following is a guest post by Andy Walpole.

There are many parts of the WordPress API which are fantastic but there are also other parts which, I would argue, are lacking.

The Settings API was introduced in version 2.7 to allow the semi-automation of form creation. All credible Content Management Systems and frameworks have their own set of functions or classes for the same purpose. Drupal has a multitude of hooks which can be leveraged, while CodeIgniter uses a combination of the Form Validation Class and the Form Helper .

When creating a WordPress plugin recently I wanted to create a dynamic form to insert data into a field in the option database table. I decided to create a class for this purpose with the intention of creating reusable code for future projects. For ideas, I closely followed previous tutorials written by Alison Kleinschmidt and Sarah Neuber .

I quickly ran into a brickwall. My main issue that I have with the Settings API is that it’s unintuitive and too basic. It is such an abstraction away from the meat and bones of PHP / HTML form creation and validation that it confuses the professional coder rather than assists them.

As a coding exercise I decided to take a different approach to WordPress form creation using OOP and one that had a more familiar user interface. The code I have written so far can be examined on a Git Gist that I have created. There are some excellent features of the Settings API, such as inserting new form fields into existing core admin pages; and this code doesn’t attempt to replicate that.

This purpose of this article is to provide some feedback on the successes and difficulties of my endeavourer.

There are some key aspects in understanding the option API access class. Firstly, I placed it into three files reflecting an MVC architecture. Admittedly, the script itself doesn’t tightly fit this segregation but it is a commonly used and understood way of organising code. In the view file there are key WordPress hooks such as add_action() and add_options_page(); in the controller class are the purpose built form field creation methods and in the model class are validation, sanitisation, security and database functions.

Another aspect is that I have used namespaces and late static bindings that arrived with PHP 5.3. It is a convention amongst WordPress developers to cater for servers that have versions of PHP lower than 5.3. I don’t follow that convention. If your hosting company is running versions of PHP which are three years behind or more then take you custom elsewhere because they are not providing to you a satisfactory service.

Completed Code

This is a lengthy tutorial, and walks you through exactly how the script works. Instead of manually copying and pasting all of the code chunks to assemble the file though, it will be simpler just to copy the finished code from here.

1 – Instantiate the Class

 
$aForm = array(
    'option_name' => 'a_url_here', // has to be alphanumeric and underscores only
    'page_title' => 'A page title here', // Main page title
    'page_url' => 'a-url-here', // URL
    'dynamic_output' => FALSE); // Should the form be generated on more input
 
new \OptionView\Form_View($aForm);

The above should be reasonably self-explanatory. In the $aform array are placed four different key values: the name of the option database field, the page title, the page URL and a boolean value – I’ll explain this in detail later but for now it’s set to false.

The create_html_cov() method is called by WordPress add_options_hook(). In here is the code that envelopes the form:

 
extract(self::$form);
 
$form = '<div class="wrap">';
$form .= screen_icon();
$form .= "<h2>{$page_title}</h2>";
$form .= '<div id="result">';
echo $form;
 
if (isset($_POST['submit'])) {
 
    $error = array();
 
    // validation here
 
    if (empty($error)) {
 
        $this->update_option($form);
 
    } else {
 
        echo $this->failure_message($error);
 
    } // end if error
 
} // end if isset submit
 
echo '</div>';
echo '</div>';

Anybody who codes PHP will be familiar with the above. After form submission any error messages are added to an array and then displayed. At the top is a div with a class of wrap. This is essential so that the admin CSS is used. Note as well that the page title originally made in the $aForm array is now been called from the static $form attribute. How? The values were used in the method below which in turn was called in the view constructor. The values within config_settings() are used throughout the script.

/**
 * Form_Controller::config_settings()
 * Main array for important values throughout the class
 * @param string $option_name
 * @param string $page_title
 * @param string $page_url
 * @param boolean $dynamic_output
 * @return array
 */
 
protected function config_settings($option_name, $page_title, $page_url, $dynamic_output = FALSE) {
 
    // put together the output array
    $output['option_name'] = $option_name; // name of option database field
    $output['page_title'] = $page_title; // name of page
    $output['page_url'] = $page_url; // url of page
    $output['dynamic_output'] = $dynamic_output;
    return $output;
 
}

2 – Creating the Form Fields

It is first necessary to create a form array:

$form = array(
    'method' => 'post',
    'action' => '#result',
    'enctype' => FALSE,
    'description' => 'Add a description here');

The description will go inbetween the legend form tags at the top of the form. The rest are basic attribute form values.

When invoking the create_form() method the form array has to be the first value:

$this->create_form($form);

From here more arrays have to be created for individual form fields. Note how closely the PHP fits the HTML form attributes – something that the user interface of the Settings API is distant from.

 
$textOne = array(
    "input" => "text", // input type
    "name" => "textOne", // name attribute
    "desc" => "This is a text field", // for use in input label
    "maxlength" => "200", // max attribute
    "value" => "YES", // value attribute
    "select" => FALSE // array only for the select input
        );
 
$textTwo = array(
    "input" => "text",
    "name" => "textTwo",
    "desc" => "This is another text field:",
    "maxlength" => "250",
    "value" => "YES",
    "select" => FALSE);

The input can be text, textfield, radio, select or checkbox. The desc key is used in the form field label. Values for text or textfields should be “YES” only and by doing so instructs the class to create a sticky form field. Select is only used for drop-down fields, radio buttons and checkboxes (more of which later). Even if an option is not used there stills needs to be “false” or “null” stated.

Now the two arrays are placed in create_form():

$this->create_form($form, $textOne, $textTwo);

The form has now been created.

3 – Validation and Sanitisation

The following method is used for sanitizing data. If, for instance, it is desirable to remove backslashes or trim white space from the ends of strings, then it is possible to do so like this:

 
$this->sanitize($form, 'trim_post');
 
$this->sanitize($form, 'stripslashes');

That’s straight forward enough. Lets take a look at the code behind this in the model class:

 
/**
 * Form_Model::sanitize()
 * 
 * @param string $handle
 * @param array $form_output
 * @return array
 */
protected function sanitize(&$form_output, $handle) {
 
    switch ($handle) {
 
        case 'sanitize_post':
            array_walk_recursive($form_output, array($this, 'sanitize_post'));
            break;
        case 'trim_post':
            array_walk_recursive($form_output, array($this, 'trim_post'));
            break;
        case 'strip_tags_post':
            array_walk_recursive($form_output, array($this, 'strip_tags_post'));
            break;
        case 'empty_value':
            array_walk_recursive($form_output, array($this, 'empty_value'));
            break;
        case 'stripslashes':
            array_walk_recursive($form_output, array($this, 'stripslashes'));
            break;
        default:
            die("The value you ended into the sanitize() method is not recognised: $handle");
            break;
 
    } // end switch
 
}
 
 
/**
 * Form_Model::trim_post()
 * 
 * @param string $att
 * @param string $single
 * @param array $form_output
 * @return array
 */
 
function trim_post(&$form_output, $att = NULL, $single = NULL) {
 
    if ($single == NULL) {
 
        if (is_array($form_output)) {
            array_walk_recursive($form_output, 'trim');
        } else {
            $form_output = trim($form_output);
        }
 
    } else {
 
        extract(static::$form);
 
        foreach ($form_output[$option_name] as $thisKey => $result) {
 
            if (preg_match("/$att/i", $thisKey)) {
 
                if (is_string($thisKey)) {
                    $form_output[$option_name][$thisKey] = trim($result);
                }
 
            }
 
        }
 
        return $form_output;
    }
 
}

As demonstrated by the trim_post() method it is easy to widen the sanitisation features of the code. Just copy and paste the method, change the name and change trim to another function such as strip_tags() or htmlspecialchars(). Then add a new section to the switch statement in sanitize().

Notice that $this->sanitize($form, ‘trim_post’) trims all input data. It is possible to just sanitise one field by bypassing sanitize() and call its child method directly:

 
$this->strip_tags_post($form, 'feedName', true);
$this->trim_post($form, 'feedName', true);
$this->stripslashes($form, 'feedName', true);

Necessary parameters above are the form array, the name of the attribute and a boolean value of true.

Validation is similar to sanitization. Here are two examples of using validation methods:

 
/**
 * Form_Model::validate_email()
 * 
 * @param string $att
 * @return boolean
 */
 
protected function validate_email($form_output, $att) {
 
    extract(static::$form);
 
    if (is_array($form_output) && is_string($att)) {
 
        foreach ($form_output[$option_name] as $thisKey => $result) {
 
            if (preg_match("/$att/i", $thisKey)) {
 
                if ($result !== "") {
 
                    if (!filter_var($result, FILTER_VALIDATE_EMAIL)) {
                        return false;
                    }
 
                } // end if
 
            } // end if
 
        } // end foreach
 
    } else {
        die("Make sure that the inputs for validate_url() is an array and a string");
    }
 
 
}

To extend the different types of validation it is, like sanitization, a straightforward process of copying the code, renaming it and changing the filter_var().

One other useful validation method is duplicate_entries():

 
if ($this->duplicate_entries($form) == false) {
    $error[] = "Please make sure that all input values are unique";
}

This will ensure that no one inputted value is identical.

4 – Select Drop-Down Lists, Radio Buttons & Checkboxes.

The creating and validating of these form fields is slightly different.

Firstly, when you create radio buttons or checkboxes you must do so by specifying the total number of each in the select value of the field creation array:

 
$aRadioButton = array(
    "input" => "radio",
    "name" => "radioButtonName",
    "desc" => "A radio button",
    "maxlength" => FALSE,
    "value" => "mac",
    "select" => 3);
 
$aCheckBox = array(
    "input" => "radio",
    "name" => "aCheckBoxName",
    "desc" => "First checkbox",
    "maxlength" => FALSE,
    "value" => "mushrooms",
    "select" => 3);

The above would specify that there are three checkboxes in the form and three radio buttons (obviously, it is necessary to create separate arrays for all individual fields).

As is standard in HTML, the radio buttons must have the same name attribute. In this script checkboxes must have individual name attributes. I’m working on a solution to creating checkboxes with the same name attributes! Notice as well that, unlike for text or textareas, the value is no longer “YES” but a value unique to all fields.

Previously, checking for empty submitted values was done so through the empty_value() method

 
// Check whether there are any empty form values:
if ($this->empty_value($form) === FALSE) {
    $error[] = "Please don't leave any input values empty";
}

Radio buttons and checkboxes have their own methods:

 
if ($this->empty_checkboxes($form, 2) === FALSE) {
    $error[] = "Please check at least two checkboxes";
}
 
if ($this->empty_radio_butts($form) === FALSE) {
    $error[] = "Please make sure that you check one of the radio buttons";
}

In empty_checkboxes() the second digit is the minimum number of checkboxes that the user needs to click. If none is specified then the default is one.

When creating a drop-down list it is necessary to add any array of values to the select key:

 
$cities = array(
    'Shanghai',
    'Karachi',
    'Mumbai',
    'Beijing',
    'Moscow',
    'Sao Paulo',
    'Tianjin',
    'Guangzhou',
    'Delhi',
    'Seoul',
    'Shenzhen',
    'Jakarta',
    'Tokyo',
    'Mexico City',
    'Istanbul');
 
$select = array(
    "input" => "select",
    "name" => "selectName",
    "desc" => "Select here select here",
    "maxlength" => FALSE,
    "value" => TRUE,
    "select" => $cities);

Putting together the above examples, the code for a form with multiple different types of input would look like this:

 
function create_html_cov() {
 
    // essential.
    extract(self::$form);
 
    $form = '<div class="wrap">';
    $form .= screen_icon();
    $form .= "<h2>{$page_title}</h2>";
    $form .= '<p>This is the admin section for Affiliate Hoover plugin</p>';
    $form .= '<div id="result">';
    echo $form;
 
    if (isset($_POST['submit'])) {
 
        $error = array();
 
        // ESSENTIAL! Do not leave this out. Needs to come first
        $form = $this->security_check($_POST);
 
        // Sanitization
 
        $this->sanitize($form, 'trim_post');
 
        $this->sanitize($form, 'stripslashes');
 
        // Validation
 
        if ($this->validate_email($form, 'feedName') === FALSE) {
            $error[] = "Please make sure that the email addresses are correct";
        }
 
        if ($this->validate_url($form, 'urlName') === FALSE) {
            $error[] = "URL is not right";
        }
 
        // Check whether there are any empty form values for text or textarea fields:
        if ($this->empty_value($form) === FALSE) {
            $error[] = "Please don't leave any input values empty";
        }
 
        // don't allow empty checkboxes
        if ($this->empty_checkboxes($form, 2) === FALSE) {
            $error[] = "Please check at least two checkboxes";
        }
 
        // don't allow empty radio buttons
        if ($this->empty_radio_butts($form) === FALSE) {
            $error[] = "Please make sure that you check one of the radio buttons";
        }
 
        // Make sure that none of the form values are duplicates
        if ($this->duplicate_entries($form) === FALSE) {
            $error[] = "Please make sure that all input values are unique";
        }
 
        if (empty($error)) {
 
            $this->update_option($form);
 
        } else {
 
            echo $this->failure_message($error);
        } // end if error
 
    } // end if isset submitForm
 
    echo '</div>';
 
    // Create the form here:
 
    $aTextField = array(
        "input" => "text", // input type
        "name" => "textOne", // name attribute
        "desc" => "This is a text field", // for use in input label
        "maxlength" => "200", // max attribute
        "value" => "YES", // value attribute
        "select" => FALSE // array only for the select input
            );
 
    $aTextArea = array(
        "input" => "textarea",
        "name" => "aTextArea",
        "desc" => "This is a textarea:",
        "maxlength" => FALSE,
        "value" => "YES",
        "select" => FALSE);
 
    $aRadioButtonOne = array(
        "input" => "radio",
        "name" => "radioButtonName",
        "desc" => "A radio button one",
        "maxlength" => FALSE,
        "value" => "mac",
        "select" => 3);
 
    $aRadioButtonTwo = array(
        "input" => "radio",
        "name" => "radioButtonName",
        "desc" => "A radio button two",
        "maxlength" => FALSE,
        "value" => "linux",
        "select" => 3);
 
    $aRadioButtonThree = array(
        "input" => "radio",
        "name" => "radioButtonName",
        "desc" => "A radio button three",
        "maxlength" => FALSE,
        "value" => "pc",
        "select" => 3);
 
    $aCheckBoxOne = array(
        "input" => "checkbox",
        "name" => "aCheckBoxOne",
        "desc" => "A radio button one",
        "maxlength" => FALSE,
        "value" => "mushrooms",
        "select" => 3);
 
    $aCheckBoxTwo = array(
        "input" => "checkbox",
        "name" => "aCheckBoxTwo",
        "desc" => "First checkbox",
        "maxlength" => FALSE,
        "value" => "pizza",
        "select" => 3);
 
    $aCheckBoxThree = array(
        "input" => "checkbox",
        "name" => "aCheckBoxThree",
        "desc" => "First checkbox",
        "maxlength" => FALSE,
        "value" => "chicken",
        "select" => 3);
 
    $form = array(
        'method' => 'post',
        'action' => '#result',
        'enctype' => 'multipart/form-data',
        'description' => 'Add a new form underneath');
    $this->create_form($form, $aTextField, $aTextArea, $aRadioButtonOne, $aRadioButtonTwo, $aRadioButtonThree,
        $aCheckBoxOne, $aCheckBoxTwo, $aCheckBoxThree);
 
    echo '</div><!-- end of wrap div -->';
 
}

The above will create a form that looks like this:

screenshot two

5 – Dynamic Forms

Creating a form layout that enables the easy adding, deleting or editing of multiple form blocks with one submit button is almost impossible with PHP alone. The best solution would be AJAX but this is primarily intended as a server-side coding exercise.

To create a dynamic form with my Option API access class then the only value to change is the dynamic output booleanwhen instantiating the class.

It is now possible to create a form with different sections that can be independently deleted or edited.

An example being:

screenshot three

Conclusion

The code I linked to in the Git Gist is still in its development stage. There are a number of issues. Firstly, it is not possible to create checkboxes with the same name attribute – they have to be separate. Secondly, it is only possible to create one set of radio buttons per form section.

Attempting to create a dynamic form like I have done so in this Option API access class create an entire new level of code complexity. Because a server-side script like PHP has only limited use of the DOM, it is necessary to find solutions based on loops of the form arrays and a lot of maths. This is an aspect of the code that I will continue working on.

But, positive parts of the Option API access class are that it takes care of all tables HTML and CSS peculiar to the WordPress admin section; it is relatively easy to create and validate new forms with a multitude of fields, and all security aspects of the WordPress backend like wp_verify_nonce() are incorporated into the form submission process.

Enjoy this post? You should follow me on Twitter!