Skip to content

Commit

Permalink
dev: refactor ContentBlocksResolver and deprecate TraverseHelpers
Browse files Browse the repository at this point in the history
  • Loading branch information
justlevine committed Sep 16, 2024
1 parent 196893f commit f0860a6
Show file tree
Hide file tree
Showing 4 changed files with 168 additions and 138 deletions.
2 changes: 1 addition & 1 deletion .phpcs.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
Tests for WordPress version compatibility.
https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards/wiki/Customizable-sniff-properties
-->
<config name="minimum_supported_wp_version" value="5.3"/>
<config name="minimum_supported_wp_version" value="5.7"/>

<!-- Rules: WPGraphQL Coding Standards -->
<!-- https://github.com/AxeWP/WPGraphQL-Coding-Standards/WPGraphQL/ruleset.xml -->
Expand Down
195 changes: 147 additions & 48 deletions includes/Data/ContentBlocksResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@

namespace WPGraphQL\ContentBlocks\Data;

use WPGraphQL\ContentBlocks\Utilities\TraverseHelpers;
use WPGraphQL\Model\Post;

/**
Expand All @@ -17,9 +16,9 @@ final class ContentBlocksResolver {
/**
* Retrieves a list of content blocks
*
* @param mixed $node The node we are resolving.
* @param array $args GraphQL query args to pass to the connection resolver.
* @param array $allowed_block_names The list of allowed block names to filter.
* @param \WPGraphQL\Model\Model|mixed $node The node we are resolving.
* @param array<string,mixed> $args GraphQL query args to pass to the connection resolver.
* @param string[] $allowed_block_names The list of allowed block names to filter.
*/
public static function resolve_content_blocks( $node, $args, $allowed_block_names = [] ): array {
/**
Expand Down Expand Up @@ -64,57 +63,18 @@ public static function resolve_content_blocks( $node, $args, $allowed_block_name
}

// Parse the blocks from HTML comments to an array of blocks
$parsed_blocks = parse_blocks( $content );
$parsed_blocks = self::parse_blocks( $content );
if ( empty( $parsed_blocks ) ) {
return [];
}

// 1st Level filtering of blocks that are empty
$parsed_blocks = array_filter(
$parsed_blocks,
static function ( $parsed_block ) {
if ( ! empty( $parsed_block['blockName'] ) ) {
return true;
}

// Strip empty comments and spaces
$stripped = preg_replace( '/<!--(.*)-->/Uis', '', render_block( $parsed_block ) );
return ! empty( trim( $stripped ?? '' ) );
},
ARRAY_FILTER_USE_BOTH
);

// 2nd Level assigning of unique id's and missing blockNames
$parsed_blocks = array_map(
static function ( $parsed_block ) {
$parsed_block['clientId'] = uniqid();
// Since Gutenberg assigns an empty blockName for Classic block
// we define the name here
if ( empty( $parsed_block['blockName'] ) ) {
$parsed_block['blockName'] = 'core/freeform';
}
return $parsed_block;
},
$parsed_blocks
);

// Resolve reusable blocks - replaces "core/block" with the corresponding block(s) from the reusable ref ID
TraverseHelpers::traverse_blocks( $parsed_blocks, [ TraverseHelpers::class, 'replace_reusable_blocks' ], 0, PHP_INT_MAX );
// Flatten block list here if requested or if 'flat' value is not selected (default)
if ( ! isset( $args['flat'] ) || 'true' == $args['flat'] ) { // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual
$parsed_blocks = self::flatten_block_list( $parsed_blocks );
}

// Final level of filtering out blocks not in the allowed list
if ( ! empty( $allowed_block_names ) ) {
$parsed_blocks = array_filter(
$parsed_blocks,
static function ( $parsed_block ) use ( $allowed_block_names ) {
return in_array( $parsed_block['blockName'], $allowed_block_names, true );
},
ARRAY_FILTER_USE_BOTH
);
}
$parsed_blocks = self::filter_allowed_blocks( $parsed_blocks, $allowed_block_names );

/**
* Filters the content blocks after they have been resolved.
Expand All @@ -129,6 +89,120 @@ static function ( $parsed_block ) use ( $allowed_block_names ) {
return is_array( $parsed_blocks ) ? $parsed_blocks : [];
}

/**
* Get blocks from html string.
*
* @param string $content Content to parse.
*
* @return array<string,mixed> List of blocks.
*/
private static function parse_blocks( $content ): array {
$blocks = parse_blocks( $content );

return self::handle_do_blocks( $blocks );
}

/**
* Recursively process blocks.
*
* This mirrors the `do_blocks` function in WordPress which is responsible for hydrating certain block attributes and supports, but without the forced rendering.
*
* @param array<string,mixed>[] $blocks Blocks data.
*
* @return array<string,mixed>[] The processed blocks.
*/
private static function handle_do_blocks( array $blocks ): array {
$parsed = [];
foreach ( $blocks as $block ) {
$block_data = self::handle_do_block( $block );

if ( $block_data ) {
$parsed[] = $block_data;
}
}

// Remove empty blocks.
return array_filter( $parsed );
}

/**
* Process a block, getting all extra fields.
*
* @param array<string,mixed> $block Block data.
*
* @return ?array<string,mixed> The processed block.
*/
private static function handle_do_block( array $block ): ?array {
if ( self::is_block_empty( $block ) ) {
return null;
}

// Since Gutenberg assigns an empty blockName for Classic block, we define it here.
if ( empty( $block['blockName'] ) ) {
$block['blockName'] = 'core/freeform';
}

// Assign a unique clientId to the block.
$block['clientId'] = uniqid();

// @todo apply more hydrations.

$block = self::populate_reusable_blocks( $block );

// Prepare innerBlocks.
if ( ! empty( $block['innerBlocks'] ) ) {
$block['innerBlocks'] = self::handle_do_blocks( $block['innerBlocks'] );
}

return $block;
}

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

// @todo add more checks and avoid using render_block().

// Strip empty comments and spaces
$stripped = preg_replace( '/<!--(.*)-->/Uis', '', render_block( $block ) );

return empty( trim( $stripped ?? '' ) );
}

/**
* Populates reusable blocks with the blocks from the reusable ref ID.
*
* @param array<string,mixed> $block The block to populate.
*
* @return array<string,mixed> The populated block.
*/
private static function populate_reusable_blocks( array $block ): array {
if ( 'core/block' !== $block['blockName'] || ! isset( $block['attrs']['ref'] ) ) {
return $block;
}

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

if ( ! $reusable_block ) {
return $block;
}

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

if ( empty( $parsed_blocks ) ) {
return $block;
}

return array_merge( ...$parsed_blocks );
}

/**
* Flattens a list blocks into a single array
*
Expand All @@ -145,16 +219,41 @@ private static function flatten_block_list( $blocks ): array {
/**
* Flattens a block and its inner blocks into a single while attaching unique clientId's
*
* @param mixed $block A block.
* @param array<string,mixed> $block A parsed block.
*/
private static function flatten_inner_blocks( $block ): array {
$result = [];
$result = [];

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

foreach ( $block['innerBlocks'] as $child ) {
$child['parentClientId'] = $block['clientId'];
$result = array_merge( $result, self::flatten_inner_blocks( $child ) );

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

return $result;
}

/**
* Filters out disallowed blocks from the list of blocks
*
* @param array<string,mixed> $blocks A list of blocks to filter.
* @param string[] $allowed_block_names The list of allowed block names to filter.
*/
private static function filter_allowed_blocks( array $blocks, array $allowed_block_names ): array {
if ( empty( $allowed_block_names ) ) {
return $blocks;
}

return array_filter(
$blocks,
static function ( $block ) use ( $allowed_block_names ) {
return in_array( $block['blockName'], $allowed_block_names, true );
}
);
}
}
20 changes: 20 additions & 0 deletions includes/Utilities/TraverseHelpers.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,37 @@

namespace WPGraphQL\ContentBlocks\Utilities;

use WPGraphQL\ContentBlocks\Data\ContentBlocksResolver;

/**
* Class TraverseHelpers
*
* Provides utility functions to traverse and manipulate blocks.
*
* @deprecated @todo Blocks should be manipulated directly inside ContentBlocksResolver::handle_do_block()
*/
final class TraverseHelpers {
/**
* Traverse blocks and apply a callback with optional depth limit.
*
* @deprecated @todo Blocks should be manipulated directly inside ContentBlocksResolver::handle_do_block()
*
* @param array &$blocks The blocks to traverse.
* @param callable $callback The callback function to apply to each block.
* @param int $depth The current depth of traversal.
* @param int $max_depth The maximum depth to traverse.
*/
public static function traverse_blocks( &$blocks, $callback, $depth = 0, $max_depth = PHP_INT_MAX ): void {
_deprecated_function(
__METHOD__,
'@todo',
sprintf(
// translators: %s: class name
esc_html__( 'Manipulate blocks directly inside %s::handle_do_block', 'wp-graphql-content-blocks' ),
ContentBlocksResolver::class
)
);

foreach ( $blocks as &$block ) {
$callback( $block );
if ( ! empty( $block['innerBlocks'] ) && $depth < $max_depth ) {
Expand All @@ -33,9 +49,13 @@ public static function traverse_blocks( &$blocks, $callback, $depth = 0, $max_de
/**
* Example callback function to replace reusable blocks.
*
* @deprecated @todo Blocks should be manipulated directly inside ContentBlocksResolver::handle_do_block()
*
* @param array $block The block to potentially replace.
*/
public static function replace_reusable_blocks( &$block ): void {
_deprecated_function( __METHOD__, '@todo', ContentBlocksResolver::class . '::populate_reusable_blocks' );

if ( 'core/block' === $block['blockName'] && isset( $block['attrs']['ref'] ) ) {
$post = get_post( $block['attrs']['ref'] );
$reusable_blocks = ! empty( $post->post_content ) ? parse_blocks( $post->post_content ) : null;
Expand Down
89 changes: 0 additions & 89 deletions tests/unit/TraverseHelpersTest.php

This file was deleted.

0 comments on commit f0860a6

Please sign in to comment.