Quantcast
Viewing latest article 17
Browse Latest Browse All 101

A PHP class to create a Wordpress custom post type with dynamic metaboxes

On a theme I have been working on there was the need to create dynamic metaboxes for a basic jobs listing section. The listing page had four different fields for every job: Title, description, location and date. The user needed the ability to add, edit or delete jobs.

Coding this feature into Wordpress was quite a task. It's one thing to code a complex layout, but quite another to create a user-friendly interface in the admin area. I find with Wordpress that doing so in a theme means you really have to jump through coding hoops, whereas Drupal loves these types of complex forms / layouts.

The first problem is that there is no inbuilt Wordpress validation hook that displays error messages – so you need to write your own. The second issue is the use of multidimensional arrays for creating the form and then processing the data after editing, deletions or additions. This can create some challenging coding problems.

I already have a PHP class that I use when creating post types so I modified that for this purpose. As a caveat, the original code was based on somebody else's blog post that I came across sometime ago. I've tried to find it so as to give acknowledgement and a link. Apologies to that person but if you get in touch I will amend this article to do so. Their particular blog post first introduced me to the idea of using the add_filter() hooks to change the post setting from published to draft as well as adding a redirect that allows the displaying of an error message.

The final code is on this GitGist, but I'll run through it all one method at a time.

Before I start I'll state that this approach is really only for serious PHP heads. If that's not you, then it's best if you try to find a plugin to achieve the same functionality.

This is the structure of the class:

namespace content;

class Content_Type_Jobs_Template {

    static $wpdb = null;

    public function __construct() {

        global $wpdb;
        self::$wpdb = $wpdb;

        add_action('init', array(&$this, 'content_type'));

        // I have a habit of using sessions but forgetting to use session_start()
        // So I place this in the __construct method and use session_regenerate_id() for security
        if (session_id() == '') {
            session_start();
            session_regenerate_id(true);
        }

    }

    public function metabox_attributes() {
    } 

    public function content_type() {
    }

    public function add_metaboxes() {
    }

    public function text_one_html() {
    } 

    protected function validation($form) {
    }

    public function save_postdata() {
    } 

    public function add_redirect_filter() {
    } 

    public function my_redirect_post_location_filter($location) {
    } 

    public function my_post_admin_notices() {
    } 

} // end class

new contentContent_Type_Jobs_Template;

As the code serves one purpose only it makes sense to take an Object Orientated approach to keep it organised in one file. If you are going to use classes then also using a namespace helps to avoid naming clashes. This means that it won't work on servers that run less than PHP 5.3, but as that release is a few years old now that isn't something I'm bothered about.

The first method is metabox_attributes():

public function metabox_attributes() {

    return array(

        'title' => array(
            'name' => 'title',
            'type' => 'text',
            'title' => 'Title',
            'description' => 'The job title',
            ),

        'description' => array(
            'name' => 'description',
            'type' => 'text',
            'title' => 'Description',
            'description' => 'Short description',
            ),

        'location' => array(
            'name' => 'location',
            'type' => 'text',
            'title' => 'Location',
            'description' => 'Location of job',
            ),

        'date' => array(
            'name' => 'date',
            'type' => 'text',
            'title' => 'Date',
            'description' => 'Date posted',
            ),

        );

} 

This is where you declare your form field name and type attributes plus a title and description that is used in the display.

Next comes the content_type() with uses the Wordpress register_post_type() function. For more information on this read the guide on the official Wordpress website.

public function content_type() {

    $labels = array(
        'name' => _x('Jobs Layout', 'post type general name'),
        'singular_name' => _x('Jobs Layout', 'post type singular name'),
        'add_new' => _x('Add New', 'book'),
        'add_new_item' => __('Jobs Layout'),
        'edit_item' => __('Edit Jobs Layout type'),
        'new_item' => __('New Jobs Layout type'),
        'all_items' => __('All Jobs Layout types'),
        'view_item' => __('View Jobs Layout types'),
        'search_items' => __('Search Jobs Layout types'),
        'not_found' => __('No Jobs Layout types found'),
        'not_found_in_trash' => __('No Jobs Layout types found in Trash'),
        'parent_item_colon' => '',
        'menu_name' => __('Jobs Layout'),
        );

    $args = array(
        'labels' => $labels,
        'description' => 'Jobs',
        'public' => true,
        'publicly_queryable' => true,
        'show_ui' => true,
        'show_in_menu' => true,
        'query_var' => true,
        'rewrite' => true,
        'capability_type' => 'post',
        'has_archive' => true,
        'hierarchical' => false,
        'menu_position' => null,
        'register_meta_box_cb' => array(&$this, 'add_metaboxes'),
        'taxonomies' => array('category'),
        'supports' => array(
            'title',
            'editor',
            'author',
            'thumbnail',
            'excerpt',
            ));

    add_action('save_post', array(&$this, 'save_postdata'), 10, 2);
    add_action('admin_notices', array(&$this, 'my_post_admin_notices'), 10, 1);
    register_post_type('jobs', $args);

}

The register_meta_box_cb key uses the following method:

public function add_metaboxes() {

        add_meta_box('jobs_metabox', 'Add individual jobs below', array(&$this, 'text_one_html'),
            'jobs', 'normal', 'high');

    }

Which in turn calls text_one_html().

This is where things get tricky. The method is in two different parts.

The first uses the array in metabox_attributes() to create an empty form:

$new_meta_boxes = $this->metabox_attributes();

$form = '<table id="list-table">';
$form .= '<tbody id="the-list" width="100%">';

foreach ($new_meta_boxes as $key => $result) {

    if ($result['name'] == 'title') {

        $form .= '<thead>';
        $form .= '<tr>';
        $form .= '<th class="left"><h4>Add a new job below</h4></th>';
        $form .= '<th><h4>Values</h4></th>';
        $form .= '</tr>';
        $form .= '</thead>';

    }

    if ($result['type'] == 'text') {

        $form .= '<tr>';
        $form .= '<td class="left" width="30%">';
        $form .= '<label for="'.$result['name'].'"><strong>'.$result['title'].'</strong></label>';
        $form .= '<div>'.$result['description'].'</div>';
        $form .= '</td>';
        $form .= '<td width="70%">';
        $form .= '<input type="text" name="'.$result['name'].'" value="';
        $form .= isset($_SESSION[$result['name']]) ? $_SESSION[$result['name']] : null;
        $form .= '" class="regular-text"/>';
        $form .= '</td>';
        $form .= '</tr>';

    }

} // end foreach

$form .= '</tbody>';
$form .= '</table>';

echo $form;

If the user has already created job listings then this data needs to be placed into forms underneath the empty one.

$meta = get_post_meta($post->ID);

$z = $y = $x = 0;

// takes the meta data from the database and creates forms
// populated by the data

if (!empty($meta)) {

    // remove the _edit_last and _edit_lock arrays from the returned multidimensional array
    $remove = array('_edit_last', '_edit_lock');
    $meta = array_diff_key($meta, array_flip($remove));

    $form = '<table id="list-table">';
    $form .= '<tbody id="the-list" width="100%">';

    foreach ($meta as $key => $value) {

        // remove all digits from the key. New form unique form attributes are created below
        $key = preg_replace('/d/', '', $key);

        // make sure that all returned keys fit the original keys in the metabox_attributes method
        if (!in_array($key, array_keys($this->metabox_attributes()))) continue;

        // for every number of total keys in the metabox_attributes add an extra digit
        // this is to ensure that all name attributes are unique
        if ($x++ % count($this->metabox_attributes()) == 0) {

            $y += 1;

        }

        // remove digit from the string
        // and then use array_intersect_key() and array_flip() to find the array of
        // the relevant corresponding key from the meta_attributes() method
        // this is then used in the html

        $key_array = array_intersect_key($this->metabox_attributes(), array_flip(array($key)));

        if ($key_array[$key]['name'] == 'title') {

            $form .= '<thead>';
            $form .= '<tr>';
            $form .= '<th class="left"><h4>'.implode('', $value).'</h4></th>';
            $form .= '<th> </th>';
            $form .= '</tr>';
            $form .= '</thead>';

        }

        if ($key_array[$key]['type'] == 'text') {

            $form .= '<tr>';
            $form .= '<td class="left" width="30%">';
            $form .= '<label for="'.$key.$y.'"><strong>'.$key_array[$key]['title'].
                '</strong></label>';
            $form .= '<div>'.$key_array[$key]['description'].'</div>';
            $form .= '</td>';
            $form .= '<td width="70%">';
            $form .= '<input type="text" name="'.$key.$y.'" value="';
            $form .= isset($_SESSION[$key.$y]) ? $_SESSION[$key.$y] : implode('', $value);
            $form .= '" class="regular-text"/>';
            $form .= '</td>';
            $form .= '</tr>';

        }

    }

    $form .= '</tbody>';
    $form .= '</table>';

    echo $form;

}

What the code is doing is removing unwanted keys from the array ('_edit_last' and '_edit_lock'), stripping the previously added digits at the end of the name attribute values; making sure that the key is in the metabox_attributes() array and it uses the modulus operator to add new a digit with every iteration of the total number of arrays in metabox_attributes() . This is necessary not just to keep the form accessible with valid HTML, but also to ensure that the database entries are unique. Phew, there's a lot happening in this block of code.

Here is the validation:

protected function validation($form) {

    $error = array();

    // filter out unwanted parts of the $_POST
    foreach ($form as $key => $value) {
        if (!in_array(preg_replace('/d/', '', $key), array_keys($this->metabox_attributes()))) {
            unset($form[$key]);
        } else {
            // this session data is used if there is a form error to repopulate the form fields
            // if the form has no error then the session data is destroyed
            $_SESSION[$key] = $value;
        }
    }

    // make sure that the the values are in proportion to that specified in the number
    // of fields in the metabox_attributes. For instance if there are 4 fields in the method, then there must
    // be no blank values in multiplications of 4, ie 4, 8 12
    if (count(array_values(array_filter($form, 'strlen'))) % count($this->metabox_attributes()) == 0) {

        return true;

    } else {

        $error[] = 'Please make sure that all '.count($this->metabox_attributes()).
            ' fields are filled in or leave all form fields empty for the job listing to be deleted';

        return $error;

    }

}

Firstly it again uses the metabox_attributes() array so as to remove unwanted parts of the form. What is left are only the elements that are used for the job listing data. Then it counts the total number of FULL values and using the modulus operator compares it to the number of keys in the multidimensional array in metabox_attributes(). count(array_values(array_filter($form, 'strlen'))) was something I took from php.net and which removes empty array values.

If the number is different it means that a field has been left blank which then creates an error message.

Note how all job form attributes and values are saved as a session. These are used in the forms if validation fails. They are destroyed on successful completion.

The save_postdata() method is in two parts. After validation, the code is forked between success or failure.

If there are no errors, then the data is either saved or deleted:

if ($error === true) {

	// destory session here and save meta data

	foreach ($form as $key => $value) {

		// filter out unwanted parts of the $_POST
		if (!in_array(preg_replace('/d/', '', $key), array_keys($this->metabox_attributes()))) continue;


		if ($value != '') {

			update_post_meta($post->ID, $key, strip_tags($value), false);

		} else {

			delete_post_meta($post->ID, $key, false);

		}

		// destroy session

		$_SESSION = array();

	} // end foreach

}

The second part handle errors:

if ((isset($_POST['publish']) || isset($_POST['save'])) && $_POST['post_status'] == 'publish') {

    self::$wpdb->update(self::$wpdb->posts, array('post_status' => 'pending'), array('ID' => $post->
            ID));

} // end fi

add_filter('wp_insert_post_data', array(&$this, 'add_redirect_filter'), 102);

$this->add_redirect_filter();

$_SESSION['error'] = serialize($error);

Firstly it changes the post status from published to pending, it then calls the add_filter() hook and the add_redirect_filter() method and finally the $error array is serialised into a session to be used later on.

The last method in the class is my_post_admin_notices(). This is how the error messages are displayed to the user.

public function my_post_admin_notices() {

    if (!isset($_GET['message'])) return;

    if ($_GET['message'] == '102') {

        global $post;

        // abort if not my custom type
        if ($post->post_type != 'jobs') return $post->ID;

        $message = '<div id="notice" class="error"><p>';
        $message .= "Error: please check that all form values are correct - <br>";
        $message .= isset($_SESSION['error']) ? implode('<br>', unserialize($_SESSION['error'])) : null;
        $message .= '</p></div>';
        echo $message;

        unset($_SESSION['error']);

    } // end if

} 

Conclusion

The code works and is used on a site soon to be moved to production. I'll post up the URL when it is live. Again, the full class can be found on this GitGist.

I accessed the data to create a table in the theme with this code:

if (class_exists('contentContent_Type_Jobs_Template')) {

    $meta = get_post_meta($id);

    $table = '<table>
<caption>Current vacancies</caption>
<tr class="header-row">
<td id="job-title">Title</td>
<td id="job-description">Description</td>
<td id="job-location">Location</td>
<td id="job-date">Date posted</td>
</tr>';

    $array_keys = array_keys(contentContent_Type_Jobs_Template::metabox_attributes());

    // loop through metabox jobs content to create the table data

    foreach ($meta as $key => $value) {

        // make sure that all returned keys fit the original keys in the metabox_attributes method
        if (!in_array(preg_replace('/d/', '', $key), $array_keys))
            continue;

        static $i;
        $number = $i++;

        if ($number % (count($array_keys)) == 0) {
            $table .= '<tr>';
        }

        if (stristr($key, $array_keys['0'])) {
            $table .= '<td headers="job-title">' . implode('', $value) . '</td>';
        }

        if (stristr($key, $array_keys['1'])) {
            $table .= '<td headers="job-description">' . implode('', $value) . '</td>';
        }

        if (stristr($key, $array_keys['2'])) {
            $table .= '<td headers="job-location">' . implode('', $value) . '</td>';
        }

        if (stristr($key, $array_keys['3'])) {
            $table .= '<td headers="job-date">' . implode('', $value) . '</td>';
        }

        if (($number - (count($array_keys) - 1)) % (count($array_keys)) == 0) {
            $table .= '</tr>';
        }

    } // end foreach

    $table .= '</table>';

    echo $table;

} // end if  if (class_exists('menuContent_Type_Jobs_Template'))


Viewing latest article 17
Browse Latest Browse All 101

Trending Articles