Ep #1: WordPress things you should never do

Here is a somewhat random list of rules containing things I hope you never do in PHP or WordPress.

Do not ignore your error_ and access_ logs

If you don’t have your Apache error_log running 100% of the time you are coding, your site is probably full of bugs. Your error_log should only contain intentional messages you have included to benchmark or log specific activity. If should NEVER contain PHP Errors, Warnings or Notices. Those should be corrected immediately. Once your site rolls to production and you start getting a lot of traffic, monitor your logs there as well. Production will expose every edge case you could possibly dream up.

You should regularly check your Apache access_log as well for requests to your domain that don’t return something less than HTTP error code 400. If any of your files or scripts return 400, 404, or 500 in WordPress, you may get into a loop of infinite recursion that your Web Server will choke on after about 10 times through this circle of hell.

To tail your error_log for PHP-only errors, etc (using MacPorts):

tail -f /opt/local/apache2/logs/error_log | grep "PHP " &

Also make sure your wp-config.php contains the following:

// turn on all errors
error_reporting( -1 );

Don’t try to access Array / Object properties that don’t exist

90% of the errors that inexperienced PHP developers generate are based on doing something like this:

// $data might be empty
$data = give_me_what_I_want();

echo $data[0]['items'][7]->fail();

The second you try to access the 0th index of $data, your code will fire off a PHP Notice. DO NOT IGNORE IT. You have 2 choices with arrays to avoid this:

// check that the whole path is cool

if ( isset( $data[0]['items'][7] ) )
     $data[0]['items'][7]->fail();

// be safe all the way down

if ( !empty( $data ) )
    if ( !empty( $data['items'] ) )
        if ( is_a( $data[0]['items'][7], 'SomeObj' ) )
            $data[0]['items'][7]->fail();

You don’t have to get ridiculous, but you also don’t have to be careless.

isset() won’t throw any log notices when you’re checking for depth in an array that may not have it.

empty() is a great utility. It will return a boolean for 0, empty string, empty array, false, or null. The best way to check a variable that might be an array, but might be nothing. You should always check an array for empty before iterating over it, this will prevent  an error being thrown when you start your foreach loop:

// do this

if ( !empty( $might_be_empty_array ) ) foreach ( $might_be_empty_array as $item ) {
    blame_nacin( $item );
}

// not this

foreach ( $im_empty_and_logging_it as $item ) {
     hope_its_not_empty( $item );
}

Don’t use mysql_real_escape_string

If you think you can just call this function out of nowhere to escape some text to be used in a SQL statement, you server is probably barfing and you don’t know it:

$whoops = mysql_real_escape_string( 'escape me!' );

// your logs are blowing up while server
// tries to connect to MySQL with default connection params
// because you didn't pass in an open connection to MySQL
// as the 2nd parameter

// use me instead

$cool = $wpdb->escape( 'escape me!' );

Most functions that start with mysql_* are the procedural counterpart to the object-oriented functions made available by the MySQL extension to PHP. Most of the procedural functions (similar to Memcache functions in this way) require the connection, or current resource instance, passed in as an argument as well. Even if you did pass in the resource, you should be using the mysqli functions instead (MySQL Improved Interface). WordPress uses mysql_* everywhere, so I guess forget what I just said….

ereg_*

This should be blatantly obvious by now, but if you aren’t a whiz at RegEx and Google that wrong site, you may end up pasting in some deprecated POSIX regular expressions when you should be using PCRE = Perl-compatible Regular Expressions.

POSIX = ereg_* functions
PCRE = preg_* functions

serialize

I am going to dare you to name me a good reason to create serialized arrays in your code. Since you can’t find one, I am going to ask you how great of an idea it is to store serialized arrays in the database. Since you don’t know the answer, I am going to ask you how knowledgable you are about ASCII, UTF-*, and ISO-*. Since you have no clue why I am asking you that, I want you to heed my warning: DO NOT USE SERIALIZED ARRAYS. If you do use serialized arrays, store NUMBERS and CHARs only. DO NOT STORE STRINGS of any length that constitute any amount of whitespace.

Here’s why:

Serialized arrays that contain strings bind those strings to their string length, making them as non-portable as possible. If you import / export data and have ANY weird characters that were pasted in from Word or worse, your strings may become invisibly altered and won’t match your bound string lengths. This is super important because of the way we get our data into a usable format when it is stored this way is through unserialize. unserialize fails easy and often when dealing with weird strings.

In WordPress, maybe_unserialize is a function invoked to do this to strings stored in the wp_*meta tables. maybe_unserialize will fail as well, easily and often, but does so….SILENTLY.

You might ask yourself, when would I ever do this anyways?

Does this look familiar?

$data = array( 'format' => 'long', 'color' => 'red' );
update_post_meta( $post_id, 'stuff', $data );

My example is harmless – but let’s say you are using Post Meta to store some SEO text, you are susceptible to maybe_unserialize failing. maybe_unserialize won’t return mangled text when this happens, it will return quite literally nothing.

Another huge problem is the version of MySQL you might be running. MySQL 5.5 is WAY more forgiving with invisible characters in UTF-8 strings with illegal bytes. If you are running any flavor of MySQL 5.1.* and you import data from a MySQL 5.5 database, you mean get bombarded with foreign characters you didn’t have in your 5.5 environment. If you were storing that data in serialized arrays, the data will cause unserialize to fail.

Be careful using $_SESSION

Using $_SESSIONs with Memcached can be palatable, but for most people that don’t know what they are doing, using PHP sessions can be extremely problematic. In almost every case, PHP will store your session_id( ) for a user as a Cookie, it will then throw no-cache headers with every request. Which sucks.

When no-cache is triggered by the session_start() function, this is what the HTTP headers look like:

Cache-Control: no-cache, must-revalidate, max-age=0
Pragma: no-cache

Those headers instruct your browser to re-request the page every time you visit it in your browser. Luckily for us, this little nugget tell sessions to not use a cache limiter. It will allow Batcache to work as well. Without it, Batcache will not work:

To turn off the no-cache headers:

session_cache_limiter( false );
session_start();

“switch_to_blog( )” is an Unfunny Nightmare

It can be extremely challenging to write WordPress code that works across many environments and allows you to use a custom location for WordPress media, WordPress multisite media, and WordPress core itself. I wrote about this extensively here. The code I included in that post works, although a lot of it was excerpted to be shown as a short example here or there. If your site switches from one blog to the next and doesn’t intermingle content – you really don’t have much to worry about after your initial setup. But in almost all cases, if you want to start using switch_to_blog() to co-mingle content from multiple sites inline, get ready to do some debugging!

switch_to_blog() works like so:

// I am on my blog

switch_to_blog( 2 );

// I am on your mom's blog

restore_current_blog();

// I am on my blog

Simple enough. This will switch context all over the place in WP core. wp_posts will become wp_2_posts. get_current_blog_id() will return 2 because the global variable $blog_id will be set to 2, etc.

In my post about environments, I had a lot of filters that I suggested adding in wp-content/sunrise.php that work just great in switching initial context to a specific blog and in setting up overrides for default media upload paths etc. They work fine… unless you ever plan on using switch_to_blog().

Here’s an example:

// Before
add_filter( 'pre_option_home', function ( $str ) use ( $domain ) {
    return 'http://' . $domain;
} );

// After using switch_to_blog and realizing I needed to account
// for any sort of weird context I may find myself in

add_filter( 'pre_option_home', function () {
    global $current_blog;
    $extra = rtrim( $current_blog->path, '/' );
    return 'http://' . MY_ENVIRONMENT_HOST . $extra;
} );

Ok cool, so you pull a path from a global variable and append it if it still has a value after being rtrim‘d? Easy. That would be true if $current_blog and its properties were updated every time switch_to_blog() is called. It is not!

$current_blog is one of the values we set in wp-content/sunrise.php to override the database and set our $current_blog->domain value to our current environment. $current_blog and $current_site exist mainly for use on the initialization of Multisite. Outside of startup, they aren’t really accessed or modified.

Because I want a static way to access dynamic variables, I have added some code to change the context of $current_blog when switch_to_blog() is called in wp-content/sunrise.php:

function emusic_switch_to_blog( $blog_id, $prev_blog_id = 0 ) {
    if ( $blog_id === $prev_blog_id )
        return;

    global $current_blog, $wpdb;
    $current_blog = $wpdb->get_row( "SELECT * FROM {$wpdb->blogs} WHERE blog_id = {$blog_id} LIMIT 1" );
    $current_blog->domain = MY_ENVIRONMENT_HOST;
}

emusic_switch_to_blog( $the_id );

add_action( 'switch_blog', 'emusic_switch_to_blog', 10, 2 );

Now that I have added that action, my filter pre_option_home will work. I use the same method for pre_option_siteurl. What I was previously doing to retrieve the path of the current blog didn’t work:

add_filter( 'pre_option_siteurl', function () {
    $extra = rtrim( get_blog_details( get_current_blog_id() )->path, '/' );
    return 'http://' . EMUSIC_CURRENT_HOST . $extra;
} );

Why didn’t it work? get_blog_details() eventually does $details->siteurl = get_blog_option( $blog_id, 'siteurl' ); giving us a nice and hardy dose of infinite recursion. So to combat it – I implemented the setting of $current_blog on the switch_blog action so its properties are always the current blog’s properties. Boom.

The next 2 annoyances are media upload urls / paths and admin paths. We use custom media locations for the main blog and network blogs:

add_filter( 'pre_option_upload_path', function () {
    $id = get_current_blog_id();
    if ( 1 < $id )
        return $_SERVER['DOCUMENT_ROOT'] . "/blogs/{$id}/files";

     return $_SERVER['DOCUMENT_ROOT'] . '/' . EMUSIC_UPLOADS;
} );

add_filter( 'pre_option_upload_url_path', function () {
    $id = get_current_blog_id();
    if ( 1 < $id )
        return 'http://' . EMUSIC_CURRENT_HOST . "/blogs/{$id}/files";

     return 'http://' . EMUSIC_CURRENT_HOST  . '/' . EMUSIC_UPLOADS;
} );

We switched blog context using switch_to_blog(), which triggers our action, which then sets the global variable $blog_id to our current blog’s id, which can then be retrieved from within functions by get_current_blog_id(). Yeah, that’s a mouthful. We are also using a constant for our host name, so we don’t have to query for it / output-buffer it / or str_replace() it.

The admin is a little bit trickier because we usually don’t call switch_to_blog() in the code, but the admin bar will list your blogs and has URLs to their Dashboards etc. We can filter those as well:

function get_admin_host( $url, $path, $blog_id = '' ) {
    $path = ltrim( $path, '/' );

    if ( empty( $blog_id ) ) {
        $blog_id = get_current_blog_id();
    }

    $blog_path = rtrim( get_blog_details( $blog_id )->path, '/' );
    return sprintf( 'http://%s%s/wp-admin/%s', EMUSIC_CURRENT_HOST, $blog_path, $path );
}

add_filter( 'admin_url', 'get_admin_host', 10, 3 );

function get_network_admin_host( $url, $path, $blog_id = '' ) {
    $path = ltrim( $path, '/' );

    if ( empty( $blog_id ) ) {
        $blog_id = get_current_blog_id();
    }

    $blog_path = rtrim( get_blog_details( $blog_id )->path, '/' );
    return sprintf( 'http://%s%s/wp-admin/network/%s', EMUSIC_CURRENT_HOST, $blog_path, $path );
}

add_filter( 'network_admin_url', 'get_network_admin_host', 10, 3 );

If you want to see this in action, I recently integrated the eMusic editors’ 17 Dots blog into eMusic proper, check it out: 17 Dots. You can see multiple blogs intertwining on the homepage.

Major bug with WordPress + New Relic

If you haven’t seen New Relic yet, definitely take a look, it’s awesome.

That being said… if you are running New Relic monitoring on a huge production site, your logs might be spinning out of control without you knowing it, and I’ll explain why.

We use New Relic at eMusic to monitor our WordPress servers and many other application servers – Web Services, Databases etc – and we started noticing this in the logs like whoa: failed to delete New Relic auto-RUM

In New Relic’s FAQ section:

Beginning with release 2.6 of the PHP agent, the automatic Real User Monitoring (auto-RUM) feature is implemented using an output buffer. This has several advantages, but the most important one is that it is now completely accurate for all frameworks, not just Drupal and WordPress. The old mechanism was fraught with problems and highly sensitive to things like extra Drupal modules being installed, or customization of the header format. Using this new scheme, all of the problems go away. However, there is a down-side, but only for specific PHP code. This manifests itself as a PHP notice that PHP failed to delete buffer New Relic auto-RUM in…. If you do not have notices enabled, you may not see this and depending on how your code is written, you may enter an infite loop in your script, which will eventually time out, and simply render either an empty or a partial page.

To understand the reason for this error and how it can create an infinite loop in code that previously appeared to work, it is worth reading the PHP documentation on the ob_start() PHP function. Of special interest is the last optional parameter, which is a boolean value called erase that defaults to true. If you call ob_start() yourself and pass in a value of false for that argument, you will encounter the exact same warning and for the same reason. If that variable is set to false, it means that the buffer, once created, can not be destroyed with functions like ob_end_clean(), ob_get_clean(), ob_end_flush() etc. The reason is that PHP assumes that if a buffer is created with that flag that it modifies the buffer contents in such a way that the buffer cannot be arbitrarily stopped and deleted, and this is indeed the case with the auto-RUM buffer. Essentially, inside the agent code, we start an output buffer with that flag set to false, in order to prevent anyone from deleting that buffer. It should also be noted that New Relic is not the only extension that does this. The standard zlib extension that ships with PHP does the same thing, for the exact same reasons.

We have had several customers that were affected by this, and in all cases it was due to problematic code. Universally, they all had code similar to the following:

while (ob_get_level()) {
  ob_end_flush ();
}

The intent behind this code is to get rid of all output buffers that may exist prior to this code, ostensibly to create a buffer that the code has full control over. The problem with this is that it will create an infinite loop if you use New Relic, the zlib extension, or any buffer created with the erase parameter set to false. The reason is pretty simple. The call to ob_get_level() will eventually reach a point where it encounters a non-erasable buffer. That means the loop will never ever exit, because ob_get_level() will always return a value. To make matters worse, PHP tries to be helpful and spit out a notice informing you it couldn’t close whatever the top-most non-erasable buffer is. Since you are doing this in a loop, that message will be repeated for as long as the loop repeats itself, which could be infinitely.

So basically, you’re cool if you don’t try to flush all of the output buffers in a loop, because you will end up breaking New Relic’s buffer. Problematic as well if you are managing several of your own nested output buffers. But the problem might not be you, the problem is / could be WordPress.

Line 250 of wp-includes/default-filters.php:

add_action( 'shutdown', 'wp_ob_end_flush_all', 1 );

What does that code do?

/**
 * Flush all output buffers for PHP 5.2.
 *
 * Make sure all output buffers are flushed before our singletons our destroyed.
 *
 * @since 2.2.0
 */
function wp_ob_end_flush_all() {
	$levels = ob_get_level();
	for ($i=0; $i

So that’s not good. We found our culprit (if we were having the problem New Relic describes above). How to fix it?

I put this in wp-content/sunrise.php

<?php
remove_action( 'shutdown', 'wp_ob_end_flush_all', 1 );

function flush_no_new_relic() {
	$levels = ob_get_level();
	for ( $i = 0; $i < $levels - 1; $i++ )
		ob_end_flush();
}

add_action( 'shutdown', 'flush_no_new_relic', 1, 0 );

This preserves New Relic’s final output buffer. An esoteric error, but something to be aware of if you are monitoring WordPress with New Relic.