remove_action From PHP Class in WooCommerce Memberships - php

I have previously used a solution described here: remove_action From PHP Class for removing an action in the WooCommerce membership plugin.
However, the solution no longer works, as WooComemerce have changed the code behind the membership plugin.
So this is the new code.
Main woocommerce-memberships.php
public function includes() {
// load post types
require_once( $this->get_plugin_path() . '/includes/class-wc-memberships-post-types.php' );
// load user messages helper
require_once( $this->get_plugin_path() . '/includes/class-wc-memberships-user-messages.php' );
// load helper functions
require_once( $this->get_plugin_path() . '/includes/functions/wc-memberships-functions.php' );
// init general classes
$this->rules = $this->load_class( '/includes/class-wc-memberships-rules.php', 'WC_Memberships_Rules' );
$this->plans = $this->load_class( '/includes/class-wc-memberships-membership-plans.php', 'WC_Memberships_Membership_Plans' );
$this->emails = $this->load_class( '/includes/class-wc-memberships-emails.php', 'WC_Memberships_Emails' );
$this->user_memberships = $this->load_class( '/includes/class-wc-memberships-user-memberships.php', 'WC_Memberships_User_Memberships' );
$this->capabilities = $this->load_class( '/includes/class-wc-memberships-capabilities.php', 'WC_Memberships_Capabilities' );
$this->member_discounts = $this->load_class( '/includes/class-wc-memberships-member-discounts.php', 'WC_Memberships_Member_Discounts' );
$this->restrictions = $this->load_class( '/includes/class-wc-memberships-restrictions.php', 'WC_Memberships_Restrictions' );
Main instance
function wc_memberships() {
return WC_Memberships::instance();
}
From included class-wc-memberships-restrictions.php file
/**
* Returns the general content restrictions handler.
*
* #since 1.9.0
*
* #return null|\WC_Memberships_Posts_Restrictions
*/
public function get_posts_restrictions_instance() {
if ( ! $this->posts_restrictions instanceof WC_Memberships_Posts_Restrictions ) {
$this->posts_restrictions = wc_memberships()->load_class( '/includes/frontend/class-wc-memberships-posts-restrictions.php', 'WC_Memberships_Posts_Restrictions' );
}
return $this->posts_restrictions;
}
Then in class-wc-memberships-posts-restrictions.php
public function __construct() {
// decide whether attempting to access restricted content has to be redirected
add_action( 'wp', array( $this, 'handle_restriction_modes' ) );
// restrict the post by filtering the post object and replacing the content with a message and maybe excerpt
add_action( 'the_post', array( $this, 'restrict_post' ), 0 );
How do i remove the 'the_post' action?
So far i have the following in functions.php theme file:
function weteach_remove_actions(){
if(is_singular( 'post' )) {
if( function_exists( 'wc_memberships' ) ){
remove_action( 'the_post', array( wc_memberships()->restrictions, 'restrict_post' ));
}
}
return;
}
add_action( 'the_post', 'weteach_remove_actions', 1 );
Which gives me a "blank-page"-error.

Could you tell us what the error message was? My guess is that restrictions and post_restrictions aren't the same property and so you aren't finding the restrict_post method in the right class.
Edited now that I have looked at Memberships, this seems to work for me:
function so_41431558_remove_membership_post_restrictions(){
if( function_exists( 'wc_memberships' ) && version_compare( WC_Memberships::VERSION, '1.9.0', '>=' ) && is_singular( 'post' ) ){
remove_action( 'the_post', array( wc_memberships()->get_restrictions_instance()->get_posts_restrictions_instance(), 'restrict_post' ), 0 );
}
}
add_action( 'wp_head', 'so_41431558_remove_membership_post_restrictions', 1 );
Your add_action attempt is happening on priority 1, which is after the function has already run the Memberships method on priority 0, so even if the rest of your code was correct it would be too late.
So 1. I think we need to go to an earlier hook.
And 2. I think we need to use the new method for accessing the post restrictions class instance.
edited to add
and 3. I've switched to a direct version compare condition
and 4. I misread where the get_posts_restrictions_instance() method was... it is accessed via wc_memberships()->get_restrictions_instance()->get_posts_restrictions_instance()

Related

How to use WC_Order class properly?

I'm writing a Woocommerce plugin and I'm trying to get order information from the class WC_Order, and I get two different types of error.
In the first case, I'm using the following code:
public function woocommerce_loaded() {
global $woocommerce, $post;
$order = new WC_Order($post->ID);
}
And I get this:
PHP Fatal error: Uncaught Error: Class 'WC_Order' not found in C:\wamp64\www\mysite\wp-content\plugins\tutorial-plugin\tutorial-plugin.php
In the second case, I'm using this code:
public function woocommerce_loaded() {
global $woocommerce, $post;
$order = $woocommerce -> WC_Order($post->ID);
}
And I get the following error message:
PHP Fatal error: Uncaught Error: Call to a member function WC_Order() on null in C:\wamp64\www\mysite\wp-content\plugins\tutorial-plugin\tutorial-plugin.php
And for both cases, I'm hooking my function at my __construct() method with the following code:
add_action( 'woocommerce_init', array( $this, 'woocommerce_loaded' ) );
I also tried using this class with my orders id that I have access in the Woocommerce Orders page, but it didn't work either.
I'm new in this area, I don't know what I am doing wrong. I could use some help!
Here's the complete version of my plugin code:
<?php
//Make sure we don't expose any information if called directly
if ( ! defined( 'ABSPATH' ) ) exit;
//Check if Woocommerce is active
if ( in_array( 'woocommerce/woocommerce.php', apply_filters( 'active_plugins', get_option( 'active_plugins' ) ) ) ) {
//Check if a class with the same name doesn’t already exist
if ( ! class_exists( 'WC_tutorial_plugin' ) ) {
//Provide translation files
load_plugin_textdomain( 'tutorial-plugin', false, dirname( plugin_basename( __FILE__ ) ) . '/' );
//Plugin code
class WC_tutorial_plugin {
/**
* Constructor for the WC_tutorial_plugin class
*
* #access public
* #return void
*/
public function __construct() {
$this->id = 'tutorial-plugin'; // Id of the class
$this->method_title = __( 'Tutorial plugin' ); // Title shown in admin
$this->init();
global $woocommerce;
$this->woocommerce_loaded();
// called just before the woocommerce template functions are included
add_action( 'init', array( $this, 'include_template_functions' ), 20 );
// called only after woocommerce has finished loading
add_action( 'woocommerce_init', array( $this, 'woocommerce_loaded' ) );
// called after all plugins have loaded
add_action( 'plugins_loaded', array( $this, 'plugins_loaded' ) );
add_action( 'plugins_loaded', array( $this, 'init' ) );
}
/**
* Initialize settings
*
* #access public
* #return void
*/
public function init() {
// Load the settings API
$this->init_form_fields(); // This is part of the settings API.
}
function activate(){
//Flush rewrite rules
flush_rewrite_rules();
}
function deactivate(){
//Flush rewrite rules
flush_rewrite_rules();
}
/**
* Take care of anything that needs woocommerce to be loaded.
* For instance, if you need access to the $woocommerce global
*/
public function woocommerce_loaded() {
global $woocommerce;
$order = $woocommerce -> WC_Order(17);
}
/**
* Take care of anything that needs all plugins to be loaded
*/
public function plugins_loaded() {
do_action('woocommerce_init', array($this, 'woocommerce_loaded'));
}
}
//Finally instantiate our plugin class and add it to the set of globals
if ( class_exists('WC_tutorial_plugin')){
$GLOBALS['tutorial-plugin'] = new WC_tutorial_plugin();
$plugin = new WC_tutorial_plugin(__FILE__ );
$plugin -> woocommerce_loaded();
}
}
}
//Activation
register_activation_hook( __FILE__, array($plugin, 'activate'));
//Deactivation
register_deactivation_hook( __FILE__, array($plugin, 'deactivate'));
I tried a lot to find the solution for this, and in the end I found two things that can be useful:
we can use the init hook and put the priority to 100, and then use our function in it, if you can use your code this way then it is for you:
add_action('init', 'my_init', 1);
function my_init(){
$order = new WC_Order(5273);
}
The 2nd way is to fetch the order details directly from the database using the $wpdb. the order is stored in the post table and order details like all the default order meta are stored in the post_meta table
global $wpdb;
// Get all customer orders
$subscriptions = get_posts(array(
'numberposts' => -1,
'post_type' => 'shop_subscription', // Subscription post type
'post_status' => 'wc-active', // Active subscription
'orderby' => 'post_date', // ordered by date
'order' => 'ASC',
'date_query' => array( // Start & end date
array(
'after' => $from_date,
'before' => $to_date,
'inclusive' => true,
),
),
));
I found this way much useful and also if you need to get the order custom meta fields then those fields are stored in the woocommerce_order_itemmeta table but to fetch these you need order_item_meta id which you can get from the table woocommerce_order_items, you can use the code below giving it the orderIds array containing the order ids:
foreach ($orderIds as $orderId) {
$result = $wpdb->get_results($wpdb->prepare(
"
SELECT m.meta_value,i.order_item_name
FROM {$wpdb->prefix}woocommerce_order_itemmeta as m
INNER JOIN {$wpdb->prefix}woocommerce_order_items as i ON
m.order_item_id = i.order_item_id
WHERE `order_id` = %s and i.order_item_type = 'line_item'
and m.meta_key = '_wapf_meta'
",
"$orderId"
), object);
$results[] = $result;
}

WordPress AJAX is_admin is true, causing issues.

i'm trying to make a plugin for WordPress, which is has got an admin section for some basic settings, and also registers some shortcode to display some HTML, which is basically a form.
Here is my main plugin file, plugins/my-plugin/my-plugin.php:
/**
* Plugin Name: Pathway
* Plugin URI: http://www.martynleeball.com/
* Description: Pathway integration.
* Version: 1.0
* Author: Martyn Lee Ball
* Author URI: https://www.martynleeball.com/
**/
define('PATHWAY_VERSION', '0.0.8');
define('PATHWAY_AUTHOR', 'Martyn Lee Ball');
define('PATHWAY__MINIMUM_WP_VERSION', '4.*');
define('PATHWAY_CONTACT', 'martynleeball#gmail.com');
add_action(
'plugins_loaded',
array ( Pathway::get_instance(), 'plugin_setup' )
);
class Pathway
{
protected static $instance = NULL;
public $plugin_url = '';
private $cpt = 'post'; # Adjust the CPT
public function __construct() {}
public static function get_instance()
{
NULL === self::$instance and self::$instance = new self;
return self::$instance;
}
public function plugin_setup()
{
$this->plugin_url = '';
add_action( 'wp_enqueue_scripts', array( $this, 'enqueue' ) );
// if (is_admin()) {
//
// require_once( $this->plugin_url . 'admin/index.php' );
//
// register_activation_hook( __FILE__, 'install' );
//
// return;
// }
add_shortcode( 'pathway', array($this, 'shortcode'));
add_action( 'wp_ajax_ajax_login', array( $this, 'ajax_login' ) );
add_action( 'wp_ajax_nopriv_ajax_login', array( $this, 'ajax_login' ) );
add_action( 'wp_ajax_ajax_register', array( $this, 'ajax_register' ) );
add_action( 'wp_ajax_nopriv_ajax_register', array( $this, 'ajax_register' ) );
}
public function enqueue()
{
wp_enqueue_script( 'vuejs', 'https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.js' );
wp_enqueue_script(
'ajax-handle-form',
"{$this->plugin_url}/wp-content/plugins/pathway/frontend/js/scripts.js"
);
wp_localize_script(
'ajax-handle-form',
'wp_ajax',
array(
'ajaxurl' => admin_url( 'admin-ajax.php' ),
'ajaxnonce' => wp_create_nonce( 'ajax_post_validation' )
)
);
}
public function ajax_login()
{
echo 'login';exit;
}
public function ajax_register()
{
echo 'register';exit;
}
public function shortcode()
{
if (!isset($_SESSION['pathway_login'])) {
self::view('forms/login');
}
}
public static function view( $name, array $args = array() ) {
foreach ( $args AS $key => $val ) {
$$key = $val;
}
// $file = $this->plugin_url . 'views/'. $name . '.php';
$file = 'views/'. $name . '.php';
include( $file );
}
}
Please correct me if i'm going wrong somewhere, there's so many mixed guides online showing different ways. Within this file i'm basically:
Adding my scripts and assigning the PHP values.
I would be then starting the admin section however has to comment this out for the AJAX call, this is my issue.
Registering my shortcode.
Adding the actions for the AJAX form submit.
Obviously my issue is that when I hit the is_admin from the AJAX call it is returning true, when it should be false as an public visitor can submit this form. The wp_ajax_nopriv action doesn't appear to work which would solve the issue, this is probably due to me being logged into WordPress.
I have tried logging out of WordPress but the is_admin still returns true!
Can someone advise?
is_admin will return true on all ajax calls.
It is not actually a useful function to check the user as it checks the uri rather than the user details, i.e. if on a admin page = true, if not false.
Now I was a little confused about your question here, it appears you want the is_admin to return false if its actually an ajax call?
if ( is_admin() && ! wp_doing_ajax() ) {}
It will return false on ajax calls.
If you are checking there is an "admin" logged in, as in can edit posts, see the other capabilities here
if ( current_user_can( 'edit_post' ) ) {}
The no_priv hook will not work when logged in, its not called.

How to override a class with constructor on a WordPress plugin?

I'm trying to override a plugin class on a WordPress plugin.
Here is the original plugin class :
class WCV_Vendor_Dashboard
{
/**
* __construct()
*/
function __construct()
{
add_shortcode( 'wcv_shop_settings', array( $this, 'display_vendor_settings' ) );
add_shortcode( 'wcv_vendor_dashboard', array( $this, 'display_vendor_products' ) );
add_action( 'template_redirect', array( $this, 'check_access' ) );
add_action( 'init', array( $this, 'save_vendor_settings' ) );
}
public function save_vendor_settings(){
//some codes here
}
}
Here is what I'm trying (in functions.php), but it doesn't work :
$wcv_vendor_dashboard = new WCV_Vendor_Dashboard();
global $wcv_vendor_dashboard;
remove_action( 'init', array( $wcv_vendor_dashboard , 'save_vendor_settings' ) );
How to remove it correctly and how to create the replacement?
Additional info:
I did similar thing on the WooCommerce core. When I want to override a class / function, I use this (for example):
remove_action( 'template_redirect', array( 'WC_Form_Handler', 'save_account_details' ) );
function new_save_account_details() {
//custom code here
}
add_action( 'template_redirect', 'new_save_account_details' );
It's working properly on WooCommerce core. I tried something similar on WCV_Vendor_Dashboard but it doesn't work.
Why it was working with woocommerce, but it does not work in this case?
When you attach a function on to a specific action, WoredPress creates unique id for that callback and stores that in global $wp_filter array ( in fact, it was an array before, but now it is an object ). For object method callbacks ( like array( $this, 'save_vendor_settings' ) ), the id is generated with spl_object_hash php function. For the above example,
spl_object_hash( $this ) . 'save_vendor_settings'
, and it looks like 000000001c0af63f000000006d7fe83asave_vendor_settings.
To "legally" remove the object method with remove_action() you will need to have access to the original object that was used to attach the function in the first place. If the object exists in global namespace:
global $wcv;
remove_action( 'init', array( $wcv, 'save_vendor_settings' ) );
Creating another class instance will not work because the generated id is unique for each object, even if they are instances of the same class.
In the case of WooCommerce I guess it was about static class methods. Different logic is used to generate the ids for static class methods, functions and static method callbacks are just returned as strings. For your example it would be:
'WC_Form_Handler' . '::' . 'save_account_details'
You see why it works for one case, but not for the other.
There is a hack to replace functions attached by replacing them directly in global $wp_filter object, but it's not 100% reliable. Since we do not have access to the original object, we can only filter $wp_filter by the name of the function, if there are identical names for the same action it will replace wrong handlers.
global $wp_filter;
foreach ( $wp_filter['init']->callbacks as $priority => &$callbacks ) {
foreach ( $callbacks as $id => &$callback ) {
if ( substr( $id, -strlen( 'save_vendor_settings' ) ) === 'save_vendor_settings' ) {
// replace the callback with new function
$callback['function'] = 'new_save_vendor_settings';
}
}
}
I hope it will work, greetings.
Example for child class
class WCV_Vendor_Dashboard_Child extends WCV_Vendor_Dashboard
{
/**
* __construct()
*/
function __construct()
{
parent::__construct();
}
public function new_save_vendor_settings(){
//some codes here
}
}

need help removing action from plugin file

Hello I am trying to remove an action from a wordpress plugin file. The plugin is called Woocommerce Points and Rewards. I have found the action I want to remove in one of the class files. When I comment out the "add_action" it does exactly what I want. But I am trying to remove the action from functions.php in my child them. I have been reading on this and I think my problem is I need to "globalize" the class variable that the action is in; but I am not sure what that class variable is…
here is the code where it adds the action (part of a file):
class WC_Points_Rewards_Cart_Checkout {
/**
* Add cart/checkout related hooks / filters
*
* #since 1.0
*/
public function __construct() {
// Coupon display
add_filter( 'woocommerce_cart_totals_coupon_label', array( $this, 'coupon_label' ) );
// Coupon loading
add_action( 'woocommerce_cart_loaded_from_session', array( $this, 'points_last' ) );
add_action( 'woocommerce_applied_coupon', array( $this, 'points_last' ) );
// add earn points/redeem points message above cart / checkout
add_action( 'woocommerce_before_cart', array( $this, 'render_earn_points_message' ), 15 );
add_action( 'woocommerce_before_cart', array( $this, 'render_redeem_points_message' ), 16 );
add_action( 'woocommerce_before_checkout_form', array( $this, 'render_earn_points_message' ), 5 );
add_action( 'woocommerce_before_checkout_form', array( $this, 'render_redeem_points_message' ), 6 );
// handle the apply discount submit on the cart page
add_action( 'wp', array( $this, 'maybe_apply_discount' ) );
// handle the apply discount AJAX submit on the checkout page
add_action( 'wp_ajax_wc_points_rewards_apply_discount', array( $this, 'ajax_maybe_apply_discount' ) );
}
The function I want to remove is this one:
add_action( 'woocommerce_before_cart', array( $this, 'render_redeem_points_message' ), 16 );
so far no luck in getting it removed; here is what I have in functions.php:
global $woocommerce, $wc_points_rewards;
/*
global $this;
*/
remove_action( 'woocommerce_before_cart', array( $this, 'render_redeem_points_message' ), 16 );
so - I am sure this can be done in this way at least I have read that it can be done, I think I just have some thing wrong on this…
I tried globalizing $this, but that just gave me an error message...
if you need to see the entire file or something else please just let me know…
So I am hoping someone on here can help me identify what I am doing wrong…
** UPDATE Monday 8/18 ********
Looking for where class is instantiated; I have found this in the "woo commerce-points-and-rewards.php" file; this looks like this may be it but not sure what I am looking at;
does this look like where the "WC_Points_Rewards_Cart_Checkout" is instantiated?
And if so I am not sure how i use this to write my "remove action" in functions.php...
private function includes() {
// product class
require( 'classes/class-wc-points-rewards-product.php' );
$this->product = new WC_Points_Rewards_Product();
// cart / checkout class
require( 'classes/class-wc-points-rewards-cart-checkout.php' );
$this->cart = new WC_Points_Rewards_Cart_Checkout();
// order class
require( 'classes/class-wc-points-rewards-order.php' );
$this->order = new WC_Points_Rewards_Order();
// discount class
require( 'classes/class-wc-points-rewards-discount.php' );
$this->discount = new WC_Points_Rewards_Discount();
// actions class
require( 'classes/class-wc-points-rewards-actions.php' );
$this->actions = new WC_Points_Rewards_Actions();
// manager class
require( 'classes/class-wc-points-rewards-manager.php' );
// points log access class
require( 'classes/class-wc-points-rewards-points-log.php' );
if ( is_admin() )
$this->admin_includes();
}
Thanks so much...
Try this:
// Use the class name instead of a globalized $this
remove_action( 'woocommerce_before_cart', array( 'WC_Points_Rewards_Cart_Checkout', 'render_redeem_points_message' ), 16 );
As $this is an internal referrer to the class it is used in, globalizing it may not be a good thing.
Does the plugin allow you to extend the class with your own and use it instead?
you found a solution for this? having the same problem with another wc plugin ;)
alright. found an answer in the wc docs. in my case:
function wc_move_checkout_addons() {
remove_action( 'woocommerce_checkout_after_customer_details', array( $GLOBALS['wc_checkout_add_ons']->frontend, 'render_add_ons' ) );
add_action( 'woocommerce_checkout_before_customer_details', array( $GLOBALS['wc_checkout_add_ons']->frontend, 'render_add_ons' ) );
}
add_action( 'init', 'wc_move_checkout_addons' );
so in your case it should be something like that:
function wc_remove_message() {
remove_action( 'woocommerce_before_cart', array( $GLOBALS['wc_points_rewards_cart_checkout']->frontend, 'render_redeem_points_message' ) );
}
add_action( 'init', 'wc_remove_message' );
In case anyone else is wondering, I just accomplished removing the action in this plugin in the following way.
I wanted to remove the action defined in class-wc-points-rewards.php on line 34:
add_action( 'woocommerce_single_product_summary', array( $this, 'render_product_message' ) );
To do that, I looked at woocommerce-points-and-rewards.php. On line 46 it shows:
$GLOBALS['wc_points_rewards'] = new WC_Points_Rewards();
And in the same file for the definition of the WC_Points_Rewards class, on line 249-250 it shows:
require( 'includes/class-wc-points-rewards-product.php' );
$this->product = new WC_Points_Rewards_Product();
With that information, I was able to remove the action:
remove_action( 'woocommerce_single_product_summary', array( $GLOBALS['wc_points_rewards']->product, 'render_product_message' ) );
I was able to figure out how to remove the render_redeem_points_message effectively with the following code:
/* Removes render_redeem_points_message() */
function wc_remove_points_message() {
global $woocommerce;
global $wc_points_rewards;
// Removes message from cart page
remove_action( 'woocommerce_before_cart', array( $wc_points_rewards->cart, 'render_redeem_points_message' ), 16 );
// Removes message from checkout page
remove_action( 'woocommerce_before_checkout_form', array( $wc_points_rewards->cart, 'render_redeem_points_message' ), 6 );
}
// Removes action on init
add_action( 'init', 'wc_remove_points_message' );
To share a bit more, I wanted to create a minimum purchase amount to be able to redeem points:
/* Adds Minimum Order for Points Redemption */
function wc_min_order_points_message() {
global $woocommerce;
global $wc_points_rewards;
// Get cart subtotal, excluding tax
$my_cart_total = $woocommerce->cart->subtotal_ex_tax;
if ($my_cart_total < 30) { // $30 minimum order
remove_action( 'woocommerce_before_cart', array( $wc_points_rewards->cart, 'render_redeem_points_message' ), 16 );
remove_action( 'woocommerce_before_checkout_form', array( $wc_points_rewards->cart, 'render_redeem_points_message' ), 6 );
} // endif $my_cart_total
}
// Adds action for cart and checkout pages instead of on init
add_action( 'woocommerce_before_cart', 'wc_min_order_points_message' );
add_action( 'woocommerce_before_checkout_form', 'wc_min_order_points_message' );
I hope this helps anyone else trying to similarly expand on the WooCommerce Points and Rewards plugin.
SOLUTION-1:
In this case, as we have the plugin object's global instance, then we can do it easily:
remove_action('woocommerce_before_cart',array($GLOBALS['wc_points_rewards']->cart,'render_redeem_points_message'),16);
SOLUTION-2:
We will not be lucky always like the above solution getting the instance of the class object if any plugin author creates the class object anonymously without storing it to any global variables or keeping no method which can return it's own instance. In those cases, we can use the following :)
//keeping this function in our functions.php
function remove_anonymous_object_action( $tag, $class, $method, $priority=null ){
if( empty($GLOBALS['wp_filter'][ $tag ]) ){
return;
}
foreach ( $GLOBALS['wp_filter'][ $tag ] as $filterPriority => $filter ){
if( !($priority===null || $priority==$filterPriority) )
continue;
foreach ( $filter as $identifier => $function ){
if( is_array( $function)
and is_a( $function['function'][0], $class )
and $method === $function['function'][1]
){
remove_action(
$tag,
array ( $function['function'][0], $method ),
$filterPriority
);
}
}
}
}
And calling the following line appropriately when we need (may be with a hook or something):
//-->Actual Target: this line does not work;
//remove_action( 'personal_options', array('myCRED_Admin','show_my_balance') );
//-->But instead this line will work ;)
remove_anonymous_object_action('personal_options','myCRED_Admin','show_my_balance');
exactly this function worked for me
if (class_exists('WC_Points_Rewards')) {
global $woocommerce, $wc_points_rewards;
remove_action( 'woocommerce_before_cart', array( $wc_points_rewards->cart, 'render_earn_points_message' ), 15 );
remove_action( 'woocommerce_before_cart', array( $wc_points_rewards->cart, 'render_redeem_points_message' ), 16 );
}

How to adapt my plugin to Multisite?

I have many plugins that I wrote for WordPress, and now I want to adapt them to MU.
What are the considerations / best practices / workflow / functions / pitfalls that I have to follow / avoid / adapt in order to 'upgrade' my plugins to support also Multisite installations?
For example, but not limited to:
Enqueue scripts/register
Incuding files (php, images)
Paths for custom files uploads
$wpdb
Activation, uninstall, deactivation
Handling of admin specific pages
In the Codex, there are sometimes remarks about Multisite in single function description, but I did not find any one-stop page that address this subject.
As for enqueuing and including, things go as normal. Plugin path and URL are the same.
I never dealt with anything related to upload paths in Multisite and I guess normally WP takes care of this.
$wpdb
There is a commonly used snippet to iterate through all blogs:
global $wpdb;
$blogs = $wpdb->get_results("
SELECT blog_id
FROM {$wpdb->blogs}
WHERE site_id = '{$wpdb->siteid}'
AND spam = '0'
AND deleted = '0'
AND archived = '0'
");
$original_blog_id = get_current_blog_id();
foreach ( $blogs as $blog_id )
{
switch_to_blog( $blog_id->blog_id );
// do something in the blog, like:
// update_option()
}
switch_to_blog( $original_blog_id );
You may find examples where restore_current_blog() is used instead of switch_to_blog( $original_blog_id ). But here's why switch is more reliable: restore_current_blog() vs switch_to_blog().
$blog_id
Execute some function or hook according to the blog ID:
global $blog_id;
if( $blog_id != 3 )
add_image_size( 'category-thumb', 300, 9999 ); //300 pixels wide (and unlimited height)
Or maybe:
if(
'child.multisite.com' === $_SERVER['SERVER_NAME']
||
'domain-mapped-child.com' === $_SERVER['SERVER_NAME']
)
{
// do_something();
}
Install - Network Activation only
Using the plugin header Network: true (see: Sample Plugin) will only display the plugin in the page /wp-admin/network/plugins.php. With this header in place, we can use the following to block certain actions meant to happen if the plugin is Network only.
function my_plugin_block_something()
{
$plugin = plugin_basename( __FILE__ );
if( !is_network_only_plugin( $plugin ) )
wp_die(
'Sorry, this action is meant for Network only',
'Network only',
array(
'response' => 500,
'back_link' => true
)
);
}
Uninstall
For (De)Activation, it depends on each plugin. But, for Uninstalling, this is the code I use in the file uninstall.php:
<?php
/**
* Uninstall plugin - Single and Multisite
* Source: https://wordpress.stackexchange.com/q/80350/12615
*/
// Make sure that we are uninstalling
if ( !defined( 'WP_UNINSTALL_PLUGIN' ) )
exit();
// Leave no trail
$option_name = 'HardCodedOptionName';
if ( !is_multisite() )
{
delete_option( $option_name );
}
else
{
global $wpdb;
$blog_ids = $wpdb->get_col( "SELECT blog_id FROM $wpdb->blogs" );
$original_blog_id = get_current_blog_id();
foreach ( $blog_ids as $blog_id )
{
switch_to_blog( $blog_id );
delete_option( $option_name );
}
switch_to_blog( $original_blog_id );
}
Admin Pages
1) Adding an admin page
To add an administration menu we check if is_multisite() and modify the hook accordingly:
$hook = is_multisite() ? 'network_' : '';
add_action( "{$hook}admin_menu", 'unique_prefix_function_callback' );
2) Check for Multisite dashboard and modify the admin URL:
// Check for MS dashboard
if( is_network_admin() )
$url = network_admin_url( 'plugins.php' );
else
$url = admin_url( 'plugins.php' );
3) Workaround to show interface elements only in main site
Without creating a Network Admin Menu (action hook network_admin_menu), it is possible to show some part of the plugin only in the main site.
I started to include some Multisite functionality in my biggest plugin and did the following to restrict one part of the plugin options to the main site. Meaning, if the plugin is activated in a sub site, the option won't show up.
$this->multisite = is_multisite()
? ( is_super_admin() && is_main_site() ) // must meet this 2 conditions to be "our multisite"
: false;
Looking at this again, maybe it can be simply: is_multisite() && is_super_admin() && is_main_site(). Note that the last two return true in single sites.
And then:
if( $this->multisite )
echo "Something only for the main site, i.e.: Super Admin!";
4) Collection of useful hooks and functions.
Hooks: network_admin_menu, wpmu_new_blog, signup_blogform, wpmu_blogs_columns, manage_sites_custom_column, manage_blogs_custom_column, wp_dashboard_setup, network_admin_notices, site_option_active_sitewide_plugins, {$hook}admin_menu
Functions: is_multisite, is_super_admin is_main_site, get_blogs_of_user, update_blog_option, is_network_admin, network_admin_url, is_network_only_plugin
PS: I rather link to WordPress Answers than to the Codex, as there'll be more examples of working code.
Sample plugin
I've just rolled a Multisite plugin, Network Deactivated but Active Elsewhere, and made a non-working resumed annotated version bellow (see GitHub for the finished full working version). The finished plugin is purely functional, there's no settings interface.
Note that the plugin header has Network: true. It prevents the plugin from showing in child sites.
<?php
/**
* Plugin Name: Network Deactivated but Active Elsewhere
* Network: true
*/
/**
* Start the plugin only if in Admin side and if site is Multisite
*/
if( is_admin() && is_multisite() )
{
add_action(
'plugins_loaded',
array ( B5F_Blog_Active_Plugins_Multisite::get_instance(), 'plugin_setup' )
);
}
/**
* Based on Plugin Class Demo - https://gist.github.com/toscho/3804204
*/
class B5F_Blog_Active_Plugins_Multisite
{
protected static $instance = NULL;
public $blogs = array();
public $plugin_url = '';
public $plugin_path = '';
public static function get_instance()
{
NULL === self::$instance and self::$instance = new self;
return self::$instance;
}
/**
* Plugin URL and Path work as normal
*/
public function plugin_setup()
{
$this->plugin_url = plugins_url( '/', __FILE__ );
$this->plugin_path = plugin_dir_path( __FILE__ );
add_action(
'load-plugins.php',
array( $this, 'load_blogs' )
);
}
public function __construct() {}
public function load_blogs()
{
/**
* Using "is_network" property from $current_screen global variable.
* Run only in /wp-admin/network/plugins.php
*/
global $current_screen;
if( !$current_screen->is_network )
return;
/**
* A couple of Multisite-only filter hooks and a regular one.
*/
add_action(
'network_admin_plugin_action_links',
array( $this, 'list_plugins' ),
10, 4
);
add_filter(
'views_plugins-network', // 'views_{$current_screen->id}'
array( $this, 'inactive_views' ),
10, 1
);
add_action(
'admin_print_scripts',
array( $this, 'enqueue')
);
/**
* This query is quite frequent to retrieve all blog IDs.
*/
global $wpdb;
$this->blogs = $wpdb->get_results(
" SELECT blog_id, domain
FROM {$wpdb->blogs}
WHERE site_id = '{$wpdb->siteid}'
AND spam = '0'
AND deleted = '0'
AND archived = '0' "
);
}
/**
* Enqueue script and style normally.
*/
public function enqueue()
{
wp_enqueue_script(
'ndbae-js',
$this->plugin_url . '/ndbae.js',
array(),
false,
true
);
wp_enqueue_style(
'ndbae-css',
$this->plugin_url . '/ndbae.css'
);
}
/**
* Check if plugin is active in any blog
* Using Multisite function get_blog_option
*/
private function get_network_plugins_active( $plug )
{
$active_in_blogs = array();
foreach( $this->blogs as $blog )
{
$the_plugs = get_blog_option( $blog['blog_id'], 'active_plugins' );
foreach( $the_plugs as $value )
{
if( $value == $plug )
$active_in_blogs[] = $blog['domain'];
}
}
return $active_in_blogs;
}
}
Other resources - e-books
Not directly related to plugin development, but kind of essential to Multisite management.
The e-books are written by no less than two giants of Multisite: Mika Epstein (aka Ipstenu) and Andrea Rennick.
WordPress Multisite 101
WordPress Multisite 110

Categories