Table of Contents
- Introduction: Feature Flags in WordPress
- Feature Flag Fundamentals
- Options-Based Toggle Implementation
- User-Level Feature Flags
- A/B Testing Integration
- Gradual Rollout Strategies
- Cleanup and Removal
- FAQ
Introduction: Feature Flags in WordPress
Shipping code doesn't mean shipping features. Feature flags decouple deployment from feature release, enabling powerful development practices:
- Test features safely in production before enabling for all users
- Roll out features gradually to catch issues early
- Run A/B tests to validate improvements
- Kill features instantly if problems arise
- Experiment with confidence knowing changes can revert immediately
WordPress plugins can be shipped with incomplete features, letting you develop continuously without worrying about breaking users. Feature flags transform plugin development from binary (shipped/not shipped) to granular control.
This guide explores WordPress plugin feature flags patterns, from simple options-based toggles to sophisticated user-level targeting and A/B testing. You'll learn to implement flags that grow with your plugin while remaining maintainable and performant.
Feature Flag Fundamentals
Feature flags are conditional branches that enable or disable functionality. At their core, they represent a decision point in your code where behavior changes based on some configuration. Instead of shipping code that's either "on" or "off" at deployment time, feature flags allow you to control visibility and availability of features at runtime, dynamically adapting to your users' needs and your operational requirements.
The power of feature flags lies in decoupling deployment from release. You can deploy code containing a new feature into production without exposing it to users. This separation enables safer deployments, faster iteration cycles, and the ability to respond immediately to issues without rolling back entire deployments. When paired with monitoring and analytics, feature flags become a risk mitigation tool that lets you validate changes in production with real user traffic before general availability.
Simplest implementation:
// Check flag before executing feature
if (get_option('enable_advanced_reporting')) {
add_action('admin_menu', 'register_advanced_reporting_menu');
add_action('wp_ajax_generate_report', 'handle_advanced_report_generation');
}
This works but has problems: flags leak throughout codebase, testing becomes complex, and removing flags requires code cleanup.
Good Flag Design
Well-designed flags follow principles that make them maintainable, testable, and safe to remove. Poor flag design leads to technical debt that accumulates as your plugin grows. Scattered flag checks throughout your codebase become impossible to track, making it difficult to know when and how to remove them. Bad flag implementations can also create subtle bugs when behavior diverges unexpectedly between flagged and non-flagged code paths.
Well-designed flags:
- Centralize decisions: One place to check all flags. This makes it trivial to see all decision points and understand what features are controlled by flags. Centralization also enables you to change flag evaluation logic without touching code throughout your plugin.
- Are self-documenting: Clear what behavior changes. When other developers read flag checks, they should immediately understand what feature is controlled and why it matters. Good flag names and nearby comments prevent misunderstandings about what a flag does.
- Enable testing: Easy to mock in tests. Well-designed flags are testable in isolation, allowing you to verify both enabled and disabled code paths. Tests should run the same feature code with different flag states and verify behavior is correct in both cases.
- Plan for removal: Track dependencies on each flag. Every flag you add is a piece of technical debt. Planning for removal from day one means you have metadata about when flags were introduced, what version should remove them, and what code still depends on them. This prevents accumulating "zombie" flags that nobody knows how to remove.
Flag Lifecycle
Development → Testing → Gradual Rollout → Monitoring → Removal
1. Enable flag in development
2. Test with internal team
3. Release to subset of users
4. Monitor metrics
5. Expand to more users
6. Full rollout
7. Remove flag and clean code
Options-Based Toggle Implementation
Start with WordPress options for simple, persistent feature flags. The WordPress options API provides a convenient, built-in system for storing configuration data that persists across requests and survives plugin deactivation and reactivation. Options are automatically serialized and deserialized, they integrate with WordPress caching mechanisms, and they work reliably across all WordPress installations.
Options-based flags are ideal for site-level feature toggles that apply uniformly to all users, or when you need simple on/off switches accessible from the WordPress admin. They're especially useful during early feature development when you're testing with a small team and haven't yet built sophisticated targeting logic. The simplicity of options-based flags means less code to maintain and debug, though they don't support the advanced per-user, per-role, or percentage-based targeting that larger features may require.
Basic Toggle
// Define default flags
function get_feature_flags() {
return [
'advanced_reporting' => false,
'new_dashboard_ui' => false,
'beta_api' => false,
];
}
// Initialize on plugin activation
register_activation_hook(__FILE__, function() {
$flags = get_feature_flags();
foreach ($flags as $name => $default) {
if (get_option('feature_flag_' . $name) === false) {
update_option('feature_flag_' . $name, $default);
}
}
});
// Check if feature enabled
function is_feature_enabled($feature_name) {
return (bool) get_option('feature_flag_' . $feature_name);
}
// Usage
if (is_feature_enabled('advanced_reporting')) {
add_action('admin_menu', 'register_advanced_reporting_menu');
}
Admin Interface for Toggles
Making flags configurable through the WordPress admin interface is crucial for non-technical users who need to control feature visibility without touching code. A clean admin interface shows the current state of all flags at a glance, makes it easy to toggle flags, and provides context about what each flag controls. The admin interface becomes the single source of truth for flag state, and good UX here prevents configuration errors.
When building admin interfaces for flags, consider adding helpful descriptions of what each feature does, information about when the flag was introduced, and guidance about side effects of enabling/disabling the feature. You might also want to log flag changes for audit purposes, so you can track who enabled a feature and when. For critical features, consider requiring confirmation before enabling or disabling them.
// Add settings page
add_action('admin_menu', function() {
add_management_page(
'Feature Flags',
'Feature Flags',
'manage_options',
'feature-flags',
'render_feature_flags_page'
);
});
function render_feature_flags_page() {
$flags = get_feature_flags();
?>
<div class="wrap">
<h1>Feature Flags</h1>
<form method="post">
<?php wp_nonce_field('update_feature_flags'); ?>
<table class="form-table">
<tbody>
<?php foreach ($flags as $name => $default): ?>
<tr>
<th scope="row">
<label for="<?php echo esc_attr($name); ?>">
<?php echo esc_html(ucwords(str_replace('_', ' ', $name))); ?>
</label>
</th>
<td>
<input type="checkbox"
name="feature_flags[<?php echo esc_attr($name); ?>]"
id="<?php echo esc_attr($name); ?>"
value="1"
<?php checked(is_feature_enabled($name)); ?> />
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php submit_button(); ?>
</form>
</div>
<?php
}
// Handle form submission
add_action('admin_init', function() {
if (!isset($_POST['feature_flags'])) return;
check_admin_referer('update_feature_flags');
if (!current_user_can('manage_options')) {
wp_die('Unauthorized');
}
$flags = get_feature_flags();
foreach ($flags as $name => $default) {
$enabled = isset($_POST['feature_flags'][$name]);
update_option('feature_flag_' . $name, $enabled);
}
wp_redirect(add_query_arg('updated', '1', admin_url('tools.php?page=feature-flags')));
exit;
});
Flag with Versions
Track when flags were introduced for cleanup planning. Versioning is essential for managing flag lifecycle. When you introduce a flag in version 2.0.0 and plan to remove it in version 3.0.0, you create a clear contractual obligation to clean it up. Without this metadata, flags tend to accumulate indefinitely—nobody remembers why a flag exists, what it controls, or whether the feature it gates has been fully adopted.
Versioning also helps with communication. When you release version 2.5.0, you can tell users "this version introduces three new feature flags, available for testing" and "this version removes four flags that are now standard features." Users know what's changing and can plan accordingly. Teams using your plugin know when they need to update their code to remove flag checks.
Beyond just versions, consider tracking additional metadata: whether the feature behind the flag is in alpha, beta, or RC status; what the performance characteristics are compared to the feature it gates; and what metrics indicate whether the feature is ready for full release.
// Define flags with metadata
function get_feature_flags() {
return [
'advanced_reporting' => [
'enabled' => false,
'introduced_version' => '2.0.0',
'description' => 'Advanced reporting dashboard',
'target_removal_version' => '3.0.0'
],
'new_dashboard_ui' => [
'enabled' => false,
'introduced_version' => '2.5.0',
'description' => 'Redesigned dashboard interface',
'target_removal_version' => '4.0.0'
]
];
}
// Check if flag should be removed
function should_remove_flag($feature_name) {
$flags = get_feature_flags();
$flag = $flags[$feature_name] ?? null;
if (!$flag) return false;
$plugin_version = get_option('my_plugin_version');
$target_version = $flag['target_removal_version'];
return version_compare($plugin_version, $target_version, '>=');
}
User-Level Feature Flags
Different users should access different features. Implement user-targeted flags. Site-level feature toggles work fine for internal testing or when you're rolling out features uniformly across all users, but real-world scenarios often demand granular control. Some users might be part of a beta testing program and should see all experimental features. Others might have earned access to premium capabilities through their subscription level. Still others might need feature access restricted based on their role within the organization.
User-level flags enable sophisticated targeting without needing to push code changes. You can enable a feature for all administrator-level users while hiding it from subscribers. You can enable it for a single user to help them troubleshoot an issue. You can enable it for all users in a specific geographic region. The targeting logic becomes configuration rather than code, making it dynamic and responsive to your operational needs.
User Meta Flags
// Check user-specific flag
function user_has_feature($user_id, $feature_name) {
// Check user-specific override first
$user_flag = get_user_meta($user_id, 'feature_flag_' . $feature_name, true);
if ($user_flag !== '') {
return (bool) $user_flag;
}
// Fall back to global flag
return is_feature_enabled($feature_name);
}
// Enable feature for specific user
function enable_feature_for_user($user_id, $feature_name) {
update_user_meta($user_id, 'feature_flag_' . $feature_name, 1);
}
// Disable feature for specific user
function disable_feature_for_user($user_id, $feature_name) {
update_user_meta($user_id, 'feature_flag_' . $feature_name, 0);
}
// Usage
if (user_has_feature(get_current_user_id(), 'advanced_reporting')) {
// Show advanced reporting interface
}
Role-Based Flags
Role-based flags leverage WordPress's built-in role and capability system. Rather than checking individual users, you check user roles. This approach scales well because you maintain a small number of role definitions instead of managing exceptions for hundreds of individual users. Role-based targeting is also more maintainable—if you want to enable a feature for all editors, you configure it once in the role settings and all editors immediately get access.
Role-based flags work particularly well for feature access that follows organizational hierarchy. Premium features might be available only to administrators. Advanced options might be visible to editors but not contributors. Moderation tools might be restricted to specific roles. By anchoring access decisions to roles, you make feature visibility follow the same permission model as the rest of WordPress.
// Check flag by user role
function role_has_feature($role, $feature_name) {
$role_flags = get_option('feature_flags_by_role', []);
if (isset($role_flags[$role][$feature_name])) {
return (bool) $role_flags[$role][$feature_name];
}
// Fall back to global flag
return is_feature_enabled($feature_name);
}
// Current user by role
function current_user_has_feature($feature_name) {
$user = wp_get_current_user();
foreach ($user->roles as $role) {
if (role_has_feature($role, $feature_name)) {
return true;
}
}
return is_feature_enabled($feature_name);
}
// Admin UI for role flags
add_action('admin_init', function() {
register_setting('my_plugin_settings', 'feature_flags_by_role');
add_settings_section(
'feature_flags_roles',
'Feature Flags by Role',
function() {
$wp_roles = wp_roles();
$role_flags = get_option('feature_flags_by_role', []);
$features = array_keys(get_feature_flags());
foreach ($wp_roles->get_names() as $role => $name) {
echo '<h4>' . esc_html($name) . '</h4>';
foreach ($features as $feature) {
$enabled = $role_flags[$role][$feature] ?? false;
?>
<label>
<input type="checkbox"
name="feature_flags_by_role[<?php echo esc_attr($role); ?>][<?php echo esc_attr($feature); ?>]"
value="1"
<?php checked($enabled); ?> />
<?php echo esc_html(ucwords(str_replace('_', ' ', $feature))); ?>
</label><br />
<?php
}
}
},
'my_plugin_role_settings'
);
});
Beta Tester Program
Running a formal beta testing program with selected users is one of the most effective ways to validate new features before general release. Beta testers are typically power users, early adopters, or strategic customers who are willing to experiment with incomplete features in exchange for early access and influence over the product direction. They catch real-world issues you'd never find in internal testing, provide valuable feedback on usability, and become advocates for features they've helped shape.
Implementing a beta tester program with feature flags is straightforward. You create a boolean flag on each user indicating whether they're a beta tester. When evaluating features, beta testers automatically get all features currently in beta, without needing individual per-feature configuration. This scales naturally—as you add new beta features, all beta testers automatically see them. You can manage beta tester status through the WordPress admin, and you can easily track which users are testing which features through your metrics system.
// Mark users as beta testers
function is_beta_tester($user_id) {
return (bool) get_user_meta($user_id, 'beta_tester', true);
}
// Beta testers get all beta features
function user_has_feature($user_id, $feature_name) {
if (is_beta_tester($user_id)) {
return true; // Beta testers get everything
}
// Check user-specific override
$user_flag = get_user_meta($user_id, 'feature_flag_' . $feature_name, true);
if ($user_flag !== '') {
return (bool) $user_flag;
}
// Fall back to global
return is_feature_enabled($feature_name);
}
// Admin UI to manage beta testers
add_action('admin_init', function() {
add_filter('manage_users_columns', function($columns) {
$columns['beta_tester'] = 'Beta Tester';
return $columns;
});
add_filter('manage_users_custom_column', function($output, $column_name, $user_id) {
if ($column_name === 'beta_tester') {
return is_beta_tester($user_id) ? '✓' : '–';
}
return $output;
}, 10, 3);
});
A/B Testing Integration
Feature flags enable A/B testing by showing different features to different user segments. A/B testing is a systematic way to validate that a feature actually improves the metrics you care about. Before rolling out a new feature to all users, you show it to a subset (the "variant" group) while the rest see the original experience (the "control" group). By comparing metrics between groups, you can measure whether the new feature actually improves user engagement, conversion rates, or whatever else matters to your product.
Without A/B testing, you make decisions based on assumptions and gut feelings. You might implement a feature you think users will love, only to find they rarely use it or find it confusing. A/B testing lets you measure impact before committing to a feature. If a new feature doesn't move your metrics, you know to either improve it or kill it rather than shipping it to all users and discovering the problem later.
Proper A/B testing requires consistent user assignment (the same user always sees the same variant), careful experiment setup to avoid biasing results, and statistical analysis to determine whether differences are real or due to random chance. Feature flags provide the infrastructure that makes A/B testing feasible to implement and manage.
Simple A/B Test
// Assign user to test variant deterministically
function get_user_test_variant($user_id, $test_name) {
// Hash user ID to get consistent variant
$hash = crc32($user_id . ':' . $test_name) % 100;
return $hash < 50 ? 'control' : 'variant';
}
// Usage
$variant = get_user_test_variant(get_current_user_id(), 'new_dashboard');
if ($variant === 'variant') {
// Show new dashboard to 50% of users
echo render_new_dashboard();
} else {
// Show old dashboard to other 50%
echo render_old_dashboard();
}
Weighted A/B Tests
// Assign variant with custom weights
function get_weighted_variant($user_id, $test_name, $weights) {
$hash = crc32($user_id . ':' . $test_name) % 100;
$cumulative = 0;
foreach ($weights as $variant => $weight) {
$cumulative += $weight;
if ($hash < $cumulative) {
return $variant;
}
}
return 'control';
}
// 70% control, 30% variant
$variant = get_weighted_variant(
get_current_user_id(),
'new_reporting',
['control' => 70, 'variant' => 30]
);
Test Analytics
// Track test exposure
function record_test_exposure($user_id, $test_name, $variant) {
global $wpdb;
$wpdb->insert($wpdb->prefix . 'test_analytics', [
'user_id' => $user_id,
'test_name' => $test_name,
'variant' => $variant,
'timestamp' => current_time('mysql')
]);
}
// Track events during test
function record_test_event($user_id, $test_name, $event_name, $value = null) {
global $wpdb;
$wpdb->insert($wpdb->prefix . 'test_events', [
'user_id' => $user_id,
'test_name' => $test_name,
'event_name' => $event_name,
'event_value' => $value,
'timestamp' => current_time('mysql')
]);
}
// Usage
$variant = get_user_test_variant(get_current_user_id(), 'new_reporting');
record_test_exposure(get_current_user_id(), 'new_reporting', $variant);
if ($variant === 'variant') {
add_action('my_plugin_report_generated', function($report_id) {
record_test_event(get_current_user_id(), 'new_reporting', 'report_generated', $report_id);
});
}
Analyze Test Results
// Get test statistics
function get_test_statistics($test_name, $metric = 'conversion') {
global $wpdb;
// Count exposures by variant
$exposures = $wpdb->get_results($wpdb->prepare(
"SELECT variant, COUNT(*) as count
FROM {$wpdb->prefix}test_analytics
WHERE test_name = %s
GROUP BY variant",
$test_name
));
// Count events by variant
if ($metric === 'conversion') {
$events = $wpdb->get_results($wpdb->prepare(
"SELECT e.variant, COUNT(*) as conversions
FROM {$wpdb->prefix}test_events e
JOIN {$wpdb->prefix}test_analytics a
ON e.user_id = a.user_id AND e.test_name = a.test_name
WHERE e.test_name = %s AND e.event_name = %s
GROUP BY e.variant",
$test_name,
'report_generated'
));
}
// Calculate conversion rates
$results = [];
foreach ($exposures as $exposure) {
$events_for_variant = array_filter($events, function($e) use ($exposure) {
return $e->variant === $exposure->variant;
});
$conversion_count = $events_for_variant[0]->conversions ?? 0;
$results[$exposure->variant] = [
'exposures' => $exposure->count,
'conversions' => $conversion_count,
'conversion_rate' => ($exposure->count > 0) ? ($conversion_count / $exposure->count) : 0
];
}
return $results;
}
Gradual Rollout Strategies
Roll out features safely to catch issues before full release. Launching a feature to 100% of users at once is risky. If there's a bug, performance issue, or user experience problem, you've impacted everyone immediately. Gradual rollouts reduce risk by validating features with small subsets of users first, then expanding to larger groups as confidence increases.
Gradual rollouts also buy you time. Rather than launching a feature and then scrambling to fix issues affecting all users, you launch to 5% of users, let it run for a few days to ensure stability and monitor metrics, then expand to 10%, then 25%, and finally 100%. If an issue appears at any stage, you can pause the rollout and investigate before impacting more users. If metrics look bad, you can pause the rollout and decide whether to fix the feature or abandon it.
Gradual rollouts work best when coupled with monitoring and alerting. You watch key metrics as you increase the rollout percentage. If error rates spike at 25% rollout, you know something's wrong with the feature or your infrastructure. If user engagement drops, you know the feature isn't resonating. This data guides decisions about whether to continue expanding or pause and investigate.
Percentage-Based Rollout
// Enable for percentage of users
function get_rollout_percentage($feature_name) {
return (int) get_option('feature_rollout_percentage_' . $feature_name, 0);
}
function set_rollout_percentage($feature_name, $percentage) {
update_option('feature_rollout_percentage_' . $feature_name, max(0, min(100, $percentage)));
}
// Check if user is in rollout
function user_in_rollout($user_id, $feature_name) {
$percentage = get_rollout_percentage($feature_name);
if ($percentage >= 100) return true;
if ($percentage <= 0) return false;
// Deterministic: same user always in/out
$hash = crc32($user_id . ':' . $feature_name) % 100;
return $hash < $percentage;
}
// Usage
if (user_in_rollout(get_current_user_id(), 'new_dashboard')) {
add_action('admin_menu', 'register_new_dashboard_menu');
}
Rollout Schedule
// Schedule rollout percentage over time
function get_scheduled_rollout_percentage($feature_name) {
$schedule = get_option('feature_rollout_schedule_' . $feature_name, [
['timestamp' => 0, 'percentage' => 0]
]);
$current_time = current_time('timestamp');
$current_percentage = 0;
foreach ($schedule as $point) {
if ($point['timestamp'] <= $current_time) {
$current_percentage = $point['percentage'];
}
}
return $current_percentage;
}
// Set rollout schedule
function set_rollout_schedule($feature_name, $schedule) {
// $schedule = [
// ['timestamp' => strtotime('2026-03-20'), 'percentage' => 10],
// ['timestamp' => strtotime('2026-03-25'), 'percentage' => 50],
// ['timestamp' => strtotime('2026-04-01'), 'percentage' => 100],
// ]
usort($schedule, function($a, $b) {
return $a['timestamp'] - $b['timestamp'];
});
update_option('feature_rollout_schedule_' . $feature_name, $schedule);
}
// Admin UI for schedule
add_action('admin_init', function() {
add_settings_section(
'feature_rollout',
'Feature Rollout Schedule',
function() {
$feature_name = 'new_dashboard';
$schedule = get_option('feature_rollout_schedule_' . $feature_name, []);
echo '<p>Current rollout percentage: ' . esc_html(get_scheduled_rollout_percentage($feature_name)) . '%</p>';
foreach ($schedule as $i => $point) {
$date = date('Y-m-d H:i', $point['timestamp']);
echo '<p>' . esc_html($point['percentage']) . '% on ' . esc_html($date) . '</p>';
}
},
'my_plugin_settings'
);
});
Rollout by User Segment
// Target specific user segments
function user_matches_segment($user_id, $segment) {
$user = get_userdata($user_id);
switch ($segment) {
case 'enterprise':
// Users with custom capability
return user_can($user_id, 'manage_enterprise_features');
case 'api_users':
// Users with API keys
$keys = get_user_meta($user_id, 'api_keys', true);
return !empty($keys);
case 'beta_program':
// Explicitly added to beta program
return (bool) get_user_meta($user_id, 'beta_program', true);
case 'recent_signup':
// Signed up in last 30 days
$user_registered = strtotime($user->user_registered);
return ($user_registered > current_time('timestamp') - 30 * DAY_IN_SECONDS);
}
return false;
}
// Feature available to segment
function is_feature_available_for_user($user_id, $feature_name) {
$segment_targets = get_option('feature_segment_targets_' . $feature_name, []);
foreach ($segment_targets as $segment) {
if (user_matches_segment($user_id, $segment)) {
return true;
}
}
return false;
}
Cleanup and Removal
Features eventually ship fully or are abandoned. Remove flags and clean code. This is the most commonly neglected part of feature flag management. Teams eagerly add flags to ship features safely, but they rarely come back to clean up flags once features are stable and shipped. Accumulated flags become technical debt—they make code harder to understand, increase testing complexity, and slow down development.
Clean code requires scheduled flag removal. Build it into your release process. Before shipping a major version, review all feature flags. Identify flags that have been fully rolled out for several releases. Identify abandoned features whose flags can be deleted. Plan removal work and schedule it into your sprints. Without this discipline, your codebase eventually becomes littered with dead flags that nobody remembers why they exist.
Removing a flag requires more than just deleting the flag check. You must remove all code paths that depend on the flag. You must update tests that mock the flag. You must update documentation. This is non-trivial work, which is why planning for removal from day one matters. If you know you'll eventually need to remove a flag, you structure your code to make removal easier.
Cleanup Checklist
// Track flag cleanup requirements
function get_flag_cleanup_status($feature_name) {
return [
'feature_name' => $feature_name,
'introduced_version' => '2.0.0',
'target_removal_version' => '3.0.0',
'all_users_enabled' => is_feature_enabled($feature_name) && get_rollout_percentage($feature_name) >= 100,
'code_references' => count_code_references($feature_name),
'user_overrides' => count_user_overrides($feature_name),
'still_in_use' => still_has_code_dependencies($feature_name)
];
}
// Count user-specific flag overrides
function count_user_overrides($feature_name) {
global $wpdb;
return (int) $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*) FROM {$wpdb->usermeta}
WHERE meta_key = %s",
'feature_flag_' . $feature_name
));
}
// Check if code still references feature
function still_has_code_dependencies($feature_name) {
// Scan plugin files for feature references
$plugin_dir = plugin_dir_path(__FILE__);
$pattern = 'is_feature_enabled.*' . preg_quote($feature_name);
$files = glob($plugin_dir . '**/*.php', GLOB_RECURSIVE);
foreach ($files as $file) {
$content = file_get_contents($file);
if (preg_match('/' . $pattern . '/', $content)) {
return true;
}
}
return false;
}
Remove Flags Safely
// Deprecation notice before removal
function deprecate_feature_flag($feature_name, $target_version) {
add_action('admin_notices', function() use ($feature_name, $target_version) {
$plugin_version = get_option('my_plugin_version');
if (version_compare($plugin_version, $target_version, '>=')) {
echo '<div class="notice notice-warning"><p>';
echo 'Feature flag "' . esc_html($feature_name) . '" is deprecated and will be removed.';
echo '</p></div>';
}
});
}
// Remove flag after sufficient notice period
function remove_feature_flag($feature_name) {
// Delete all references
delete_option('feature_flag_' . $feature_name);
delete_option('feature_rollout_percentage_' . $feature_name);
delete_option('feature_rollout_schedule_' . $feature_name);
delete_option('feature_flags_by_role');
// Delete user overrides
global $wpdb;
$wpdb->query($wpdb->prepare(
"DELETE FROM {$wpdb->usermeta} WHERE meta_key = %s",
'feature_flag_' . $feature_name
));
// Remove code references (manually)
// Search codebase and remove is_feature_enabled() checks
}
Feature Flag Registry
// Central registry of all flags
function register_feature_flag($name, $options = []) {
$defaults = [
'introduced_version' => '1.0.0',
'target_removal_version' => null,
'description' => '',
'default_enabled' => false
];
$flag = wp_parse_args($options, $defaults);
// Store in transient registry
$registry = get_transient('my_plugin_feature_flag_registry') ?: [];
$registry[$name] = $flag;
set_transient('my_plugin_feature_flag_registry', $registry, HOUR_IN_SECONDS);
}
// List all registered flags
function get_feature_flag_registry() {
return get_transient('my_plugin_feature_flag_registry') ?: [];
}
// Usage
register_feature_flag('advanced_reporting', [
'introduced_version' => '2.0.0',
'target_removal_version' => '3.0.0',
'description' => 'Advanced reporting dashboard',
'default_enabled' => false
]);
Additional Resources
Frequently Asked Questions
How do feature flags affect performance?
Minimal if implemented correctly. Option checks cache well. Use a feature flag manager that caches decisions:
// Cache flag decisions
static $cache = [];
function is_feature_enabled($feature_name) {
global $cache;
if (isset($cache[$feature_name])) {
return $cache[$feature_name];
}
$result = (bool) get_option('feature_flag_' . $feature_name);
$cache[$feature_name] = $result;
return $result;
}
Should features in feature flags be fully tested?
Yes. Test both enabled and disabled states. Use flags to hide incomplete features, not to deploy untested code.
How do I prevent flag sprawl?
Maintain registry of all flags. Set target removal version for every flag. Review and remove old flags quarterly. Use deprecation notices before removal.
Can I use feature flags with caching plugins?
Yes, but be careful. Page caches don't respect per-user flags. Either:
- Don't cache pages with personalized features
- Cache by user variant
- Load personalized features via AJAX
What if a flag enables breaking changes?
Feature flags don't prevent breaking changes—they let you control who experiences them. Always make breaking changes opt-in for safety.
Does WP HealthKit help manage feature flags?
WP HealthKit analyzes your plugin for feature flag patterns and cleanup opportunities. When you upload your plugin, WP HealthKit identifies deprecated flags needing removal, suggests caching improvements for flag checks, and recommends flag architecture improvements. Get specific guidance for managing your plugin's feature flags safely.
How do I test A/B test implementations?
Use environment variables or constants to override user assignment:
// Override in tests
if (defined('WP_TESTING')) {
function get_user_test_variant($user_id, $test_name) {
// Return fixed variant for reproducible tests
return defined('TEST_VARIANT_' . strtoupper($test_name))
? constant('TEST_VARIANT_' . strtoupper($test_name))
: 'control';
}
}
Conclusion
Feature flags transform plugin development from binary releases to controlled experiments. Ship code confidently with incomplete features hidden behind flags. Roll out features gradually, monitoring metrics and staying ready to kill features that underperform.
Master options-based toggles for simple flags. Add user-level targeting for personalization. Implement A/B testing to validate improvements. Use gradual rollouts to catch issues before they impact all users.
Feature flags enable modern development practices in WordPress. Experiment with confidence. Release faster. Fail safely. Ship features users actually want.
Upload your plugin to WP HealthKit to audit your feature flag implementation. WP HealthKit identifies flag cleanup opportunities, suggests caching improvements, and recommends how to structure flags for scalability. Get specific recommendations for managing feature flags throughout your plugin's lifecycle.