I've been working on what is essentially my first proper attempt at making a useful PHP wrapper class for the Wordpress Settings API. It works great so far and I plan to add more methods as I go so that I eventually build up a class that can be used in pretty much any situation.As this is my first proper PHP class, I'm looking for suggestions of ways that I could refactor or do things more efficiently. Perhaps places where I could write code differently/use different functions to improve it.The code is also on my GitHub repo, so I'd appreciate any constructive feedback.<?php/** * A PHP Wrapper class to help set up an options page for the Wordpress API. * * @package Wordpress * @subpackage ThemeName * @version 1.0 * */class ThemeOptions{    // Declare class variables    public $pages = array();    public $sections = array();    public $fields = array();    /**     * Construct     *     * @since 1.0     */    public function __construct()    {           // Set the pages, sections and fields           add_action( 'admin_menu', array( &$this, 'addOptionsPages' ) );        add_action( 'admin_init', array( &$this, 'initializeThemeOptions' ) );    }    /**     * Add the options pages     *     * @since 1.0     * @todo Add support for other menu types     */    public function addOptionsPages()    {           foreach ( $this->pages as $page )        {            if ($page['type']=='menu')            {                add_menu_page(                    __( $page['title'] ),                    __( $page['title'] ),                    'administrator',                    $page['id'],                    array( &$this, 'displayOptionsPage' )                );            }            else if ($page['type'] == 'submenu')            {                add_submenu_page(                    $page['parent'],                    __( $page['title'] ),                    __( $page['title'] ),                    'administrator',                    $page['id'],                    array( &$this, 'displayOptionsPage' )                );            }        }    }    /**     * Add the settings sections     *     * @since 1.0     */    private function addOptionsSections()    {        foreach( $this->sections as $section )        {            add_settings_section(                $section['id'],                __($section['title']),                '', // Add the description for the section in later                $section['page']            );        }    }    /**     * Add the settings fields     *     * @since 1.0     */    private function addOptionsFields()    {        foreach ($this->fields as $section => $field)        {            foreach ($field as $option)            {                add_settings_field(                    $option['name'],                    __($option['label']),                    array(&$this, 'display' . ucfirst(strtolower($option['type'])) . 'Field'),                    $section,                    $option['section'],                    array(                        'option'        => $section,                        'optionName'    => $option['name']                     )                );            }        }    }    /**     * Register the settings with register_setting()     *     * @since 1.0     */    private function registerSettings()    {        foreach ($this->pages as $page)        {            register_setting(                $page['id'],                $page['id']            );        }    }    /**     * Display options page     *     * @since 1.0     */    public function displayOptionsPage()    {        if (isset($_GET['tab']))        {            $activeTab = $_GET['tab'];        }        else        {            $activeTab = strtolower( str_replace('_', '-', $_GET['page']) );        }    ?>        <div class=wrap>            <div id=icon-themes class=icon32></div>            <h2><?php _e( $this->pageTitle ); ?></h2>            <?php            settings_errors();            $this->displayOptionsTabs( $activeTab );            ?>            <form method=post action=options.php>                <?php $this->displayOptionsSettings( $activeTab ); ?>                <?php submit_button(); ?>            </form>        </div>    <?php    }    /**     * Set the pages, sections and fields     *     * @since 1.0     */    public function initializeThemeOptions()    {        $this->setOptionsDefaults();        // Create the sections        $this->addOptionsSections();        // Create the fields        $this->addOptionsFields();         // Register the settings        $this->registerSettings();    }    /**     * Display the tabs     *     * @param string $activeTab;     * @since 1.0     */    private function displayOptionsTabs( $activeTab = '' )    {        if ( count($this->pages) > 0 )        {               $output = '<h2 class=nav-tab-wrapper>';            foreach ( $this->pages as $page )            {                $currentTab = strtolower(str_replace('_', '-', $page['id']));                $activeClass = $activeTab == $currentTab ? ' nav-tab-active' : '';                $output .= sprintf(                    '<a href=?page=%1$s&tab=%2$s class=nav-tab%4$s id=%2$s-tab>%3$s</a>',                    $page['id'],                    $currentTab,                    $page['title'],                    $activeClass                );            }            $output .= '</h2>';            echo $output;        }    }    /**     * Display the sections and fields according to the page     *     * @param string $activeTab;     * @since 1.0     */    private function displayOptionsSettings( $activeTab = '' )    {        $currentTab = strtolower( str_replace('-', '_', $activeTab) );        settings_fields( $currentTab );        do_settings_sections( $currentTab );    }    /**     * HTML output for text field     *     * @param array $option;     * @since 1.0     */    public function displayTextField( $option = array() )    {        $value = get_option( $option['option'] );        printf(            '<input type=text id=%1$s class=regular-text name=%2$s[%3$s] value=%4$s>',            str_replace('_', '-', $option['optionName']),            $option['option'],            $option['optionName'],            sanitize_text_field($value[$option['optionName']])        );    }    /**     * HTML output for textarea field     *     * @param array $option;     * @since 1.0     */    public function displayTextareaField( $option = array() )    {        $value = get_option( $option['option'] );        printf(            '<textarea id=%1$s name=%2$s[%3$s] rows=5 cols=60>%4$s</textarea>',            str_replace('_', '-', $option['optionName']),            $option['option'],            $option['optionName'],            esc_textarea($value[$option['optionName']])        );    }    /**     * HTML output for url field     *     * @param array $option;     * @since 1.0     */    public function displayUrlField( $option = array() )    {        $value = get_option( $option['option'] );        printf(            '<input type=url id=%1$s class=regular-text name=%2$s[%3$s] value=%4$s>',            str_replace('_', '-', $option['optionName']),            $option['option'],            $option['optionName'],            esc_url($value[$option['optionName']])        );    }    /**     * Set default values for the settings.     *     * @since 1.0     */    private function setOptionsDefaults()    {        $defaults = array();        foreach ($this->fields as $optionsName => $options)        {            $defaults[$optionsName] = array();            foreach($options as $option)            {                $defaults[$optionsName][$option['name']] = '';            }        }        foreach ($defaults as $option => $value)        {            if( get_option( $option ) == false )            {                add_option(                    $option,                    apply_filters( $option . '_defaults', $value )                );            }        }    }    /**     * Validate the input depending on it's type. Used in register_setting().     *     * @since 1.0     * @todo Write the function!!!     */    public function validateOptions( $input )    {        return $input;    }}
First attempt - Wordpress PHP Settings API wrapper
php;wordpress
The first small thing that pops out is array( &$this, 'method_name' ). That's PHP 4 and you can totally drop the & (WordPress requires PHP 5.2.4+).The second is that if your going to use localization, better to put all in place throughout your code, ie: $translated = __( 'Hello World!', 'mytextdomain' );.And finally, and maybe that's micro-optimization, it's said that === is faster than ==.Notes regarding the code samples bellow:I try to follow WordPress Coding Standards. I prefer your coding style, but will shorten some things for brevity. InterfaceI'd really consider moving the theme options to a second level menu. We tend to think that our product deserves the spotlight, but, from a users point of view, theme settings are something that you do once in a while, maybe only once. The first thing that I do when I setup a site is removing stuff from my first level menu, as I value that real estate a lot.The method addOptionsPages() would be:public function add_options_pages() {     foreach ( $this->pages as $page ) {        if ( $page['type'] == 'menu' )            add_theme_page( $args );        else if ( $page['type'] == 'submenu' )            add_submenu_page(                null, # <-- Invisible page attached to /wp-admin/options.php?page=THIS-SUBMENU                __( $page['title'], 'mytextdomain' ),                __( $page['title'], 'mytextdomain' ),                'administrator',                $page['id'],                array( $this, 'display_options_page' )            );    }}And adjust the method displayOptionsTabs() adding the full URL in %5$s:$output .= sprintf(    '<a href=%5$s?page=%1$s&tab=%2$s class=nav-tab%4$s id=%2$s-tab>%3$s</a>',    $page['id'],    $currentTab,    $page['title'],    $activeClass,    ( $page['type'] == 'menu' ) ? admin_url( 'themes.php') : admin_url( 'options.php'));You'll note that there'll be an issue with the current admin menu indicator, it can be solved like this or this. Given that all this ends in a jQuery fix, you can consider only add_theme_page() and deal with the Tabs display doing a switch( $_GET['tab'] ) {}.Scripts and stylesTo target our own pages, the add_(sub)menu_page functions return the hook name that we can use like:$hook = add_menu_page( $args );add_action( admin_print_scripts-$hook, array( $this, 'print_scripts' );public function print_scripts(){    ?>    <style> /* declare styles */ </style>    <script>/* run scripts */ </style>    <?php}And to enqueue, and using your code:private $hooks = array();public function add_options_pages() {     foreach ( $this->pages as $page ) {        if ($page['type']=='menu')            $this->hooks[] = add_theme_page( $args );        else if ($page['type'] == 'submenu')            $this->hooks[] = add_submenu_page( $args );    }    add_action( 'admin_enqueue_scripts', array( $this, 'enqueue' ) );}public function enqueue( $hook ){    if( !in_array( $hook, $this->hooks ) )        return;    wp_enqueue_script( 'my-scp', plugins_url( '/js/my-script.js', __FILE__) ); # Adjust to use with themes    wp_enqueue_style( 'my-stl', plugins_url( '/css/my-style.js', __FILE__) ); }Settings APIThe only thing I'm missing is validation and sanitization in your register_setting( $option_group, $option_name, $sanitize_callback );. Two excellent references by leading WP developers:WordPress Settings API Tutorial, by Otto Wood// validate our optionsfunction plugin_options_validate( $input ) {    $newinput['text_string'] = trim( $input['text_string'] );    if( !preg_match( '/^[a-z0-9]{32}$/i', $newinput['text_string'] ) )        $newinput['text_string'] = '';    return $newinput;}Take special note of the fact that I dont return the original array. [...] In short, $input is untrusted data, but the returned $newinput should be trusted data.The WordPress Settings API, by Konstantin Kovsheninfunction my_settings_validate( $input ) {    $output = get_option( 'my-settings' );            if ( is_email( $input['email'] ) )        $output['email'] = $input['email'];    else        add_settings_error( 'my-settings', 'invalid-email', 'You have entered an invalid e-mail address.' );            return $output;}Heres the trick with validation. Well use the same technique as sanitization, but we will return the current option value, if the new input value was not good enough.You'd probably have to add a validation type inside your $PBThemeOptions->fields;DatabaseIn setOptionsDefaults() method, you're saving one option for each item in the fields array (example-options.php in your repo):$fields = array(    // Example Section One    'sandbox_theme_menu' => array( /* etc */ ),    // Example Section Two    'sandbox_theme_display_options' => array( /* etc */ ));Those are only two items, but as good practice it's recommended that we store all our plugin/theme settings inside one option only. Truth be told, as you have it it's much better than what we see in the wild, one db entry for each and every sub-option.