-
Notifications
You must be signed in to change notification settings - Fork 7
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add GraphQL API #51
Add GraphQL API #51
Changes from 14 commits
b1ee4fa
5d9a6a6
a386950
bf73c1c
83bfd74
7e42577
cf0286d
61bee9d
4c00b0d
d810a0f
71dd8ae
d98425a
d5ab09e
778509d
75e992f
04237ed
145ea17
1df6165
830894b
e06b15a
7e5ae7e
33fc4dd
1dda9a7
df10512
5ad55f1
a07df2b
984f519
43560a8
9f03a0c
0e63fe4
47f2efb
17000db
3c6cdd0
5bb6030
e4acbf8
bd255c2
a25b630
272e92b
fac6ebf
d578fe4
8cdc1e2
90c77de
a64af6d
6e2c367
f89ed3c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,231 @@ | ||||||
<?php | ||||||
/** | ||||||
* GraphQL API | ||||||
* | ||||||
* @package vip-block-data-api | ||||||
*/ | ||||||
|
||||||
namespace WPCOMVIP\BlockDataApi; | ||||||
|
||||||
defined( 'ABSPATH' ) || die(); | ||||||
|
||||||
/** | ||||||
* GraphQL API to offer an alternative to the REST API. | ||||||
*/ | ||||||
class GraphQLApi { | ||||||
/** | ||||||
* Initiatilize the graphQL API, if its allowed | ||||||
* | ||||||
* @access private | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. not private There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Taken it out |
||||||
*/ | ||||||
public static function init() { | ||||||
$is_graphql_to_be_enabled = apply_filters( 'vip_block_data_api__is_graphql_enabled', true ); | ||||||
smithjw1 marked this conversation as resolved.
Show resolved
Hide resolved
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good point, I have moved this into the |
||||||
|
||||||
if ( ! $is_graphql_to_be_enabled ) { | ||||||
return; | ||||||
} | ||||||
|
||||||
add_filter( 'vip_block_data_api__sourced_block_result_transform', [ __CLASS__, 'transform_block_attributes' ], 10, 5 ); | ||||||
|
||||||
add_action( 'graphql_register_types', [ __CLASS__, 'register_types' ] ); | ||||||
} | ||||||
|
||||||
/** | ||||||
* Extract the blocks data for a post, and return back in the format expected by the graphQL API. | ||||||
* | ||||||
* @param WPGraphQL\Model\Post $post_model Post model for post. | ||||||
* @return array | ||||||
*/ | ||||||
public static function get_blocks_data( $post_model ) { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there a PHP version policy for this plugin? Can you use type declarations?
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We have set it at 7.4, but will be upping it to 8.0. At that point we were gonna go in and set the type declarations. That is on the cards, so I haven't done that here. That said, I do have to figure out when I do how to pass in post model of type \WPGraphQL\Model\Post when we dont bundle in WP-GraphQL with the plugins. The tests fail due to it, I realized. I'll leave this one as is for this PR |
||||||
$post_id = $post_model->ID; | ||||||
$post = get_post( $post_id ); | ||||||
$filter_options = [ 'graphQL' => true ]; | ||||||
alecgeatches marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
|
||||||
$content_parser = new ContentParser(); | ||||||
|
||||||
$parser_results = $content_parser->parse( $post->post_content, $post_id, $filter_options ); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can access on $model->fields
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I had tried to do it that way, but no matter what I did the fields were null. That's why I did it this way. The plugin internally calls get_post already so the info is quickly returned. Is there anyone special I need to do? I had called it the way u suggested 🤔 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I realized I summed up the same comment that's in the
|
||||||
|
||||||
// ToDo: Verify if this is better, or is returning it another way in graphQL is better. | ||||||
if ( is_wp_error( $parser_results ) ) { | ||||||
// Return API-safe error with extra data (e.g. stack trace) removed. | ||||||
return new \Exception( $parser_results->get_error_message() ); | ||||||
chriszarate marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
} | ||||||
|
||||||
// ToDo: Provide a filter to modify the output. Not sure if the individual block, or the entire thing should be allowed to be modified. | ||||||
|
||||||
return $parser_results; | ||||||
} | ||||||
|
||||||
/** | ||||||
* Transform the block attribute's format to the format expected by the graphQL API. | ||||||
* | ||||||
* @param array $sourced_block An associative array of parsed block data with keys 'name' and 'attribute'. | ||||||
* @param string $block_name Name of the parsed block, e.g. 'core/paragraph'. | ||||||
* @param int $post_id Post ID associated with the parsed block. | ||||||
* @param array $block Result of parse_blocks() for this block. Contains 'blockName', 'attrs', 'innerHTML', and 'innerBlocks' keys. | ||||||
* @param array $filter_options Options to filter using, if any. | ||||||
* | ||||||
* @return array | ||||||
*/ | ||||||
public static function transform_block_attributes( $sourced_block, $block_name, $post_id, $block, $filter_options ) { // phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed | ||||||
if ( isset( $filter_options['graphQL'] ) && $filter_options['graphQL'] ) { | ||||||
|
||||||
// Flatten the inner blocks, if any. | ||||||
if ( isset( $sourced_block['innerBlocks'] ) && ! isset( $sourced_block['parentId'] ) ) { | ||||||
$sourced_block['innerBlocks'] = self::flatten_inner_blocks( $sourced_block ); | ||||||
} | ||||||
|
||||||
if ( isset( $sourced_block['attributes'] ) && ! isset( $sourced_block['attributes'][0]['name'] ) ) { | ||||||
$sourced_block['attributes'] = array_map( | ||||||
function ( $name, $value ) { | ||||||
return [ | ||||||
'name' => $name, | ||||||
'value' => $value, | ||||||
]; | ||||||
}, | ||||||
array_keys( $sourced_block['attributes'] ), | ||||||
array_values( $sourced_block['attributes'] ) | ||||||
); | ||||||
} | ||||||
} | ||||||
|
||||||
return $sourced_block; | ||||||
} | ||||||
|
||||||
/** | ||||||
* Flatten the inner blocks, no matter how many levels of nesting is there. | ||||||
* | ||||||
* @param array $inner_blocks the inner blocks in the block. | ||||||
* @param array $flattened_blocks the flattened blocks that's built up as we go through the inner blocks. | ||||||
* | ||||||
* @return array | ||||||
*/ | ||||||
public static function flatten_inner_blocks( $inner_blocks, $flattened_blocks = [] ) { | ||||||
if ( ! isset( $inner_blocks['innerBlocks'] ) ) { | ||||||
array_push( $flattened_blocks, $inner_blocks ); | ||||||
} else { | ||||||
$inner_blocks_copy = $inner_blocks['innerBlocks']; | ||||||
unset( $inner_blocks['innerBlocks'] ); | ||||||
if ( isset( $inner_blocks['parentId'] ) ) { | ||||||
array_push( $flattened_blocks, $inner_blocks ); | ||||||
} | ||||||
foreach ( $inner_blocks_copy as $inner_block ) { | ||||||
$flattened_blocks = self::flatten_inner_blocks( $inner_block, $flattened_blocks ); | ||||||
} | ||||||
} | ||||||
|
||||||
return $flattened_blocks; | ||||||
} | ||||||
|
||||||
/** | ||||||
* Register types and fields graphql integration. | ||||||
* | ||||||
* @return void | ||||||
*/ | ||||||
public static function register_types() { | ||||||
// Register the type corresponding to the attributes of each individual block. | ||||||
register_graphql_object_type( | ||||||
'BlockDataAttribute', | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Naming is hard, but isn't this a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yip, changed it |
||||||
[ | ||||||
'description' => 'Block data attribute', | ||||||
'fields' => [ | ||||||
'name' => [ | ||||||
'type' => 'String', | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's good practice to designate fields as non-nullable whenever possible. That way the consumer can have confidence that a field will always have a value and can skip inspection.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good point, I have done this now |
||||||
'description' => 'Block data attribute name', | ||||||
], | ||||||
'value' => [ | ||||||
'type' => 'String', | ||||||
'description' => 'Block data attribute value', | ||||||
], | ||||||
], | ||||||
], | ||||||
); | ||||||
|
||||||
// Register the type corresponding to the individual inner block, with the above attribute. | ||||||
register_graphql_type( | ||||||
'InnerBlockData', | ||||||
[ | ||||||
'description' => 'Block data', | ||||||
'fields' => [ | ||||||
'id' => [ | ||||||
'type' => 'String', | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good point, I have done this now for the ID and the parentId. |
||||||
'description' => 'ID of the block', | ||||||
], | ||||||
'parentId' => [ | ||||||
'type' => 'String', | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. review all these fields for non-nullability There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good point, I have done this now for all the fields |
||||||
'description' => 'ID of the parent for this inner block, if it is an inner block. This will match the ID of the block', | ||||||
], | ||||||
'name' => [ | ||||||
'type' => 'String', | ||||||
'description' => 'Block name', | ||||||
], | ||||||
'attributes' => [ | ||||||
'type' => [ | ||||||
'list_of' => 'BlockDataAttribute', | ||||||
], | ||||||
'description' => 'Block data attributes', | ||||||
], | ||||||
], | ||||||
], | ||||||
); | ||||||
|
||||||
// Register the type corresponding to the individual block, with the above attribute. | ||||||
register_graphql_type( | ||||||
'BlockData', | ||||||
chriszarate marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
[ | ||||||
'description' => 'Block data', | ||||||
ingeniumed marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
'fields' => [ | ||||||
'id' => [ | ||||||
'type' => 'String', | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good point, I have done this now |
||||||
'description' => 'ID of the block', | ||||||
], | ||||||
'name' => [ | ||||||
'type' => 'String', | ||||||
'description' => 'Block name', | ||||||
], | ||||||
'attributes' => [ | ||||||
'type' => [ | ||||||
'list_of' => 'BlockDataAttribute', | ||||||
], | ||||||
'description' => 'Block data attributes', | ||||||
], | ||||||
'innerBlocks' => [ | ||||||
'type' => [ 'list_of' => 'InnerBlockData' ], | ||||||
'description' => 'Flattened list of inner blocks of this block', | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I get why these are flattened, but is the "reparenting" going to be annoying / error-prone? Is the nesting issue severe enough to warrant this approach? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The core problem we wanted to solve was that, you should be able to get back the complicated block hierarchy in a post without having any knowledge of the depth involved. This gets us past that, and to ensure that the re-parenting isn't annoying we added a little utility function in the README that could be used. Admittedly that function could be simplified further, but I kept it simple enough to follow. By ensuring that each block gets an id and if applicable, a parent id the likelihood of an error should be minimal (one would hope, but obviously things can go wrong). The other approach would be to flatten the innerBlocks alongside all the blocks but that proved to be even more complicated (in terms of code and the outcome) and wouldn't necessarily match the structure of a parent-child in a post with Gutenberg blocks. So with that in mind, this was the ultimate solution that was picked to allow graphQL to be added on top of the block data api without the innerBlocks being a problem. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok. I totally get the why, just wondering how big a deal this actually is in practice:
If you use fragments in your query, adding another layer isn't a huge deal. And since you are reconstructing the deep tree on the client side, your block parsing code isn't less complex. Yes, if you add another level beyond what you are querying for, it simply won't be in the response. But adding a Another drawback of this approach is that it makes human inspection of the response data much less friendly, possibly impossible. Human debugging is useful and common, especially with the built-in GraphiQL client. Again, totally understand where this is coming from, just want to make sure the devex has been fully considered. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just an idea: If you use the same type for both inner and outer blocks, you could add a GraphQL field directive that allows the queryer to choose whether to flatten or not. If would look like this: query FlattenBlocks {
post(id: "abc123=") {
blockData(flatten: true) {
# ...
}
}
}
query NestedBlocks {
post(id: "abc123=") {
blockData(flatten: false) { # or omit the directive
# ...
}
}
} There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In the research specifically mentioned flattening the hierarchy and using parent/child relationships. No one balked at it. I'm not trying to suggest that flattening is the right choice. I think optionality is good, as are Chris' points. My take is we have options that customers will accept, and we can choose from among them, which is a great place to be. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks to the re-write I did of the way the parser's result is transformed into the right graphQL schema, I was able to add in this flatten option. So by default it's set to true because we want the inner blocks to be flattened. Omitting it sets it to true by default. If you set it to false, it will not flatten the inner blocks, and send back the original hierarchy. So best of both options are available, but we give back the flattened hierarchy by default as thats the best option we want to be used. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice! FYI it is not mentioned in the README There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good point, added it in under usage! |
||||||
], | ||||||
], | ||||||
], | ||||||
); | ||||||
|
||||||
// Register the type corresponding to the list of individual blocks, with each item being the above type. | ||||||
register_graphql_type( | ||||||
'BlocksData', | ||||||
[ | ||||||
'description' => 'Data for all the blocks', | ||||||
'fields' => [ | ||||||
'blocks' => [ | ||||||
'type' => [ 'list_of' => 'BlockData' ], | ||||||
'description' => 'List of blocks data', | ||||||
], | ||||||
'warnings' => [ | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Warning / error data typically doesn't ship in the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm still working on this, ran into some problems that are of my own making. |
||||||
'type' => [ 'list_of' => 'String' ], | ||||||
'description' => 'List of warnings related to processing the blocks data', | ||||||
], | ||||||
], | ||||||
], | ||||||
); | ||||||
|
||||||
// Register the field on every post type that supports 'editor'. | ||||||
register_graphql_field( | ||||||
'NodeWithContentEditor', | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I know you are following the pattern of the decoupled bundle here, but let's discuss the pros and cons of adding the field to this union. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I had looked up this page as well as this one to see what it could support. From what I gathered this would be what we support - pages and posts that would be made using the content editor. We have a check with the contentParser that already checks to see if the provided post has blocks within it or not, so that should account for the classic editor. That was my thinking, alongside this being used in the decoupled bundle already. Was there something else or a downside that I might have missed in using this? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nevermind, I was conflating this with querying for
The |
||||||
'BlocksData', | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. field name should be camel case
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Changed this as well |
||||||
[ | ||||||
'type' => 'BlocksData', | ||||||
'description' => 'A block representation of post content', | ||||||
'resolve' => [ __CLASS__, 'get_blocks_data' ], | ||||||
] | ||||||
); | ||||||
} | ||||||
} | ||||||
|
||||||
GraphQLApi::init(); |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -19,7 +19,7 @@ class CoreImage { | |
* @access private | ||
*/ | ||
public static function init() { | ||
add_filter( 'vip_block_data_api__sourced_block_result', [ __CLASS__, 'add_image_metadata' ], 5, 4 ); | ||
add_filter( 'vip_block_data_api__sourced_block_result_transform', [ __CLASS__, 'add_image_metadata' ], 5, 5 ); | ||
} | ||
|
||
/** | ||
|
@@ -29,12 +29,13 @@ public static function init() { | |
* @param string $block_name Name of the block. | ||
* @param int|null $post_id Id of the post. | ||
* @param array $block Block being parsed. | ||
* @param array $filter_options Options for the filter, if any. | ||
* | ||
* @access private | ||
* | ||
* @return array Updated sourced block with new metadata information | ||
*/ | ||
public static function add_image_metadata( $sourced_block, $block_name, $post_id, $block ) { // phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed | ||
public static function add_image_metadata( $sourced_block, $block_name, $post_id, $block, $filter_options ) { // phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. unused? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yip, cleaned this up |
||
if ( 'core/image' !== $block_name ) { | ||
return $sourced_block; | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This comment could be better (either here or below), to explain that it hooks into the
graphql_register_types
action, which only fires if WPGraphQL is installed and enabled, and is further controlled by thevip_block_data_api__is_graphql_enabled
filter.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have made this change as well