diff --git a/.changeset/brave-dragons-laugh.md b/.changeset/brave-dragons-laugh.md new file mode 100644 index 00000000..dfd6d70e --- /dev/null +++ b/.changeset/brave-dragons-laugh.md @@ -0,0 +1,58 @@ +--- +"@wpengine/wp-graphql-content-blocks": minor +--- + +Adds support for specifying typed and queryable properties for object attributes in block.json. + +Example: Defining a Typed Object in `block.json`: + +```json +"attributes": { + "film": { + "type": "object", + "default": { + "id": 0, + "title": "Film Title", + "director": "Director Name", + "__typed": { + "id": "integer", + "title": "string", + "director": "string", + "year": "string" + } + } + } +} +``` + +In this example, the `film` attribute is an object with defined types for each property (`id`, `title`, `director`, and optionally `year`). + +Querying Object Properties in GraphQL: + + +```graphql +fragment Film on MyPluginFilmBlock { + attributes { + film { + id, + title, + director, + year + } + }, +} + +query GetAllPostsWhichSupportBlockEditor { + posts { + edges { + node { + editorBlocks { + __typename + name + ...Film + } + } + } + } +} +``` \ No newline at end of file diff --git a/README.md b/README.md index 55235280..304194ab 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,64 @@ If the resolved block has values for those fields, it will return them, otherwis } ``` +## Querying Object Types with Typed Properties + +You can now query object-type block attributes with each property, provided the `__typed` structure is defined in the block's JSON configuration. +This allows you to query individual properties of the object for finer control and enhanced usability. + +Example: Defining a Typed Object in `block.json`: + +```json +"attributes": { + "film": { + "type": "object", + "default": { + "id": 0, + "title": "Film Title", + "director": "Director Name", + "__typed": { + "id": "integer", + "title": "string", + "director": "string", + "year": "string" + } + } + } +} +``` + +In this example, the `film` attribute is an object with defined types for each property (`id`, `title`, `director`, and optionally `year`). + +Querying Object Properties in GraphQL: + + +```graphql +fragment Film on MyPluginFilmBlock { + attributes { + film { + id, + title, + director, + year + } + }, +} + +query GetAllPostsWhichSupportBlockEditor { + posts { + edges { + node { + editorBlocks { + __typename + name + ...Film + } + } + } + } +} +``` + ## What about innerBlocks? In order to facilitate querying `innerBlocks` fields more efficiently you want to use `editorBlocks(flat: true)` instead of `editorBlocks`. diff --git a/includes/Blocks/Block.php b/includes/Blocks/Block.php index 94258282..b1a26edf 100644 --- a/includes/Blocks/Block.php +++ b/includes/Blocks/Block.php @@ -19,7 +19,7 @@ */ class Block { /** - * The Block Type + * The Block Type. * * @var \WP_Block_Type */ @@ -40,18 +40,18 @@ class Block { protected Registry $block_registry; /** - * The attributes of the block + * The attributes of the block. * - * @var array|null + * @var array */ - protected ?array $block_attributes; + protected array $block_attributes = []; /** - * Any Additional attributes of the block not defined in block.json + * Any Additional attributes of the block not defined in block.json. * - * @var array|null + * @var array */ - protected ?array $additional_block_attributes; + protected array $additional_block_attributes = []; /** * Block constructor. @@ -62,7 +62,7 @@ class Block { public function __construct( WP_Block_Type $block, Registry $block_registry ) { $this->block = $block; $this->block_registry = $block_registry; - $this->block_attributes = $this->block->attributes; + $this->block_attributes = array_merge( $this->block->attributes ?? [], $this->additional_block_attributes ); $this->type_name = WPGraphQLHelpers::format_type_name( $block->name ); $this->register_block_type(); } @@ -81,21 +81,14 @@ 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. - $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' ); + $block_attribute_type_name = $this->type_name . 'Attributes'; + $block_attribute_fields = $this->get_block_attribute_fields( $block_attribute_type_name ); - // 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 - $block_attribute_type_name = $this->type_name . 'Attributes'; register_graphql_object_type( $block_attribute_type_name, [ @@ -126,7 +119,7 @@ private function register_block_attributes_as_fields(): void { } /** - * Returns the type of the block attribute + * Returns the type of the block attribute. * * @param string $name The block name * @param array $attribute The block attribute config @@ -137,50 +130,56 @@ private function register_block_attributes_as_fields(): void { private function get_attribute_type( $name, $attribute, $prefix ) { $type = null; - if ( isset( $attribute['type'] ) ) { - switch ( $attribute['type'] ) { - case 'rich-text': - case 'string': - $type = 'String'; - break; - case 'boolean': - $type = 'Boolean'; - break; - case 'number': - $type = 'Float'; - break; - case 'integer': - $type = 'Int'; - break; - case 'array': - if ( isset( $attribute['query'] ) ) { - $type = [ 'list_of' => $this->get_query_type( $name, $attribute['query'], $prefix ) ]; - } elseif ( isset( $attribute['items'] ) ) { - $of_type = $this->get_attribute_type( $name, $attribute['items'], $prefix ); - - if ( null !== $of_type ) { - $type = [ 'list_of' => $of_type ]; - } else { - $type = Scalar::get_block_attributes_array_type_name(); - } - } else { - $type = Scalar::get_block_attributes_array_type_name(); - } - break; - case 'object': - $type = Scalar::get_block_attributes_object_type_name(); - break; + if ( ! isset( $attribute['type'] ) ) { + if ( ! isset( $attribute['source'] ) ) { + return null; // No type or source defined, return null. } - } elseif ( isset( $attribute['source'] ) ) { - $type = 'String'; + + $type = 'String'; // Default to String if only 'source' is defined. } - if ( null !== $type ) { - $default_value = $attribute['default'] ?? null; + switch ( $attribute['type'] ) { + case 'rich-text': + case 'string': + $type = 'String'; + break; + case 'boolean': + $type = 'Boolean'; + break; + case 'number': + $type = 'Float'; + break; + case 'integer': + $type = 'Int'; + break; + case 'array': + // Process attributes query. + if ( isset( $attribute['query'] ) ) { + $type = [ 'list_of' => $this->create_and_register_inner_object_type( $name, $attribute['query'], $prefix ) ]; + break; + } + + // Process scalar array or list of items. + $of_type = null; + if ( isset( $attribute['items'] ) ) { + $of_type = $this->get_attribute_type( $name, $attribute['items'], $prefix ); + } + + $type = null !== $of_type ? [ 'list_of' => $of_type ] : Scalar::get_block_attributes_array_type_name(); + break; + case 'object': + // Proceed with the typed object if typing provided, otherwise continue with scalar object. + $typed = []; + if ( ! empty( $attribute['default']['__typed'] ) ) { + $typed = $this->build_typed_object_config( $attribute['default'] ); + } + + $type = $typed ? $this->create_and_register_inner_object_type( $name, $typed, $prefix ) : Scalar::get_block_attributes_object_type_name(); + break; + } - if ( isset( $default_value ) ) { - $type = [ 'non_null' => $type ]; - } + if ( null !== $type && isset( $attribute['default'] ) ) { + $type = [ 'non_null' => $type ]; } return $type; @@ -189,17 +188,11 @@ private function get_attribute_type( $name, $attribute, $prefix ) { /** * Gets the WPGraphQL field registration config for the block attributes. * - * @param ?array $block_attributes The block attributes. * @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 { - // Bail early if no attributes are defined. - if ( null === $block_attributes ) { - return []; - } - + private function get_block_attribute_fields( string $prefix = '' ): array { $fields = []; - foreach ( $block_attributes as $attribute_name => $attribute_config ) { + foreach ( $this->block_attributes as $attribute_name => $attribute_config ) { $graphql_type = $this->get_attribute_type( $attribute_name, $attribute_config, $prefix ); if ( empty( $graphql_type ) ) { @@ -224,29 +217,28 @@ private function get_block_attribute_fields( ?array $block_attributes, string $p return $result[ $attribute_name ]; }, ]; - }//end foreach + } return $fields; } /** - * Returns the type of the block query attribute + * Dynamically creates and registers a GraphQL object type for queries or typed object attributes. * - * @param string $name The block name - * @param array $query The block query config - * @param string $prefix The current prefix string to use for registering the new query attribute type + * @param string $name The block name. + * @param array $config The block config. + * @param string $prefix The current prefix string to use for registering the new attribute type. */ - private function get_query_type( string $name, array $query, string $prefix ): string { - $type = $prefix . ucfirst( $name ); - - $fields = $this->create_attributes_fields( $query, $type ); + private function create_and_register_inner_object_type( string $name, array $config, string $prefix ): string { + $type = $prefix . ucfirst( $name ); + $fields = $this->create_attributes_fields( $config, $type ); register_graphql_object_type( $type, [ 'fields' => $fields, 'description' => sprintf( - // translators: %1$s is the attribute name, %2$s is the block attributes field. + // translators: %1$s is the attribute name, %2$s is the block attributes field. __( 'The "%1$s" field on the "%2$s" block attribute field', 'wp-graphql-content-blocks' ), $type, $prefix @@ -258,34 +250,58 @@ private function get_query_type( string $name, array $query, string $prefix ): s } /** - * Creates the new attribute fields for query types + * Verifies if the default record has a typed object configuration for the object type and generates config for it. + * + * @param array $default_attribute Default record of the attribute. + */ + private function build_typed_object_config( $default_attribute ): array { + if ( ! is_array( $default_attribute['__typed'] ) ) { + return []; + } + + return array_combine( + array_keys( $default_attribute['__typed'] ), + array_map( + static fn ( $key ) => [ + 'type' => $default_attribute['__typed'][ $key ], + 'default' => $default_attribute[ $key ] ?? null, + ], + array_keys( $default_attribute['__typed'] ) + ) + ) ?: []; + } + + /** + * Creates the new attribute fields for inner block types. * - * @param array $attributes The query attributes config - * @param string $prefix The current prefix string to use for registering the new query attribute type + * @param array $attributes The attributes config. + * @param string $prefix The current prefix string to use for registering the new inner block attribute type. */ private function create_attributes_fields( $attributes, $prefix ): array { $fields = []; foreach ( $attributes as $name => $attribute ) { $type = $this->get_attribute_type( $name, $attribute, $prefix ); - if ( isset( $type ) ) { - $default_value = $attribute['default'] ?? null; - - $fields[ Utils::format_field_name( $name ) ] = [ - 'type' => $type, - 'description' => sprintf( - // translators: %1$s is the attribute name, %2$s is the block attributes field. - __( 'The "%1$s" field on the "%2$s" block attribute field', 'wp-graphql-content-blocks' ), - $name, - $prefix - ), - 'resolve' => function ( $attributes ) use ( $name, $default_value, $type ) { - $value = $attributes[ $name ] ?? $default_value; - - return $this->normalize_attribute_value( $value, $type ); - }, - ]; + if ( ! isset( $type ) ) { + continue; } + + $default_value = $attribute['default'] ?? null; + + $fields[ Utils::format_field_name( $name ) ] = [ + 'type' => $type, + 'description' => sprintf( + // translators: %1$s is the attribute name, %2$s is the block attributes field. + __( 'The "%1$s" field on the "%2$s" block attribute field', 'wp-graphql-content-blocks' ), + $name, + $prefix + ), + 'resolve' => function ( $attributes ) use ( $name, $default_value, $type ) { + $value = $attributes[ $name ] ?? $default_value; + + return $this->normalize_attribute_value( $value, $type ); + }, + ]; } return $fields; diff --git a/includes/Blocks/CoreButton.php b/includes/Blocks/CoreButton.php index c9cef2ce..59aea11c 100644 --- a/includes/Blocks/CoreButton.php +++ b/includes/Blocks/CoreButton.php @@ -14,9 +14,9 @@ class CoreButton extends Block { /** * {@inheritDoc} * - * @var array|null + * @var array */ - protected ?array $additional_block_attributes = [ + protected array $additional_block_attributes = [ 'cssClassName' => [ 'type' => 'string', 'selector' => 'div', diff --git a/includes/Blocks/CoreButtons.php b/includes/Blocks/CoreButtons.php index 851e8c13..1492155c 100644 --- a/includes/Blocks/CoreButtons.php +++ b/includes/Blocks/CoreButtons.php @@ -14,9 +14,9 @@ class CoreButtons extends Block { /** * {@inheritDoc} * - * @var array|null + * @var array */ - protected ?array $additional_block_attributes = [ + protected array $additional_block_attributes = [ 'cssClassName' => [ 'type' => 'string', 'selector' => 'div', diff --git a/includes/Blocks/CoreCode.php b/includes/Blocks/CoreCode.php index cf8659aa..fe69c39b 100644 --- a/includes/Blocks/CoreCode.php +++ b/includes/Blocks/CoreCode.php @@ -14,9 +14,9 @@ class CoreCode extends Block { /** * {@inheritDoc} * - * @var array|null + * @var array */ - protected ?array $additional_block_attributes = [ + protected array $additional_block_attributes = [ 'cssClassName' => [ 'type' => 'string', 'selector' => 'pre', diff --git a/includes/Blocks/CoreColumn.php b/includes/Blocks/CoreColumn.php index fca22e1d..0ddd117a 100644 --- a/includes/Blocks/CoreColumn.php +++ b/includes/Blocks/CoreColumn.php @@ -14,9 +14,9 @@ class CoreColumn extends Block { /** * {@inheritDoc} * - * @var array|null + * @var array */ - protected ?array $additional_block_attributes = [ + protected array $additional_block_attributes = [ 'cssClassName' => [ 'type' => 'string', 'selector' => 'div', diff --git a/includes/Blocks/CoreColumns.php b/includes/Blocks/CoreColumns.php index 7f982b38..cb38944a 100644 --- a/includes/Blocks/CoreColumns.php +++ b/includes/Blocks/CoreColumns.php @@ -14,9 +14,9 @@ class CoreColumns extends Block { /** * {@inheritDoc} * - * @var array|null + * @var array */ - protected ?array $additional_block_attributes = [ + protected array $additional_block_attributes = [ 'cssClassName' => [ 'type' => 'string', 'selector' => 'div', diff --git a/includes/Blocks/CoreGroup.php b/includes/Blocks/CoreGroup.php index 96187215..27b52943 100644 --- a/includes/Blocks/CoreGroup.php +++ b/includes/Blocks/CoreGroup.php @@ -16,9 +16,9 @@ class CoreGroup extends Block { * * Note that no selector is set as it can be a variety of selectors * - * @var array|null + * @var array */ - protected ?array $additional_block_attributes = [ + protected array $additional_block_attributes = [ 'cssClassName' => [ 'type' => 'string', 'source' => 'attribute', diff --git a/includes/Blocks/CoreHeading.php b/includes/Blocks/CoreHeading.php index 480745b2..bb1e14ba 100644 --- a/includes/Blocks/CoreHeading.php +++ b/includes/Blocks/CoreHeading.php @@ -14,9 +14,9 @@ class CoreHeading extends Block { /** * {@inheritDoc} * - * @var array|null + * @var array */ - protected ?array $additional_block_attributes = [ + protected array $additional_block_attributes = [ 'cssClassName' => [ 'type' => 'string', 'selector' => 'h1, h2, h3, h4, h5, h6', diff --git a/includes/Blocks/CoreImage.php b/includes/Blocks/CoreImage.php index caf6c81a..8461f06e 100644 --- a/includes/Blocks/CoreImage.php +++ b/includes/Blocks/CoreImage.php @@ -17,9 +17,9 @@ class CoreImage extends Block { /** * {@inheritDoc} * - * @var array|null + * @var array */ - protected ?array $additional_block_attributes = [ + protected array $additional_block_attributes = [ 'cssClassName' => [ 'type' => 'string', 'selector' => 'figure', diff --git a/includes/Blocks/CoreList.php b/includes/Blocks/CoreList.php index de33f114..ac0d773e 100644 --- a/includes/Blocks/CoreList.php +++ b/includes/Blocks/CoreList.php @@ -14,9 +14,9 @@ class CoreList extends Block { /** * {@inheritDoc} * - * @var array|null + * @var array */ - protected ?array $additional_block_attributes = [ + protected array $additional_block_attributes = [ 'cssClassName' => [ 'type' => 'string', 'selector' => 'ul,ol', diff --git a/includes/Blocks/CoreParagraph.php b/includes/Blocks/CoreParagraph.php index 5b1fd7ee..d06865e8 100644 --- a/includes/Blocks/CoreParagraph.php +++ b/includes/Blocks/CoreParagraph.php @@ -14,9 +14,9 @@ class CoreParagraph extends Block { /** * {@inheritDoc} * - * @var array|null + * @var array */ - protected ?array $additional_block_attributes = [ + protected array $additional_block_attributes = [ 'cssClassName' => [ 'type' => 'string', 'selector' => 'p', diff --git a/includes/Blocks/CoreQuote.php b/includes/Blocks/CoreQuote.php index 6774cb30..c1c2b806 100644 --- a/includes/Blocks/CoreQuote.php +++ b/includes/Blocks/CoreQuote.php @@ -14,9 +14,9 @@ class CoreQuote extends Block { /** * {@inheritDoc} * - * @var array|null + * @var array */ - protected ?array $additional_block_attributes = [ + protected array $additional_block_attributes = [ 'cssClassName' => [ 'type' => 'string', 'selector' => 'blockquote', diff --git a/includes/Blocks/CoreSeparator.php b/includes/Blocks/CoreSeparator.php index 6e42260c..c47db1b7 100644 --- a/includes/Blocks/CoreSeparator.php +++ b/includes/Blocks/CoreSeparator.php @@ -14,9 +14,9 @@ class CoreSeparator extends Block { /** * {@inheritDoc} * - * @var array|null + * @var array */ - protected ?array $additional_block_attributes = [ + protected array $additional_block_attributes = [ 'cssClassName' => [ 'type' => 'string', 'selector' => 'hr',