From 518af636d4e5032f78401b41303f83a49ddbded8 Mon Sep 17 00:00:00 2001 From: David Levine Date: Tue, 17 Sep 2024 11:31:45 +0000 Subject: [PATCH] dev: Refactor attribute resolution into `Data\BlockAttributeResolver` --- .changeset/odd-pens-rule.md | 5 + includes/Blocks/Block.php | 282 +++++------------------ includes/Data/BlockAttributeResolver.php | 184 +++++++++++++++ includes/Data/ContentBlocksResolver.php | 2 + phpstan-baseline.neon | 2 +- 5 files changed, 256 insertions(+), 219 deletions(-) create mode 100644 .changeset/odd-pens-rule.md create mode 100644 includes/Data/BlockAttributeResolver.php diff --git a/.changeset/odd-pens-rule.md b/.changeset/odd-pens-rule.md new file mode 100644 index 00000000..d4e2f159 --- /dev/null +++ b/.changeset/odd-pens-rule.md @@ -0,0 +1,5 @@ +--- +"@wpengine/wp-graphql-content-blocks": minor +--- + +dev: Refactor attribute resolution into `Data\BlockAttributeResolver` diff --git a/includes/Blocks/Block.php b/includes/Blocks/Block.php index 66a697fb..85360087 100644 --- a/includes/Blocks/Block.php +++ b/includes/Blocks/Block.php @@ -7,9 +7,9 @@ namespace WPGraphQL\ContentBlocks\Blocks; +use WPGraphQL\ContentBlocks\Data\BlockAttributeResolver; use WPGraphQL\ContentBlocks\Registry\Registry; use WPGraphQL\ContentBlocks\Type\Scalar\Scalar; -use WPGraphQL\ContentBlocks\Utilities\DOMHelpers; use WPGraphQL\ContentBlocks\Utilities\WPGraphQLHelpers; use WPGraphQL\Utils\Utils; use WP_Block_Type; @@ -81,44 +81,48 @@ private function register_block_type() { * Registers the block attributes GraphQL type and adds it as a field on the Block. */ private function register_block_attributes_as_fields(): void { - // Grab any additional block attributes attached into the class itself - if ( isset( $this->additional_block_attributes ) ) { - $block_attributes = ! empty( $this->block_attributes ) ? array_merge( $this->block_attributes, $this->additional_block_attributes ) : $this->additional_block_attributes; - } else { - $block_attributes = $this->block_attributes; - } + // Grab any additional block attributes attached into the class itself. + $block_attributes = array_merge( + $this->block_attributes ?? [], + $this->additional_block_attributes ?? [], + ); + $block_attribute_fields = $this->get_block_attribute_fields( $block_attributes, $this->type_name . 'Attributes' ); + + // Bail early if no attributes are defined. + if ( empty( $block_attribute_fields ) ) { + return; + } + // For each attribute, register a new object type and attach it to the block type as a field - if ( ! empty( $block_attribute_fields ) ) { - $block_attribute_type_name = $this->type_name . 'Attributes'; - register_graphql_object_type( - $block_attribute_type_name, - [ - 'description' => sprintf( - // translators: %s is the block type name. - __( 'Attributes of the %s Block Type', 'wp-graphql-content-blocks' ), - $this->type_name - ), - 'interfaces' => $this->get_block_attributes_interfaces(), - 'fields' => $block_attribute_fields, - ] - ); - register_graphql_field( - $this->type_name, - 'attributes', - [ - 'type' => $block_attribute_type_name, - 'description' => sprintf( - // translators: %s is the block type name. - __( 'Attributes of the %s Block Type', 'wp-graphql-content-blocks' ), - $this->type_name - ), - 'resolve' => static function ( $block ) { - return $block; - }, - ] - ); - }//end if + $block_attribute_type_name = $this->type_name . 'Attributes'; + register_graphql_object_type( + $block_attribute_type_name, + [ + 'description' => sprintf( + // translators: %s is the block type name. + __( 'Attributes of the %s Block Type', 'wp-graphql-content-blocks' ), + $this->type_name + ), + 'interfaces' => $this->get_block_attributes_interfaces(), + 'fields' => $block_attribute_fields, + ] + ); + register_graphql_field( + $this->type_name, + 'attributes', + [ + 'type' => $block_attribute_type_name, + 'description' => sprintf( + // translators: %s is the block type name. + __( 'Attributes of the %s Block Type', 'wp-graphql-content-blocks' ), + $this->type_name + ), + 'resolve' => static function ( $block ) { + return $block; + }, + ] + ); } /** @@ -189,19 +193,19 @@ private function get_attribute_type( $name, $attribute, $prefix ) { * @param string $prefix The current prefix string to use for the get_query_type */ private function get_block_attribute_fields( ?array $block_attributes, string $prefix = '' ): array { - $fields = []; - // Bail early if no attributes are defined. if ( null === $block_attributes ) { - return $fields; + return []; } + $fields = []; foreach ( $block_attributes as $attribute_name => $attribute_config ) { $graphql_type = $this->get_attribute_type( $attribute_name, $attribute_config, $prefix ); if ( empty( $graphql_type ) ) { continue; } + // Create the field config. $fields[ Utils::format_field_name( $attribute_name ) ] = [ 'type' => $graphql_type, @@ -318,31 +322,10 @@ private function normalize_attribute_value( $value, $type ) { } } - /** - * Gets the GraphQL interfaces that should be implemented by the block. - * - * @return string[] - */ - private function get_block_interfaces(): array { - return $this->block_registry->get_block_interfaces( $this->block->name ); - } - - /** - * Gets the GraphQL interfaces that should be implemented by the block attributes object. - * - * @return string[] - */ - private function get_block_attributes_interfaces(): array { - return $this->block_registry->get_block_attributes_interfaces( $this->block->name ); - } - /** * Register the Type for the block. This happens after all other object types are already registered. */ private function register_type(): void { - /** - * Register the Block Object Type to the Schema - */ register_graphql_object_type( $this->type_name, [ @@ -363,179 +346,42 @@ private function register_type(): void { } /** - * Resolved the value of the block attributes based on the specified config - * - * @param array $attributes The block current attributes value. - * @param string $html The block rendered html. - * @param array $config The block current attribute configuration, keyed to the attribute name. - */ - private function resolve_block_attributes_recursive( $attributes, string $html, array $config ): array { - $result = []; - - // Clean up the html. - $html = trim( $html ); - - foreach ( $config as $key => $value ) { - // Get default value. - $default = $value['default'] ?? null; - $source = $value['source'] ?? null; - - switch ( $source ) { - case 'rich-text': - case 'html': - // If there is no selector, we are dealing with single source. - if ( ! isset( $value['selector'] ) ) { - $result[ $key ] = $this->parse_single_source( $html, $source ); - break; - } - - $result[ $key ] = $this->parse_html_source( $html, $value ); - break; - case 'attribute': - $result[ $key ] = $this->parse_attribute_source( $html, $value ); - break; - case 'text': - $result[ $key ] = $this->parse_text_source( $html, $value ); - break; - case 'query': - $result[ $key ] = $this->parse_query_source( $html, $value, $attributes ); - break; - } - - // Post processing of return value based on configured type - if ( array_key_exists( $key, $result ) ) { - switch ( $value['type'] ) { - case 'integer': - $result[ $key ] = intval( $result[ $key ] ); - break; - case 'boolean': - if ( false === $result[ $key ] ) { - break; - } - if ( is_null( $result[ $key ] ) ) { - $result[ $key ] = false; - break; - } - $result[ $key ] = true; - break; - } - } - - // Fallback to the attributes or default value if the result is empty. - if ( empty( $result[ $key ] ) ) { - $result[ $key ] = $attributes[ $key ] ?? $default; - } - } - return $result; - } - - /** - * Parses the block content of a source only block type - * - * @param string $html The html value - * @param string $source The source type - */ - private function parse_single_source( string $html, $source ): ?string { - if ( empty( $html ) ) { - return null; - } - - switch ( $source ) { - case 'html': - return DOMHelpers::find_nodes( $html )->innerHTML(); - } - - return null; - } - - /** - * Parses the block content of an HTML source block type. - * - * Includes `multiline` handling. - * - * @param string $html The html value. - * @param array $value The value configuration. - */ - private function parse_html_source( string $html, $value ): ?string { - if ( empty( $html ) || ! isset( $value['selector'] ) ) { - return null; - } - - $result = DOMHelpers::parse_html( $html, $value['selector'] ); - - // Multiline values are located somewhere else. - if ( isset( $value['multiline'] ) && ! empty( $result ) ) { - $result = DOMHelpers::get_elements_from_html( $result, $value['multiline'] ); - } - - return $result; - } - - /** - * Parses an attribute source block type. + * Gets the GraphQL interfaces that should be implemented by the block. * - * @param string $html The html value. - * @param array $value The value configuration. + * @return string[] */ - private function parse_attribute_source( string $html, $value ): ?string { - if ( empty( $html ) || ! isset( $value['selector'] ) || ! isset( $value['attribute'] ) ) { - return null; - } - - return DOMHelpers::parse_attribute( $html, $value['selector'], $value['attribute'] ); + private function get_block_interfaces(): array { + return $this->block_registry->get_block_interfaces( $this->block->name ); } /** - * Parses a text source block type. + * Gets the GraphQL interfaces that should be implemented by the block attributes object. * - * @param string $html The html value. - * @param array $value The value configuration. + * @return string[] */ - private function parse_text_source( string $html, $value ): ?string { - if ( ! isset( $value['selector'] ) ) { - return null; - } - - return DOMHelpers::parse_text( $html, $value['selector'] ); + private function get_block_attributes_interfaces(): array { + return $this->block_registry->get_block_attributes_interfaces( $this->block->name ); } /** - * Parses a query source block type. - * - * @param string $html The html value. - * @param array $value The value configuration. - * @param array $attributes The block attributes. + * Resolved the value of the block attributes based on the specified config * - * @return ?mixed[] + * @param array $attribute_values The block current attributes value. + * @param string $html The block rendered html. + * @param array $attribute_configs The block current attribute configuration, keyed to the attribute name. */ - private function parse_query_source( string $html, $value, $attributes ): ?array { - if ( ! isset( $value['selector'] ) || ! isset( $value['query'] ) ) { - return null; - } - - $nodes = DOMHelpers::find_nodes( $html, $value['selector'] ); - - // Coerce nodes to an array if it's not already. - if ( ! is_array( $nodes ) ) { - $nodes = [ $nodes ]; - } + private function resolve_block_attributes_recursive( $attribute_values, string $html, array $attribute_configs ): array { + $result = []; - $temp = []; - $results = []; - foreach ( $nodes as $source_node ) { - foreach ( $value['query'] as $q_key => $q_value ) { - /** @var array $temp_config */ - $temp_config = [ - $q_key => $q_value, - ]; + // Clean up the html. + $html = trim( $html ); - $res = $this->resolve_block_attributes_recursive( $attributes, $source_node->html(), $temp_config ); - $temp[ $q_key ] = $res[ $q_key ]; - } + foreach ( $attribute_configs as $key => $config ) { + $attribute_value = $attribute_values[ $key ] ?? null; - $results[] = $temp; + $result[ $key ] = BlockAttributeResolver::resolve_block_attribute( $config, $html, $attribute_value ); } - return $results; + return $result; } } diff --git a/includes/Data/BlockAttributeResolver.php b/includes/Data/BlockAttributeResolver.php new file mode 100644 index 00000000..241c78d9 --- /dev/null +++ b/includes/Data/BlockAttributeResolver.php @@ -0,0 +1,184 @@ + $attribute The configuration for the specific attribute. + * @param string $html The block rendered html. + * @param mixed $attribute_value The value from the parsed block attributes. + * + * @return mixed + */ + public static function resolve_block_attribute( $attribute, string $html, $attribute_value ) { + $value = null; + + if ( isset( $attribute['source'] ) ) { + switch ( $attribute['source'] ) { + case 'attribute': + $value = self::parse_attribute_source( $html, $attribute ); + break; + case 'html': + case 'rich-text': + // If there is no selector, we are dealing with single source. + if ( ! isset( $attribute['selector'] ) ) { + $value = self::parse_single_source( $html, $attribute['source'] ); + break; + } + $value = self::parse_html_source( $html, $attribute ); + break; + case 'text': + $value = self::parse_text_source( $html, $attribute ); + break; + case 'query': + $value = self::parse_query_source( $html, $attribute, $attribute_value ); + break; + } + + // Sanitize the value type. + if ( isset( $attribute['type'] ) ) { + switch ( $attribute['type'] ) { + case 'integer': + $value = intval( $value ); + break; + case 'boolean': + $value = ! empty( $value ); + break; + } + } + } + + // Fallback to the attributes or default value if the result is empty. + if ( empty( $value ) ) { + $default = $attribute['default'] ?? null; + + $value = $attribute_value ?? $default; + } + + return $value; + } + + /** + * Parses the block content of a source only block type + * + * @param string $html The html value + * @param string $source The source type + */ + private static function parse_single_source( string $html, $source ): ?string { + if ( empty( $html ) ) { + return null; + } + + switch ( $source ) { + case 'html': + return DOMHelpers::find_nodes( $html )->innerHTML(); + } + + return null; + } + + /** + * Parses the block content of an HTML source block type. + * + * Includes `multiline` handling. + * + * @param string $html The html value. + * @param array $config The value configuration. + */ + private static function parse_html_source( string $html, array $config ): ?string { + if ( empty( $html ) || ! isset( $config['selector'] ) ) { + return null; + } + + $result = DOMHelpers::parse_html( $html, $config['selector'] ); + + // Multiline values are located somewhere else. + if ( isset( $config['multiline'] ) && ! empty( $result ) ) { + $result = DOMHelpers::get_elements_from_html( $result, $config['multiline'] ); + } + + return $result; + } + + /** + * Parses an attribute source block type. + * + * @param string $html The html value. + * @param array $config The value configuration. + */ + private static function parse_attribute_source( string $html, array $config ): ?string { + if ( empty( $html ) || ! isset( $config['selector'] ) || ! isset( $config['attribute'] ) ) { + return null; + } + + return DOMHelpers::parse_attribute( $html, $config['selector'], $config['attribute'] ); + } + + /** + * Parses a text source block type. + * + * @param string $html The html value. + * @param array $config The value configuration. + */ + private static function parse_text_source( string $html, $config ): ?string { + if ( ! isset( $config['selector'] ) ) { + return null; + } + + return DOMHelpers::parse_text( $html, $config['selector'] ); + } + + /** + * Parses a query source block type. + * + * @param string $html The html value. + * @param array $config The value configuration. + * @param array $attribute_values The attribute values for the block. + * + * @return ?mixed[] + */ + private static function parse_query_source( string $html, array $config, array $attribute_values ): ?array { + if ( ! isset( $config['selector'] ) || ! isset( $config['query'] ) ) { + return null; + } + + $nodes = DOMHelpers::find_nodes( $html, $config['selector'] ); + + // Coerce nodes to an array if it's not already. + if ( ! is_array( $nodes ) ) { + $nodes = [ $nodes ]; + } + + $results = []; + foreach ( $nodes as $source_node ) { + // Holds the results for each query. + $temp = []; + + foreach ( $config['query'] as $q_key => $q_value ) { + $attribute_value = $attribute_values[ $q_key ] ?? null; + + $res = self::resolve_block_attribute( $q_value, $source_node->html(), $attribute_value ); + + $temp[ $q_key ] = $res; + } + + $results[] = $temp; + } + + return $results; + } +} diff --git a/includes/Data/ContentBlocksResolver.php b/includes/Data/ContentBlocksResolver.php index a7493b3e..fa1e75e0 100644 --- a/includes/Data/ContentBlocksResolver.php +++ b/includes/Data/ContentBlocksResolver.php @@ -19,6 +19,8 @@ final class ContentBlocksResolver { * @param \WPGraphQL\Model\Model|mixed $node The node we are resolving. * @param array $args GraphQL query args to pass to the connection resolver. * @param string[] $allowed_block_names The list of allowed block names to filter. + * + * @return array The resolved parsed blocks. */ public static function resolve_content_blocks( $node, $args, $allowed_block_names = [] ): array { /** diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index cfa731d0..d9db91f2 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -3,7 +3,7 @@ parameters: - message: "#^Cannot call method innerHTML\\(\\) on array\\\\|DiDom\\\\Element\\.$#" count: 1 - path: includes/Blocks/Block.php + path: includes/Data/BlockAttributeResolver.php - message: "#^Access to an undefined property object\\:\\:\\$response\\.$#"