Tag Archives: Plugin

How to use PHP’s namespaces and constants within your WordPress plugins

Recently I switched from using a class to emulate a namespace in my WordPress plugins to actually using PHP’s namespace, which is available in PHP 5.3.0 or greater. I’m going to demonstrate some things that all my plugins thus far have in common.

1. Create a namespace

Right before your plugin meta data comment, you must lead with your namespace (otherwise PHP will complain):

1
namespace CompanyName\PluginName;
namespace CompanyName\PluginName;

I use a master namespace for everything I do (CompanyName) and a sub-namespace for the specific plugin I’m working on (PluginName).

2. Define a namespace constant

After the plugin meta data comment I define a constant, in other words, a shortcut to reference my namespace within WordPress hooks. It’s just less to type in the future and I can change my namespace without affecting plugin functionality.

1
define(__NAMESPACE__ . '\NS', __NAMESPACE__ . '\\');
define(__NAMESPACE__ . '\NS', __NAMESPACE__ . '\\');

What this allows me to do is use a constant called NS to render CompanyName\PluginName\ so that after leading with NS in a WordPress hook callback, all I need to provide is my function name.

To create additional constants within your namespace, you can do that like so:

1
define(NS . 'PS', 'plugin-slug');
define(NS . 'PS', 'plugin-slug');

3. Use your namespace constant in a WordPress hook

Here is what a function and a hook looks like in a namespaced environment:

1
2
3
4
5
6
7
8
function initialize_plugin() {
 
}
 
add_action(
  'init',
  NS . 'initialize_plugin'
);
function initialize_plugin() {

}

add_action(
  'init',
  NS . 'initialize_plugin'
);

It’s really not that much different compared to not using a namespace and it certainly looks a lot cleaner than if you were using a class.

4. Use core WordPress functions

There are times where you need to use WordPress functions like the register_activation_hook, for example. It’s important to remember to change the namespace back to the global namespace for those functions.

The way to do that is to precede the function with a backslash:

1
\register_activation_hook(__FILE__, NS . 'register_activation_hook');
\register_activation_hook(__FILE__, NS . 'register_activation_hook');

This should get you started and if you have any questions, leave them in the comments below.

$wpdb queries not working properly on child sites in WordPress multisite install

I’m working on a WordPress multisite plugin that uses a global table to store logs. When a user performs a certain action on my site, I’m executing a function similar to this one:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function add_log($source, $user_id, $action, $status) {
    global $wpdb, $blog_id;
    $table_name = $wpdb->prefix . LOG_TABLE;
    $IP = $_SERVER['REMOTE_ADDR'];
    $result = $wpdb->insert($table_name, 
        array(
            'source'    => $source,
            'site_id'   => $blog_id,
            'user_id'   => $user_id,
            'action'    => $action,
            'status'    => $status,
            'IP'        => $IP
        ), 
        array(
            '%s',
            '%d', 
            '%d',
            '%s',
            '%s',
            '%s'
        ) 
    );
}
function add_log($source, $user_id, $action, $status) {
	global $wpdb, $blog_id;
	$table_name = $wpdb->prefix . LOG_TABLE;
	$IP = $_SERVER['REMOTE_ADDR'];
	$result = $wpdb->insert($table_name, 
		array(
			'source'	=> $source,
			'site_id' 	=> $blog_id,
			'user_id'	=> $user_id,
			'action'	=> $action,
			'status'	=> $status,
			'IP'		=> $IP
		), 
		array(
			'%s',
			'%d', 
			'%d',
			'%s',
			'%s',
			'%s'
		) 
	);
}

This works well on the base site (where blog_id = 1) of the WordPress multisite install, but for some reason it wasn’t capturing any logs on the other sites.

After some troubleshooting, which I did by modifying the function temporarily to this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function add_log($source, $user_id, $action, $status) {
    global $wpdb, $blog_id;
    define('DIEONDBERROR', true);
    $wpdb->show_errors();
    $table_name = $wpdb->prefix . LOG_TABLE;
    $IP = $_SERVER['REMOTE_ADDR'];
    $result = $wpdb->insert($table_name, 
        array(
            'source'        => $source,
            'site_id'   => $blog_id,
            'user_id'   => $user_id,
            'action'        => $action,
            'status'        => $status,
            'IP'            => $IP
        ), 
        array(
            '%s',
            '%d', 
            '%d',
            '%s',
            '%s',
            '%s'
        ) 
    );
    $wpdb->print_error();
}
function add_log($source, $user_id, $action, $status) {
	global $wpdb, $blog_id;
	define('DIEONDBERROR', true);
	$wpdb->show_errors();
	$table_name = $wpdb->prefix . LOG_TABLE;
	$IP = $_SERVER['REMOTE_ADDR'];
	$result = $wpdb->insert($table_name, 
		array(
			'source'		=> $source,
			'site_id' 	=> $blog_id,
			'user_id'	=> $user_id,
			'action'		=> $action,
			'status'		=> $status,
			'IP'			=> $IP
		), 
		array(
			'%s',
			'%d', 
			'%d',
			'%s',
			'%s',
			'%s'
		) 
	);
	$wpdb->print_error();
}

I found out that $wpdb->prefix was not always wp_ as I expected, but it was wp_2_ or wp_3_, depending on the site the log was generated on. That actually makes a whole lot of sense.

The proper way to fix this is to indicate that you want the base prefix, not the individual site’s prefix.

The solution is to change $wpdb->prefix to $wpdb->base_prefix.

PS: If you are creating a plugin that will work on both single and multisite WordPress installs, it’s helpful to know that $wpdb->base_prefix is set on both of those versions, meaning you can always use it.

Passing in a “from” header to wp_mail() does not work

I ran into an issue today where I was setting the “from” header in wp_mail(), but it didn’t show up when I received the email. The code looks like this:

1
2
$headers = 'From: Company <[email protected]>' . "\r\n";
wp_mail($email, $subject, $message, $headers);
$headers = 'From: Company <[email protected]>' . "\r\n";
wp_mail($email, $subject, $message, $headers);

I learned that the only reason my “from” header wouldn’t show up correctly is if one of the following filters overwrites it:

1
2
3
4
5
6
7
8
9
add_filter('wp_mail_from','custom_wp_mail_from');
function custom_wp_mail_from($email) {
  return '[email protected]';
}
 
add_filter('wp_mail_from_name','custom_wp_mail_from_name');
function custom_wp_mail_from_name($name) {
  return 'WordPress';
}
add_filter('wp_mail_from','custom_wp_mail_from');
function custom_wp_mail_from($email) {
  return '[email protected]';
}

add_filter('wp_mail_from_name','custom_wp_mail_from_name');
function custom_wp_mail_from_name($name) {
  return 'WordPress';
}

Most likely you have plugin enabled that uses the previous filters to overwrite the “from” header that is being passed into wp_mail().

You have two options to fix the issue:

  1. You disable the plugin that’s causing this.
  2. You overwrite the plugin’s filter by creating another filter right before calling wp_mail().

It would be a good practice for you to remove that filter upon sending the email, especially if you are creating a plugin that’s going to be used by an audience other than yourself. The reason for this is now apparent :).

You can do this via the remove_filter() function:

1
2
remove_filter('wp_mail_from','custom_wp_mail_from');
remove_filter('wp_mail_from_name','custom_wp_mail_from_name');
remove_filter('wp_mail_from','custom_wp_mail_from');
remove_filter('wp_mail_from_name','custom_wp_mail_from_name');

Important: WordPress notes the following:

To remove a hook, the function and priority arguments must match when the hook was added. This goes for both filters and actions. No warning will be given on removal failure.

So if you set a priority, be sure to match it. In my example above, I didn’t specify a priority, so the default of 10 was used.

WordPress taxonomy terms don’t insert when cron job executes wp_insert_post()

I have a PHP script that connects to a third-party application, retrieves data, and then inserts it into WordPress using wp_insert_post(). The data also has a custom taxonomy associated with it, which is being passed into the wp_insert_post() function via the $args array. Lastly, the script executes every hour via a cron job.

Here is the WordPress insert:

1
2
3
4
5
6
7
8
9
10
11
12
13
$args = array(
  'comment_status'  => 'closed',
  'ping_status'     => 'closed',
  'post_author'     => 1,
  'post_content'    => $content,
  'post_status'     => 'publish',
  'post_title'      => $title,
  'post_type'       => 'movie',
  'tax_input'       => array(
    'genre' => array('term1', 'term2', 'term3')
  )
);
$wp_movie_id = wp_insert_post($args);
$args = array(
  'comment_status'	=> 'closed',
  'ping_status'		=> 'closed',
  'post_author'		=> 1,
  'post_content'	=> $content,
  'post_status'		=> 'publish',
  'post_title'		=> $title,
  'post_type'		=> 'movie',
  'tax_input'		=> array(
    'genre' => array('term1', 'term2', 'term3')
  )
);
$wp_movie_id = wp_insert_post($args);

And here the cron job:

1
@hourly /usr/bin/curl --silent http://example.com/cron.php
@hourly /usr/bin/curl --silent http://example.com/cron.php

The problem was that every time the cron job ran, my custom taxonomy terms were missing, while everything else inserted just fine.

Whenever I executed the script manually from my browser though, the taxonomy terms were present, so I was sure that it was related to the cron job.

Turns out, the problem was within the wp_insert_post() function. Prior to inserting the taxonomy terms, WordPress checks whether that user has permission via current_user_can():

1
2
3
4
5
6
7
8
9
10
if(!empty($tax_input)) {
  foreach($tax_input as $taxonomy => $tags) {
    $taxonomy_obj = get_taxonomy($taxonomy);
    if(is_array($tags)) {
      $tags = array_filter($tags);
    }
    if(current_user_can($taxonomy_obj->cap->assign_terms)) {
      wp_set_post_terms($post_ID, $tags, $taxonomy);
    }
}
if(!empty($tax_input)) {
  foreach($tax_input as $taxonomy => $tags) {
    $taxonomy_obj = get_taxonomy($taxonomy);
    if(is_array($tags)) {
      $tags = array_filter($tags);
    }
    if(current_user_can($taxonomy_obj->cap->assign_terms)) {
      wp_set_post_terms($post_ID, $tags, $taxonomy);
    }
}

The cron job didn’t have authority to insert taxonomy terms. This also explains why it always worked in my browser, because I was logged in as an admin in the background.

Luckily there is a quick solution that solves this problem. Instead of inserting the taxonomy terms using the $args array, you can use another WordPress function called wp_set_object_terms() to perform the insert separately:

1
2
$terms = array('term1', 'term2', 'term3');
wp_set_object_terms($wp_movie_id, $terms, 'genre');
$terms = array('term1', 'term2', 'term3');
wp_set_object_terms($wp_movie_id, $terms, 'genre');

Hopefully this will save someone a couple hours of research!

WordPress get_post_type() doesn’t work when restoring a trashed post via undo

I have a custom post type that uses the following hook:

1
2
3
4
add_action(
    'untrash_post',
    array($this, 'custom_restore_function')
);
add_action(
	'untrash_post',
	array($this, 'custom_restore_function')
);

And the custom function looks as follows:

1
2
3
4
5
function custom_restore_function() {
    if(get_post_type() == 'custom_post_type') {
        $this->set_custom_post_type_display_status(1);
    }
}
function custom_restore_function() {
	if(get_post_type() == 'custom_post_type') {
		$this->set_custom_post_type_display_status(1);
	}
}

I have a very similar setup for custom_save_function, custom_trash_function, and custom_delete_function, and in all instances, get_post_type() works like a charm.

When you use the Trash action link to move a post to the trash, the link looks something like this:

post.php?post=118&action=trash&_wpnonce=910f2d7f30

And if you choose to restore that post from the trash screen via the Restore action link, it looks very similar:

post.php?post=118&action=untrash&_wpnonce=ddb4c9c68b

The problem is that when you trash a post and then decide to restore it again via the Undo link in the confirmation message, the custom_restore_function will fail.

The Undo link looks something like this:

edit.php?post_type=custom_post_type&doaction=undo&action=untrash&
ids=118&_wpnonce=ea0d2a97d2

And as you can see, the format is a bit different.

After doing more research and taking a closer look inside post.php to see what get_post_type() is actually doing:

1
2
3
4
5
6
7
8
9
10
function get_post_type($the_post = false) {
    global $post;
    if(false === $the_post)
        $the_post = $post;
    elseif(is_numeric($the_post))
        $the_post = get_post($the_post);
    if(is_object($the_post))
        return $the_post->post_type;
    return false;
}
function get_post_type($the_post = false) {
	global $post;
	if(false === $the_post)
		$the_post = $post;
	elseif(is_numeric($the_post))
		$the_post = get_post($the_post);
	if(is_object($the_post))
		return $the_post->post_type;
	return false;
}

It appears that $post doesn’t contain the post when using the Undo action link. I would be interested in knowing why?

Nevertheless, the easiest way to solve this problem was to modify the if statement to check for the post_type query string, which fortunately is available inside the custom_restore_function:

1
2
3
4
5
6
function custom_restore_function() {
    if(get_post_type() == 'custom_post_type'
    || $_GET['post_type'] == 'custom_post_type') {
        $this->set_custom_post_type_display_status(1);
    }
}
function custom_restore_function() {
	if(get_post_type() == 'custom_post_type'
	|| $_GET['post_type'] == 'custom_post_type') {
		$this->set_custom_post_type_display_status(1);
	}
}