Helper classes
26 stateless utilities in src/Helpers/. The eight deep-dives below cover the ones you’ll reach for most often.
Before you write a utility, check here
26 stateless helper classes live in src/Helpers/. Mostly static methods, small APIs, no surprising side effects. The plugin uses them anywhere logic is shared between two or more callsites — date formatting, capability checks, signed URL generation, presentation glue.
If you’re writing addon code and you find yourself reaching for “hmm, I need to format a timestamp” or “hmm, I need to check if this update is solved” or “hmm, I need to know if HPOS is on” — check this page first. The answer is probably already in here, and using the existing helper means your addon picks up bug fixes automatically.
The full alphabetical list is at the bottom. The deep dives below are the ones you’ll reach for almost every day.
Check update state (is_resolved, is_customer_visible, can_edit)
An update record is a flat array straight from the database. Three of its columns are booleans masquerading as integers: is_resolved, customer_visible, customer_note_edits_locked. You could write (int) $update['is_resolved'] === 1 everywhere. Please don’t.
UpdateState has static methods for each meaningful question:
use OrderUpdatesForWoo\Helpers\UpdateState;
if ( UpdateState::is_resolved( $update ) ) { /* ... */ }
if ( UpdateState::is_customer_visible( $update ) ) { /* ... */ }
if ( UpdateState::can_edit( $update ) ) { /* ... */ }
Why bother? Three reasons. is_resolved on the database can be '0', 0, null, or false depending on where the array came from — the helper handles all of them. can_edit isn’t a single column; it’s a derived check across multiple fields (resolved? rated? edit-window setting?) — you’d be re-implementing that logic at every callsite. And if the semantics ever change in a future plugin version (say, the “solved” concept gets split into “solved” and “archived”), every callsite using UpdateState::is_resolved() picks up the new behaviour for free.
This is the helper you’ll touch most often. Get comfortable with it.
Render templates with theme-override support (View::render)
Anywhere the plugin shows markup, it goes through View::render(). Never include, never require, never file_get_contents + echo. Your addon should follow the same rule.
use OrderUpdatesForWoo\Helpers\View;
View::render( 'src/Admin/Orders/Templates/card/header.php', [
'update' => $update,
'is_solved' => UpdateState::is_resolved( $update ),
] );
The second argument is the context array — each key becomes a locally-scoped variable inside the template. $update and $is_solved are now available in header.php.
The thing that makes View::render() worth using over a plain include: it checks your-theme/order-updates-for-woo/<lowercased-path-after-Templates/> first and falls back to the plugin source if the theme override doesn’t exist. That’s the entire mechanism behind the Theme overrides system — you get it for free by routing through this helper.
One catch: only paths containing a Templates/ segment are theme-overridable. Templates outside that convention render straight from the plugin source — the override lookup gets skipped. If you’re writing a view that should be customisable, put it inside a Templates/ sub-folder under the feature that owns it.
HPOS-safe order lookups + orders-list URL (HposHelper)
WooCommerce supports two order storage modes: classic (orders in wp_posts) and HPOS (orders in custom tables). Most code paths don’t care — wc_get_order() hides the difference. A few do.
Two places you’ll need this helper:
use OrderUpdatesForWoo\Helpers\HposHelper;
// Is HPOS turned on?
if ( HposHelper::is_enabled() ) { /* ... */ }
// What URL is the orders list at? Different on HPOS vs classic.
$href = HposHelper::orders_list_url();
// HPOS: admin.php?page=wc-orders
// Classic: edit.php?post_type=shop_order
Three rules to internalise:
Never hardcode edit.php?post_type=shop_order. It 404s on HPOS-only stores. Always go through HposHelper::orders_list_url(). The plugin had bugs here in early versions; both got caught and fixed.
Never get_post_meta() on an order ID. On HPOS the order isn’t a post; the call returns nothing. Use $order->get_meta() via wc_get_order() — works on both storage modes.
Never new WP_Query( [ 'post_type' => 'shop_order' ] ). Use wc_get_orders(). Same reason.
Format timestamps in the site locale (DateHelper)
Every timestamp the plugin shows on screen goes through DateHelper::format_date(). It reads the site’s timezone and date/time format settings and produces something like “May 22, 3:00 PM” instead of “2026-05-22 15:00:00”.
use OrderUpdatesForWoo\Helpers\DateHelper;
$display = DateHelper::format_date( '2026-05-22 15:00:00' );
// "May 22, 3:00 PM" in en_US, "22. Mai, 15:00" in de_DE, etc.
It accepts MySQL timestamps (the format $wpdb returns), Unix timestamps, and DateTimeImmutable objects. Empty strings and invalid inputs return an empty string — safe to call without a null guard.
One thing worth knowing: the plugin stores all timestamps in UTC in the database (current_time( 'mysql', true ) — the true matters), but displays them in the site’s timezone. DateHelper does the conversion. If you’re writing a timestamp into the DB from your addon, store UTC. If you’re showing one, route it through DateHelper.
Queue background jobs with Action Scheduler (AsyncJob)
You’ve added a hook that needs to send an email or hit an external API. If you do that work inline, every save call waits for SMTP or your CRM to respond. A slow third party turns into a slow admin page.
AsyncJob wraps Action Scheduler with a one-line API:
use OrderUpdatesForWoo\Helpers\AsyncJob;
AsyncJob::queue( 'my_addon_dispatch_crm', [
'update_id' => 42,
'note_id' => 7,
] );
The first argument is your action hook name; the second is the payload (it gets passed as the single argument when the hook fires). The job runs a few seconds later, off the request thread.
On the receiving end, attach a normal action handler:
add_action( 'my_addon_dispatch_crm', 'my_addon_dispatch_crm_handler', 10, 1 );
function my_addon_dispatch_crm_handler( array $args ): void {
// wp_remote_post(), etc.
}
The plugin uses this everywhere it queues an email or a heavy notification. If your addon does anything that takes more than a few hundred milliseconds — HTTP calls, large file processing, multi-row DB updates — route it through here.
Check per-update mute / per-order email opt-out (StaffEmailPreference, CustomerEmailPreference)
Two helpers, same shape. StaffEmailPreference is the per-update mute toggle on the admin card (the “Get notifications” switch); CustomerEmailPreference is the per-order email opt-out the customer can flip on their portal.
use OrderUpdatesForWoo\Helpers\StaffEmailPreference;
use OrderUpdatesForWoo\Helpers\CustomerEmailPreference;
// Is this user muted for this specific update? (skip queuing notifications for them)
if ( StaffEmailPreference::is_muted( $update_id, $user_id ) ) {
return;
}
// Has the customer opted out of emails for this order?
if ( ! CustomerEmailPreference::is_enabled( $order_id, $customer_user_id ) ) {
return;
}
Use these in any addon hook that queues an email. If a user has muted a specific update, your addon shouldn’t override that preference and email them anyway — that’s a fast way to get uninstalled.
One exception worth knowing. @mentions skip the StaffEmailPreference check on purpose — if a colleague tags you directly, you hear about it even if you’ve muted the thread. That’s the plugin’s rule; your add-on should follow the same exception.
Resolve everyone involved in an update (ParticipantResolver)
An update accumulates a small crowd over its life: the creator, the assignee, every staff member who’s posted a note, every user who’s been @mentioned. ParticipantResolver figures that out for you.
use OrderUpdatesForWoo\Helpers\ParticipantResolver;
// Returns int[] of WordPress user IDs.
$participant_ids = ParticipantResolver::resolve( $update_id );
Use this when you want to fan-out a notification or a Slack message to everyone involved, not just the assignee. The plugin’s own ParticipantUpdateEmail goes to exactly this list when a new note is added to a thread.
The resolver is cache-wrapped — calling it repeatedly within a request is cheap. It invalidates whenever the underlying data changes (new assignee, new mention, new note author).
Convert text emoticons to emoji at save (EmoticonConverter)
Customers and staff sometimes type plain-text emoticons. The plugin converts them to actual emoji once at save time, so every downstream surface (admin card, customer portal, emails) reads real glyphs from the database. Means you can write :) on a phone keyboard and get 🙂 in the customer’s email.
use OrderUpdatesForWoo\Helpers\EmoticonConverter;
$body = EmoticonConverter::convert( "Looking into this :) will let you know :+1:" );
// "Looking into this 🙂 will let you know 👍"
The plugin already runs this on every note save. You don’t need to call it yourself unless your addon accepts free-text input from a different surface (a Slack reply, an SMS, etc.) and you want the same conversion behaviour.
The complete list
The eight above are the ones you’ll touch most. Here’s the rest — each one’s class file is small; if a one-liner isn’t enough, open the source.
| Class | What it does |
|---|---|
AdminBarNotificationStore | Queues and reads unread mentions/assignments for the admin-bar badge. |
AssigneeHelper | Formats and resolves assignee data for display in cards and emails. |
AssigneePicker | Powers the autocomplete picker for the assignee field. |
AsyncHealth | Diagnostics for Action Scheduler — shows stuck jobs on the admin notice bar. |
AttachmentPresenter | Formats attachment records (filename, signed URL, icon) for display. |
CustomerHelper | Customer identity resolution for note display (name, email, avatar). |
CustomerNotePresenter | Formats customer-thread notes (author, timestamp, edited pill) for the portal. |
Icons | Dashicon SVG helper — Icons::dashicon('edit') returns the right markup. |
NoteAuthor | Resolves the author of a note — staff vs customer vs guest. |
NotesHelper | Shared note formatting (timestamps, edited badges, “view history” link). |
OrderUpdateInfoMethods | Deliberate backwards-compatibility shim. Do not delete even if grep shows zero callers. |
RatingShareLinks | Generates pre-filled share URLs (Facebook, X, LinkedIn, WhatsApp) for the promoter follow-up. |
RestUrlHelper | Mints REST URLs with the plugin’s namespace — cleaner than concatenating rest_url() strings. |
UpdateAuthorHelper | Resolves who created an update (staff vs customer vs guest). |
UpdatePresentationHelper | Status pills, colour stripes, summary formatting. |
UpdateResolver | Loads a full update record with relations — notes, attachments, ratings — in one call. |
UpdateStatusHelper | Resolves a status key to its label and colour from the configured status list. |