Don’t Put That In Functions.php!

(Or, How to Write a Plugin)

Presented at WCPDX 2012

Oh, the Possibilities

Plugins can do almost anything

There are hundreds of functions in WP core

We’ll touch on a few, but mostly good practices that apply to any plugin

What’s wrong with functions.php?

Nothing!

As long as the code in it directly relates to your theme.

functions.php is not a snippet bucket.

When functions.php starts controlling a lot of non-theme functionality, you risk losing that functionality if you switch or update your theme.

What you’ve seen

Add Google Analytics code to your site:

add_action( 'wp_head', 'google_analytics' );
function google_analytics() {
?>
<script type="text/javascript">
  var _gaq = _gaq || [];
  _gaq.push(['_setAccount', 'UA-XXXXXXX-YY']);
  _gaq.push(['_trackPageview']);

  (function() {
    var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
    ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
    var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
  })();
</script>
<?php
}

Reference:

If not functions.php, where?

A few options. To start: Must-Use Plugins

Create a folder called mu-plugins and put it in wp-content

php files in that directory will be executed automatically, no activation or enabling required.

Must-Use Plugin

google-analytics.php

<?php

add_action( 'wp_head', 'google_analytics' );
function google_analytics() {
?>
<script type="text/javascript">
  var _gaq = _gaq || [];
  _gaq.push(['_setAccount', 'UA-XXXXXXX-YY']);
  _gaq.push(['_trackPageview']);

  (function() {
    var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
    ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
    var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
  })();
</script>
<?php
}

Reference:

What it looks like

But it can be better!

No generic function names

<?php

add_action( 'wp_head', 'kdl_google_analytics' );
function kdl_google_analytics() {
?>
<script type="text/javascript">
  var _gaq = _gaq || [];
  _gaq.push(['_setAccount', 'UA-XXXXXXX-YY']);
  _gaq.push(['_trackPageview']);

  (function() {
    var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
    ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
    var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
  })();
</script>
<?php
}

Reference:

Give it a name

<?php
//Plugin Name: Basic Google Analytics

add_action( 'wp_head', 'kdl_google_analytics' );
function kdl_google_analytics() {
?>
<script type="text/javascript">
  var _gaq = _gaq || [];
  _gaq.push(['_setAccount', 'UA-XXXXXXX-YY']);
  _gaq.push(['_trackPageview']);

  (function() {
    var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
    ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
    var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
  })();
</script>
<?php
}

Reference:

Now it looks like

Make it better

<?php
/*
Plugin Name: Basic Google Analytics
Plugin URI: trepmal.com
Description: Adds very basic Google Analytics code. Need complex tracking? This isn't for you.
Version: 1
Author: Kailey Lampert
Author URI: kaileylampert.com
*/

add_action( 'wp_head', 'bga_google_analytics' );
function bga_google_analytics() {
//...
}

Now that the plugin is named, that makes a better prefix for the function names.

And…

Doesn’t that look better?

A “real” plugin

Once named, the plugin can be moved to usual plugin directory

/wp-content/plugins

where it can then be activated like any other.

Option for Multisite

With the Network: true header, the plugin can only be network-activated when on Multisite.

<?php
/*
Plugin Name: Basic Google Analytics
Plugin URI: trepmal.com
Description: Adds very basic Google Analytics code. Need complex tracking? This isn't for you.
Version: 1
Author: Kailey Lampert
Author URI: kaileylampert.com
Network: true
*/

add_action( 'wp_head', 'bga_google_analytics' );
function bga_google_analytics() {
//...
}

Enough! Moar code!

Let’s make an admin-side option, so it can work on multisite.

Using the Settings API we can create an option in the admin.

add_action( 'admin_init', 'bga_new_setting' );
function bga_new_setting() {
	//               option group, option name, callback function to sanitize the input value
    register_setting( 'general', 'bga-google_analytics_id', 'strip_tags' );

    //                  id,                        label,                  html field,       option group
    add_settings_field( 'bga-google_analytics_id', 'Google Analytics ID', 'bga_form_field', 'general' );
}

function bga_form_field() {
	//                 the name of our option (defined above)
    $ga_id = get_option( 'bga-google_analytics_id', '' );
    // the input field
    echo "<input type='text' id='bga-google_analytics_id' name='bga-google_analytics_id' value='$ga_id' /><p class='description'>UA-XXXXXXX-YY</p>";
}

Reference:

The option

Now that the option is saved

add_action( 'wp_head', 'bga_google_analytics' );
function bga_google_analytics() {
    $ga_id = get_option( 'bga-google_analytics_id', '' );
?>
<script type="text/javascript">
  var _gaq = _gaq || [];
  _gaq.push(['_setAccount', '<?php echo $ga_id; ?>']);
  _gaq.push(['_trackPageview']);
 
  (function() {
    var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
    ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
    var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
  })();
</script>
<?php
}

Reference:

A note about mu-plugins vs. network-activated.

There are no hard and fast rule about what can or can’t be an mu-plugin, but there are some important things to keep in mind

Back to the code

Let’s improve how the ID is saved. Any user-supplied data needs to be sanitized and validated (where applicable). Since the Analytics ID is pretty predictable, we can be strict.

register_setting( 'general', 'bga-google_analytics_id', 'bga_sanitize_validate' );
function bga_sanitize_validate( $given_id ) {
    $saved_id = get_option('bga-google_analytics_id', '');
    $clean_id = strtoupper( strip_tags( trim( $given_id ) ) );

    if ( $saved_id == $clean_id ) {
        return $saved_id;
    }

    if ( empty( $clean_id ) ) {
        add_settings_error('', '', 'Your Google Analytics ID number has been removed.', 'updated' );
        return '';
    }

    $almost_ready_id = 'UA-'. trim( $clean_id, 'UA-' );

    preg_match( '/(UA-\d+-\d+)$/', $almost_ready_id, $match );

    if ( empty( $match ) ) {
        add_settings_error('', '', $almost_ready_id . ' is not a valid ID number', 'error' );
        return '';
    } else {
        $ready_id = $almost_ready_id;
    }

    add_settings_error('', '', 'Your Google Analytics ID number has been saved.', 'updated' );
    return $ready_id;
}

Reference:

Escape!

Sanitization is always a good thing. So is escaping, make sure the data won’t break anything when it is outputted in the HTML.

add_action( 'wp_head', 'bga_google_analytics' );
function bga_google_analytics() {
    $ga_id = get_option( 'bga-google_analytics_id', '' );
?>
<script type="text/javascript">
  var _gaq = _gaq || [];
  _gaq.push(['_setAccount', '<?php echo esc_js( $ga_id ); ?>']);
  _gaq.push(['_trackPageview']);

  (function() {
    var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
    ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
    var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
  })();
</script>
<?php
}

(Our sanitize function was pretty restrictive, and we know where it will always be outputted so escaping isn’t required, but it’s always good practice.)

Reference:

One more thing?

Ever install a plugin, activate it, then scan through the admin menu to try and find the options page?

Let’s give the user a hint

add_action( 'admin_init', 'bga_new_setting' );
function bga_new_setting() {
    add_filter( 'plugin_action_links_'. plugin_basename( __FILE__ ), 'bga_plugin_action_links', 10, 4 );
    //...
}
function bga_plugin_action_links( $actions, $plugin_file, $plugin_data, $context ) {
    if ( is_plugin_active( $plugin_file ) )
        $actions[] = 'Setup';
    return $actions;
}

Reference:

Which will give us

Kick it up a notch

<?php
/*
Plugin Name: Basic Google Analytics
Plugin URI: trepmal.com
Description: Adds very basic Google Analytics code. Need complex tracking? This isn't for you.
Version: 1
Author: Kailey Lampert
Author URI: kaileylampert.com
*/

global $basic_google_analytics;
$basic_google_analytics = new Basic_Google_Analytics();

class Basic_Google_Analytics {
    function __construct( ) {
        add_filter( 'plugin_action_links_'. plugin_basename( __FILE__ ), array( &$this, 'plugin_action_links' ), 10, 4 );
        add_action( 'admin_init' , array( &$this, 'admin_init' ) );
        add_action( 'wp_head', array( &$this, 'wp_head' ) );
    }

    function plugin_action_links( $actions, $plugin_file, $plugin_data, $context ) {
        if ( is_plugin_active( $plugin_file ) )
            $actions[] = '<a href="' . admin_url('options-general.php#bga-google_analytics_id') . '">' . __( 'Setup', 'basic-google-analytics' ) . '</a>';
        return $actions;
    }

    function admin_init() {
        register_setting( 'general', 'bga-google_analytics_id', array( &$this, 'sanitize_validate' ) );
        add_settings_field( 'bga-google_analytics_id', '<label for="bga-google_analytics_id">' . __( 'Google Analytics ID', 'basic-google-analytics' ) . '</label>' , array( &$this, 'fields_html') , 'general' );
    }

    function fields_html() {
        $ga_id = get_option( 'bga-google_analytics_id', '' );
        echo "<input type='text' id='bga-google_analytics_id' name='bga-google_analytics_id' value='$ga_id' /><p class='description'>UA-XXXXXXX-YY</p>";
    }

    function sanitize_validate( $given_id ) {
        $saved_id = get_option('bga-google_analytics_id', '');
        $clean_id = strtoupper( strip_tags( trim( $given_id ) ) ); //original, cleaned input

        // if no change, carry on
        if ( $saved_id == $clean_id ) {
            return $saved_id;
        }

        // has the id been removed entirely?
        if ( empty( $clean_id ) ) {
            add_settings_error('', '', __( 'Your Google Analytics ID number has been removed.', 'basic-google-analytics' ), 'updated' );
            return '';
        }

        $almost_ready_id = 'UA-'. trim( $clean_id, 'UA-' ); // account for variance, make sure number is prefixed with "UA-"

        preg_match( '/(UA-\d+-\d+)$/', $almost_ready_id, $match ); // expects: UA-XXXXXXX-YY

        // if empty, then the regex failed to find the correct pattern
        if ( empty( $match ) ) {
            add_settings_error('', '', sprintf( __( '%s is not a valid ID number', 'basic-google-analytics' ), $almost_ready_id ), 'error' );
            return '';
        } else {
            $ready_id = $almost_ready_id;
        }

        // if we've made it this far, we have a good id
        add_settings_error('', '', __( 'Your Google Analytics ID number has been saved.', 'basic-google-analytics' ), 'updated' );
        return $ready_id;
    }

    function wp_head() {
        if ( is_super_admin() ) return; //if not MS, will return true if user can delete other users
        // if ( is_user_logged_in() ) return; //or ignore any logged in user

        $ga_id = get_option( 'bga-google_analytics_id', '' );

        if ( empty( $ga_id ) ) return;
        ?>
<script type="text/javascript">
  var _gaq = _gaq || [];
  _gaq.push(['_setAccount', '<?php echo esc_js( $ga_id ); ?>']);
  _gaq.push(['_trackPageview']);

  (function() {
    var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
    ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
    var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
  })();
</script>
        <?php
    }

}

Etiquette

Resources

I use regularly:

I use less-regularly

Plugins

Not a plugin, but read this first: Debugging in WordPress

That’s All, Folks!

Kailey Lampert

Kailey Lampert

kaileylampert.com
@trepmal

http://kly.me/wcpdx12