A client recently asked to be able to put time-sensitive “flags” (badges) on products for showcasing that products were “new”, “web-only” or “exclusive” to them. These flags would only exist for particular periods as products wouldn’t be new or exclusive forever.

As a solution, I used Advanced Custom Fields to create a section on product pages to add flags that had 6 fields:

  1. The text that appears in the flag
  2. Background colour of the circle
  3. Colour of the text inside the circle
  4. Start date & time – when should the flag start appearing?
  5. End date & time – when should the flag stop appearing?
  6. The timezone of the start/end times you’ve chosen
  7. Add another flag

We only need options 1, 4, 5 and 6 – the rest are to alter the styling of the badge. At the bottom of this post is the full coded solution used but I’d like to talk about pieces of it in detail.

Basic Overview

The basic flow of execution is:

  1. Intercept post saving
  2. For every “flag”, adjust for timezone and check if the period is happening now or sometime in the future
  3. If it’s right now, add the product to the flag category and schedule a task to remove it later
  4. If it’s in the future, schedule a task to add it and schedule a task to remove it

Then, later on, the tasks will be performed by WordPress’ in-built CRON system as defined by the hooks we created in our constructor.

Task Schedule

Within the scheduler method, I have the following two lines:

// If a CRON job exists, we remove it to ensure only the current values are considered
wp_clear_scheduled_hook('clear_flag_category', [$postID, $flagCategory]);
wp_clear_scheduled_hook('add_flag_category', [$postID, $flagCategory]);

Which exist to make sure only the tasks we set right now are executed later. If these didn’t exist, the user might update the flag times several times and all of the events associated with those times would continue to run regardless of the current values. For example, let’s say we set a start date a month from now and an end date for two months away but later updated it to end next week instead. Next week, the product would be removed from the category like we expected but next month, it would be added again which we don’t want. To fix this, we clear the current schedule and add our events as if it were the first time.

Also, I’d just like to take a moment to say timezones are a bitch and thank-god WordPress uses base GMT to schedule its events.

The Code

To use the following code, create a file in your theme or plugin called something like “TimedProductCategories.php” and simply require/include that file into your project. (require_once “TimedProductCategories.php”)

<?php

/**
 * Adds or removes a product from a particular category
 * based on the time-sensitive flags field from the product.
 *
 * @author  Evan Smith <[email protected]>
 */
class TimedProductCategories
{
    const PARENT_CATEGORY = "flags";
    const TAXONOMY = "product_cat";
    const FLAGS_FIELD = 'time-sensitive_flags';

    private static $instance;

    public static function register()
    {
        if (self::$instance == null) {
            self::$instance = new TimedProductCategories();
        }
    }

    public function __construct()
    {
        add_action('acf/save_post', [$this, 'scheduleFlagCategories'], 20);
        add_action('clear_flag_category', [$this, 'removeFlagCategory'], 0, 2);
        add_action('add_flag_category', [$this, 'createAndAddFlagCategory'], 0, 2);
    }

    /**
     * Setup WP CRON events to add/remove products from categories
     *
     * @param     int    $postID
     */
    public function scheduleFlagCategories($postID)
    {
        if (get_post_type($postID) == 'product') {
            foreach ( get_field( self::FLAGS_FIELD, $postID ) as $flag ) {
                // Save current timezone
                $current_timezone = date_default_timezone_get();
                date_default_timezone_set($flag['timezone']);

                // If a CRON job exists, we remove it to ensure only the current values are considered
                wp_clear_scheduled_hook('clear_flag_category', [$postID, $flagCategory]);
                wp_clear_scheduled_hook('add_flag_category', [$postID, $flagCategory]);

                // Reset timezone
                date_default_timezone_set($current_timezone);

                // The time adjustment between timezones
                $dtz = new DateTimeZone($flag['timezone']);
                $current_time_in_zone = new DateTime('now', $dtz);
                $timeDifference = $dtz->getOffset($current_time_in_zone);

                // Adjust the start/end times
                $flagEnd = strtotime($flag['end']) - $timeDifference;
                $flagStart = strtotime($flag['start']) - $timeDifference;

                if ($isAfterStart && $isBeforeEnd) {
                    // If the flag is currently active, add the category
                    $this->createAndAddFlagCategory($postID, $flag['flag_text']);

                    // Remove product from the category at end time
                    wp_schedule_single_event($flagEnd, 'clear_flag_category', [$postID, $flagCategory]);
                } else if ($isBeforeEnd && !$isAfterStart) {
                    // If we're before the end but the timeslot hasn't started yet

                    // Remove product from the category at end time
                    wp_schedule_single_event($flagEnd, 'clear_flag_category', [$postID, $flagCategory]);
                    // Add product to the category at start time
                    wp_schedule_single_event($flagStart, 'add_flag_category', [$postID, $flagCategory]);
                }
            }
        }
    }

    /**
     * Remove a product from a particular flag category
     *
     * @param    int        $postID
     * @param    string        $flagCategory
     */
    public function removeFlagCategory($postID, $flagCategory)
    {
        wp_remove_object_terms($postID, $flagCategory, self::TAXONOMY);
    }

    /**
     * Add product to particular flag category, create category if it
     * doesn't exist
     *
     * @param     int        $postID
     * @param     string    $text
     */
    public function createAndAddFlagCategory($postID, $text)
    {
        $flagsCat = get_term_by('slug', self::PARENT_CATEGORY, self::TAXONOMY);

        if ($flagsCat === false) {
            // If the parent category doesn't exist, create category at top-level
            $allFlagsCategoryID = 0;
        } else {
            // If parent category exists, make it the parent of the new category
            $allFlagsCategoryID = $flagsCat->term_id;
        }

        wp_insert_term($text, self::TAXONOMY, ['parent' => $allFlagsCategoryID]);

        $flagCategory = sanitize_title($text);
        wp_add_object_terms($postID, $flagCategory, self::TAXONOMY);
    }
}

TimedProductCategories::register();

Leave a Reply