Skip to content

Instantly share code, notes, and snippets.

@JiveDig
Last active April 23, 2024 14:24
Show Gist options
  • Save JiveDig/0d3658676127f30a098859228fc5d8eb to your computer and use it in GitHub Desktop.
Save JiveDig/0d3658676127f30a098859228fc5d8eb to your computer and use it in GitHub Desktop.
A PHP class to create new WooCommerce account tabs.

A PHP class to create new WooCommerce account tabs.

  1. Register a new tab on the My Account page.
  2. Optionally position the tab before or after an existing tab, by endpoint name. Default is last item before logout, if it exists, otherwise it's added as last item.
  3. Optionally add a condition to show or hide the tab. If the condition is not met, the endpoint url will redirect to the main account page.

Make sure you flush permalinks after using or modifying this code!

Understanding the args

endpoint: (required) The endpoint slug. This is used to build the url... /my-account/{endpoint}.
label: (required) The menu item name.
content: (optional) A string, or a callable function to output the page content.
condition: (optional) A boolean value, or callable function that returns boolean to determine of the menu item/content should display.
position: (optional) The position for the menu item, before or after a specific item (by endpoint). Default is the last item before the logout item if it exists, otherwise the last item.\

How to use

/**
 * Registers a new WooCommerce tab on the My Account page.
 * 
 * @return void
 */
add_action( 'woocommerce_init', function() {
	// Bail if class does not exist.
	if ( ! class_exists( 'Mai_WooCommerce_Account_Tab' ) ) {
		return;
	}
	
	new Mai_WooCommerce_Account_Tab(
		[
			'endpoint'  => 'new-endpoint',
			'label'     => __( 'Testing', 'textdomain' ),
			'content'   => '',
			'condition' => current_user_can( 'edit_posts' ),
			'position'  => [
				'after' => 'edit-account',
			],
		]
	);
});

If you don't want to use the content parameter, you can use the WooCommerce endpoint action hook directly for your new endpoint.

/**
 * Add content to the new tab.
 * 
 * @return void
 */
add_action( 'woocommerce_account_new-endpoint_endpoint', function() {
	// Do your thing.
});

Use the helper is_tab() static method in other code.

/**
 * Enqueue scripts and styles for the new tab.
 *
 * @return void
 */
add_action( 'wp_enqueue_scripts', function() {
	if ( ! class_exists( 'Mai_WooCommerce_Account_Tab' ) ) {
		return;
	}

	if ( ! Mai_WooCommerce_Account_Tab::is_tab( 'edit-profile' ) ) {
		return;
	}

	// Enqueue styles.
	wp_enqueue_style( 'my-style-handle', get_stylesheet_directory_uri() . '/assets/css/filename.css', [], '1.0.0' );

	// Enqueue scripts.
	wp_enqueue_script( 'my-script-handle', get_stylesheet_directory_uri() . '/assets/js/filename.js', [], '1.0.0' );
});
<?php
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) exit;
if ( ! class_exists( 'Mai_WooCommerce_Account_Tab' ) ):
/**
* A class to create new WooCommerce account tabs.
*
* @version 0.1.0
*
* @link https://gist.github.com/JiveDig/0d3658676127f30a098859228fc5d8eb
*
* 1. Register a new tab on the My Account page.
* 2. Optionally position the tab before or after an existing tab by endpoint name.
* Default is last item before logout, if it exists, otherwise it's added as last item.
* 3. Optionally add a condition to show or hide the tab.
* 4. Use `Mai_WooCommerce_Account_Tab::is_tab( $endpoint )` to check if the current page is the tab.
* This is useful for enqueueing scripts or styles only on the tab page.
*
* Example usage:
* add_action( 'woocommerce_init', function() {
* new Mai_WooCommerce_Account_Tab(
* [
* 'endpoint' => 'new-endpoint', // Required.
* 'label' => __( 'New Endpoint', 'textdomain' ), // Required.
* 'content' => '', // Optional. String or callable.
* 'condition' => current_user_can( 'edit_posts' ), // Optional. Boolean or callable.
* 'position' => [ // Optional.
* 'after' => 'edit-account',
* ],
* ]
* );
* });
*
* // Helper method:
* `Mai_WooCommerce_Account_Tab::is_tab( 'new-endpoint' )`
*
* @return void
*/
class Mai_WooCommerce_Account_Tab {
/**
* The args.
*
* @var array
*/
protected $args;
/**
* Gets is started.
*
* @since 0.1.0
*/
function __construct( $args ) {
// Set the data.
$this->args = $this->sanitize( $args );
// Bail if no args.
if ( ! $this->args ) {
return;
}
// Hooks.
$this->hooks();
}
/**
* Checks if the current page is the account page endpoint.
*
* @since 0.1.0
*
* @return bool
*/
static function is_tab( $endpoint ) {
static $cache = [];
// Maybe return cached value.
if ( isset( $cache[ $endpoint ] ) ) {
return $cache;
}
// Set cache.
$cache[ $endpoint ] = class_exists( 'WooCommerce' ) && is_account_page() && is_wc_endpoint_url( $endpoint );
return $cache[ $endpoint ];
}
/**
* Runs hooks.
*
* @since 0.1.0
*/
function hooks() {
add_filter( 'woocommerce_get_query_vars', [ $this, 'add_query_vars' ] );
add_filter( 'woocommerce_account_menu_items', [ $this, 'add_menu_items' ] );
add_action( 'template_redirect', [ $this, 'do_redirect' ] );
// Bail if no content.
if ( ! $this->args['content'] ) {
return;
}
add_action( "woocommerce_account_{$this->args['endpoint']}_endpoint", [ $this, 'do_content' ] );
}
/**
* Adds WooCommerce query vars.
* Woo uses these to add the endpoint.
*
* @since 0.1.0
*
* @param array $vars The existing query vars.
*
* @return array
*/
function add_query_vars( $vars ) {
$vars[ $this->args['endpoint'] ] = $this->args['endpoint'];
return $vars;
}
/**
* Adds account menu item.
*
* @since 0.1.0
*
* @param array $items The existing items.
*
* @return array
*/
function add_menu_items( $items ) {
// Bail if condition is not met.
if ( ! $this->condition_met() ) {
return $items;
}
// Get values.
$location = array_key_first( $this->args['position'] );
$item = reset( $this->args['position'] );
// Bail if no location or item.
if ( ! $location || ! $item ) {
return $items;
}
// If the item exists.
if ( isset( $items[ $item ] ) ) {
// Add the new item.
switch ( $location ) {
case 'before':
$items = $this->insert_before( $items, $item, [ $this->args['endpoint'] => $this->args['label'] ] );
break;
case 'after':
$items = $this->insert_after( $items, $item, [ $this->args['endpoint'] => $this->args['label'] ] );
break;
}
}
// Doesn't exist, check for logout.
elseif ( isset( $items['customer-logout'] ) ) {
$items = $this->insert_before( $items, 'customer-logout', [ $this->args['endpoint'] => $this->args['label'] ] );
}
// Fallback to the last item.
else {
$items[] = [ $this->args['endpoint'] => $this->args['label'] ];
}
return $items;
}
/**
* Redirect endpoint if condition is not met.
*
* @since 0.1.0
*
* @return void
*/
function do_redirect() {
// Bail if not this endpoint.
if ( ! self::is_tab( $this->args['endpoint'] ) ) {
return;
}
// Bail if conditions are met.
if ( $this->condition_met() ) {
return;
}
// Safely redirect to the main account page.
wp_safe_redirect( wc_get_page_permalink( 'myaccount' ) );
exit;
}
/**
* Adds content to the tab.
*
* @since 0.1.0
*
* @return string
*/
function do_content() {
// Bail if condition is not met.
if ( ! $this->condition_met() ) {
return;
}
// If callable, run it.
if ( is_callable( $this->args['content'] ) ) {
call_user_func( $this->args['content'] );
return;
}
// Output content.
echo $this->args['content'];
}
/**
* Checks if the condition is met.
*
* @since 0.1.0
*
* @return bool
*/
function condition_met() {
static $cache = null;
// Maybe return cached value.
if ( ! is_null( $cache ) ) {
return $cache;
}
// Bail if callable condition is not met.
if ( is_callable( $this->args['condition'] ) && (bool) ! call_user_func( $this->args['condition'] ) ) {
$cache = false;
return $cache;
}
// Bail if condition and it's not met.
if ( '' !== $this->args['condition'] && ! $this->args['condition'] ) {
$cache = false;
return $cache;
}
// Set cache.
$cache = true;
return $cache;
}
/**
* Insert a value or key/value pair before a specific key in an array.
* If key doesn't exist, value is appended to the end of the array.
*
* @since 0.1.0
*
* @param array $array
* @param string $key
* @param array $new
*
* @return array
*/
function insert_before( array $array, $key, array $new ) {
$keys = array_keys( $array );
$index = array_search( $key, $keys );
$pos = $index !== false ? $index : count( $array ); // If key doesn't exist, insert at the end.
return array_merge( array_slice( $array, 0, $pos ), $new, array_slice( $array, $pos ) );
}
/**
* Insert a value or key/value pair after a specific key in an array.
* If key doesn't exist, value is appended to the end of the array.
*
* @since 0.1.0
*
* @param array $array
* @param string $key
* @param array $new
*
* @return array
*/
function insert_after( array $array, $key, array $new ) {
$keys = array_keys( $array );
$index = array_search( $key, $keys );
$pos = false === $index ? count( $array ) : $index + 1;
return array_merge( array_slice( $array, 0, $pos ), $new, array_slice( $array, $pos ) );
}
/**
* Sanitizes the args.
*
* @since 0.1.0
*
* @param array $args The args.
*
* @return array
*/
function sanitize( $args ) {
$sanitized = [];
$args = wp_parse_args( $args, [
'endpoint' => '',
'label' => '',
'content' => '',
'condition' => '',
'position' => [],
] );
// Bail if no endpoint or label.
if ( ! $args['endpoint'] || ! $args['label'] ) {
return $sanitized;
}
// Sanitize.
foreach ( $args as $key => $value ) {
switch ( $key ) {
case 'endpoint':
$sanitized[ $key ] = sanitize_title( $value );
break;
case 'label':
$sanitized[ $key ] = sanitize_text_field( $value );
break;
default:
$sanitized[ $key ] = $value;
break;
}
}
return $sanitized;
}
}
endif;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment