Skip to content

Commit

Permalink
dev: refactor ContentBlocksResolver and deprecate TraverseHelpers (w…
Browse files Browse the repository at this point in the history
…pengine#279)

* fix: cleanup constants and refactor autoload handling

* dev: refactor ContentBlocksResolver and deprecate `TraverseHelpers`
  • Loading branch information
justlevine authored Sep 19, 2024
1 parent 4f4b851 commit 766737d
Show file tree
Hide file tree
Showing 15 changed files with 473 additions and 270 deletions.
5 changes: 5 additions & 0 deletions .changeset/giant-buttons-decide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@wpengine/wp-graphql-content-blocks": minor
---

fix: cleanup constants and refactor autoload handling to improve Composer compatibility.
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
2 changes: 1 addition & 1 deletion bin/versionPlugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ async function bumpStableTag(readmeTxt, version) {
async function bumpVersionConstant(pluginFile, version) {
return bumpVersion(
pluginFile,
/^\s*\$this->define\(\s*'WPGRAPHQL_CONTENT_BLOCKS_VERSION', '([0-9.]+)/gm,
/^\s*define\(\s*'WPGRAPHQL_CONTENT_BLOCKS_VERSION', '([0-9.]+)/gm,
version
);
}
Expand Down
125 changes: 125 additions & 0 deletions includes/Autoloader.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
<?php
/**
* Includes the Composer Autoloader used for packages and classes in the includes/ directory.
*
* @package WPGraphQL\ContentBlocks
* @since @todo
*/

declare( strict_types = 1 );

namespace WPGraphQL\ContentBlocks;

/**
* Class - Autoloader
*
* @internal
*/
class Autoloader {
/**
* Whether the autoloader has been loaded.
*
* @var bool
*/
protected static bool $is_loaded = false;

/**
* Attempts to autoload the Composer dependencies.
*/
public static function autoload(): bool {
// If we're not *supposed* to autoload anything, then return true.
if ( defined( 'WPGRAPHQL_CONTENT_BLOCKS_AUTOLOAD' ) && false === WPGRAPHQL_CONTENT_BLOCKS_AUTOLOAD ) {
return true;
}

// If the autoloader has already been loaded, then return true.
if ( self::$is_loaded ) {
return self::$is_loaded;
}

// If the main class has already been loaded, then they must be using a different autoloader.
if ( class_exists( 'WPGraphQLContentBlocks' ) ) {
return true;
}

$autoloader = dirname( __DIR__ ) . '/vendor/autoload.php';
self::$is_loaded = self::require_autoloader( $autoloader );

return self::$is_loaded;
}

/**
* Attempts to load the autoloader file, if it exists.
*
* @param string $autoloader_file The path to the autoloader file.
*/
private static function require_autoloader( string $autoloader_file ): bool {
if ( ! is_readable( $autoloader_file ) ) {
self::missing_autoloader_notice();
return false;
}

return (bool) require_once $autoloader_file; // phpcs:ignore WordPressVIPMinimum.Files.IncludingFile.UsingVariable -- Autoloader is a Composer file.
}

/**
* Displays a notice if the autoloader is missing.
*/
private static function missing_autoloader_notice(): void {
$error_message = __( 'WPGraphQL Content Blocks appears to have been installed without its dependencies. If you meant to download the source code, you can run `composer install` to install dependencies. If you are looking for the production version of the plugin, you can download it from the <a target="_blank" href="https://github.com/wpengine/wp-graphql-content-blocks/releases">GitHub Releases tab.</a>', 'wp-graphql-content-blocks' );

if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
error_log( // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- This is a development notice.
sprintf(
wp_kses(
$error_message,
[
'a' => [
'href' => [],
'target' => [],
],
]
)
)
);
}

$hooks = [
'admin_notices',
'network_admin_notices',
];

foreach ( $hooks as $hook ) {
add_action(
$hook,
static function () use ( $error_message ) {

// Only show the notice to admins.
if ( ! current_user_can( 'manage_options' ) ) {
return;
}

?>
<div class="error notice">
<p>
<?php
printf(
wp_kses(
$error_message,
[
'a' => [
'href' => [],
'target' => [],
],
]
)
)
?>
</p>
</div>
<?php
}
);
}
}
}
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 );
}
);
}
}
Loading

0 comments on commit 766737d

Please sign in to comment.