post_status ) { FilterIndex::remove_object( 'post', $post_id ); return; } FilterIndex::reindex_object( 'post', $post_id ); self::register_filters_from_post_blocks( $post ); } /** * Parses a published post's block content and registers any * airo-wp/query-filter blocks' filters into the FilterRegistry. * * Moving registration to save time (rather than the editor) ensures that * only filters belonging to actually-published posts end up in the registry. * Abandoned drafts no longer pollute the registry. * * @param \WP_Post $post The published post whose content to scan. * @return void */ private static function register_filters_from_post_blocks( \WP_Post $post ): void { if ( ! function_exists( 'parse_blocks' ) ) { return; } $blocks = parse_blocks( $post->post_content ); self::walk_blocks_for_filters( $blocks ); } /** * Recursively walks a block tree and registers taxonomy filters for any * airo-wp/query-filter blocks found. * * @param array $blocks Parsed block array from parse_blocks(). * @return void */ private static function walk_blocks_for_filters( array $blocks ): void { foreach ( $blocks as $block ) { if ( 'airo-wp/query-filter' === ( $block['blockName'] ?? '' ) ) { $attrs = self::with_block_defaults( 'airo-wp/query-filter', isset( $block['attrs'] ) ? (array) $block['attrs'] : array() ); // Only the taxonomy-driven kinds mean anything here. Reading // defaults (above) makes `taxonomy` always present, so without // this gate a search or sort filter would register a category // filter it never uses. $kind = isset( $attrs['filterKind'] ) ? (string) $attrs['filterKind'] : ''; if ( in_array( $kind, self::TAXONOMY_FILTER_KINDS, true ) ) { $taxonomy = sanitize_key( (string) ( $attrs['taxonomy'] ?? '' ) ); if ( '' !== $taxonomy ) { FilterRegistry::register( $taxonomy, array( 'type' => 'taxonomy', 'source' => $taxonomy, ) ); } } } if ( ! empty( $block['innerBlocks'] ) ) { self::walk_blocks_for_filters( $block['innerBlocks'] ); } } } /** * Merge a parsed block's attributes over its registered defaults. * * `parse_blocks()` returns only what the block comment carries, and * WordPress never serializes an attribute whose value equals its * `block.json` default. Reading `$attrs['taxonomy']` directly therefore * misses every filter left on the default taxonomy — which is `category`, * the overwhelmingly common case. The symptom was silent: the filter never * registered, so `wp dsgo query index rebuild` reported "Indexed N objects * (0 rows)" and counts never rendered, with nothing anywhere saying why. * * Falls back to the raw attributes when the block type is not registered * yet, which is no worse than the behaviour this replaces. * * @param string $block_name Block type name. * @param array $attrs Attributes from parse_blocks(). * @return array Attributes with registered defaults filled in. */ private static function with_block_defaults( string $block_name, array $attrs ): array { if ( ! class_exists( '\WP_Block_Type_Registry' ) ) { return $attrs; } $block_type = \WP_Block_Type_Registry::get_instance()->get_registered( $block_name ); if ( ! $block_type || empty( $block_type->attributes ) ) { return $attrs; } $defaults = array(); foreach ( (array) $block_type->attributes as $key => $schema ) { if ( is_array( $schema ) && array_key_exists( 'default', $schema ) ) { $defaults[ $key ] = $schema['default']; } } return array_merge( $defaults, $attrs ); } /** * Removes a post's index rows when the post is force-deleted. * * @param int $post_id The post ID that was deleted. * @return void */ public static function on_deleted_post( int $post_id ): void { FilterIndex::remove_object( 'post', $post_id ); } /** * Purges index rows for a deleted term. * * @param int $term_id Term ID. * @param int $tt_id Unused — term taxonomy ID. * @param string $taxonomy Taxonomy slug. * @return void */ public static function on_delete_term( $term_id, $tt_id, $taxonomy ): void { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundBeforeLastUsed FilterIndex::remove_term_values( (int) $term_id, (string) $taxonomy ); } /** * Reindexes a post when its taxonomy terms are changed, but only when * the taxonomy is tracked by at least one registered filter. * * Set_object_terms fires for any taxonomy-capable object; we only index * posts in v2.2, so we short-circuit when the object ID does not resolve * to a real post. * * @param int $object_id The object ID whose terms changed. * @param array $terms Unused — new term IDs (passed by WP hook). * @param array $tt_ids Unused — new term-taxonomy IDs (passed by WP hook). * @param string $taxonomy The taxonomy slug that was updated. * @return void */ public static function on_set_object_terms( int $object_id, array $terms, array $tt_ids, string $taxonomy ): void { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundBeforeLastUsed // set_object_terms fires for any taxonomy-capable object; we only index posts in v2.2. if ( ! get_post( $object_id ) ) { return; } foreach ( FilterRegistry::all() as $config ) { if ( 'taxonomy' === ( $config['type'] ?? '' ) && ( $config['source'] ?? '' ) === $taxonomy ) { FilterIndex::reindex_object( 'post', $object_id ); return; } } } /** * Reindexes a post when one of its meta values changes, but only when * the meta key is tracked by at least one registered filter. * * @param int|string $meta_id Meta row ID (unused; required by hook signature). * @param int $object_id Post ID whose meta changed. * @param string $meta_key The changed meta key. * @return void */ public static function on_post_meta_changed( $meta_id, int $object_id, string $meta_key ): void { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundBeforeLastUsed foreach ( FilterRegistry::all() as $config ) { if ( 'meta' === ( $config['type'] ?? '' ) && ( $config['source'] ?? '' ) === $meta_key ) { FilterIndex::reindex_object( 'post', $object_id ); return; } } } }