sanitize_key( $config['type'] ?? '' ), 'source' => sanitize_text_field( $config['source'] ?? '' ), 'label' => sanitize_text_field( $config['label'] ?? $key ), ); update_option( self::OPTION, $filters, false ); self::$cache = null; // First-time registration: queue a background backfill of index rows // for all existing posts. Without this, posts that predate the filter // block (common when authors add the block to an already-populated // site) show "(0)" next to terms they legitimately belong to because // the save_post hooks only cover posts updated AFTER the filter was // registered. // // Queued (not inline) because `rebuild_filter` iterates the entire // posts table — on a large site that would stall the post-save // request that triggered registration and could time out. WP-Cron // runs the backfill on the next request instead. The hook is handled // by FilterIndexHooks::on_filter_registered below. if ( $is_new ) { /** * Fires when a new filter key is added to the registry. * * @param string $filter_key The sanitized filter key that was just registered. */ do_action( 'airowp_query_filter_registered', $sanitized_key ); } } /** * Removes a filter from the option-backed registry. * * @param string $key The filter key to remove. * @return void */ public static function unregister( string $key ): void { $filters = get_option( self::OPTION, array() ); if ( ! is_array( $filters ) ) { return; } $sanitized_key = sanitize_key( $key ); if ( ! array_key_exists( $sanitized_key, $filters ) ) { return; } unset( $filters[ $sanitized_key ] ); update_option( self::OPTION, $filters, false ); self::$cache = null; } /** * Returns all registered filters, merged with any registered via filter. * * @return array Keyed array of filter configs (key => { type, source, label }). */ public static function all(): array { self::ensure_hooks(); if ( null !== self::$cache ) { return self::$cache; } $stored = get_option( self::OPTION, array() ); if ( ! is_array( $stored ) ) { $stored = array(); } /** * Filter the registered filters. * * @since 2.2.0 * * @param array $stored Keyed array of filter configs (key => { type, source, label }). */ self::$cache = (array) apply_filters( 'airowp_query_registered_filters', $stored ); return self::$cache; } /** * Clears the per-request cache. Intended for test teardown and for callers * that mutate the underlying option directly. * * @return void */ public static function bust_cache(): void { self::$cache = null; } /** * Returns the config for a single registered filter, or null if not found. * * @param string $key Filter registry key. * @return array|null Filter config array or null if the key is not registered. */ public static function get( string $key ): ?array { $all = self::all(); $sanitized_key = sanitize_key( $key ); return $all[ $sanitized_key ] ?? null; } }