Default + Zones Show Up in Check out??? - Woocommerce Custom Shipping Method Plugin - php

I've built my first shipping method plugin based on flat rate with a few extra fields.
I have done the following:
1. Installed and activated the plugin
2. Added 2 instances of the shipping method to the UK zone
I can see in the top sub menu in the shipping section there appears to be some kind of "default" instance of the shipping plugin in a menu labelled "UK Flat Rate"
I was wondering if there's a way to remove this and ONLY have the plugin work in the shipping zones section.
The reason I ask is that then in checkout if I enter a UK address I see the 2 UK methods defined and then underneath them both there is also a radio button for UK Flat Rate which I'm trying to get rid of. It shows the default values based on the values entered in the sub-section.
if (!defined('ABSPATH')) {
exit;
}
if (in_array('woocommerce/woocommerce.php', apply_filters('active_plugins', get_option('active_plugins')))) {
add_action( 'woocommerce_shipping_init', 'uk_shipping_method');
function uk_shipping_method() {
if (!class_exists('UK_WC_Shipping_Flat_Rate')) {
class UK_WC_Shipping_Flat_Rate extends WC_Shipping_Method {
/** #var string cost passed to [fee] shortcode */
protected $fee_cost = '';
/**
* Constructor.
*
* #param int $instance_id
*/
public function __construct($instance_id = 1) {
$this->id = 'uk_flat_rate';
$this->instance_id = absint($instance_id);
$this->enabled = "yes"; // This can be added as an setting but for this example its forced enabled
$this->method_title = __('UK Flat Rate');
$this->title = 'UK Flat Rate';
$this->method_description = __('Lets you charge a fixed rate for shipping but flags for UK Status Update.');
$this->supports = array(
'shipping-zones',
'instance-settings',
'instance-settings-modal',
);
$this->init();
add_action('woocommerce_update_options_shipping_' . $this->id, array( $this, 'process_admin_options'));
}
/**
* init user set variables.
*/
public function init() {
$this->instance_form_fields = include('includes/settings-flat-rate.php');
$this->title = $this->get_option( 'title' );
$this->tax_status = $this->get_option( 'tax_status' );
$this->cost = $this->get_option( 'cost' );
$this->type = $this->get_option( 'type', 'class' );
}
/**
* Evaluate a cost from a sum/string.
* #param string $sum
* #param array $args
* #return string
*/
protected function evaluate_cost( $sum, $args = array() ) {
include_once( WC()->plugin_path() . '/includes/libraries/class-wc-eval-math.php' );
// Allow 3rd parties to process shipping cost arguments
$args = apply_filters( 'woocommerce_evaluate_shipping_cost_args', $args, $sum, $this );
$locale = localeconv();
$decimals = array( wc_get_price_decimal_separator(), $locale['decimal_point'], $locale['mon_decimal_point'], ',' );
$this->fee_cost = $args['cost'];
// Expand shortcodes
add_shortcode( 'fee', array( $this, 'fee' ) );
$sum = do_shortcode( str_replace(
array(
'[qty]',
'[cost]',
),
array(
$args['qty'],
$args['cost'],
),
$sum
) );
remove_shortcode( 'fee', array( $this, 'fee' ) );
// Remove whitespace from string
$sum = preg_replace( '/\s+/', '', $sum );
// Remove locale from string
$sum = str_replace( $decimals, '.', $sum );
// Trim invalid start/end characters
$sum = rtrim( ltrim( $sum, "\t\n\r\0\x0B+*/" ), "\t\n\r\0\x0B+-*/" );
// Do the math
return $sum ? WC_Eval_Math::evaluate( $sum ) : 0;
}
/**
* Work out fee (shortcode).
* #param array $atts
* #return string
*/
public function fee( $atts ) {
$atts = shortcode_atts( array(
'percent' => '',
'min_fee' => '',
'max_fee' => '',
), $atts, 'fee' );
$calculated_fee = 0;
if ( $atts['percent'] ) {
$calculated_fee = $this->fee_cost * ( floatval( $atts['percent'] ) / 100 );
}
if ( $atts['min_fee'] && $calculated_fee < $atts['min_fee'] ) {
$calculated_fee = $atts['min_fee'];
}
if ( $atts['max_fee'] && $calculated_fee > $atts['max_fee'] ) {
$calculated_fee = $atts['max_fee'];
}
return $calculated_fee;
}
/**
* calculate_shipping function.
*
* #param array $package (default: array())
*/
public function calculate_shipping( $package = array() ) {
$rate = array(
'id' => $this->get_rate_id(),
'label' => $this->title,
'cost' => 0,
'package' => $package,
);
// Calculate the costs
$has_costs = false; // True when a cost is set. False if all costs are blank strings.
$cost = $this->get_option('cost');
if ( '' !== $cost ) {
$has_costs = true;
$rate['cost'] = $this->evaluate_cost( $cost, array(
'qty' => $this->get_package_item_qty( $package ),
'cost' => $package['contents_cost'],
) );
}
// Add shipping class costs.
$shipping_classes = WC()->shipping->get_shipping_classes();
if ( ! empty( $shipping_classes ) ) {
$found_shipping_classes = $this->find_shipping_classes( $package );
$highest_class_cost = 0;
foreach ( $found_shipping_classes as $shipping_class => $products ) {
// Also handles BW compatibility when slugs were used instead of ids
$shipping_class_term = get_term_by( 'slug', $shipping_class, 'product_shipping_class' );
$class_cost_string = $shipping_class_term && $shipping_class_term->term_id ? $this->get_option( 'class_cost_' . $shipping_class_term->term_id, $this->get_option( 'class_cost_' . $shipping_class, '' ) ) : $this->get_option( 'no_class_cost', '' );
if ( '' === $class_cost_string ) {
continue;
}
$has_costs = true;
$class_cost = $this->evaluate_cost( $class_cost_string, array(
'qty' => array_sum( wp_list_pluck( $products, 'quantity' ) ),
'cost' => array_sum( wp_list_pluck( $products, 'line_total' ) ),
) );
if ( 'class' === $this->type ) {
$rate['cost'] += $class_cost;
} else {
$highest_class_cost = $class_cost > $highest_class_cost ? $class_cost : $highest_class_cost;
}
}
if ( 'order' === $this->type && $highest_class_cost ) {
$rate['cost'] += $highest_class_cost;
}
}
// Add the rate
if ( $has_costs ) {
$this->add_rate( $rate );
}
/**
* Developers can add additional flat rates based on this one via this action since #version 2.4.
*
* Previously there were (overly complex) options to add additional rates however this was not user.
* friendly and goes against what Flat Rate Shipping was originally intended for.
*
* This example shows how you can add an extra rate based on this flat rate via custom function:
*
* add_action( 'woocommerce_flat_rate_shipping_add_rate', 'add_another_custom_flat_rate', 10, 2 );
*
* function add_another_custom_flat_rate( $method, $rate ) {
* $new_rate = $rate;
* $new_rate['id'] .= ':' . 'custom_rate_name'; // Append a custom ID.
* $new_rate['label'] = 'Rushed Shipping'; // Rename to 'Rushed Shipping'.
* $new_rate['cost'] += 2; // Add $2 to the cost.
*
* // Add it to WC.
* $method->add_rate( $new_rate );
* }.
*/
do_action( 'woocommerce_' . $this->id . '_shipping_add_rate', $this, $rate );
}
/**
* Get items in package.
* #param array $package
* #return int
*/
public function get_package_item_qty( $package ) {
$total_quantity = 0;
foreach ( $package['contents'] as $item_id => $values ) {
if ( $values['quantity'] > 0 && $values['data']->needs_shipping() ) {
$total_quantity += $values['quantity'];
}
}
return $total_quantity;
}
/**
* Finds and returns shipping classes and the products with said class.
* #param mixed $package
* #return array
*/
public function find_shipping_classes( $package ) {
$found_shipping_classes = array();
foreach ( $package['contents'] as $item_id => $values ) {
if ( $values['data']->needs_shipping() ) {
$found_class = $values['data']->get_shipping_class();
if ( ! isset( $found_shipping_classes[ $found_class ] ) ) {
$found_shipping_classes[ $found_class ] = array();
}
$found_shipping_classes[ $found_class ][ $item_id ] = $values;
}
}
return $found_shipping_classes;
}
}
}
function add_uk_shipping_method( $methods ) {
$methods['uk_flat_rate'] = 'UK_WC_Shipping_Flat_Rate';
return $methods;
}
add_filter( 'woocommerce_shipping_methods', 'add_uk_shipping_method' );
}
}
Is there a setting in the plugin I'm missing to enforce the method is only zones based?

Turns out I needed to add filters to remove the section and the first instance of the method from shipping like so
woocommerce_shipping_option_remove( $section ) {
unset($section['uk_flat_rate']);
return $section;
}
add_filter( 'woocommerce_get_sections_shipping', 'woocommerce_shipping_option_remove' ,1 );
function woocommerce_shipping_remove_method( $rates )
{
unset($rates['uk_flat_rate:1']);
return $rates;
}
add_filter('woocommerce_package_rates','woocommerce_shipping_remove_method', 100 );
}
}

Related

Check if Mollie payment is paid

I have been stuck on this for weeks now so I'm dropping my question here, hopefully someone can help me. I have a website where people can sell their car. You buy an advertisement and you can upload your vehicle. I have integrated Mollie Payment API so people can pay with iDeal, but it doesn't seem to read the payment status.
When the payment has been successful, my system doesn't give out an advertisement. How can I check if the payment has been successful and then automatically give out an advertisement if the order has been paid?
This is the code that processes the payments:
<?php
namespace AutoListingsFrontend;
require_once __DIR__ . "/../mollie-api-php/vendor/autoload.php";
require_once __DIR__ . "/../mollie-api-php/examples/functions.php";
class Checkout {
/**
* Hold our entire Purchase data
*/
public $purchase_data = array();
public function __construct() {
add_action( 'init', array( $this, 'process_purchase' ) );
add_action( 'auto_listings_mark_as_pending', array( $this, 'pending_payment' ) );
add_action( 'auto_listings_send_to_gateway', array( $this, 'send_to_gateway' ) );
add_action( 'auto_listings_payment_successful', array( $this, 'payment_successful' ), 10, 2 );
add_action( 'auto_listings_insert_payment_note', array( $this, 'insert_payment_note' ), 10, 2 );
add_action( 'auto_listings_update_payment_status', array( $this, 'update_payment_status' ), 10, 2 );
//add_action( 'auto_listings_gateway_paypal', 'auto_listings_process_paypal_purchase' );
}
public function sanitize_post_data() {
// simple sanitizing
foreach ( $_POST as $key => $value ) {
$key = str_replace( 'auto-listings-', '', $key );
if( $key == 'user-id' ) {
$sanitized[ $key ] = absint( $value );
} else {
$sanitized[ $key ] = sanitize_text_field( $value );
}
}
return $sanitized;
}
/**
* Process Purchase Form
*
* Handles the purchase form process.
*/
public function process_purchase() {
do_action( 'auto_listings_pre_process_purchase' );
// Check if there is $_POST
if ( empty( $_POST ) ) return false;
if ( ! isset( $_POST['auto-listings-gateway'] ) || empty( $_POST['auto-listings-gateway'] ) )
return false;
if ( ! isset( $_POST['auto-listings-package'] ) || empty( $_POST['auto-listings-package'] ) )
return false;
if ( ! isset( $_POST['auto-listings-user-id'] ) || empty( $_POST['auto-listings-user-id'] ) )
return false;
$data = $this->sanitize_post_data();
// Verify there is a user_ID
if ( $data['user-id'] > 0 ) {
// Get the logged in user data
$user = get_userdata( $data['user-id'] );
// Verify data
if ( ! $user ) {
return false;
}
}
// Setup user information
$user_info = array(
'id' => $user->ID,
'email' => $user->user_email,
'first_name' => $user->first_name,
'last_name' => $user->last_name,
);
// Setup package information
$package_info = auto_listings_get_package( $data['package'] );
// Set up the unique purchase key.
$key = strtolower( md5( $user_info['id'] . date( 'Y-m-d H:i:s' ) . uniqid( 'auto', true ) ) );
// Setup purchase information
$purchase_data = array(
'package' => stripslashes_deep( $package_info ),
'purchase_key' => $key,
'user' => stripslashes_deep( $user_info ),
'date' => date( 'Y-m-d H:i:s', current_time( 'timestamp' ) ),
'gateway' => $data['gateway'],
);
// If the total amount in the cart is 0, send to the manual gateway.
if ( ! $purchase_data['package']['price'] ) {
$purchase_data['gateway'] = 'manual';
}
// Allow the purchase data to be modified before it is sent to the gateway
$this->purchase_data = apply_filters(
'auto_listings_purchase_data_before_gateway',
$purchase_data,
$data
);
// Send info to create the pending payment
// Send info to the gateway for payment processing
do_action( 'auto_listings_mark_as_pending' );
do_action( 'auto_listings_send_to_gateway' );
}
/**
* Sends all the payment data to the specified gateway.
*/
public function send_to_gateway() {
$this->purchase_data['gateway_nonce'] = wp_create_nonce( 'auto-listings-gateway' );
// $gateway must match the ID used when registering the gateway
do_action( 'auto_listings_gateway_' . $this->purchase_data['gateway'], $this->purchase_data );
}
/**
* Insert Pending Payment
*
* #param array $payment_data Payment data to process
* #return int|bool Payment ID if payment is inserted, false otherwise
*/
public function pending_payment() {
if ( empty( $this->purchase_data ) ) {
return false;
}
$payment_title = $this->purchase_data['user']['email'];
if ( $purchase_data['gateway'] = 'ideal' ) {
/*
* Initialize the Mollie API library with your API key.
*w
* See: https://www.mollie.com/dashboard/developers/api-keys
*/
$mollie = new \Mollie\Api\MollieApiClient();
$mollie->setApiKey("MOLLIE_APIKEY"); // change to LIVE key when done
$packages = auto_listings_get_packages();
foreach ( $packages as $package_id => $package ){
$orderId = $payment->id;
$prijs = '';
if(isset($_POST["auto-listings-purchase"])) {
$package_id = $_POST['auto-listings-package'];
if($package_id == "3769") {
$prijs = "10.00";
} elseif ($package_id == "3767") {
$prijs = "40.00";
} else {
echo "Something went wrong. Please contact our staff via: info#vagplace.nl.";
}
}
// Create Mollie payment
$payment = $mollie->payments->create([
"amount" => [
"currency" => "EUR",
"value" => $prijs,
],
"description" => "VAGplace order: ".$orderId,
"redirectUrl" => "https://vagplace.nl/mijn-autos/?payment=success&gateway=ideal&order_id=".$orderId,
"webhookUrl" => "https://vagplace.nl/mollie-webhook",
"method" => \Mollie\Api\Types\PaymentMethod::IDEAL,
"metadata" => [
"order_id" => $orderId,
],
]);
database_write($orderId, $payment->status);
/*
* Send the customer off to complete the payment.
* This request should always be a GET, thus we enforce 303 http response code
*/
header("Location: " . $payment->getCheckoutUrl(), true, 303);
}
}
$payment_post = array(
'post_title' => $payment_title,
'post_status' => 'pending',
'post_date' => $this->purchase_data['date'],
'post_type' => 'package-payment',
'post_content' => '',
'meta_input' => array(
'_al_payment_data' => stripslashes_deep( $this->purchase_data ),
'_al_payment_package_status' => 'pending',
'_al_payment_user_id' => $this->purchase_data['user']['id'],
),
);
$payment_id = wp_insert_post( $payment_post );
if ( ! empty( $payment_id ) ) {
$this->purchase_data['payment_id'] = $payment_id;
}
// Return false if no payment was inserted
return false;
}
/**
* Add a note to a payment
*
* #param int $payment_id The payment ID to store a note for
* #param string $note The note to store
* #return int The new note ID
*/
public function insert_payment_note( $payment_id = 0, $note = '' ) {
if ( empty( $payment_id ) )
return false;
$existing_data = get_post_meta( $payment_id, '_al_payment_data', true );
do_action( 'auto_listings_pre_insert_payment_note', $payment_id, $note );
$commentdata = array(
'comment_post_ID' => $payment_id, // to which post the comment will show up
'comment_author' => '', //fixed value - can be dynamic
'comment_author_email' => $existing_data['user']['email'], //fixed value - can be dynamic
'comment_author_url' => '', //fixed value - can be dynamic
'comment_content' => $note['heading'] . ' - ' . $note['content'], //fixed value - can be dynamic
'comment_type' => '', //empty for regular comments, 'pingback' for pingbacks, 'trackback' for trackbacks
'comment_parent' => 0, //0 if it's not a reply to another comment; if it's a reply, mention the parent comment ID here
'user_id' => get_current_user_id() ? get_current_user_id() : 1, //passing current user ID or any predefined as per the demand
);
//Insert new comment and get the comment ID
$note_id = wp_new_comment( $commentdata );
return $note_id;
}
/**
* Updates a payment status.
*
* #param int $payment_id Payment ID
* #param string $new_status New Payment Status (default: publish)
* #return bool If the payment was successfully updated
*/
public function update_payment_status( $payment_id = 0, $new_status = 'publish' ) {
if ( empty( $payment_id ) ) {
return false;
}
if ( empty( $data ) ) {
return false;
}
//Trying to verify payment
//$payment = $mollie->payments->get($payment->id);
$payment = $mollie->payments->get($_POST["id"]);
$orderId = $payment->metadata->order_id;
if ($payment->isPaid())
{
echo "Payment received.";
}
$post_arr = array(
'ID' => $payment_id,
'post_status' => $new_status,
);
$updated = wp_update_post( $post_arr );
return $updated;
}
/**
* What to do when a payment completes successfully
*
* #param array $data payment success data, sent from gateway
* #return bool If the payment was successfully updated
*/
public function payment_successful( $data ) {
$start_time = current_time( 'timestamp', $gmt = 0 );
$end_time = null;
if( $data['package_id']['duration'] > 0 ) {
$end_time = strtotime( '+' . $data['package_id']['duration'] . ' days', date( 'Y-m-d H:i:s', $start_time ) );
}
update_post_meta( $data['payment_id'], '_al_payment_package_time_start', $start_time );
update_post_meta( $data['payment_id'], '_al_payment_package_time_end', $end_time );
update_post_meta( $data['payment_id'], '_al_payment_package_status', 'active' );
update_post_meta( $data['payment_id'], '_al_payment_package_listings', $data['package_id']['listings'] );
update_post_meta( $data['payment_id'], '_al_payment_package_listings_used', '0' );
}
}
Solved. I used the Mollie Webhook to check the status of the payment: https://github.com/mollie/mollie-api-php/blob/master/examples/payments/webhook.php.

How to convert a WC_Data_Store to a WC_Product_Variable_Data_Store_CPT or something that has $prices_array property?

I have to do the following reflection code because the WooCommerce version I have available has a bug (v4.9.2).
Please see the comments in the code below:
// checked before the existence of the class with class_exists
$rp = new ReflectionProperty('WC_Product_Variable_Data_Store_CPT', 'prices_array');
$rp->setAccessible(true);
var_dump('start'); // echoes something to the html code
$protected_prices_array = $rp->getValue($ths); // crashes the PHP script instance
var_dump('stop'); // this is not printed anymore
If requested, I can offer more code.
Currently I am attempting to inherit the given class to see if I can walk around the bug.
On staging site I have PHP 7.4.16.
Update 1
I have this code in my own function my_read_price_data( $ths, &$product, $for_display = false ) { ... which does the same as WC's data store's read_price_data public method which accesses the prices_array property which is protected.
Update 2
/**
* Modified function from WC.
*
* #param WC_Product_Variable_Data_Store_CPT $ths
* #param WC_Product_Variable $product
* #param boolean $for_display
* #return void
*/
function my_read_price_data( $ths, &$product, $for_display = false ) {
/**
* Transient name for storing prices for this product (note: Max transient length is 45)
*
* #since 2.5.0 a single transient is used per product for all prices, rather than many transients per product.
*/
$transient_name = 'wc_var_prices_' . $product->get_id();
$transient_version = WC_Cache_Helper::get_transient_version( 'product' );
$price_hash = my_get_price_hash($ths, $product, $for_display); // with this it does not crash (*)
// NOTE: maybe inherit from WC_Product_Variable_Data_Store_CPT to not use reflection.
$rp = new ReflectionProperty('WC_Product_Variable_Data_Store_CPT', 'prices_array'); // the class exists
$rp->setAccessible(true);
var_dump('start');
$protected_prices_array = $rp->getValue($ths); // (*) until this
var_dump('stop');
// Check if prices array is stale.
if ( ! isset( $protected_prices_array['version'] ) || $protected_prices_array['version'] !== $transient_version ) {
$rp->setValue($ths, array(
'version' => $transient_version,
));
}
$protected_prices_array = $rp->getValue($ths);
/**
* $this->prices_array is an array of values which may have been modified from what is stored in transients - this may not match $transient_cached_prices_array.
* If the value has already been generated, we don't need to grab the values again so just return them. They are already filtered.
*/
if ( empty( $protected_prices_array[ $price_hash ] ) ) {
$transient_cached_prices_array = array_filter( (array) json_decode( strval( get_transient( $transient_name ) ), true ) );
// If the product version has changed since the transient was last saved, reset the transient cache.
if ( ! isset( $transient_cached_prices_array['version'] ) || $transient_version !== $transient_cached_prices_array['version'] ) {
$transient_cached_prices_array = array(
'version' => $transient_version,
);
}
// If the prices are not stored for this hash, generate them and add to the transient.
if ( empty( $transient_cached_prices_array[ $price_hash ] ) ) {
$prices_array = array(
'price' => array(),
'regular_price' => array(),
'sale_price' => array(),
);
$variation_ids = $product->get_visible_children();
if ( is_callable( '_prime_post_caches' ) ) {
_prime_post_caches( $variation_ids );
}
foreach ( $variation_ids as $variation_id ) {
$variation = wc_get_product( $variation_id );
if ( $variation ) {
$price = apply_filters( 'woocommerce_variation_prices_price', $variation->get_price( 'edit' ), $variation, $product );
$regular_price = apply_filters( 'woocommerce_variation_prices_regular_price', $variation->get_regular_price( 'edit' ), $variation, $product );
$sale_price = apply_filters( 'woocommerce_variation_prices_sale_price', $variation->get_sale_price( 'edit' ), $variation, $product );
// Skip empty prices.
if ( '' === $price ) {
continue;
}
// If sale price does not equal price, the product is not yet on sale.
if ( $sale_price === $regular_price || $sale_price !== $price ) {
$sale_price = $regular_price;
}
// If we are getting prices for display, we need to account for taxes.
if ( $for_display ) {
if ( 'incl' === get_option( 'woocommerce_tax_display_shop' ) ) {
$price = '' === $price ? '' : wc_get_price_including_tax(
$variation,
array(
'qty' => 1,
'price' => $price,
)
);
$regular_price = '' === $regular_price ? '' : wc_get_price_including_tax(
$variation,
array(
'qty' => 1,
'price' => $regular_price,
)
);
$sale_price = '' === $sale_price ? '' : wc_get_price_including_tax(
$variation,
array(
'qty' => 1,
'price' => $sale_price,
)
);
} else {
$price = '' === $price ? '' : wc_get_price_excluding_tax(
$variation,
array(
'qty' => 1,
'price' => $price,
)
);
$regular_price = '' === $regular_price ? '' : wc_get_price_excluding_tax(
$variation,
array(
'qty' => 1,
'price' => $regular_price,
)
);
$sale_price = '' === $sale_price ? '' : wc_get_price_excluding_tax(
$variation,
array(
'qty' => 1,
'price' => $sale_price,
)
);
}
}
$prices_array['price'][ $variation_id ] = wc_format_decimal( $price, wc_get_price_decimals() );
$prices_array['regular_price'][ $variation_id ] = wc_format_decimal( $regular_price, wc_get_price_decimals() );
$prices_array['sale_price'][ $variation_id ] = wc_format_decimal( $sale_price, wc_get_price_decimals() );
$prices_array = apply_filters( 'woocommerce_variation_prices_array', $prices_array, $variation, $for_display );
}
}
// Add all pricing data to the transient array.
foreach ( $prices_array as $key => $values ) {
$transient_cached_prices_array[ $price_hash ][ $key ] = $values;
}
set_transient( $transient_name, wp_json_encode( $transient_cached_prices_array ), DAY_IN_SECONDS * 30 );
}
/**
* Give plugins one last chance to filter the variation prices array which has been generated and store locally to the class.
* This value may differ from the transient cache. It is filtered once before storing locally.
*/
$protected_prices_array = $rp->getValue($ths);
$protected_prices_array[$price_hash] = apply_filters( 'woocommerce_variation_prices', $transient_cached_prices_array[ $price_hash ], $product, $for_display );
$rp->setValue($ths, $protected_prices_array);
}
return $rp->getValue($ths)[ $price_hash ];
}
Update 3
The function above, my_read_price_data, is called by:
/**
* Function modified from WC.
*
* #param WC_Product_Variable $p
* #param boolean $for_display
* #return void
*/
function my_get_variation_prices( $p, $for_display = false ) {
$ds = $p->get_data_store(); // $p->data_store;
$prices = my_read_price_data($ds, $p, $for_display);
foreach ( $prices as $price_key => $variation_prices ) {
$prices[ $price_key ] = asort( $variation_prices );
}
return $prices;
}
This is called by the following function which is a modified version of a WC function, but this time the modification is done to change the output to something the client wants:
function my_get_price_html( $price = '' ) {
global $product;
$prices = my_get_variation_prices($product, true);
if ( empty( $prices['price'] ) ) {
$price = apply_filters( 'woocommerce_variable_empty_price_html', '', $product );
} else {
$min_price = current( $prices['price'] );
$max_price = end( $prices['price'] );
$min_reg_price = current( $prices['regular_price'] );
$max_reg_price = end( $prices['regular_price'] );
if ( $min_price !== $max_price ) {
$price = wc_format_price_range( $min_price, $max_price );
} elseif ( $product->is_on_sale() && $min_reg_price === $max_reg_price ) {
$price = my_wc_format_sale_price( $prices['regular_price'] , $prices['price'] );
} else {
$price = wc_price( $min_price );
}
$price = apply_filters( 'woocommerce_variable_price_html', $price . $product->get_price_suffix(), $product );
}
return apply_filters( 'woocommerce_get_price_html', $price, $product );
}
As you can see above, I use my_wc_format_sale_price which is here:
/**
* Format a sale price for display.
*
* #since 3.0.0
* #param float $regular_price Regular price.
* #param float $sale_price Sale price.
* #return string
*/
function my_wc_format_sale_price( $regular_price, $sale_price ) {
$price = '<span>' . get_my_percent($regular_price, $sale_price) . '</span> <ins>' . ( is_numeric( $sale_price ) ? wc_price( $sale_price ) : $sale_price ) . '</ins>';
return apply_filters( 'woocommerce_format_sale_price', $price, $regular_price, $sale_price );
}
Here is the last function that matters, I think (it has a doc comment that says it returns a string):
function get_my_percent($regular_price, $sale_price) {
$a = ($regular_price - $sale_price) / $regular_price * 100;
return "$a% reducere";
}
Update 4
I discovered the following through https://stackoverflow.com/a/21429652/258462.
It seems that the object given to the reflection mechanism is of a different type than the expected type.
From the source code of WooCommerce 4.9.2:
/**
* WC Variable Product Data Store: Stored in CPT.
*
* #version 3.0.0
*/
class WC_Product_Variable_Data_Store_CPT extends WC_Product_Data_Store_CPT implements WC_Object_Data_Store_Interface, WC_Product_Variable_Data_Store_Interface {
/**
* Cached & hashed prices array for child variations.
*
* #var array
*/
protected $prices_array = array()
...
So the question is how to convert the WC_Data_Store into something that has the $prices_array property?

Call a function from a Wordpress plugin

I have a wordpress plugin that I'm looking to call one of the functions on a custom template. The function I want to call is get_ship_now_adjust_date_link.
Is it possible to just do the following on a template:
echo get_ship_now_adjust_date_link( $subscription['id'] );
Here's the plugins full code:
<?php
namespace Javorszky\Toolbox;
add_filter( 'wcs_view_subscription_actions', __NAMESPACE__ . '\\add_ship_reschedule_action', 10, 2 );
add_action( 'wp_loaded', __NAMESPACE__ . '\\handle_ship_now_adjust_date_request' );
/**
* Extra actions on the subscription. Only there if the subscription is active.
*
* #param array $actions existing actions on the subscription
* #param \WC_Subscription $subscription the subscription we're adding new actions to
* #return array $actions
*/
function add_ship_reschedule_action( $actions, $subscription ) {
$next_timestamp = $subscription->get_time( 'next_payment' );
if ( 0 != $next_timestamp && 'active' == $subscription->get_status() ) {
$new_actions = array(
'ship_now_recalculate' => array(
'url' => get_ship_now_adjust_date_link( $subscription ),
'name' => Utilities\replace_key_dates( Utilities\get_button_text( 'ship_reschedule_button_text', 'Ship now and recalculate from today' ), $subscription ),
),
);
$actions = array_merge( $actions, $new_actions );
}
return $actions;
}
/**
* URL to be used on the "Ship now and adjust the date" button.
*
* #param \WC_Subscription $subscription Subscription we're getting the link for
* #return string URL to trigger shipping now and keeping the date with
*/
function get_ship_now_adjust_date_link( $subscription ) {
if ( version_compare( \WC_Subscriptions::$version, '2.6.0', '>=' ) ) {
$completed_payments = $subscription->get_payment_count('completed');
} else {
$completed_payments = $subscription->get_completed_payment_count();
}
$action_link = Utilities\strip_custom_query_args();
$action_link = add_query_arg( array( 'subscription_id' => $subscription->get_id(), 'ship_now_adjust_date' => 1 ), $action_link );
$action_link = wp_nonce_url( $action_link, $subscription->get_id() . '_completed_adjust_' . $completed_payments );
return $action_link;
}
/**
* Hooked into `wp_loaded`, this is responsible for charging the subscription now and adjusting the date if certain
* GET variables are present.
*/
function handle_ship_now_adjust_date_request() {
if ( isset( $_GET['ship_now_adjust_date'] ) && isset( $_GET['subscription_id'] ) && isset( $_GET['_wpnonce'] ) && !isset( $_GET['wc-ajax'] ) ) {
$user_id = get_current_user_id();
$subscription = wcs_get_subscription( $_GET['subscription_id'] );
$nonce = $_GET['_wpnonce'];
if ( Utilities\Process\process_ship_now_adjust_date( $user_id, $subscription, $nonce ) ) {
wc_add_notice( _x( 'Your order has been placed!', 'Notice after ship now adjust date request succeeded.', 'jg-toolbox' ) );
wp_safe_redirect( wc_get_endpoint_url( 'view-subscription', $subscription->get_id(), wc_get_page_permalink( 'myaccount' ) ) );
exit;
}
}
}
try to use \Javorszky\Toolbox\get_ship_now_adjust_date_link

WooCommerce Order Export: Get custom data per line item

I'm using the "WooCommerce Customer / Order / Coupon Export" plugin to export my orders as a CSV file.
I'm exporting the orders in a one row per item format. This means that every order has multiple lines. One line per order item.
Now I want to add some extra data to every line item. For example the author (in our case vendor) of the item.
I found a good way to add exta data to the export. It could be found in the documentation of the plugin: https://docs.woocommerce.com/document/ordercustomer-csv-export-developer-documentation/#section-14
It works but it's not exactly what I need. I could add some data to the whole order.
So every line item gets the same data.
So I tried to change the code and go through every line item. Unfortunatly with the same result.
Every line item gets the same data. In my case the data from the last item of the order.
Here's how I've changed the step 2 of the example from the official docs:
function sv_wc_csv_export_modify_row_data_example( $order_data, $order, $csv_generator ) {
// Example showing how to extract order metadata into it's own column
//$meta_key_example = is_callable( array( $order, 'get_meta' ) ) ? $order->get_meta( 'meta_key_example' ) : $order->meta_key_example;
//$meta_key_example = is_callable( array( $order, 'get_meta' ) ) ? $order->get_meta( 'meta_key_example' ) : $order->meta_key_example;
// Loop through order line items
$allItems = $order->get_items();
foreach( $allItems as $item_line ){
$item_vendor_id = get_post_field( 'post_author', $item_line->get_product_id() );
$custom_data = array(
// User/Vendor
'item_vendor_id' => $item_vendor_id,
'vendor_username' => get_the_author_meta( 'username', $item_vendor_id ),
'vendor_user_email' => get_the_author_meta( 'user_email', $item_vendor_id ),
// Address
'vendor_company' => get_the_author_meta( '_wcv_custom_settings_company_name', $item_vendor_id ),
'vendor_street' => get_the_author_meta( '_wcv_store_address1', $item_vendor_id ),
'vendor_housenumber' => get_the_author_meta( '_wcv_store_address2', $item_vendor_id ),
'vendor_zip' => get_the_author_meta( '_wcv_store_postcode', $item_vendor_id ),
'vendor_city' => get_the_author_meta( '_wcv_store_city', $item_vendor_id ),
'vendor_country' => get_the_author_meta( '_wcv_store_country', $item_vendor_id ),
// Bank
'vendor_bank_name' => get_the_author_meta( 'wcv_bank_name', $item_vendor_id ),
'vendor_bank_account_name' => get_the_author_meta( 'wcv_bank_account_name', $item_vendor_id ),
'vendor_iban' => get_the_author_meta( 'wcv_bank_iban', $item_vendor_id ),
'vendor_bic' => get_the_author_meta( 'wcv_bank_bic_swift', $item_vendor_id ),
);
}
return sv_wc_csv_export_add_custom_order_data( $order_data, $custom_data, $csv_generator );
}
add_filter( 'wc_customer_order_export_csv_order_row', 'sv_wc_csv_export_modify_row_data_example', 10, 3 );
I guess that I'm adding the data at the wrong position?!
But I couldn't figure out where the problem is.
EDIT: After the comment from #CBroe I tried to use wc_customer_order_csv_export_order_line_item.
I found an example here but that's destroying my CSV file:
/**
* Add weight as line item meta in CSV export Default format
*/
function sv_add_weight_to_csv_export_line_item( $line_item, $item, $product, $order ) {
$line_item['weight'] = $product->get_weight();
return $line_item;
}
add_filter( 'wc_customer_order_csv_export_order_line_item', 'sv_add_weight_to_csv_export_line_item', 10, 4 );
/**
* Add weight as line item meta in CSV export CSV Import format
*/
function sv_add_weight_to_csv_export_import_format( $order_data, $order ) {
$count = 1;
// add line items
foreach ( $order->get_items() as $item ) {
$product = $order->get_product_from_item( $item );
if ( ! is_object( $product ) ) {
$product = new WC_Product( 0 );
}
if ( $weight = $product->get_weight() ) {
$order_data[ "order_item_{$count}" ] .= '|weight: ' . $weight;
}
$count++;
}
return $order_data;
}
add_filter( 'wc_customer_order_csv_export_order_row', 'sv_add_weight_to_csv_export_import_format', 20, 2 );
But I'm still trying...
I found an answer. It was an example hidden in the official docs: https://github.com/skyverge/wc-plugins-snippets/blob/master/woocommerce-customer-order-coupon-export/csv/add-item-meta-to-order-export.php
/**
* Add line item meta to the Order CSV Export in Default format
* Example: add weight to the item meta data
*/
/**
* Step 1. Add weight to line item data
*
* #param array $line_item the original line item data
* #param array $item the item's order data
* #param object $product the \WC_Product object for the line
* #param object $order the \WC_Order object being exported
* #return array the updated line item data
*/
function sv_wc_csv_export_add_weight_to_line_item( $line_item, $item, $product, $order ) {
$new_item_data = array();
foreach ( $line_item as $key => $data ) {
$new_item_data[ $key ] = $data;
if ( 'sku' === $key && $product ) {
$new_item_data['weight'] = wc_format_decimal( $product->get_weight(), 2 );
}
}
return $new_item_data;
}
add_filter( 'wc_customer_order_export_csv_order_line_item', 'sv_wc_csv_export_add_weight_to_line_item', 10, 4 );
/**
* (optional) Step 2. Add a separate `item_weight` column to the default and custom formats
*
* #param array $column_headers the original column headers
* #param \CSV_Export_Generator $csv_generator the generator instance
* #return array - the updated column headers
*/
function sv_wc_csv_export_modify_column_headers_item_price( $column_headers, $csv_generator ) {
$new_headers = array();
if ( sv_wc_csv_export_is_one_row( $csv_generator ) ) {
foreach( $column_headers as $key => $column ) {
$new_headers[ $key ] = $column;
// add the item_price after the SKU column
if ( 'item_sku' === $key ) {
$new_headers['item_weight'] = 'item_weight';
}
}
} else {
return $column_headers;
}
return $new_headers;
}
add_filter( 'wc_customer_order_export_csv_order_headers', 'sv_wc_csv_export_modify_column_headers_item_price', 10, 2 );
/**
* (optional) Step 3. Add the item weight data (step 1) to the new item_weight column (step 2)
* for the Default - One Row per Item format
*
* #param array $order_data the original order data
* #param array $item the item for this row
* #return array - the updated order data
*/
function sv_wc_csv_export_order_row_one_row_per_item_weight( $order_data, $item ) {
$order_data['item_weight'] = $item['weight'];
return $order_data;
}
add_filter( 'wc_customer_order_export_csv_order_row_one_row_per_item', 'sv_wc_csv_export_order_row_one_row_per_item_weight', 10, 2 );
if ( ! function_exists( 'sv_wc_csv_export_is_one_row' ) ) :
/**
* Helper function to check the export format
*
* #param \WC_Customer_Order_CSV_Export_Generator $csv_generator the generator instance
* #return bool - true if this is a one row per item format
*/
function sv_wc_csv_export_is_one_row( $csv_generator ) {
$one_row_per_item = false;
if ( version_compare( wc_customer_order_csv_export()->get_version(), '4.0.0', '<' ) ) {
// pre 4.0 compatibility
$one_row_per_item = ( 'default_one_row_per_item' === $csv_generator->order_format || 'legacy_one_row_per_item' === $csv_generator->order_format );
} elseif ( isset( $csv_generator->format_definition ) ) {
// post 4.0 (requires 4.0.3+)
$one_row_per_item = 'item' === $csv_generator->format_definition['row_type'];
}
return $one_row_per_item;
}
endif;

how do I load a wordpress custom table on a custom plugin menu page

To get the ball rolling I used this sitepoint template to put together a custom table to use in a plugin on an admin options page...So far so good, but the stock plugin is designed to live on its own page...the thing is I need it to live on an existing page...though when I tried to move the code over into the specific file but it doesn't seem to fire at all. There is more code of course, I'm just listing the relevant portion that creates the menu page in the admin menu....so I ask how do I add this into an existing page/action rather than create a new page?
...
class SP_Plugin {
// class instance
static $instance;
// customer WP_List_Table object
public $customers_obj;
// class constructor
public function __construct() {
add_filter( 'set-screen-option', [ __CLASS__, 'set_screen' ], 10, 3 );
add_action( 'admin_menu', [ $this, 'plugin_menu' ] );
}
public static function set_screen( $status, $option, $value ) {
return $value;
}
public function plugin_menu() {
$hook = add_menu_page(
'Sitepoint WP_List_Table Example',
'SP WP_List_Table',
'manage_options',
'wp_list_table_class',
[ $this, 'plugin_settings_page' ]
);
add_action( "load-$hook", [ $this, 'screen_option' ] );
}
/**
* Plugin settings page
*/
public function plugin_settings_page() {
?>
<div class="wrap">
<h2>WP_List_Table Class Example</h2>
<div id="poststuff">
<div id="post-body" class="metabox-holder columns-2">
<div id="post-body-content">
<div class="meta-box-sortables ui-sortable">
<form method="post">
<?php
$this->customers_obj->prepare_items();
$this->customers_obj->display(); ?>
</form>
</div>
</div>
</div>
<br class="clear">
</div>
</div>
<?php
}
/**
* Screen options
*/
public function screen_option() {
$option = 'per_page';
$args = [
'label' => 'Records',
'default' => 5,
'option' => 'customers_per_page'
];
add_screen_option( $option, $args );
$this->customers_obj = new Customers_List();
}
/** Singleton instance */
public static function get_instance() {
if ( ! isset( self::$instance ) ) {
self::$instance = new self();
}
return self::$instance;
}
}
add_action( 'plugins_loaded', function () {
SP_Plugin::get_instance();
} );
The code used to initialize the my admin page for the plugin:
add_action('load_admin_faucet_page', 'plugin_settings_page');
function faucet_admin_page_container(){
require '../../../myfolder/myfile.php';
}
From there I thought it was going to be as simple as:
add_action('load_admin_faucet_page', 'plugin_settings_page')
But well...no dice and here we are.
Edit 1: as requested, here is the entire affected code portion that I was trying to load on myfile.php (not everything, just the class to extend wp_list_table - I would keep the actions on the main plugin file as it is now)...now I'm simply trying to keep it on the main plugin file and have it load on that specific page.
function load_admin_faucet_page(){
add_menu_page('Faucet Settings', 'Faucet', 'manage_options', 'bitcoinfaucet-settings', 'faucet_admin_page_container', 'dashicons-welcome-view-site', 1);
}
function faucet_admin_page_container(){
require '../../myfile.php';
}
...
add_action('admin_menu', 'load_admin_faucet_page');
add_action('load_admin_faucet_page', 'plugin_settings_page')
//allow redirection, even if my theme starts to send output to the browser
add_action('init', 'do_output_buffer');
function do_output_buffer() {
ob_start();
}
...
if ( ! class_exists( 'WP_List_Table' ) ) {
require_once('/wp-admin/includes/class-wp-list-table.php' );
}
class Customers_List extends WP_List_Table {
/** Class constructor */
public function __construct() {
parent::__construct( array(
'singular' => __( 'Record', 'sp' ), //singular name of the listed records
'plural' => __( 'Records', 'sp' ), //plural name of the listed records
'ajax' => false //does this table support ajax?
) );
}
/**
* Retrieve customers data from the database
*
* #param int $per_page
* #param int $page_number
*
* #return mixed
*/
public static function get_customers( $per_page = 5, $page_number = 1 ) {
global $wpdb;
$sql = "SELECT * FROM table";
if ( ! empty( $_REQUEST['orderby'] ) ) {
$sql .= ' ORDER BY ' . esc_sql( $_REQUEST['orderby'] );
$sql .= ! empty( $_REQUEST['order'] ) ? ' ' . esc_sql( $_REQUEST['order'] ) : ' ASC';
}
$sql .= " LIMIT $per_page";
$sql .= ' OFFSET ' . ( $page_number - 1 ) * $per_page;
$result = $wpdb->get_results( $sql, 'ARRAY_A' );
return $result;
}
/**
* Associative array of columns
*
* #return array
*/
function get_columns() {
$columns = array(
'cb' => '<input type="checkbox" />',
'id' => __( 'Id Number', 'sp' ),
'date' => __( 'Date', 'sp' ),
'user' => __( 'Address', 'sp' ),
'amount' => __( 'Amount', 'sp'),
'message' => __( 'Message', 'sp' ),
);
return $columns;
}
/**
* Columns to make sortable.
*
* #return array
*/
public function get_sortable_columns() {
$sortable_columns = array(
'user' => array( 'user', false ),
'date' => array( 'date', false ),
'id' => array( 'id', true )
);
return $sortable_columns;
}
/**
* Returns the count of records in the database.
*
* #return null|string
*/
public static function record_count() {
global $wpdb;
$sql = "SELECT COUNT(*) FROM table";
return $wpdb->get_var( $sql );
}
/** Text displayed when no customer data is available */
public function no_items() {
_e( 'No customers avaliable.', 'sp' );
}
/**
* Render a column when no column specific method exist.
*
* #param array $item
* #param string $column_name
*
* #return mixed
*/
public function column_default( $item, $column_name ) {
switch ( $column_name ) {
case 'id':
case 'user':
case 'amount':
case 'date':
case 'message':
return $item[ $column_name ];
default:
return print_r( $item, true ); //Show the whole array for troubleshooting purposes
}
}
/**
* Render the bulk edit checkbox
*
* #param array $item
*
* #return string
*/
function column_cb( $item ) {
return sprintf(
'<input type="checkbox" name="my_CheckBoxes[]" value="%s" />', $item['id']
);
}
/**
* Method for name column
*
* #param array $item an array of DB data
*
* #return string
*/
function column_name( $item ) {
$delete_nonce = wp_create_nonce( 'sp_delete_customer' );
$reset_nonce = wp_create_nonce( 'sp_reset_payouts' );
$title = '<strong>' . $item['name'] . '</strong>';
$actions = [
'delete' => sprintf( 'Delete', esc_attr( $_REQUEST['page'] ), 'delete', absint( $item['id'] ), $delete_nonce ),
'reset' => sprintf( 'Reset', esc_attr( $_REQUEST['page'] ), 'reset', absint( $item['id'] ), $reset_nonce )
];
return $title . $this->row_actions( $actions );
}
/**
* Returns an associative array containing the bulk action
*
* #return array
*/
public function get_bulk_actions() {
$actions = array(
'bulk-delete' => 'Delete',
'bulk-reset' => 'Reset'
);
return $actions;
}
/**
* Handles data query and filter, sorting, and pagination.
*/
public function prepare_items() {
$this->_column_headers = $this->get_column_info();
/** Process bulk action */
$this->process_bulk_action();
$per_page = $this->get_items_per_page( 'customers_per_page', 5 );
$current_page = $this->get_pagenum();
$total_items = self::record_count();
$this->set_pagination_args( [
'total_items' => $total_items, //WE have to calculate the total number of items
'per_page' => $per_page //WE have to determine how many items to show on a page
] );
$this->items = self::get_customers( $per_page, $current_page );
}
/**
* Delete a customer record.
*
* #param int $id customer ID
*/
public static function delete_customer( $id ) {
global $wpdb;
$wpdb->delete(
"table",
array( 'id' => $id ),
array('%d' )
);
}
/**
* Reset Message To Null.
*
* #param int $id customer ID
*/
public static function reset_record( $id ) {
global $wpdb;
$wpdb->update(
"table",
array('message' => NULL),
array('id' => $id )
);
}
public function process_bulk_action() {
//Detect when a bulk action is being triggered...
if ( 'delete' === $this->current_action()){
// In our file that handles the request, verify the nonce.
$nonce = esc_attr( $_REQUEST['_wpnonce'] );
if ( ! wp_verify_nonce( $nonce, 'sp_delete_customer' ) ) {
die( 'Go get a life script kiddies' );
}
else {
self::delete_customer( absint( $_GET['customer'] ) );
wp_redirect( esc_url( add_query_arg() ) );
exit;
}
}
//Detect when a Reset is being triggered...
if ( 'reset' === $this->current_action()){
// In our file that handles the request, verify the nonce.
$nonce = esc_attr( $_REQUEST['_wpnonce'] );
if ( ! wp_verify_nonce( $nonce, 'sp_reset_payouts' ) ) {
die( 'Go get a life script kiddies' );
}
else {
self::reset_record( absint ( $_GET['customer']));
wp_redirect( esc_url( add_query_arg() ) );
exit;
}
}
// // If the delete bulk action is triggered
if ( ( isset( $_POST['action'] ) && $_POST['action'] == 'bulk-delete' )
|| ( isset( $_POST['action2'] ) && $_POST['action2'] == 'bulk-delete' )
) {
$delete_ids = esc_sql( $_POST['my_CheckBoxes'] );
// loop over the array of record IDs and delete them
foreach ( $delete_ids as $id ) {
self::delete_customer( $id );
}
wp_redirect( esc_url( add_query_arg() ) );
exit;
}
//If the delete bulk action is triggered
if ( ( isset( $_POST['action'] ) && $_POST['action'] == 'bulk-reset' )
|| ( isset( $_POST['action2'] ) && $_POST['action2'] == 'bulk-reset' )
) {
$reset_ids = esc_sql( $_POST['my_CheckBoxes'] );
// loop over the array of record IDs and delete them
foreach ( $reset_ids as $id ) {
self::reset_record( $id );
}
wp_redirect( esc_url( add_query_arg() ) );
exit;
}
}
}
class SP_Plugin {
// class instance
static $instance;
// customer WP_List_Table object
public $customers_obj;
// class constructor
public function __construct() {
add_filter( 'set-screen-option', [ __CLASS__, 'set_screen' ], 10, 3 );
add_action( 'admin_menu', [ $this, 'plugin_menu' ] );
}
public static function set_screen( $status, $option, $value ) {
return $value;
}
public function plugin_menu() {
$hook = add_menu_page(
'Sitepoint WP_List_Table Example',
'SP WP_List_Table',
'manage_options',
'wp_list_table_class',
[ $this, 'plugin_settings_page' ]
);
add_action( "load-$hook", [ $this, 'screen_option' ] );
}
/**
* Plugin settings page
*/
public function plugin_settings_page() {
?>
<div class="wrap">
<h2>WP_List_Table Class Example</h2>
<div id="poststuff">
<div id="post-body" class="metabox-holder columns-2">
<div id="post-body-content">
<div class="meta-box-sortables ui-sortable">
<form method="post">
<?php
$this->customers_obj->prepare_items();
$this->customers_obj->display(); ?>
</form>
</div>
</div>
</div>
<br class="clear">
</div>
</div>
<?php
}
/**
* Screen options
*/
public function screen_option() {
$option = 'per_page';
$args = [
'label' => 'Records',
'default' => 5,
'option' => 'customers_per_page'
];
add_screen_option( $option, $args );
$this->customers_obj = new Customers_List();
}
/** Singleton instance */
public static function get_instance() {
if ( ! isset( self::$instance ) ) {
self::$instance = new self();
}
return self::$instance;
}
}
add_action( 'plugins_loaded', function () {
SP_Plugin::get_instance();
} );

Categories