,

A quick guide to cronjobs in WordPress

Whenever you want to automate some recurring tasks cronjobs are one of if not the first thing we can think of. In this article, we’ll go over few examples of how to use them.

“WP-Cron” vs “Server Cron”

The first important thing that you need to know is that WordPress by default uses “WP-Cron” which basically runs scheduled tasks (events) only when a page is loaded. That means if you schedule something to run at 3 AM the task might be actually run at 5 AM since there was no traffic during the night.
If this is an important thing to you need to look into switching to a “Server Cron” where the server will periodically make WordPress to go over scheduled tasks. The details may differ depending on your hosting provider (eg. Kinsta by default triggers cron every 15min but you can ask support to change it to every 5min), the general idea is described in this WordPress Handbook page.

Scheduling custom tasks (events)

Here are few of WP functions that you need to know to be able to schedule your own event (task):

During development, plugins like WP Crontrol might be useful for debugging and testing the code.

Adding a new scheduled task starts with running the first function:

wp_schedule_event(
  $startTimestamp, //here you can set the time when the event should start running
  'weekly', // the recurrance of how often the event should be triggered after start date
  'hook_that_should_be_triggered'
);Code language: PHP (php)

Note that you can just use time() as the first argument if you don’t care about the details, But if you need, for example, export some data during night hours then specifying the timestamp at a given hour of next/closest run day will make WordPress wait until the date in timestamp and then at that time start your event.

You can easily get the timestamp on pages like epochconverter.com

After scheduling the event you need to “tell” WordPress what function should actually be run:
add_action('hook_that_should_be_triggered', 'foo');
function foo() {
  error_log('Hello World at ' . date('d.m.Y H:i:s'));
}Code language: JavaScript (javascript)

More advanced example

Let’s say you need to run a task every day of the week but as one of the requirements, you have to “know” on what day the current event is being run.

One of the neat things that we can leverage here is that you can add your own data as arguments to pass to your own hook.

if (false === wp_next_scheduled("coditive/example/cron/monday", [[$data]])) {
  wp_schedule_event($timestamp, "weekly", "coditive/example/cron/monday", [[$data]]);
}Code language: PHP (php)

As you can see we’ve also checked if the even is not already scheduled – if yes then we don’t want to do it again. What is also very important here is you have to pass the very same data towp_next_scheduled(...) as you’ve passed to wp_schedule_event(...) otherwise, you’ll run into problems because WordPress won’t recognize correctly your event. The same goes for unscheduling eventwp_unschedule_event($nextRunTimestamp, "coditive/example/cron/monday", [[$data]]);

What you may often overlook (and cause yourself a headache) is that you don’t want to run some events in non-production environments – eg. you don’t want to export your local (test) data somewhere or don’t want to send a bunch of emails from staging server ?
To still be sure that code is running well in such cases I’m implementing something like is_production_env() that will return true on production or false otherwise. This way I can schedule the same events but like 100 years from now so they will still show up in scheduled events but won’t run anytime soon.

Adding a way to remove all scheduled events is also a good idea since this way you can remove all of them if your module, theme or plugin will be deactivated – in edge case scenarios you could run into bugs when reactivating.

Note that in the constructor we’re dynamically setting start timestamps – this will let you omit problems during turning off and back on your code. WordPress works in a way that if the start timestamp is in the past it will run the event right away (since it is actually delayed) and then will proceed with the set schedule. Because of that, you may end up executing an event at an “unwanted” time.

Below you will find the full code of an example class that would handle the events as described above.

class EventsExample
{
  private const WEEK_DAYS = [
    1 => 'monday',
    2 => 'tuesday',
    3 => 'wednesday',
    4 => 'thursday',
    5 => 'friday',
    6 => 'saturday',
    7 => 'sunday',
  ];

  private const START_TIMESTAMP_DEV_ENV = 5277744001; // just so the event is somewhere there but won't run anytime soon

  private array $start_timestamps = [
    1 => 1635728400, // Monday, 1 November 2021 01:00:00
    2 => 1635814800, // +24h from date above
    3 => 1635901200,
    4 => 1635987600,
    5 => 1636074000,
    6 => 1636160400,
    7 => 1636246800,
  ];

  public function __construct()
  {
    $this->start_timestamps = [
      1 => (new \DateTime('next monday'))->setTime(0, 1, 0, 0)->getTimestamp(),
      2 => (new \DateTime('next tuesday'))->setTime(0, 1, 0, 0)->getTimestamp(),
      3 => (new \DateTime('next wednesday'))->setTime(0, 1, 0, 0)->getTimestamp(),
      4 => (new \DateTime('next thursday'))->setTime(0, 1, 0, 0)->getTimestamp(),
      5 => (new \DateTime('next friday'))->setTime(0, 1, 0, 0)->getTimestamp(),
      6 => (new \DateTime('next saturday'))->setTime(0, 1, 0, 0)->getTimestamp(),
      7 => (new \DateTime('next sunday'))->setTime(0, 1, 0, 0)->getTimestamp(),
    ];
    $this->scheduleEvents();
    $this->addEventHooks();
  }

  private function addEventHooks(): void
  {
    foreach ($this::WEEK_DAYS as $<meta charset="utf-8">dayNumber => $dayName) {
      add_action("coditive/example/cron/$dayName", [$this, 'runEvent'], 10, 1);
    }
  }

  public function runEvent(array $args = []): void
  {
    if (empty($args) || ! is_int($args[0])) {
      error_log('Coditive: ERROR Can\'t run event without required data!');
      return;
    }

    switch ($args[0]) {
      case 1:
      case 2:
      case 3:
      case 4:
      case 5:
      case 6:
      case 7:
            $this->exportData($args[0]);
            break;

      default:
            error_log('Coditive: ERROR Can\'t run event - unknown argument passed: ' . print_r($args[0], true));
            break;
    }
  }

  private function exportData(int $day): void
  {
    error_log("Coditive: Exporting data on {$this::WEEK_DAYS[$day]}!");
  }

  public function scheduleEvents(): void
  {
    foreach ($this::WEEK_DAYS as $dayNumber => $dayName) {
      if (! is_production_env()) { // custom check for production env
        // maybe change schedule if NOT production server is detected
        if (
            $this::START_TIMESTAMP_DEV_ENV + $<meta charset="utf-8">dayNumber * DAY_IN_SECONDS === wp_next_scheduled("coditive/example/cron/$dayName", [[$<meta charset="utf-8">dayNumber]])
            || 1 >= wp_next_scheduled("<meta charset="utf-8">coditive/example/cron/$dayName", [[$<meta charset="utf-8">dayNumber]]) // allow WP Crontrol to "run now" the event
        ) {
          // no need rescheduling if already changed
          continue;
        }
        wp_unschedule_event(
            wp_next_scheduled("coditive/example/cron/$dayName", [[$<meta charset="utf-8">dayNumber]]),
            "coditive/example/cron/$dayName",
            [[$<meta charset="utf-8">dayNumber]],
        );
        wp_schedule_event(
            $this::START_TIMESTAMP_DEV_ENV + $<meta charset="utf-8">dayNumber * DAY_IN_SECONDS,
            "weekly",
            "coditive/example/cron/$dayName",
            [[$<meta charset="utf-8">dayNumber]]
        );
      } else {
        // schedule events normally
        if (false === wp_next_scheduled("coditive/example/cron/$dayName", [[$<meta charset="utf-8">dayNumber]])) {
          wp_schedule_event($this->start_timestamps[$<meta charset="utf-8">dayNumber], "weekly", "coditive/example/cron/$dayName", [[$<meta charset="utf-8">dayNumber]]);
        }
      }
    }
  }

  public function unscheduleEvents(): void
  { // maybe run this on switch_theme action or add it to plugin deactivation hook?
    foreach ($this::WEEK_DAYS as $<meta charset="utf-8">dayNumber => $dayName) {
      wp_unschedule_event(
          wp_next_scheduled("coditive/example/cron/$dayName", [[$<meta charset="utf-8">dayNumber]]),
          "coditive/example/cron/$dayName",
          [[$<meta charset="utf-8">dayNumber]]
      );
    }
  }
}Code language: PHP (php)

Summary

While scheduling events is easy to start it is also easy to run into problems in more advanced scenarios – “the devil is in the details“. Standard WP-Cron may be enough for some cases but if you want to build reliable features it is good to set up server cron. We went over a basic setup and also shown the example code of a more advanced one. If you’ll be careful you can build advanced features without too much hassle.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Calling all proficient WordPress gurus and agencies! Are you in search of a way to optimize your time and resources while still providing your clients with exceptional websites? In that case, Astratic Theme is the definitive solution for you.

Topics

AJAX astratic Attribute inheritance backup blocks bounce rate code smell Coditive Contact Form cronjobs custom blocks database formatting rules GIT Git Flow GitHub Flow GitLab Flow JavScript loading speed MAMP message broker nuxt nuxt3 overlays patterns PHP PHP rules plugin Popups Post Draft Preview RabbitMQ schedule Simple Customizations for WooCommerce Simple Floating Contact Form software development Vue.js web development WooCommerce WordPress WordPress CLI WordPress Gutenberg Wordpress plugins WordPress updates WP-CLI wp-cron