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
Extend Without Modifying
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
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
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
Using Custom Hooks
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
Overriding Their Core Callbacks
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
- Creating Custom Hooks from the Plugin Developer Handbook
- Extendable Extensions by Michael Fields
- WordPress Plugins as Frameworks by Josh Harrison
- The Right Way to Customize a WordPress Plugin by Ian Dunn