Steve Taylor photo

Control your own WordPress custom fields

PLEASE NOTE: This code has now evolved into my Developer’s Custom Fields plugin. Please use this plugin instead of the code below, which is kept here out of historical interest only!

I’m currently working on a client’s WordPress site where there’s quite a few bits of custom functionality in my custom theme that rely on them entering values for posts or pages using WP’s custom fields.

Custom fields are really flexible. However, they’re not perfectly user-friendly. For instance, if no post or page is currently using a custom field that you’ve built functionality on, the user has to enter the name as well as the value of the field the first time it’s used. The drop-down of field names is dynamically gathered from the fields currently in use. Also, sometimes you want to make things easier for clients by having inline tips, and inputs that suit the field (e.g. a checkbox or select drop-down instead of just a plain text entry).

So, I set about piecing together a way to take over the Custom Fields meta box…

First off, if you’re not developing custom themes, and you want a flexible plugin that will do this sort of thing, including an easy interface for defining custom fields, what I’ve got here isn’t for you. You might want to check out Custom Field Template, Flutter, or Magic Fields. Really, the power in any of those blows the code below out the water. My concern here is some lean, only-what-you-need code that you have total control over.

Before we begin, here’s a taster. This is a grab from a page edit screen on the project I’ve developed this code for:

custom-fields

I started off working with Function’s Custom Write Panels code, but their approach ended up being most concerned with post “metadata”. It’s all stored in one field in the postmeta table, in a serialized array. Which is great if you just need a clump of different values to output for any particular post. My needs are more diverse, and I often access individual fields for various reasons.

(TIP: If you store custom fields separately, as I will, and do end up wanting to grab them all at once, use get_post_custom().)

One more shout. I don’t know PHP objects very well, I’m absorbing it as I go. My class structure for this code was taken from the very useful WordPress Plugin Template from Pressography.

OK, let’s see what we’ve got. I’ll go through it step-by-step. This code is designed to go into your custom theme’s functions.php file—though if you wouldn’t have guessed, it might not be for you! (N.B. It must not be placed into /wp-includes/functions.php—see comments below.)

if ( !class_exists('myCustomFields') ) {

	class myCustomFields {
		/**
		* @var  string  $prefix  The prefix for storing custom fields in the postmeta table
		*/
		var $prefix = '_mcf_';
		/**
		* @var  array  $postTypes  An array of public custom post types, plus the standard "post" and "page" - add the custom types you want to include here
		*/
		var $postTypes = array( "page", "post" );

First a simple check to prevent class clashes, and the definition of the first property. This is the prefix that we’ll use for the meta_key when we store values in the postmeta table.

An important point to note here is that if you put an underscore at the start of a meta_key, the custom field will be “hidden”. That means it won’t show up in the default Custom Fields meta box. This code will include the option to hide that default box anyway, but there might be situations where you want to keep that in place for other reasons, alongside your custom fields box. In this case, it’s good to keep your fields in your box with this method.

Then, we set up an array of the post types that the standard Custom Fields should be overridden for. Obviously we include post and page. If your theme has custom post types, add any you want to include here.

		/**
		* @var  array  $customFields  Defines the custom fields available
		*/
		var $customFields =	array(
			array(
				"name"			=> "block-of-text",
				"title"			=> "A block of text",
				"description"	=> "",
				"type"			=> "textarea",
				"scope"			=>	array( "page" ),
				"capability"	=> "edit_pages"
			),
			array(
				"name"			=> "short-text",
				"title"			=> "A short bit of text",
				"description"	=> "",
				"type"			=>	"text",
				"scope"			=>	array( "post" ),
				"capability"	=> "edit_posts"
			),
			array(
				"name"			=> "checkbox",
				"title"			=> "Checkbox",
				"description"	=> "",
				"type"			=> "checkbox",
				"scope"			=>	array( "post", "page" ),
				"capability"	=> "manage_options"
			)
		);
		/**

Now we define our custom fields. I’ve set up a little system that’s quite flexible, and included examples of three common field types. Obviously, you can extend this however you want. The field types are up to you. I’ve been using custom types that create, say, drop-downs of users of a certain role, things like that.

Anyway, the basic values set for each field are:

  • name: This gets used with the prefix to create the meta_key for the field.
  • title: This is for the form’s .
  • description: This is a bit of descriptive text that goes under the field input, to help remind the user what it’s for.
  • type: The types above happen to correspond to HTML form input types, but they don’t have to.
  • scope: Where should these fields get used? Here it’s simply about whether they go on post or page screens, but again, you can make your own scopes up to restrict the fields to certain sections of your site, pages using certain templates, etc.
  • capability: Which capability is required to edit the field? If you don’t understand WordPress capabilities, Justin Tadlock has an excellent introduction to users, roles and capabilities.
		/**
		* PHP 4 Compatible Constructor
		*/
		function myCustomFields() { $this->__construct(); }
		/**
		* PHP 5 Constructor
		*/
		function __construct() {
			add_action( 'admin_menu', array( &$this, 'createCustomFields' ) );
			add_action( 'save_post', array( &$this, 'saveCustomFields' ), 1, 2 );
			// Comment this line out if you want to keep default custom fields meta box
			add_action( 'do_meta_boxes', array( &$this, 'removeDefaultCustomFields' ), 10, 3 );
		}
		/**
		* Remove the default Custom Fields meta box
		*/
		function removeDefaultCustomFields( $type, $context, $post ) {
			foreach ( array( 'normal', 'advanced', 'side' ) as $context ) {
				foreach ( $this->postTypes as $postType ) {
					remove_meta_box( 'postcustom', $postType, $context );
				}
			}
		}
		/**
		* Create the new Custom Fields meta box
		*/
		function createCustomFields() {
			if ( function_exists( 'add_meta_box' ) ) {
				foreach ( $this->postTypes as $postType ) {
					add_meta_box( 'my-custom-fields', 'Custom Fields', array( &$this, 'displayCustomFields' ), $postType, 'normal', 'high' );
				}
			}
		}

Now there’s the class’s constructor, which basically adds the action hooks for outputting the new meta box, saving values when the form’s submitted, and removing the default Custom Fields meta box if necessary.

Then there’s the functions to remove the default box, and to create the new one, on the edit screens for the post types defined at the start.

		/**
		* Display the new Custom Fields meta box
		*/
		function displayCustomFields() {
			global $post;
			?>
			<div class="form-wrap">
				<?php
				wp_nonce_field( 'my-custom-fields', 'my-custom-fields_wpnonce', false, true );
				foreach ( $this->customFields as $customField ) {
					// Check scope
					$scope = $customField[ 'scope' ];
					$output = false;
					foreach ( $scope as $scopeItem ) {
						switch ( $scopeItem ) {
							default: {
								if ( $post->post_type == $scopeItem )
									$output = true;
								break;
							}
						}
						if ( $output ) break;
					}
					// Check capability
					if ( !current_user_can( $customField['capability'], $post->ID ) )
						$output = false;

Here’s the start of the output of the new custom fields box. We used wp_nonce_field() to improve security, then start our loop through all the defined fields.

The first check is on the scope of the field, which is an array of strings indicating the context in which they should be used. Outputting defaults to false, then gets set to true if any of the scope conditions are fulfilled. As I said earlier, you can define your own scopes for particular needs—the sky’s the limit. Here, there’s a simple check to make sure the custom field has been assigned to the current post type.

Then there’s a quick check on the capability.

					// Output if allowed
					if ( $output ) { ?>
						<div class="form-field form-required">
							<?php
							switch ( $customField[ 'type' ] ) {
								case "checkbox": {
									// Checkbox
									echo '<label for="' . $this->prefix . $customField[ 'name' ] .'" style="display:inline;"><b>' . $customField[ 'title' ] . '</b></label>&amp;nbsp;&amp;nbsp;';
									echo '<input type="checkbox" name="' . $this->prefix . $customField['name'] . '" id="' . $this->prefix . $customField['name'] . '" value="yes"';
									if ( get_post_meta( $post->ID, $this->prefix . $customField['name'], true ) == "yes" )
										echo ' checked="checked"';
									echo '" style="width: auto;" />';
									break;
								}
								case "textarea":
								case "wysiwyg": {
									// Text area
									echo '<label for="' . $this->prefix . $customField[ 'name' ] .'"><b>' . $customField[ 'title' ] . '</b></label>';
									echo '<textarea name="' . $this->prefix . $customField[ 'name' ] . '" id="' . $this->prefix . $customField[ 'name' ] . '" columns="30" rows="3">' . htmlspecialchars( get_post_meta( $post->ID, $this->prefix . $customField[ 'name' ], true ) ) . '</textarea>';
									// WYSIWYG
									if ( $customField[ 'type' ] == "wysiwyg" ) { ?>
										<script type="text/javascript">
											jQuery( document ).ready( function() {
												jQuery( "<?php echo $this->prefix . $customField[ 'name' ]; ?>" ).addClass( "mceEditor" );
												if ( typeof( tinyMCE ) == "object" &amp;&amp; typeof( tinyMCE.execCommand ) == "function" ) {
													tinyMCE.execCommand( "mceAddControl", false, "<?php echo $this->prefix . $customField[ 'name' ]; ?>" );
												}
											});
										</script>
									<?php }
									break;
								}
								default: {
									// Plain text field
									echo '<label for="' . $this->prefix . $customField[ 'name' ] .'"><b>' . $customField[ 'title' ] . '</b></label>';
									echo '<input type="text" name="' . $this->prefix . $customField[ 'name' ] . '" id="' . $this->prefix . $customField[ 'name' ] . '" value="' . htmlspecialchars( get_post_meta( $post->ID, $this->prefix . $customField[ 'name' ], true ) ) . '" />';
									break;
								}
							}
							?>
							<?php if ( $customField[ 'description' ] ) echo '<p>' . $customField[ 'description' ] . '</p>'; ?>
					<?php
					}
				} ?>
			</div>
			<?php
		}

Now the output—pretty self-explanatory!

		/**
		* Save the new Custom Fields values
		*/
		function saveCustomFields( $post_id, $post ) {
			if ( !isset( $_POST[ 'my-custom-fields_wpnonce' ] ) || !wp_verify_nonce( $_POST[ 'my-custom-fields_wpnonce' ], 'my-custom-fields' ) )
				return;
			if ( !current_user_can( 'edit_post', $post_id ) )
				return;
			if ( ! in_array( $post->post_type, $this->postTypes ) )
				return;
			foreach ( $this->customFields as $customField ) {
				if ( current_user_can( $customField['capability'], $post_id ) ) {
					if ( isset( $_POST[ $this->prefix . $customField['name'] ] ) &amp;&amp; trim( $_POST[ $this->prefix . $customField['name'] ] ) ) {
						$value = $_POST[ $this->prefix . $customField['name'] ];
						// Auto-paragraphs for any WYSIWYG
						if ( $customField['type'] == "wysiwyg" ) $value = wpautop( $value );
						update_post_meta( $post_id, $this->prefix . $customField[ 'name' ], $_POST[ $this->prefix . $customField['name'] ] );
					} else {
						delete_post_meta( $post_id, $this->prefix . $customField[ 'name' ] );
					}
				}
			}
		}

	} // End Class

} // End if class exists statement

// Instantiate the class
if ( class_exists('myCustomFields') ) {
	$myCustomFields_var = new myCustomFields();
}

Finally, a function to save the values when the form is submitted. The nonce is checked, then we loop through all the fields, checking that the capability is there and the field has been submitted. Then, we either add / update the field value if there is one, or delete it. You might want to just add / update, and store empty values for fields that apply to a post or page, but don’t have anything set. I thought I’d err on the side of keeping the database lean.

Then we instantiate the class, and that’s it.

The full code

Below is the code in full. To use it:

  1. Paste it into your functions.php.
  2. Change the $prefix if you want.
  3. Adapt the $postTypes array to define your post types.
  4. Adapt the $customFields array to define your fields.
  5. If you’ve added your own scopes, add case statements for them around line 82.
  6. If you’ve added your own field types, add case statements for them around line 105.

I’ve been adapting certain bits as a write this post, making the bits that are specific to my use of the code more general for this example, so forgive me if there’s any bugs that have crept in. Let me know if you find any problems with the code!

if ( !class_exists('myCustomFields') ) {

	class myCustomFields {
		/**
		* @var  string  $prefix  The prefix for storing custom fields in the postmeta table
		*/
		var $prefix = '_mcf_';
		/**
		* @var  array  $postTypes  An array of public custom post types, plus the standard "post" and "page" - add the custom types you want to include here
		*/
		var $postTypes = array( "page", "post" );
		/**
		* @var  array  $customFields  Defines the custom fields available
		*/
		var $customFields =	array(
			array(
				"name"			=> "block-of-text",
				"title"			=> "A block of text",
				"description"	=> "",
				"type"			=> "textarea",
				"scope"			=>	array( "page" ),
				"capability"	=> "edit_pages"
			),
			array(
				"name"			=> "short-text",
				"title"			=> "A short bit of text",
				"description"	=> "",
				"type"			=>	"text",
				"scope"			=>	array( "post" ),
				"capability"	=> "edit_posts"
			),
			array(
				"name"			=> "checkbox",
				"title"			=> "Checkbox",
				"description"	=> "",
				"type"			=> "checkbox",
				"scope"			=>	array( "post", "page" ),
				"capability"	=> "manage_options"
			)
		);
		/**
		* PHP 4 Compatible Constructor
		*/
		function myCustomFields() { $this->__construct(); }
		/**
		* PHP 5 Constructor
		*/
		function __construct() {
			add_action( 'admin_menu', array( &amp;$this, 'createCustomFields' ) );
			add_action( 'save_post', array( &amp;$this, 'saveCustomFields' ), 1, 2 );
			// Comment this line out if you want to keep default custom fields meta box
			add_action( 'do_meta_boxes', array( &amp;$this, 'removeDefaultCustomFields' ), 10, 3 );
		}
		/**
		* Remove the default Custom Fields meta box
		*/
		function removeDefaultCustomFields( $type, $context, $post ) {
			foreach ( array( 'normal', 'advanced', 'side' ) as $context ) {
				foreach ( $this->postTypes as $postType ) {
					remove_meta_box( 'postcustom', $postType, $context );
				}
			}
		}
		/**
		* Create the new Custom Fields meta box
		*/
		function createCustomFields() {
			if ( function_exists( 'add_meta_box' ) ) {
				foreach ( $this->postTypes as $postType ) {
					add_meta_box( 'my-custom-fields', 'Custom Fields', array( &amp;$this, 'displayCustomFields' ), $postType, 'normal', 'high' );
				}
			}
		}
		/**
		* Display the new Custom Fields meta box
		*/
		function displayCustomFields() {
			global $post;
			?>
			<div class="form-wrap">
				<?php
				wp_nonce_field( 'my-custom-fields', 'my-custom-fields_wpnonce', false, true );
				foreach ( $this->customFields as $customField ) {
					// Check scope
					$scope = $customField[ 'scope' ];
					$output = false;
					foreach ( $scope as $scopeItem ) {
						switch ( $scopeItem ) {
							default: {
								if ( $post->post_type == $scopeItem )
									$output = true;
								break;
							}
						}
						if ( $output ) break;
					}
					// Check capability
					if ( !current_user_can( $customField['capability'], $post->ID ) )
						$output = false;
					// Output if allowed
					if ( $output ) { ?>
						<div class="form-field form-required">
							<?php
							switch ( $customField[ 'type' ] ) {
								case "checkbox": {
									// Checkbox
									echo '<label for="' . $this->prefix . $customField[ 'name' ] .'" style="display:inline;"><b>' . $customField[ 'title' ] . '</b></label>&amp;nbsp;&amp;nbsp;';
									echo '<input type="checkbox" name="' . $this->prefix . $customField['name'] . '" id="' . $this->prefix . $customField['name'] . '" value="yes"';
									if ( get_post_meta( $post->ID, $this->prefix . $customField['name'], true ) == "yes" )
										echo ' checked="checked"';
									echo '" style="width: auto;" />';
									break;
								}
								case "textarea":
								case "wysiwyg": {
									// Text area
									echo '<label for="' . $this->prefix . $customField[ 'name' ] .'"><b>' . $customField[ 'title' ] . '</b></label>';
									echo '<textarea name="' . $this->prefix . $customField[ 'name' ] . '" id="' . $this->prefix . $customField[ 'name' ] . '" columns="30" rows="3">' . htmlspecialchars( get_post_meta( $post->ID, $this->prefix . $customField[ 'name' ], true ) ) . '</textarea>';
									// WYSIWYG
									if ( $customField[ 'type' ] == "wysiwyg" ) { ?>
										<script type="text/javascript">
											jQuery( document ).ready( function() {
												jQuery( "<?php echo $this->prefix . $customField[ 'name' ]; ?>" ).addClass( "mceEditor" );
												if ( typeof( tinyMCE ) == "object" &amp;&amp; typeof( tinyMCE.execCommand ) == "function" ) {
													tinyMCE.execCommand( "mceAddControl", false, "<?php echo $this->prefix . $customField[ 'name' ]; ?>" );
												}
											});
										</script>
									<?php }
									break;
								}
								default: {
									// Plain text field
									echo '<label for="' . $this->prefix . $customField[ 'name' ] .'"><b>' . $customField[ 'title' ] . '</b></label>';
									echo '<input type="text" name="' . $this->prefix . $customField[ 'name' ] . '" id="' . $this->prefix . $customField[ 'name' ] . '" value="' . htmlspecialchars( get_post_meta( $post->ID, $this->prefix . $customField[ 'name' ], true ) ) . '" />';
									break;
								}
							}
							?>
							<?php if ( $customField[ 'description' ] ) echo '<p>' . $customField[ 'description' ] . '</p>'; ?>
						</div>
					<?php
					}
				} ?>
			</div>
			<?php
		}
		/**
		* Save the new Custom Fields values
		*/
		function saveCustomFields( $post_id, $post ) {
			if ( !isset( $_POST[ 'my-custom-fields_wpnonce' ] ) || !wp_verify_nonce( $_POST[ 'my-custom-fields_wpnonce' ], 'my-custom-fields' ) )
				return;
			if ( !current_user_can( 'edit_post', $post_id ) )
				return;
			if ( ! in_array( $post->post_type, $this->postTypes ) )
				return;
			foreach ( $this->customFields as $customField ) {
				if ( current_user_can( $customField['capability'], $post_id ) ) {
					if ( isset( $_POST[ $this->prefix . $customField['name'] ] ) &amp;&amp; trim( $_POST[ $this->prefix . $customField['name'] ] ) ) {
						$value = $_POST[ $this->prefix . $customField['name'] ];
						// Auto-paragraphs for any WYSIWYG
						if ( $customField['type'] == "wysiwyg" ) $value = wpautop( $value );
						update_post_meta( $post_id, $this->prefix . $customField[ 'name' ], $value );
					} else {
						delete_post_meta( $post_id, $this->prefix . $customField[ 'name' ] );
					}
				}
			}
		}

	} // End Class

} // End if class exists statement

// Instantiate the class
if ( class_exists('myCustomFields') ) {
	$myCustomFields_var = new myCustomFields();
}

UPDATE 18/11/10: Significant changes to allow for custom post types.

UPDATE 10/8/10: Added support for placing a TinyMCE editor instead of a text area!

UPDATE 5/3/10: I’ve been alerted to a recent change in WordPress 2.9.1+ that affects the code in the removeDefaultCustomFields function to remove the default Custom Fields box on page screens. The change is highlighted in the above code.

UPDATE 18/11/09: I’ve just found an important issue with using the save_post hook. Alex King explains it here. The executive summary is that when the saveCustomFields function above is added to the save_post hook, it’s now set to receive 2 arguments—the post ID, as before, plus the post’s data. Using the passed $post argument, the save function checks that the post_type is “page” or “post”. This stops the save code being run for autosaves or saving revisions, which could create duplicate postmeta fields.

60 comments

  1. Steve Taylor avatar Steve Taylor

    Same as you would in any HTML page. Not sure what the problem would be?

  2. flavius avatar flavius

    well, i’d like to use it like this:


    var $seoFields = array(
    array(
    "name" => "color-change",
    "title" => "Change the theme:",
    "description" => "(Pick the color scheme you want to use)",
    "options" => array( "Blue", "Red", "Black" ),
    "type" => "dropdown",
    "scope" => array( "post" ),
    "capability" => "edit_posts"
    ));

    but i don’t know how to save it. and when displaying it’s contents in the meta box, it only shows _mcf_Array.

    i was thinking of exploding the array, to populate the dropdown but still, i wouldn’t know how to save it.

  3. Steve Taylor avatar Steve Taylor

    As long as you’ve added a dropdown case to the switch ( $customField[ 'type' ] ), you should be fine. There’s no issue with saving it: when you submit the form, the select field’s value will be that of the selected option, and that’ll get saved in the meta field.

  4. flavius avatar flavius

    please, delete my above comments.

    any idea how to remove the duplicate value? one is being read from the array and the other one is the one previousely saved.

    thanks.

  5. flavius avatar flavius

    uh… i already used pastebin and posted a comment with the link here, maybe it was marked as spam?

    anyway, i solved the problem.

    thanks a lot for this great article (A LOT better than others i found on the web), been a real help. keep up the great work!

  6. Steve Taylor avatar Steve Taylor

    Sorry, you got spammed! I’ve tidied things up. Glad you got it sorted, and glad the code was useful! My version has expanded immensely since I posted this – it can be really powerful. I’m sure the plugins I mentioned are even better, but I’m a sucker for rolling my own ;-)

  7. flavius avatar flavius

    i really don’t wanna use plugins, especially when developing themes for other people no-so-into-computers. easier for them to have everything laid out for them.

    besides, i really like it to make my own custom things, and with my knowledge and the help of guys like you, i do a pretty good job :)

    again, thanks!

  8. Jason avatar Jason

    How can I show the existing custom fields already being used by my theme?
    Before adding this code to my functions.php file I had 3 custom fields that are no longer visible.

  9. Steve Taylor avatar Steve Taylor

    You could just comment out line 48, which adds the action to remove the default custom fields box. If you use field names with an underscore at the start, the fields this code uses won’t appear in the default box – where you’ll still be able to manage the rest of your custom fields.

  10. sebastien avatar sebastien

    Superbe article ! Très professionnel, très bien documenté ! Merci. Tu expliques très bien qu’on peut limiter l’affichage d’un champs aux articles ou aux pages. mais est-il possible de limiter l’affichage d’un champ aux articles d’une catégorie spécifique ? Par-exemple, si on ne veut afficher une checkbox seulement si le post est dans la catégorie “portfolio”.

    merci !
    —-

    Very nice post ! Very professional, very well documented ! Thank you. You explain very well that we can limit the display of a custom field in posts or pages. But is it possible to limit the display of a field to posts with a specific category? For example, if i want display a checkbox only if the post is in the category “portfolio”.

    Thanks !

  11. sebastien avatar sebastien

    with your code it’s not possible to update the checkbox. What can i do to update a checkbox ?

  12. Steve Taylor avatar Steve Taylor

    Regarding checkboxes – works fine for me.

    And regarding limiting the display of a field, you can just customize the “scope” system. Maybe add “portfolio” to the scope array for the definition of that checkbox field. Then, in the scope check around line 82 add a case for “portfolio” where you make sure that the post being edited is in that category. You could add an extra check in the save function, but it’s not much of a risk to leave that out.

  13. sebastien avatar sebastien

    Thanx Steve, and for the checkbox, what’s about the update ?

  14. sebastien avatar sebastien

    Excuse-moi, je n’avais pas vu que tu avais écrit “Regarding checkboxes – works fine for me.”.
    Pour la checkbox, tu as écrit dans le code : value=”yes”. Donc la valeur est toujours = “yes”. Elle ne peut donc pas être false. La case est donc toujours cochée.

    Excuse me, I did not see that you had written “Regarding checkboxes – works fine for me.”.
    For the checkbox, you wrote in the code: value = “yes”. So the value is always = “yes”. It can not be false. The box is still checked, isn’t it ?

  15. Steve Taylor avatar Steve Taylor

    Sorry Sebastien, you’re right. As I said, my own version has evolved, and the above was abstracted from it to make it more general—obviously this error crept in as I adapted it for the post. Ah, the perils of not wrapping code up as a plugin!

    It was the final bit saving the fields that was wrong, around line 145. I’ve changed it now. It should delete the field if it’s not set. Simple yes / no checkboxes will have the value as “yes”, because if it’s not checked, the field simply isn’t set in the post data. You could tweak the code to test for checkboxes if you want, and update the field with “no” if the checkbox field isn’t set. Or maybe add a radio button type, and use two paired radio inputs, one “yes” and one “no”. As it is, when I’m using the data, I’m counting the absence of the checkbox field in the postmeta as a “no”.

    Anyway, thanks for reporting the bug!

  16. Steve Taylor avatar Steve Taylor

    To anyone subscribed to comments here, please check the note at the end of the post about the recent update to the code.

  17. billy avatar billy

    Very nice. We need more tutorials about this and making custom theme options.

  18. billy avatar billy

    In wordpress Version 2.8.6 it does not seem to add the field when you update the page or post.

  19. Steve Taylor avatar Steve Taylor

    I’ve got it working on 2.8.6 OK – though because this isn’t a plugin, my version has developed a lot to cater for the site I’m on.

    Do post your issue if you can be more specific (do a bit of debugging yourself and tell me if you get stuck at a part of the above code you don’t understand, or you think should be working but isn’t).

    If you’re not very experienced with PHP or WP development, maybe try one of the plugins I mention near the beginning? Good luck!

  20. billy avatar billy

    I would rather do it this way.

    /* remove_meta_box( ‘postcustom’, ‘post’, $context ); */
    /* remove_meta_box( ‘pagecustomdiv’, ‘page’, $context ); */

    I comment out the above code so I can see if it adds the field “block-of-text” but it does not for some reason.

  21. Steve Taylor avatar Steve Taylor

    Billy, not sure of your problem. Those lines of code are to remove the default Custom Fields box and replace it entirely with the new one. If you comment them out, I guess you’re getting the default Custom Fields box, plus the new one. I guess this might cause clashes between the fields? Does it work when you leave the above lines in?

  22. Steve Taylor avatar Steve Taylor

    Firstly, it looks like you’ve done something I sometimes do – you’ve copied the get_post_meta arguments from the function itself, and left $single = true. That looks like a function’s default for the argument. Calling the function, you just pass true or false.

    If that doesn’t fix it, pay attention to this line in the above code:

    var $prefix = '_mcf_';

    Read about it under the first code snippet here. Double-check whether the field has been stored by checking the postmeta table via phpMyAdmin. If the field is there, I guess either the above problem with the get_post_meta call is an issue, or you’re not including the prefix in the custom field key – or both?

  23. Steve Taylor avatar Steve Taylor

    Do remember the bit about the meta_key starting with an underscore though.

    It’s also worth having a prefix for your theme / plugin (whether there’s an underscore or not), to prevent clashes with other plugins.

  24. Steve, great work on the post. I, like the first commenter, am trying to implement a select box with certain options and an associated value for each option. I created a new case for dropdown, and added values for the options and the value in $customFields array, but still no luck. The only option I get is the _mcf_Array. thoughts?

    ds

  25. Newbie avatar Newbie

    Per the 18/11/09 update, what needs to be edited exactly to fix this bug?

  26. Steve Taylor avatar Steve Taylor

    It’s edited – the full code snippet above includes the fix. Check lines 46 & 145.

  27. Steve Taylor avatar Steve Taylor

    Paul, I’ve not tried that myself, but I’m sure one of the plugins that tackle this include upload fields.

  28. Steve

    Yes they do, but I want to do this without a plugin, and I would have thought it’s a common request but I can’t find any info about it anywhere
    thanks anyway
    paul

  29. Steve Taylor avatar Steve Taylor

    George, I wasn’t sure what problem that person talking about the drop-down had. You can see the comments I offered to help, not sure that helped, but they seemed to solve it. Basically, I think adapting the above code to have a dropdown instead of a text field is nuts-and-bolts XHTML / PHP. Do post a specific issue if you want (use pastebin.ca for code snippets), but I can’t do an XHTML / PHP tutorial here :-)

  30. George, thanks for posting the code. What’s the problem exactly? Actually, second thoughts, was there a problem, or is this the “working code”?

  31. George avatar George

    Steve,
    Hm actually no, it’s not final working code,
    later on I’ve added default option and the very important line:

    if ( get_post_meta( $post->ID, $this->prefix . $customField['name'], true ) == $key ) $selected = "selected=\"selected\"";

    This will give “selected” state on the chosen option after the page has been reloaded once again.

    Final (yes – working ) code:

    http://www.pastebin.ca/1759197

    Cheers.

  32. Csabbencs avatar Csabbencs

    Hi, I got this: Fatal error: Call to undefined function add_action() in … \wp-includes\functions.php … I have PHP 5.3.0. Could you please help? I copied the ‘full code’ part without any change into functions.php.

  33. Steve Taylor avatar Steve Taylor

    Csabbencs, as stated in the post, this code goes into the functions.php file of a customized theme. If you’re not happy customizing themes, it’s not for you—check out the plugins mentioned. If you are, put the code into /wp-content/themes/[your-theme]/functions.php.

    I’ve altered the post to emphasize this more, it seems to be a common mistake for people to put theme code into /wp-includes/functions.php. As a general rule, you should never alter WP core files like this. All customization should be via themes and plugins. If you need some customization that can’t be done with these, then… well, if you’ve decided that, you probably know what you’re doing and it’s nice that WP’s core is open. But generally—leave WP core code alone :-)

  34. Csabbencs avatar Csabbencs

    My mistake. Thanks, Steve. Now, I just don’t understand myself why I thought of putting it in the core function.php when I totally agree with you that the core should not be touched unless someone wants to go mad when upgrading. :)

  35. I can’t explain how excited I was to find a solution for a big conflict I was getting using the plugins More Fields & Adminimize!!! So thanks very much!

    I just pasted the code as you said and Voila!!! It worked!! I now have a second custom fields box :)

    Now I will admit I’m new to php but have enough knowledge to get this workign so far :) but if I ask something really dumb I do apologizes.

    So my 1st question would be. How can I use your code to pull or update all the custom fields I’ve already setup with the more More Fields plugin? I’ve deactivated it, and I can see all the date in the standard custom fields box used by wordpress, I was hoping that if I created the same fields, named them the same in the array they would show..

    My 2nd qustion, if there isn’t a solution to my 1st question, and i have to go and transfer all the date into the new custom field box added by your code, what code would I use to extract the info I need into my theme template?

    Below is an example of the code I was using to get the info when using the More Fields plugin….


    $value )
    echo $value;
    ?>

    Hope you can help, thanks for your time in advance!

  36. Steve Taylor avatar Steve Taylor

    Stephen, glad this code helped you out! I don’t know the More Fields plugin, but if it just managed standard custom fields, this code should be able to take those fields over if you add them to the $customFields array. The standard WP custom fields box is designed to output anything that’s been set for the current post (as long as it’s not hidden using the underscore prefix). The output of the above code is governed by what fields you’ve pre-defined—it really came about as a result of my wanting to control the fields that a client used. So, you have to manually specify which fields are to be managed.

    If More Fields stores anything outside the standard WP custom fields system, you’ll have to whip up some kind of PHP export / import script.

  37. Very nice tutorial, and just what I was looking for. One problem I’m seeing is that I’m getting a “Undefined index: my-custom-fields_wpnonce” as well as a header already sent warning. Not sure if it’s happening on an autosave or not. The undefined index is happening on the second reference to the wpnonce. Using 2.9.1.1. Any tips appreciated.

  38. Steve Taylor avatar Steve Taylor

    miklb, I’ve not seen this error before. Autosaves should be excluded as per the note at the end. I developed this code on 2.8, and actually I’ve not tried it on 2.9 yet (the client wants to stick to 2.8.6 until they have to upgrade due to security). Maybe an issue with 2.9? I’ll post if I run into the same thing when I upgrade—let me know if you solve this.

  39. Steve, thanks for the quick reply. I’m not sure of the ramifications, but I simply commented that line out and haven’t received either error again in testing. I’m also not sure of the ramifications of not using a prefix, and just using the existing custom fields I had, but otherwise, it’s a great example of code, and perfect for what I had originally set out to accomplish. Cheers!

  40. Hey Steve, Sorry to bother you once again!

    I’ve created all the fields I want, but now all the fields show on every post-page.php. I’ve tried a whole bunch of combintions using the scope, but can’t seem to get them to work. I tried creating new scopes and just adding page-new.php?cat=3 to the if statement but that doesn’t seem to work….

    I would really appreciated it if you could give me an example of how I could make the custom fields appear for only one category. What can or should I be looking for that makes each page unique. In your instructions you talk about showing fields for only certain page templates but I really need it to work by category.

    I’ve used a plugin called “Category Write Panels” so in my left hand menu I have a list of all my Categories each with there own “Edit” and “And New” links.

    Please help :(

    Thanks for your time in advance

  41. Steve Taylor avatar Steve Taylor

    I’ve not restricted the code to one category before, but it should be doable with some digging around with the $post object. The only thing is that of course you wouldn’t be able to display them until a post is created (and the category set).

  42. Adam van den Hoven avatar Adam van den Hoven

    I really like your code…

    I figured I’d send you this improvement. I didn’t like the fact that you couldn’t easily create a field that allowed a field on both pages and posts for people who have permissions to edit either (not that someone without edit_page permission would end up editing or creating a page).

    I also didn’t like your way of testing permissions. its a matter of taste but I like using array_reduce (I’m not really a PHP guy, more Ruby with some Java). I noodled around and I think that this is a more robust system:

    var $customFields = array(
     array(
     "name" =_ "...",
     "title" =_ "...",
     "description" =_ "...",
     "type" =_ "...",
     "permission" =_ array(
     array( "scope" =_ "post", "capability" =_"edit_posts"),
     array( "scope" =_ "page", "capability" =_"edit_pages"),
     array(
     "scope" =_ array("type-one", "type-2"),
     "capability" =_ array("cap1", "cap2")
     ),
     )
     )
    );
    function reduce_scope ($memo, $scope){
     global $post;
     return $memo || (basename( $_SERVER['SCRIPT_FILENAME'] ) == $scope . "-new.php" || $post-_post_type == $scope);
    }
    function reduce_capability ($memo, $cap){
     global $post;
     return $memo || current_user_can( $cap, $post-_ID );
    }
    function reduce_permissions( $memo, $permssion){
     $scopes = is_array($val) ? $permission['scope'] : array($permission['scope']);
     $capabilities = is_array($val) ? $permission['capability'] : array($permission['capability']);
     return $memo || (array_reduce($scopes, "reduce_scope", false) && array_reduce($scopes, "reduce_capability", capabilities));
    }
    function can_show($permissions){
     return array_reduce($permissions, "reduce_permissions", false);
    }

    You can now replace this code:

    foreach ( $scope as $scopeItem ) {
           switch ( $scopeItem ) {
                   case "post": {
                           // Output on any post screen
                           if ( basename( $_SERVER['SCRIPT_FILENAME'] )=="post-new.php" || $post-_post_type=="post" )
                                   $output = true;
                           break;
                   }
                   case "page": {
                           // Output on any page screen
                           if ( basename( $_SERVER['SCRIPT_FILENAME'] )=="page-new.php" || $post-_post_type=="page" )
                                   $output = true;
                           break;
                   }
           }
           if ( $output ) break;
    }
    // Check capability
    if ( !current_user_can( $customField['capability'], $post-_ID ) )
     $output = false;

    with simply:

    $output = $this-_can_show($customField['permission']);

    (You’ll have to put the reduce_* functions outside the class definition).

    Now, aside from simplifying the code (I’m a big fan of breaking down my functions so they are no more than 20 lines of code if possible), the permission system is now a lot more flexible.

    The logic is this:

    • You can show the field if any of the permissions is satisfied
    • A permission is satisfied if one of its scopes and one of its capabilities are satisfied
    • A scope is satisfied if the script filename is $scope . “-new.php” or the post_type == $scope
    • A capability is satisfied if the current user is capable of that action on the current post.

    This method of determining permissions has an interesting side effect (which is not necessary for typical WP installs, but interesting none the less). It lends itself to a “map-reduce” algo where you first map the arrays to booleans, which can be done in parallel (say over many processor cores and/or machines), then reduce the array to a boolean, which cannot be done in parallel but its comparitively efficient to reduce booleans.

  43. Matthias avatar Matthias

    Very nice tutorial, and just what I was looking for. One problem I’m seeing is that I’m getting a “Undefined index: my-custom-fields_wpnonce” as well as a header already sent warning. Not sure if it’s happening on an autosave or not. The undefined index is happening on the second reference to the wpnonce. Using 2.9.1.1. Any tips appreciated.

    I have the same error! Please help :)
    Using version 3.0.1

  44. Hi Matthias, I just looked into this and it looks like the error is just a PHP notice-level error. I think on the autosave $_POST[ 'my-custom-fields_wpnonce' ] isn’t defined, hence the “undefined index”. I’ve added a check for this variable and it seems to be fine.

    Note that this was just a “notice”, i.e. I don’t think it wasn’t really affecting the functionality of the code. A number of plugins, and possibly some core code, generate notice-level errors. They’re just alerting you to things that mean your code isn’t perfect, but the problem isn’t necessarily going to break anything. If you were seeing this error, I assume it was on a development server? If it’s on your live site, you might look into disabling PHP error output.

  45. Hi Steve, had some issues implementing a simple dropdown field like Flavius, so I posted a question on WPQuestions … thought maybe someone might be interested in the answers; http://wcdco.info/dy either way I appreciate the code, it’s great to work with!

  46. Hi Jayson, watch this space – I’m about to work on a project that needs some more refined custom field handling, so I’m planning an overhaul of this code, which will include drop-down handling!

  47. Greg avatar Greg

    Thanks for the code, it works great!

    I am a bit new to this as well, and I have a question about existing data. Basically, if I comment out your prefix var will content from currently populated customfields display in applicable fields defined by this code?

    For instance: I have been adding a custom field manually via the default drop down, the key is TKRNumber and a value could be 0123

    I have created a custom field array that looks like so:


    array(
    "name"=>"TKRNumber",
    "title"=>"TKR Number",
    "description"=>"Optional TKR Number for album release posts.",
    "type"=>"text",
    "scope"=>array( "post" ),
    "capability"=>"edit_posts"
    )

    Currently existing data isn’t populating this field, so I’m wondering if deleting the prefix would solve this, or if I need to do something else?

    Thanks again.
    -g

  48. Hi Greg, your above example should work, though I’ve not tried anything like this.

    The reasons for the prefix are (1) to make sure the values managed by this code don’t pop up in the default custom fields interface, if that’s left in place (the initial underscore makes sure of that), and (2) general good coding practice of avoiding field name clashes with plugins, etc.

    If you hide the default custom fields meta box (which the above code does by default), and you’re happy that your existing field names won’t clash with anything, I don’t see why your code won’t work. Maybe set the prefix to any empty string rather than comment it out though? Also, go through every place where the prefix is used, I’m not sure if there might be other impacts.

Comments are closed.