PHP Enums in WordPress: Replacing Class Constants with Type-Safe Enums

PHP 8.1 enums provide a first-class alternative to the class-constant pattern that WordPress plugins have used for decades. Enums are type-safe, support methods and interfaces, and integrate cleanly with WordPress’s hooks API, sanitize_*() family, and database layers — while making intent far more explicit in code reviews and static analysis.

Problem: WordPress plugins use class constants to represent fixed sets of values — post statuses, payment states, notification types — but plain string constants are not type-safe, can be compared incorrectly, and are not easily serialisable to REST API responses.

Solution: Replace class constants with PHP 8.1 Enums — backed enums (enum Status: string { case Active = 'active'; }) are type-safe, serialisable to their backing value, and can implement interfaces. Use Status::from() for trusted input and Status::tryFrom() for user input. Enums work with match expressions for exhaustive handling.


The examples below show a pure enum, a backed enum for post status, using enums in WordPress hook callbacks and meta sanitisation, and a helper that converts a database string value back into an enum case safely.


<?php
// ── 1. Pure enum for payment methods ─────────────────────────────────────
enum PaymentMethod {
    case CreditCard;
    case PayPal;
    case BankTransfer;
    case Crypto;

    public function label(): string {
        return match( $this ) {
            self::CreditCard   => 'Credit / Debit Card',
            self::PayPal       => 'PayPal',
            self::BankTransfer => 'Bank Transfer',
            self::Crypto       => 'Cryptocurrency',
        };
    }
}

// ── 2. Backed string enum for WooCommerce order status ───────────────────
enum OrderStatus: string {
    case Pending    = 'pending';
    case Processing = 'processing';
    case Completed  = 'completed';
    case Refunded   = 'refunded';
    case Cancelled  = 'cancelled';

    /** Safely convert a DB string to an enum; returns null on invalid value */
    public static function fromDb( string $value ): ?self {
        return self::tryFrom( $value );
    }

    public function isTerminal(): bool {
        return match( $this ) {
            self::Completed, self::Refunded, self::Cancelled => true,
            default => false,
        };
    }
}

// ── 3. Type-safe hook callback ────────────────────────────────────────────
add_action( 'woocommerce_order_status_changed',
    function ( int $order_id, string $from_str, string $to_str ) {
        $to = OrderStatus::fromDb( $to_str );
        if ( null === $to ) {
            return; // unknown status — skip silently
        }
        if ( $to->isTerminal() ) {
            // Send confirmation email, update inventory, etc.
            do_action( 'my_plugin_order_finalised', $order_id, $to );
        }
    }, 10, 3
);

// ── 4. Sanitise a POST value into an enum ─────────────────────────────────
function sanitize_order_status( mixed $raw ): OrderStatus {
    $string = is_string( $raw ) ? $raw : '';
    return OrderStatus::fromDb( $string ) ?? OrderStatus::Pending;
}

// ── 5. Store and retrieve enum values as post meta ─────────────────────────
function save_order_status_meta( int $post_id, OrderStatus $status ): void {
    update_post_meta( $post_id, '_order_status', $status->value );
}

function get_order_status_meta( int $post_id ): OrderStatus {
    $raw = (string) get_post_meta( $post_id, '_order_status', true );
    return OrderStatus::fromDb( $raw ) ?? OrderStatus::Pending;
}


NOTE: Use tryFrom() (returns null on invalid input) rather than from() (throws ValueError) whenever you are handling database or user-supplied strings — it avoids fatal errors from stale or unexpected values stored in older data.