wordpress
Aug 30 2019

Simplify WordPress Cron with a custom class

If you have ever added your own WordPress Cron task you might have noticed how clunky and error-prone the process is. This custom class takes the pain out of creating WordPress Cron jobs.

In order to simplify the process of writing a WordPress Cron, we need to add a new class to our plugin. You can find the class below. You can also rename the class to whatever suits your needs. Just be sure to update the calls to the class in the examples below if you do rename the class.

class MyWordPressCron {

	/** @var string $hook A name for this cron. */
	public $hook;

	/** @var int $interval How often to run this cron in seconds. */
	public $interval;

	/** @var Closure|string|null $callback Optional. Anonymous function, function name or null to override with your own handle() method. */
	public $callback;

	/** @var array $args Optional. An array of arguments to pass into the callback. */
	public $args;

	/** @var string $recurrence How often the event should subsequently recur. See wp_get_schedules(). */
	public $recurrence;

	private function __construct( $hook, $interval, $callback = null, $args = [] ) {

		$this->hook     = trim( $hook );
		$this->interval = absint( $interval );
		$this->callback = $callback;
		$this->args     = $args;
		$this->set_recurrence();

		add_action( 'wp', [ $this, 'schedule_event' ] );
		add_filter( 'cron_schedules', [ $this, 'add_schedule' ] );
		add_action( $this->hook, [ $this, 'handle' ] );
	}

	public static function init( $hook, $interval, $callback = null, $args = [] ) {
		return new static( $hook, $interval, $callback, $args );
	}

	public function handle() {
		if ( is_callable( $this->callback ) ) {
			call_user_func_array( $this->callback, $this->args );
		}
	}

	public function schedule_event() {
		if ( ! wp_next_scheduled( $this->hook, $this->args ) ) {
			wp_schedule_event( time(), $this->recurrence, $this->hook, $this->args );
		}
	}

	public function add_schedule( $schedules ) {

		if ( in_array( $this->recurrence, $this->default_wp_recurrences() ) ) {
			return $schedules;
		}

		$schedules[ $this->recurrence ] = [
			'interval' => $this->interval,
			'display'  => __( 'Every ' . $this->interval . ' seconds' ),
		];

		return $schedules;
	}

	private function set_recurrence() {
		foreach ( $this->default_wp_schedules() as $recurrence => $schedule ) {
			if ( $this->interval == absint( $schedule['interval'] ) ) {
				$this->recurrence = $recurrence;

				return;
			}
		}

		$this->recurrence = 'every_' . absint( $this->interval ) . '_seconds';
	}

	private function default_wp_schedules() {
		return array_filter( wp_get_schedules(), function ( $schedule ) {
			return in_array( $schedule, $this->default_wp_recurrences() );
		}, ARRAY_FILTER_USE_KEY );
	}

	private function default_wp_recurrences() {
		return [ 'hourly', 'twicedaily', 'daily' ];
	}
}

OK, now that we have added our custom class and require'd it, let's look at some examples of how to use it.

The class supports 3 different methods of triggering the cron jobs:

  1. Closure (anonymous function)
  2. Regular function
  3. Custom Class

Which one you use depends on how the code in your plugin is structured as well as the complexity of the logic required to execute the cron.

All methods are wrapped in a mycode_initialize_cron_jobs() function triggered by the wp_loaded hook. You should put all of your cron jobs in this hook.

Below we have 3 examples (one for each method). Each example performs the following actions:

  1. Every hour, query all users which have not been sent a welcome email
  2. For each user who has not received a welcome email

    1. Send them a welcome email
    2. Update usermeta table with the date the user was sent the email

Closure Example

This is the simplest way to use the class. Here we provide a hook name, the interval it should run (HOUR_IN_SECONDS) and the closure (anonymous) function that gets executed every hour.

function mycode_initialize_cron_jobs() {

	MyWordPressCron::init( 'mycode_new_user_welcome_email', HOUR_IN_SECONDS, function () {

		$meta_key = 'welcome_email_sent';

		$user_query = new WP_User_Query( [
			'meta_query' => [
				[
					'key'     => $meta_key,
					'compare' => 'NOT EXISTS'
				]
			]
		] );

		/** @var WP_User[] $users */
		$users = $user_query->get_results();

		foreach ( $users as $user ) {
			wp_mail( $user->user_email, 'Greetings!', 'Welcome to our great community.' );
			update_user_meta( $user->ID, $meta_key, date_i18n( 'Y-m-d H:i:s' ) );
		}
	} );
}

add_action( 'wp_loaded', 'mycode_initialize_cron_jobs' );

Regular Function Example

This method is similar to the Closure method above except all of the logic is moved from the closure to its own function which helps to keep your code a little cleaner. You might put the mycode_send_new_user_welcome_email() functions in some functions.php file that gets loaded on every page.

function mycode_initialize_cron_jobs() {
	MyWordPressCron::init(
		'mycode_new_user_welcome_email',
		HOUR_IN_SECONDS,
		'mycode_send_new_user_welcome_email'
	);
}

add_action( 'wp_loaded', 'mycode_initialize_cron_jobs' );



function mycode_send_new_user_welcome_email() {

	$meta_key = 'welcome_email_sent';

	$user_query = new WP_User_Query( [
		'meta_query' => [
			[
				'key'     => $meta_key,
				'compare' => 'NOT EXISTS'
			]
		]
	] );

	/** @var WP_User[] $users */
	$users = $user_query->get_results();

	foreach ( $users as $user ) {
		wp_mail( $user->user_email, 'Greetings!', 'Welcome to our great community.' );
		update_user_meta( $user->ID, $meta_key, date_i18n( 'Y-m-d H:i:s' ) );
	}
}

Custom Class Example

This is my preferred method if my plugin is structured in an Object Oriented way or the logic of the code is complex. It does require creating an additional class that must be require'd but in a lot of cases, it's worth it for readability and compartmentalization.

First we initialize the cron job:

function mycode_initialize_cron_jobs() {
	WelcomeEmailWordPressCron::init( 'mycode_new_user_welcome_email', HOUR_IN_SECONDS );
}

add_action( 'wp_loaded', 'mycode_initialize_cron_jobs' );

Then we have a new class named WelcomeEmailWordPressCron which will do all of the work.

class WelcomeEmailWordPressCron extends MyWordPressCron {

	public $meta_key = 'welcome_email_sent';

	public function handle() {
		array_map( [ $this, 'welcome_user' ], $this->unwelcomed_users() );
	}

	public function welcome_user( WP_User $user ) {
		$this->email( $user );
		$this->update( $user );
	}

	public function email( WP_User $user ) {
		wp_mail( $user->user_email, 'Greetings!', 'Welcome to our great community.' );
	}

	public function update( WP_User $user ) {
		update_user_meta( $user->ID, $this->meta_key, date_i18n( 'Y-m-d H:i:s' ) );
	}

	/** @return WP_User[] */
	public function unwelcomed_users() {

		$user_query = new WP_User_Query( [
			'meta_query' => [
				[
					'key'     => $this->meta_key,
					'compare' => 'NOT EXISTS'
				]
			]
		] );

		return $user_query->get_results();
	}
}

The handle() method is the callback method which the parent MyWordPressCron class will call. Therefore, if you are extending the MyWordPressCron class with your own class, you MUST HAVE A handle() METHOD.

That's the basic functionality of the MyWordPressCron class.

One thing to note is that you can also pass in arguements to your cron callback. Here are a few quick examples of how to pass in those arguments and access them from within your closure, function or handle() method. Here we have the 3 methods all in the single wp_loaded action and anything they require below.

function mycode_initialize_cron_jobs() {
	
  
	// Closure
	MyWordPressCron::init( 'mycode_do_something', DAY_IN_SECONDS, function ( $name, $age ) {
		// Name and age are available via $name and $age variables.
		// Do something with those values...
	}, [ 'name' => 'Sally', 'age' => 31 ] );

  
	// Regular Function
	MyWordPressCron::init(
		'mycode_do_something_else',
		DAY_IN_SECONDS,
		'mycode_handle_user_name_age_gender',
		[ 'name' => 'Frank', 'age' => 17, 'gender' => 'male' ]
	);

  
	// Custom Class
	MyCustomWordPressCron::init(
		'mycode_do_something_amazing',
		DAY_IN_SECONDS,
		null,
		[ 'name' => 'Tim', 'job' => 'Developer' ]
	);
  
}

add_action( 'wp_loaded', 'mycode_initialize_cron_jobs' );


// Regular Function
function mycode_handle_user_name_age_gender( $name, $age, $gender ) {
	// Name, age and gender are available via $name, $age and $gender variables.
	// Do something with those values...
}


// Custom Class
class MyCustomWordPressCron extends MyWordPressCron {
	public function handle() {
		// Name and job are available via $this->args['name'] and $this->args['job'].
		// Do something with those values...
	}
}

That's it! Feel free to use the MyWordPressCron class in your own projects. I believe it will simplify the process of setting up WordPress cron jobs in a more intuitive and simple approach.