Looking for a PHP enum tryFrom example that does real work in WordPress? Here is one. A WordPress setting with a fixed list of choices, such as an archive layout of grid, compact or masonry, is usually built from loose strings. The allowed values get typed into the <select>, into the sanitize callback, into the REST schema and into every if that reads the option. When one copy changes and the others don’t, a typo or a hand-edited database row slips through and your template quietly falls back to whatever the else branch does.
The snippet below keeps the list in one place: a backed enum. tryFrom() checks every value coming in, match gives each case its label, and the rest of your plugin gets a typed dpc_Archive_Layout object instead of a string it has to check again. Add a case and the dropdown, the sanitizer and the REST schema all pick it up.
The snippet
Put this in a small plugin (it needs PHP 8.1 or newer). It adds an Archive layout dropdown to Settings → Reading and puts a dpc-layout-{value} class on the body of archive pages and the blog page.
<?php
/**
* Plugin Name: DPC Archive Layout Setting
* Description: A validated select setting backed by a PHP enum.
* Requires PHP: 8.1
*/
defined( 'ABSPATH' ) || exit;
enum dpc_Archive_Layout: string {
case Grid = 'grid';
case Compact = 'compact';
case Masonry = 'masonry';
public static function fallback(): self {
return self::Grid;
}
public function label(): string {
return match ( $this ) {
self::Grid => __( 'Grid', 'dpc' ),
self::Compact => __( 'Compact list', 'dpc' ),
self::Masonry => __( 'Masonry', 'dpc' ),
};
}
// Never throws: unknown or non-string input becomes the fallback case.
public static function fromMixed( mixed $value ): self {
if ( ! is_string( $value ) ) {
return self::fallback();
}
return self::tryFrom( sanitize_key( $value ) ) ?? self::fallback();
}
public static function sanitize( mixed $value ): string {
return self::fromMixed( $value )->value;
}
public static function values(): array {
return array_map( static fn( self $layout ): string => $layout->value, self::cases() );
}
}
function dpc_get_archive_layout(): dpc_Archive_Layout {
return dpc_Archive_Layout::fromMixed( get_option( 'dpc_archive_layout' ) );
}
add_action( 'init', static function (): void {
register_setting( 'reading', 'dpc_archive_layout', [
'type' => 'string',
'default' => dpc_Archive_Layout::fallback()->value,
'sanitize_callback' => dpc_Archive_Layout::sanitize(...),
'show_in_rest' => [
'schema' => [ 'enum' => dpc_Archive_Layout::values() ],
],
] );
} );
add_action( 'admin_init', static function (): void {
add_settings_field(
'dpc_archive_layout',
__( 'Archive layout', 'dpc' ),
'dpc_render_archive_layout_field',
'reading',
'default',
[ 'label_for' => 'dpc_archive_layout' ]
);
} );
function dpc_render_archive_layout_field(): void {
$current = dpc_get_archive_layout();
echo '<select name="dpc_archive_layout" id="dpc_archive_layout">';
foreach ( dpc_Archive_Layout::cases() as $layout ) {
printf(
'<option value="%s"%s>%s</option>',
esc_attr( $layout->value ),
selected( $current->value, $layout->value, false ),
esc_html( $layout->label() )
);
}
echo '</select>';
}
add_filter( 'body_class', static function ( array $classes ): array {
if ( is_home() || is_archive() ) {
$classes[] = 'dpc-layout-' . dpc_get_archive_layout()->value;
}
return $classes;
} );
How it works
dpc_Archive_Layoutis a backed enum. Each case has a string value, and that string is what gets saved inwp_options.tryFrom()returns the matching case, ornullif nothing matches.from()would throw aValueErrorinstead. When the input comes from users or the database, you wantnull, and?? self::fallback()turns it into a safe default.fromMixed()runssanitize_key()first, soMASONRYormasonrystill match. Arrays, integers andfalse(whatget_option()returns for a missing option) fall back to Grid.dpc_Archive_Layout::sanitize(...)is a first-class callable. It makes aClosure, so you don’t have to write the class and method name as strings. WordPress runs it through thesanitize_option_dpc_archive_layoutfilter on every save, whether the value comes from the form,update_option(), WP-CLI or REST.label()usesmatch. If you add a case and forget its label, PHP throws anUnhandledMatchErrorinstead of showing an empty option.- The form is posted to
options.phpthrough the Reading screen, so the Settings API does the nonce check and themanage_optionscapability check for you. Values are escaped on output withesc_attr()andesc_html().
Customise it
- Add a layout by adding one
case, plus its line inlabel(). The dropdown, the sanitizer and the RESTenumschema update on their own. - Put more behaviour on the enum, for example
public function columns(): intwith its ownmatch, so templates calldpc_get_archive_layout()->columns()instead of switching on strings. - To give the setting its own page, change
'reading'to your page slug in bothregister_setting()andadd_settings_field(). The Developer Notes admin page post shows a starting point for a custom screen.
Gotchas
- Never pass the enum object itself to
selected(),checked()oresc_attr(). They cast to string, and enums can’t be cast, so you get a fatalError. Pass->value. - Don’t save the enum object with
update_option(). Save->valueand rebuild the object when you read it back. Otherwise the object gets serialized into the database. - A closure from
sanitize(...)can’t be removed later withremove_filter(), because you don’t have the same instance. That’s fine for your own plugin, but use a named function if other code needs to unhook it. See how to find every callback hooked to a filter when you debug this. - Don’t call WordPress core functions with named arguments. Core doesn’t treat parameter names as a stable API, so a rename in a future release could break your call.
- Declaring an enum is a parse error on PHP 8.0 and older, which takes the whole site down. Keep the
Requires PHP: 8.1header so WordPress refuses to activate the plugin on old hosts.
Snippet poll. Should this snippet be in the Scripts Organizer library for one-click import? 10 votes put it there.
0 of 10 votes