| @@ -1,53 +1,72 @@ | ||
| 1 | 1 | <?php |
| 2 | +/** | |
| 3 | + * Scans post content for inline table blocks | |
| 4 | + * | |
| 5 | + * @package TableKit | |
| 6 | + */ | |
| 2 | 7 | |
| 3 | 8 | namespace TableBuilder\Config\CPT; |
| 4 | 9 | |
| 5 | -defined('ABSPATH') || exit; | |
| 10 | +defined( 'ABSPATH' ) || exit; | |
| 6 | 11 | |
| 7 | 12 | use TableBuilder\Shortcode\ShortcodeUtils; |
| 8 | 13 | use WP_Post; |
| 9 | 14 | use WP_Query; |
| 10 | 15 | |
| 11 | -class InlineTableScanner | |
| 12 | -{ | |
| 16 | +/** | |
| 17 | + * Finds and caches table blocks embedded inline within other posts' content. | |
| 18 | + */ | |
| 19 | +class InlineTableScanner { | |
| 20 | + | |
| 13 | 21 | private const BLOCK_NAMESPACE = 'tablebuilder/'; |
| 14 | - private const CACHE_GROUP = 'tablekit_blocks'; | |
| 15 | - private const CACHE_KEY = 'inline_tables_v2'; | |
| 16 | - private const CACHE_TTL = 5 * MINUTE_IN_SECONDS; | |
| 22 | + private const CACHE_GROUP = 'tablekit_blocks'; | |
| 23 | + private const CACHE_KEY = 'inline_tables_v2'; | |
| 24 | + private const CACHE_TTL = 5 * MINUTE_IN_SECONDS; | |
| 17 | 25 | |
| 18 | - public function get_inline_table_source_options(): array | |
| 19 | - { | |
| 26 | + /** | |
| 27 | + * Builds a SELECT-style options list of source posts containing inline table | |
| 28 | + * blocks, keyed by source post ID, labeled with block count. | |
| 29 | + * | |
| 30 | + * @return array<string,string> Options keyed by source post ID (as a string). | |
| 31 | + */ | |
| 32 | + public function get_inline_table_source_options(): array { | |
| 20 | 33 | $inline_tables = $this->get_all_inline_block_tables(); |
| 21 | 34 | |
| 22 | - if (empty($inline_tables)) { | |
| 35 | + if ( empty( $inline_tables ) ) { | |
| 23 | 36 | return array(); |
| 24 | 37 | } |
| 25 | 38 | |
| 26 | 39 | $options = array(); |
| 27 | - $grouped = $this->group_inline_tables_by_source($inline_tables); | |
| 40 | + $grouped = $this->group_inline_tables_by_source( $inline_tables ); | |
| 28 | 41 | |
| 29 | - foreach (array_values($grouped) as $group) { | |
| 30 | - $block_count = count($group['blocks']); | |
| 42 | + foreach ( array_values( $grouped ) as $group ) { | |
| 43 | + $block_count = count( $group['blocks'] ); | |
| 31 | 44 | |
| 32 | - /* translators: %2$s = source post title. */ | |
| 33 | 45 | $label = sprintf( |
| 34 | - _n('Table in: %2$s', 'Tables in: %2$s', $block_count, 'table-builder-block'), | |
| 46 | + /* translators: %2$s = source post title. */ | |
| 47 | + _n( 'Table in: %2$s', 'Tables in: %2$s', $block_count, 'table-builder-block' ), | |
| 35 | 48 | $block_count, |
| 36 | 49 | $group['source_title'] |
| 37 | 50 | ); |
| 38 | 51 | |
| 39 | - $options[(string) $group['source_id']] = $label; | |
| 52 | + $options[ (string) $group['source_id'] ] = $label; | |
| 40 | 53 | } |
| 41 | 54 | |
| 42 | 55 | return $options; |
| 43 | 56 | } |
| 44 | 57 | |
| 45 | - public function get_all_inline_block_tables(): array | |
| 46 | - { | |
| 47 | - $cached = wp_cache_get(self::CACHE_KEY, self::CACHE_GROUP); | |
| 58 | + /** | |
| 59 | + * Scans every scannable post type for table blocks embedded inline (i.e. not | |
| 60 | + * inside a dedicated tablekit_table post), cached for 5 minutes. | |
| 61 | + * | |
| 62 | + * @return array Flat list of inline table block records (source_id/source_title/ | |
| 63 | + * source_type/source_author/source_date/block_name/block_id/shortcode_id/attrs). | |
| 64 | + */ | |
| 65 | + public function get_all_inline_block_tables(): array { | |
| 66 | + $cached = wp_cache_get( self::CACHE_KEY, self::CACHE_GROUP ); | |
| 48 | 67 | |
| 49 | - if (false !== $cached) { | |
| 68 | + if ( false !== $cached ) { | |
| 50 | 69 | return (array) $cached; |
| 51 | 70 | } |
| 52 | 71 | |
| 53 | 72 | $results = array(); |
| @@ -52,10 +71,10 @@ | ||
| 52 | 71 | |
| 53 | 72 | $results = array(); |
| 54 | 73 | $post_types = $this->get_scannable_post_types(); |
| 55 | 74 | |
| 56 | - if (empty($post_types)) { | |
| 57 | - $this->store_cache($results); | |
| 75 | + if ( empty( $post_types ) ) { | |
| 76 | + $this->store_cache( $results ); | |
| 58 | 77 | return $results; |
| 59 | 78 | } |
| 60 | 79 | |
| 61 | 80 | $query = new WP_Query( |
| @@ -60,122 +79,164 @@ | ||
| 60 | 79 | |
| 61 | 80 | $query = new WP_Query( |
| 62 | 81 | array( |
| 63 | 82 | 'post_type' => $post_types, |
| 64 | - 'post_status' => array('publish', 'draft', 'pending', 'private'), | |
| 83 | + 'post_status' => array( 'publish', 'draft', 'pending', 'private' ), | |
| 65 | 84 | 'posts_per_page' => -1, |
| 66 | 85 | 'no_found_rows' => true, |
| 67 | 86 | 's' => self::BLOCK_NAMESPACE, |
| 68 | - 'search_columns' => array('post_content'), | |
| 87 | + 'search_columns' => array( 'post_content' ), | |
| 69 | 88 | ) |
| 70 | 89 | ); |
| 71 | 90 | |
| 72 | - if (! $query->have_posts()) { | |
| 73 | - $this->store_cache($results); | |
| 91 | + if ( ! $query->have_posts() ) { | |
| 92 | + $this->store_cache( $results ); | |
| 74 | 93 | return $results; |
| 75 | 94 | } |
| 76 | 95 | |
| 77 | - foreach ($query->posts as $post) { | |
| 78 | - $found = $this->extract_table_blocks(parse_blocks($post->post_content)); | |
| 96 | + foreach ( $query->posts as $post ) { | |
| 97 | + $found = $this->extract_table_blocks( parse_blocks( $post->post_content ) ); | |
| 79 | 98 | |
| 80 | - if (empty($found)) { | |
| 99 | + if ( empty( $found ) ) { | |
| 81 | 100 | continue; |
| 82 | 101 | } |
| 83 | 102 | |
| 84 | - $author_name = (string) get_the_author_meta('display_name', $post->post_author); | |
| 103 | + $author_name = (string) get_the_author_meta( 'display_name', $post->post_author ); | |
| 85 | 104 | |
| 86 | - foreach ($found as $block) { | |
| 105 | + foreach ( $found as $block ) { | |
| 87 | 106 | $attrs = $block['attrs'] ?? array(); |
| 88 | - $block_id = (string) ($attrs['blockID'] ?? ''); | |
| 107 | + $block_id = (string) ( $attrs['blockID'] ?? '' ); | |
| 89 | 108 | |
| 90 | 109 | $results[] = array( |
| 91 | 110 | 'source_id' => (int) $post->ID, |
| 92 | - 'source_title' => (string) get_the_title($post), | |
| 111 | + 'source_title' => (string) get_the_title( $post ), | |
| 93 | 112 | 'source_type' => (string) $post->post_type, |
| 94 | 113 | 'source_author' => $author_name, |
| 95 | 114 | 'source_date' => (string) $post->post_date, |
| 96 | 115 | 'block_name' => (string) $block['blockName'], |
| 97 | 116 | 'block_id' => $block_id, |
| 98 | - 'shortcode_id' => $this->resolve_shortcode_id($attrs, $block_id), | |
| 117 | + 'shortcode_id' => $this->resolve_shortcode_id( $attrs, $block_id ), | |
| 99 | 118 | 'attrs' => $attrs, |
| 100 | 119 | ); |
| 101 | 120 | } |
| 102 | 121 | } |
| 103 | 122 | |
| 104 | - $this->store_cache($results); | |
| 123 | + $this->store_cache( $results ); | |
| 105 | 124 | |
| 106 | 125 | return $results; |
| 107 | 126 | } |
| 108 | 127 | |
| 109 | - public function get_scannable_post_types(): array | |
| 110 | - { | |
| 128 | + /** | |
| 129 | + * Gets the list of post types to scan for inline table blocks: every | |
| 130 | + * REST-exposed post type except the Tables CPT itself, filterable via | |
| 131 | + * "tablekit_scannable_post_types". | |
| 132 | + * | |
| 133 | + * @return string[] Post type names. | |
| 134 | + */ | |
| 135 | + public function get_scannable_post_types(): array { | |
| 111 | 136 | static $cached = null; |
| 112 | - if (is_array($cached)) { | |
| 137 | + if ( is_array( $cached ) ) { | |
| 113 | 138 | return $cached; |
| 114 | 139 | } |
| 115 | 140 | |
| 116 | - $post_types = get_post_types(array('show_in_rest' => true), 'names'); | |
| 117 | - unset($post_types[TableCPT::POST_TYPE]); | |
| 118 | - $cached = (array) apply_filters('tablekit_scannable_post_types', array_values($post_types)); | |
| 141 | + $post_types = get_post_types( array( 'show_in_rest' => true ), 'names' ); | |
| 142 | + unset( $post_types[ TableCPT::POST_TYPE ] ); | |
| 119 | 143 | |
| 144 | + /** | |
| 145 | + * Filters the post types scanned for inline table blocks. | |
| 146 | + * | |
| 147 | + * @since Unknown | |
| 148 | + * | |
| 149 | + * @param string[] $post_types REST-exposed post type names, excluding the Tables CPT itself. | |
| 150 | + */ | |
| 151 | + $cached = (array) apply_filters( 'tablekit_scannable_post_types', array_values( $post_types ) ); | |
| 152 | + | |
| 120 | 153 | return $cached; |
| 121 | 154 | } |
| 122 | 155 | |
| 123 | - public function extract_table_blocks(array $blocks): array | |
| 124 | - { | |
| 125 | - return ShortcodeUtils::filter_table_blocks($blocks); | |
| 156 | + /** | |
| 157 | + * Narrow a parsed block tree down to just the plugin's table blocks. | |
| 158 | + * | |
| 159 | + * @param array $blocks Parsed block tree. | |
| 160 | + * @return array Table blocks only. | |
| 161 | + */ | |
| 162 | + public function extract_table_blocks( array $blocks ): array { | |
| 163 | + return ShortcodeUtils::filter_table_blocks( $blocks ); | |
| 126 | 164 | } |
| 127 | 165 | |
| 128 | - public function maybe_invalidate_cache(int $post_id, ?WP_Post $post = null): void | |
| 129 | - { | |
| 130 | - if (null === $post) { | |
| 131 | - $post = get_post($post_id); | |
| 166 | + /** | |
| 167 | + * Invalidates the inline-table scan cache if the saved/deleted post is one | |
| 168 | + * of the scannable post types. Hooked to "save_post"/"delete_post". | |
| 169 | + * | |
| 170 | + * @param int $post_id Post ID being saved/deleted. | |
| 171 | + * @param WP_Post|null $post Optional. The post object, to avoid a redundant get_post() lookup. | |
| 172 | + * @return void | |
| 173 | + */ | |
| 174 | + public function maybe_invalidate_cache( int $post_id, ?WP_Post $post = null ): void { | |
| 175 | + if ( null === $post ) { | |
| 176 | + $post = get_post( $post_id ); | |
| 132 | 177 | } |
| 133 | 178 | |
| 134 | - if (! ($post instanceof WP_Post)) { | |
| 179 | + if ( ! ( $post instanceof WP_Post ) ) { | |
| 135 | 180 | return; |
| 136 | 181 | } |
| 137 | 182 | |
| 138 | - if (in_array($post->post_type, $this->get_scannable_post_types(), true)) { | |
| 183 | + if ( in_array( $post->post_type, $this->get_scannable_post_types(), true ) ) { | |
| 139 | 184 | $this->invalidate_cache(); |
| 140 | 185 | } |
| 141 | 186 | } |
| 142 | 187 | |
| 143 | - public function invalidate_cache(): void | |
| 144 | - { | |
| 145 | - wp_cache_delete(self::CACHE_KEY, self::CACHE_GROUP); | |
| 188 | + /** | |
| 189 | + * Clears the inline-table scan cache. | |
| 190 | + * | |
| 191 | + * @return void | |
| 192 | + */ | |
| 193 | + public function invalidate_cache(): void { | |
| 194 | + wp_cache_delete( self::CACHE_KEY, self::CACHE_GROUP ); | |
| 146 | 195 | } |
| 147 | 196 | |
| 148 | - public function resolve_shortcode_id(array $attrs, string $block_id): string | |
| 149 | - { | |
| 150 | - $shortcode_id = (string) ($attrs['shortId'] ?? ''); | |
| 197 | + /** | |
| 198 | + * Resolves a block's short shortcode ID: its stored "shortId" attribute if | |
| 199 | + * present, otherwise a deterministic 6-digit hash derived from its blockID. | |
| 200 | + * | |
| 201 | + * @param array $attrs Block attributes. | |
| 202 | + * @param string $block_id Block's own ID (blockID attribute), used as the hash source. | |
| 203 | + * @return string The resolved shortcode ID, or an empty string if $block_id is also empty. | |
| 204 | + */ | |
| 205 | + public function resolve_shortcode_id( array $attrs, string $block_id ): string { | |
| 206 | + $shortcode_id = (string) ( $attrs['shortId'] ?? '' ); | |
| 151 | 207 | |
| 152 | - if ('' !== $shortcode_id) { | |
| 208 | + if ( '' !== $shortcode_id ) { | |
| 153 | 209 | return $shortcode_id; |
| 154 | 210 | } |
| 155 | 211 | |
| 156 | - if ('' === $block_id) { | |
| 212 | + if ( '' === $block_id ) { | |
| 157 | 213 | return ''; |
| 158 | 214 | } |
| 159 | 215 | |
| 160 | 216 | $hash = 0; |
| 161 | 217 | |
| 162 | - foreach (str_split($block_id) as $char) { | |
| 163 | - $hash = (($hash * 31) + ord($char)) & 0x7FFFFFFF; | |
| 218 | + foreach ( str_split( $block_id ) as $char ) { | |
| 219 | + $hash = ( ( $hash * 31 ) + ord( $char ) ) & 0x7FFFFFFF; | |
| 164 | 220 | } |
| 165 | 221 | |
| 166 | - return str_pad((string) ($hash % 1000000), 6, '0', STR_PAD_LEFT); | |
| 222 | + return str_pad( (string) ( $hash % 1000000 ), 6, '0', STR_PAD_LEFT ); | |
| 167 | 223 | } |
| 168 | 224 | |
| 169 | - private function group_inline_tables_by_source(array $inline_tables): array | |
| 170 | - { | |
| 225 | + /** | |
| 226 | + * Group flat inline-table records by their source post ID. | |
| 227 | + * | |
| 228 | + * @param array $inline_tables Flat list of inline table records from get_all_inline_block_tables(). | |
| 229 | + * @return array<int,array> Records grouped by source_id, each with a "blocks" sub-array. | |
| 230 | + */ | |
| 231 | + private function group_inline_tables_by_source( array $inline_tables ): array { | |
| 171 | 232 | $grouped = array(); |
| 172 | 233 | |
| 173 | - foreach ($inline_tables as $table) { | |
| 234 | + foreach ( $inline_tables as $table ) { | |
| 174 | 235 | $sid = (int) $table['source_id']; |
| 175 | 236 | |
| 176 | - if (! isset($grouped[$sid])) { | |
| 177 | - $grouped[$sid] = array( | |
| 237 | + if ( ! isset( $grouped[ $sid ] ) ) { | |
| 238 | + $grouped[ $sid ] = array( | |
| 178 | 239 | 'source_id' => $sid, |
| 179 | 240 | 'source_title' => $table['source_title'], |
| 180 | 241 | 'source_type' => $table['source_type'], |
| 181 | 242 | 'source_author' => $table['source_author'], |
| @@ -183,9 +244,9 @@ | ||
| 183 | 244 | 'blocks' => array(), |
| 184 | 245 | ); |
| 185 | 246 | } |
| 186 | 247 | |
| 187 | - $grouped[$sid]['blocks'][] = array( | |
| 248 | + $grouped[ $sid ]['blocks'][] = array( | |
| 188 | 249 | 'block_name' => $table['block_name'], |
| 189 | 250 | 'block_id' => $table['block_id'], |
| 190 | 251 | 'shortcode_id' => $table['shortcode_id'], |
| 191 | 252 | ); |
| @@ -193,9 +254,14 @@ | ||
| 193 | 254 | |
| 194 | 255 | return $grouped; |
| 195 | 256 | } |
| 196 | 257 | |
| 197 | - private function store_cache(array $results): void | |
| 198 | - { | |
| 199 | - wp_cache_set(self::CACHE_KEY, $results, self::CACHE_GROUP, self::CACHE_TTL); | |
| 258 | + /** | |
| 259 | + * Stores a scan result set in the object cache. | |
| 260 | + * | |
| 261 | + * @param array $results Result set from get_all_inline_block_tables(). | |
| 262 | + * @return void | |
| 263 | + */ | |
| 264 | + private function store_cache( array $results ): void { | |
| 265 | + wp_cache_set( self::CACHE_KEY, $results, self::CACHE_GROUP, self::CACHE_TTL ); | |
| 200 | 266 | } |
| 201 | 267 | } |