Best Practices for
Customizing WordPress Plugins

Follow along at
http://iandunn.name/customizing-plugins

Interaction

  • Ask questions if anything's unclear
  • Jump in with comments, tips, etc

Overview

  • Collaboration
  • Extending without modifying
  • Using custom hooks
  • Overriding Core hook callbacks
  • Adding custom hooks
  • Code samples and Q&A throughout

The Problem

  • Need to customize plugins
  • Lose changes when upgrading
  • Manually diff'ing is a chore
  • Usually just never upgrade
  • There's a better way

Collaborate With the Developer

  • First, talk to the developer
  • Send a patch
  • Win-win
  • Won't always work
  • More options

Extend Without Modifying

  • If only adding new functionality
  • Create separate plugin running alongside Addon plugin running simultaneously

Extend Without Modifying

  • Example: Google Authenticator – Encourage User Activation Google Authenticator - Encourage User Activation

Extend Without Modifying

public function __construct() {
    add_action( 'init', array( $this, 'encourage_users_to_enable_2fa' ), 12 );
}

public function encourage_users_to_enable_2fa() {
    if ( ! $user_has_2fa_enabled ) {
		add_action( 'admin_notices', array( $this, 'enable_2fa_notice' ) );

		if ( 'force' == $this->settings['mode'] ) {
            add_filter( 'user_has_cap', array( $this, 'restrict_user_capabilities' ), 10, 3 );
            
            if ( is_admin() ) {
                add_action( 'current_screen', array( $this, 'redirect_to_profile' ) );
            }
        }
    }
}

Using Custom Hooks

  • Sometimes need to change or remove functionality
  • Custom hooks to the rescue
  • Examples: Tagregator, Basic Google Maps Placemarks, CampTix, CampTix Network Tools

Using Custom Hooks

Tagregator

Tagregator

Using Custom Hooks

Tagregator

protected function __construct() {
    $this->media_sources = apply_filters( 'tggr_media_sources', array(
        'TGGR_Source_Twitter'   => TGGR_Source_Twitter::get_instance(),
        'TGGR_Source_Instagram' => TGGR_Source_Instagram::get_instance(),
        'TGGR_Source_Flickr'    => TGGR_Source_Flickr::get_instance(),
    ) );
}

Tagregator - Vine Media Source

function vms_register_custom_media_sources( $media_sources ) {
    require_once( dirname( __FILE__ ) . '/classes/tggr-source-vine.php' );
    $media_sources[ 'TGGR_Source_Vine' ] = TGGR_Source_Vine::get_instance();

    return $media_sources;
}
add_filter( 'tggr_media_sources', 'vms_register_custom_media_sources' );

Using Custom Hooks

Basic Google Maps Placemarks

North Carolina Fire Station Mapping Project

Using Custom Hooks

Basic Google Maps Placemarks

public function get_map_placemarks( $attributes ) {
    // [...]
		
    foreach( $published_placemarks as $placemark ) {
        // [...]
		
        $default_icon = apply_filters(
            'bgmp_default-icon',
            plugins_url( 'images/default-marker.png', __FILE__ ),
            $post_id
        );
	
        // [...]
    }

    return apply_filters( 'bgmp_get-map-placemarks-return', $placemarks );
}

Using Custom Hooks

Snippet

function set_BGMP_default_icon_by_category( $icon_url, $placemark_id ) {
    $placemark_categories = wp_get_object_terms( $placemark_id, 'bgmp-category' );

    foreach ( $placemark_categories as $category ) {
        switch ( $category->slug ) {
            case 'restaurants':
                $icon_url = 'restaurants.png';
            break;
		
            case 'book-stores':
                $icon_url = 'book-stores.png';
            break;
        }
    }

    return $icon_url;
}
add_filter( 'bgmp_default-icon', 'set_BGMP_default_icon_by_category', 10, 2 );

Using Custom Hooks

CampTix

CampTix

Using Custom Hooks

CampTix Network Tools

CampTix Network Tools

Using Custom Hooks

CampTix

if ( $doing_upgrade ) {
    $this->log( 'Upgrade already in progress, aborting concurrent attempt.', 0, null, 'upgrade' );
} else {
    // [...]
}
function log( $message, $post_id = 0, $data = null, $module = 'general' ) {
    do_action( 'camptix_log_raw', $message, $post_id, $data, $module );
}
add_action( 'camptix_log_raw', array( $this, 'camptix_log_meta' ), 10, 4 );
function camptix_log_meta( $message, $post_id, $data, $section ) {
    if ( $post_id ) {
        // [...]
	
        update_post_meta( $post_id, 'tix_log', $log );
    }
}

Using Custom Hooks

CampTix Network Tools

add_action( 'camptix_log_raw', array( $this, 'camptix_log_db' ), 10, 4 );
function camptix_log_db( $message, $post_id, $data_raw, $section = 'general' ) {
    // [...]
		
    $wpdb->insert( $table_name, array(
        'blog_id' => $blog_id,
        'object_id' => $post_id,
        'message' => $message,
        'data' => $data,
        'section' => $section,
    ) );
	
    // [...]
}

Using Custom Hooks

  • The downside: they don't always exist

Overriding Their Core Callbacks

  • Analyze their Core hook callbacks
  • Remove their hooks
  • Add your own in their place
  • Example: Google Authenticator – Per User Prompt

Overriding Their Core Callbacks

Google Authenticator

Google Authenticator

Overriding Their Core Callbacks

Google Authenticator - Per User Prompt

Google Authenticator - Per User Prompt Google Authenticator - Per User Prompt

Overriding Their Core Callbacks

Google Authenticator

function init() {
    add_action( 'login_form',   array( $this, 'loginform' ) );
    add_action( 'login_footer', array( $this, 'loginfooter' ) );
    add_filter( 'authenticate', array( $this, 'check_otp' ), 50, 3 );
}

Overriding Their Core Callbacks

Google Authenticator - Per User Prompt

public function register_hook_callbacks() {
    // Remove Google Authenticator's callbacks
    remove_action( 'login_form',          array( GoogleAuthenticator::$instance, 'loginform' ) );
    remove_filter( 'authenticate',        array( GoogleAuthenticator::$instance, 'check_otp' ), 50, 3 );
	
    // Register our callbacks
    add_filter( 'authenticate',           array( $this, 'validate_application_password' ), 10, 3 );
    add_filter( 'authenticate',           array( $this, 'maybe_prompt_for_token' ), 25, 3 );
    add_action( 'login_form_gapup_token', array( $this, 'prompt_for_token' ) );
    add_filter( 'wp_login_errors',        array( $this, 'get_login_error_message' ) );
}

Overriding Their Core Callbacks

Google Authenticator - Per User Prompt

public function maybe_prompt_for_token( $user, $username, $attempted_password ) {
    if ( is_a( $user, 'WP_User' ) ) {	// they entered a valid username/password
        if ( 'enabled' == trim( get_user_option( 'googleauthenticator_enabled', $user->ID ) ) && ! $this->is_using_application_password ) {
            $login_nonce  = $this->create_login_nonce( $user->ID );
			
            wp_safe_redirect( $redirect_url );
            die();
        }
    }
	
    return $user;
}

Overriding Their Core Callbacks

Google Authenticator - Per User Prompt

public function prompt_for_token() {
    $redirect_to = isset( $_REQUEST['redirect_to'] ) ? $_REQUEST['redirect_to'] : '';
    $action_url  = add_query_arg( array( 'action' => 'gapup_token' ), wp_login_url( $redirect_to ) );
	
    $user = get_user_by( 'id', absint( $_REQUEST['user_id'] ) );
	
    $error_message = $this->process_token_form( $_POST, $user );
	
    require_once( dirname( __FILE__ ) . '/views/token-prompt.php' );
    exit();
}

Overriding Their Core Callbacks

Google Authenticator - Per User Prompt

<?php login_header(); ?>
		
<form action="<?php echo esc_url( $action_url ); ?>" method="post" autocomplete="off">
    <input type="hidden" name="user_id"           value="<?php echo absint( $user->ID ); ?>" />
    <input type="hidden" name="gapup_login_nonce" value="<?php echo esc_attr( $_REQUEST['gapup_login_nonce'] ) ?>" />
    <input type="hidden" name="redirect_to"       value="<?php echo esc_attr( $redirect_to ) ?>" />

    <?php GoogleAuthenticator::$instance->loginform(); ?>

    <p class="submit">
        <input type="submit" name="gapup_token_prompt" value="<?php esc_attr_e( 'Log In' ); ?>" />
    </p>
</form>

<?php login_footer( 'user_email' ); ?>

Overriding Their Core Callbacks

  • Need to monitor base plugin updates
  • Won't always work

Adding Custom Hooks

  • Last resort: mod the plugin gracefully
  • Add custom hooks
  • Submit a patch
  • Refresh and apply patch to upgrades
  • Example: ACF-WPML Helper

Adding Custom Hooks

WPML

function save_translation( $data ) {
    // [...]

    if ( $field_type == 'custom_field' ) {
        $field_translation = html_entity_decode( $field_translation );
        update_post_meta( $new_post_id, $field_name, $field_translation );

        // mod - ian@iandunn.name, 2011-07-26
        do_action( 'icl_save_translation_custom_fields', $new_post_id, $job, $field_name, $field_translation );
    }
		
    // [...]
}

Adding Custom Hooks

ACF-WPML Helper

add_action( 'icl_save_translation_custom_fields', array( $this, 'import_custom_field' ), 10, 4 );
public function import_custom_field( $new_post_id, $job, $field_name, $field_translation ) {
    $acf_post_TRID           = $this->get_ACF_post_TRID( $job->original_doc_id, $field_name );
    $source_ACF_post_id      = $this->get_ACF_post_id( $acf_post_TRID, $job->source_language_code );
    $destination_ACF_post_id = $this->get_ACF_post_id( $acf_post_TRID, $job->language_code );
    $field_type              = $this->get_ACF_field_type( $field_name, $source_ACF_post_id );
	
    // Check if field already has previous value stored in database
    $value_ids = $wpdb->get_col( $wpdb->prepare( $query, $new_post_id, $field_name, $field_name ) );

    if( $value_ids ) {
        $this->update_imported_value( $field_name, $field_type, $value_ids, $field_translation );
    } else {
        $this->insert_imported_value( $field_name, $field_type, $job->original_doc_id, $source_ACF_post_id, $destination_ACF_post_id, $field_translation, $new_post_id );
    }
}

Takeaways

  • Don't mod the plugin directly
  • Try to collaborate
  • Separate plugin running alongside base
  • Add custom hooks to your own plugins

Additional Resources

Articles, videos, etc

Plugin Examples Used

Q&A

Any questions / comments / tips?