• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

rtCamp / snapwp-helper / 13497569547

24 Feb 2025 12:03PM UTC coverage: 82.583% (-0.8%) from 83.424%
13497569547

Pull #89

github

web-flow
Merge b432a9c4a into 8e49111f1
Pull Request #89: Fix : `templateByUri.editorBlocks` to respect `flat: false` 🚧 🏗️

1522 of 1843 relevant lines covered (82.58%)

12.61 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

65.81
/src/Modules/GraphQL/Data/ContentBlocksResolver.php
1
<?php
2
/**
3
 * Used to resolve content blocks from a node.
4
 *
5
 * Replaces WPGraphQL\ContentBlocks\Data\ContentBlocksResolver to avoid bugs.
6
 *
7
 * @package SnapWP\Helper\Modules\GraphQL\Data
8
 */
9

10
namespace SnapWP\Helper\Modules\GraphQL\Data;
11

12
use WPGraphQL\Model\Post;
13

14
/**
15
 * Class ContentBlocksResolver
16
 */
17
final class ContentBlocksResolver {
18
        /**
19
         * Retrieves a list of content blocks
20
         *
21
         * @param \WPGraphQL\Model\Model|array<string,mixed> $node The node we are resolving.
22
         * @param array<string,mixed>                        $args GraphQL query args to pass to the connection resolver.
23
         * @param string[]                                   $allowed_block_names The list of allowed block names to filter.
24
         *
25
         * @return array<string,mixed> The resolved parsed blocks.
26
         */
27
        public static function resolve_content_blocks( $node, $args, $allowed_block_names = [] ): array {
30✔
28
                /**
29
                 * When this filter returns a non-null value, the content blocks resolver will use that value
30
                 *
31
                 * @see WPGraphQL\ContentBlocks\Data\ContentBlocksResolver::resolve_content_blocks()
32
                 */
33
                $pre_resolved_blocks = apply_filters( 'wpgraphql_content_blocks_pre_resolve_blocks', null, $node, $args, $allowed_block_names ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- WPGraphQL filter.
30✔
34

35
                if ( null !== $pre_resolved_blocks && is_array( $pre_resolved_blocks ) ) {
30✔
36
                        return $pre_resolved_blocks;
×
37
                }
38

39
                $content = null;
30✔
40
                if ( $node instanceof Post ) {
30✔
41

42
                        // @todo: this is restricted intentionally.
43
                        // $content = $node->contentRaw;
44

45
                        // This is the unrestricted version, but we need to
46
                        // probably have a "Block" Model that handles
47
                        // determining what fields should/should not be
48
                        // allowed to be returned?
49
                        $post    = get_post( $node->databaseId );
×
50
                        $content = ! empty( $post->post_content ) ? $post->post_content : null;
×
51
                }
52

53
                /**
54
                 * Filters the content retrieved from the node used to parse the blocks.
55
                 *
56
                 * @see WPGraphQL\ContentBlocks\Data\ContentBlocksResolver::resolve_content_blocks()
57
                 */
58
                $content = apply_filters( 'wpgraphql_content_blocks_resolver_content', $content, $node, $args ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- WPGraphQL filter.
30✔
59

60
                if ( empty( $content ) ) {
30✔
61
                        return [];
×
62
                }
63

64
                // Parse the blocks from HTML comments to an array of blocks.
65
                $parsed_blocks = self::parse_blocks( $content );
30✔
66
                if ( empty( $parsed_blocks ) ) {
30✔
67
                        return [];
×
68
                }
69

70
                $args['flat'] = 'false';
30✔
71

72
                // Flatten block list here if requested or if 'flat' value is not selected (default).
73
                if ( ! isset( $args['flat'] ) || 'true' == $args['flat'] ) { // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual
30✔
74
                        $parsed_blocks = self::flatten_block_list( $parsed_blocks );
×
75
                }
76

77
                // Final level of filtering out blocks not in the allowed list.
78
                $parsed_blocks = self::filter_allowed_blocks( $parsed_blocks, $allowed_block_names );
30✔
79

80
                /**
81
                 * Filters the content blocks after they have been resolved.
82
                 *
83
                 * @see WPGraphQL\ContentBlocks\Data\ContentBlocksResolver::resolve_content_blocks()
84
                 */
85
                $parsed_blocks = apply_filters( 'wpgraphql_content_blocks_resolve_blocks', $parsed_blocks, $node, $args, $allowed_block_names ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- WPGraphQL filter.
30✔
86

87
                return $parsed_blocks;
30✔
88
        }
89

90
        /**
91
         * Get blocks from html string.
92
         *
93
         * @param string $content Content to parse.
94
         *
95
         * @return array<string,mixed> List of blocks.
96
         */
97
        private static function parse_blocks( $content ): array {
30✔
98
                $blocks = parse_blocks( $content );
30✔
99

100
                return self::handle_do_blocks( $blocks );
30✔
101
        }
102

103
        /**
104
         * Recursively process blocks.
105
         *
106
         * This mirrors the `do_blocks` function in WordPress which is responsible for hydrating certain block attributes and supports, but without the forced rendering.
107
         *
108
         * @param array<string,mixed>[] $blocks Blocks data.
109
         *
110
         * @return array<string,mixed>[] The processed blocks.
111
         */
112
        private static function handle_do_blocks( array $blocks ): array {
30✔
113
                $parsed = [];
30✔
114
                foreach ( $blocks as $block ) {
30✔
115
                        $block_data = self::handle_do_block( $block );
30✔
116

117
                        if ( ! empty( $block_data ) ) {
30✔
118
                                $parsed[] = $block_data;
30✔
119
                        }
120
                }
121

122
                // Remove empty blocks.
123
                return $parsed;
30✔
124
        }
125

126
        /**
127
         * Process a block, getting all extra fields.
128
         *
129
         * @param array<string,mixed> $block Block data.
130
         *
131
         * @return ?array<string,mixed> The processed block.
132
         */
133
        private static function handle_do_block( array $block ): ?array {
30✔
134
                if ( self::is_block_empty( $block ) ) {
30✔
135
                        return null;
30✔
136
                }
137

138
                // Since Gutenberg assigns an empty blockName for Classic block, we define it here.
139
                if ( empty( trim( $block['blockName'] ) ) ) {
30✔
140
                        $block['blockName'] = 'core/freeform';
11✔
141
                }
142

143
                // Assign a unique clientId to the block.
144
                $block['clientId'] = uniqid();
30✔
145

146
                // Render the HTML once and store it for later use.
147
                $block['renderedHtml'] = render_block( $block );
30✔
148

149
                // @todo apply more hydrations.
150
                $block = self::populate_template_part_inner_blocks( $block );
30✔
151
                $block = self::populate_post_content_inner_blocks( $block );
30✔
152
                $block = self::populate_reusable_blocks( $block );
30✔
153
                $block = self::populate_pattern_inner_blocks( $block );
30✔
154

155
                // @todo evaluate populate_navigation_blocks().
156
                // Prepare innerBlocks.
157
                if ( ! empty( $block['innerBlocks'] ) ) {
30✔
158
                        $block['innerBlocks'] = self::handle_do_blocks( $block['innerBlocks'] );
30✔
159
                }
160

161
                return $block;
30✔
162
        }
163

164
        /**
165
         * Checks whether a block is really empty, and not just a `core/freeform`.
166
         *
167
         * @param array<string,mixed> $block The block to check.
168
         */
169
        private static function is_block_empty( array $block ): bool {
30✔
170
                // If we have a blockName, no need to check further.
171
                if ( ! empty( $block['blockName'] ) ) {
30✔
172
                        return false;
30✔
173
                }
174

175
                // If there is no innerHTML and no innerContent, we can consider it empty.
176
                if ( empty( $block['innerHTML'] ) && empty( $block['innerContent'] ) ) {
30✔
177
                        return true;
×
178
                }
179

180
                if ( ! array_key_exists( 'blockName', $block ) ) {
30✔
181
                        return true;
×
182
                }
183

184
                // Strip empty comments and spaces to see if `innerHTML` is truly empty.
185
                $stripped = preg_replace( '/<!--(.*)-->|[\s\n\r]+/Uis', '', $block['innerHTML'] );
30✔
186

187
                return empty( trim( $stripped ?? '' ) );
30✔
188
        }
189

190
        /**
191
         * Populates the innerBlocks of a template part block with the blocks from the template part.
192
         *
193
         * @param array<string,mixed> $block The block to populate.
194
         *
195
         * @return array<string,mixed> The populated block.
196
         */
197
        private static function populate_template_part_inner_blocks( array $block ): array {
30✔
198
                // Bail if not WP 5.8 or later.
199
                if ( ! function_exists( 'get_block_templates' ) ) {
30✔
200
                        return $block;
×
201
                }
202

203
                if ( 'core/template-part' !== $block['blockName'] || ! isset( $block['attrs']['slug'] ) ) {
30✔
204
                        return $block;
30✔
205
                }
206

207
                $matching_templates = get_block_templates( [ 'slug__in' => [ $block['attrs']['slug'] ] ], 'wp_template_part' );
30✔
208

209
                $template_blocks = ! empty( $matching_templates[0]->content ) ? self::parse_blocks( $matching_templates[0]->content ) : null;
30✔
210

211
                if ( empty( $template_blocks ) ) {
30✔
212
                        return $block;
×
213
                }
214

215
                $block['innerBlocks'] = $template_blocks;
30✔
216

217
                return $block;
30✔
218
        }
219

220
        /**
221
         * Populates the innerBlocks of a core/post-content block with the blocks from the post content.
222
         *
223
         * @param array<string,mixed> $block The block to populate.
224
         *
225
         * @return array<string,mixed> The populated block.
226
         */
227
        private static function populate_post_content_inner_blocks( array $block ): array {
30✔
228
                if ( 'core/post-content' !== $block['blockName'] ) {
30✔
229
                        return $block;
30✔
230
                }
231

232
                $post = get_post();
16✔
233

234
                if ( ! $post ) {
16✔
235
                        return $block;
×
236
                }
237

238
                $parsed_blocks = ! empty( $post->post_content ) ? self::parse_blocks( $post->post_content ) : null;
16✔
239

240
                if ( empty( $parsed_blocks ) ) {
16✔
241
                        return $block;
×
242
                }
243

244
                $block['innerBlocks'] = $parsed_blocks;
16✔
245

246
                return $block;
16✔
247
        }
248

249
        /**
250
         * Populates reusable blocks with the blocks from the reusable ref ID.
251
         *
252
         * @param array<string,mixed> $block The block to populate.
253
         *
254
         * @return array<string,mixed> The populated block.
255
         */
256
        private static function populate_reusable_blocks( array $block ): array {
30✔
257
                if ( 'core/block' !== $block['blockName'] || ! isset( $block['attrs']['ref'] ) ) {
30✔
258
                        return $block;
30✔
259
                }
260

261
                $reusable_block = get_post( $block['attrs']['ref'] );
×
262

263
                if ( ! $reusable_block ) {
×
264
                        return $block;
×
265
                }
266

267
                $parsed_blocks = ! empty( $reusable_block->post_content ) ? self::parse_blocks( $reusable_block->post_content ) : null;
×
268

269
                if ( empty( $parsed_blocks ) ) {
×
270
                        return $block;
×
271
                }
272

273
                return array_merge( ...$parsed_blocks );
×
274
        }
275

276
        /**
277
         * Populates the pattern innerBlocks with the blocks from the pattern.
278
         *
279
         * @param array<string,mixed> $block The block to populate.
280
         * @return array<string,mixed> The populated block.
281
         */
282
        private static function populate_pattern_inner_blocks( array $block ): array {
30✔
283
                // Bail if not WP 6.6 or later.
284
                if ( ! function_exists( 'resolve_pattern_blocks' ) ) {
30✔
285
                        return $block;
×
286
                }
287

288
                if ( 'core/pattern' !== $block['blockName'] || ! isset( $block['attrs']['slug'] ) ) {
30✔
289
                        return $block;
30✔
290
                }
291

292
                $resolved_patterns = resolve_pattern_blocks( [ $block ] );
30✔
293

294
                if ( empty( $resolved_patterns ) ) {
30✔
295
                        return $block;
×
296
                }
297

298
                $block['innerBlocks'] = $resolved_patterns;
30✔
299
                return $block;
30✔
300
        }
301

302
        /**
303
         * Flattens a list blocks into a single array
304
         *
305
         * @param array<string,mixed> $blocks A list of blocks to flatten.
306
         *
307
         * @return array<string,mixed> The flattened list of blocks.
308
         */
309
        public static function flatten_block_list( $blocks ): array {
×
310
                $result = [];
×
311
                foreach ( $blocks as $block ) {
×
312
                        $result = array_merge( $result, self::flatten_inner_blocks( $block ) );
×
313
                }
314
                return $result;
×
315
        }
316

317
        /**
318
         * Flattens a block and its inner blocks into a single while attaching unique clientId's
319
         *
320
         * @param array<string,mixed> $block A parsed block.
321
         *
322
         * @return array<string,mixed> The flattened block.
323
         */
324
        private static function flatten_inner_blocks( $block ): array {
×
325
                $result = [];
×
326

327
                // Assign a unique clientId to the block if it doesn't already have one.
328
                $block['clientId'] = isset( $block['clientId'] ) ? $block['clientId'] : uniqid();
×
329
                array_push( $result, $block );
×
330

331
                foreach ( $block['innerBlocks'] as $child ) {
×
332
                        $child['parentClientId'] = $block['clientId'];
×
333

334
                        // Flatten the child, and merge with the result.
335
                        $result = array_merge( $result, self::flatten_inner_blocks( $child ) );
×
336
                }
337

338
                /** @var array<string,mixed> $result */
339
                return $result;
×
340
        }
341

342
        /**
343
         * Filters out disallowed blocks from the list of blocks
344
         *
345
         * @param array<string,mixed> $blocks A list of blocks to filter.
346
         * @param string[]            $allowed_block_names The list of allowed block names to filter.
347
         *
348
         * @return array<string,mixed> The filtered list of blocks.
349
         */
350
        private static function filter_allowed_blocks( array $blocks, array $allowed_block_names ): array {
30✔
351
                if ( empty( $allowed_block_names ) ) {
30✔
352
                        return $blocks;
30✔
353
                }
354

355
                return array_filter(
×
356
                        $blocks,
×
357
                        static function ( $block ) use ( $allowed_block_names ) {
×
358
                                return in_array( $block['blockName'], $allowed_block_names, true );
×
359
                        }
×
360
                );
×
361
        }
362
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc