Skip to content

Commit

Permalink
Merge pull request #51 from Automattic/add/graphql-api
Browse files Browse the repository at this point in the history
Add GraphQL API
  • Loading branch information
ingeniumed authored Dec 14, 2023
2 parents 37c590a + f89ed3c commit 76b143b
Show file tree
Hide file tree
Showing 15 changed files with 960 additions and 63 deletions.
393 changes: 356 additions & 37 deletions README.md

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
"automattic/vipwpcs": "^3.0",
"yoast/phpunit-polyfills": "^2.0",
"dms/phpunit-arraysubset-asserts": "^0.5.0"

},
"config": {
"allow-plugins": {
Expand Down
2 changes: 1 addition & 1 deletion phpcs.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
<rule ref="WordPress-Extra"/>
<!-- For help in understanding these custom sniff properties:
https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards/wiki/Customizable-sniff-properties -->
<config name="minimum_supported_wp_version" value="5.8"/>
<config name="minimum_supported_wp_version" value="5.9"/>

<rule ref="WordPress-VIP-Go">
<!-- These disallow anonymous functions as action callbacks -->
Expand Down
257 changes: 257 additions & 0 deletions src/graphql/graphql-api.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
<?php
/**
* GraphQL API
*
* @package vip-block-data-api
*/

namespace WPCOMVIP\BlockDataApi;

use GraphQLRelay\Relay;

defined( 'ABSPATH' ) || die();

/**
* GraphQL API to offer an alternative to the REST API.
*/
class GraphQLApi {
/**
* Initiatilize the graphQL API by hooking into the graphql_register_types action,
* which only fires if WPGraphQL is installed and enabled, and is further controlled
* by the vip_block_data_api__is_graphql_enabled filter.
*/
public static function init() {
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 ) {
$post_id = $post_model->ID;
$post = get_post( $post_id );

$content_parser = new ContentParser();

$parser_results = $content_parser->parse( $post->post_content, $post_id );

// We need to not return a WP_Error object, and so a regular exception is returned.
if ( is_wp_error( $parser_results ) ) {
Analytics::record_error( $parser_results );

// Return API-safe error with extra data (e.g. stack trace) removed.
return new \Exception( $parser_results->get_error_message() );
}

$parser_results['blocks'] = array_map( function ( $block ) use ( $post_id ) {
return self::transform_block_format( $block, $post_id );
}, $parser_results['blocks'] );

return $parser_results;
}

/**
* Transform the block's format to the format expected by the graphQL API.
*
* @param array $block An associative array of parsed block data with keys 'name' and 'attributes'.
* @param array $post_id The associated post ID for the content being transformed. Used to produce unique block IDs.
*
* @return array
*/
public static function transform_block_format( $block, $post_id ) {
// Generate a unique ID for the block.
$block['id'] = Relay::toGlobalId( 'BlockData', sprintf( '%d:%d', $post_id, wp_unique_id() ) );

// Convert the attributes to be in the name-value format that the schema expects.
$block = self::map_attributes( $block );

if ( isset( $block['innerBlocks'] ) && ! empty( $block['innerBlocks'] ) ) {
$block['innerBlocks'] = array_map( function ( $block ) use ( $post_id ) {
return self::transform_block_format( $block, $post_id );
}, $block['innerBlocks'] );
}

return $block;
}

/**
* Convert the attributes to be in the name-value format that the schema expects.
*
* @param array $block An associative array of parsed block data with keys 'name' and 'attributes'.
*
* @return array
*/
public static function map_attributes( $block ) {
// check if type of attributes is stdClass and unset it as that's not supported by graphQL.
if ( isset( $block['attributes'] ) && is_object( $block['attributes'] ) ) {
unset( $block['attributes'] );
} elseif ( isset( $block['attributes'] ) && ! empty( $block['attributes'] ) ) {
$block['attributes'] = array_map(
function ( $name, $value ) {
return [
'name' => $name,
'value' => strval( $value ),
];
},
array_keys( $block['attributes'] ),
array_values( $block['attributes'] )
);
}

return $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 string $parent_id ID of the parent block, that the inner blocks belong to.
* @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, $parent_id, $flattened_blocks = [] ) {
foreach ( $inner_blocks as $inner_block ) {
// Set the parentId to be the ID of the parent block whose inner blocks are being flattened.
$inner_block['parentId'] = $parent_id;

if ( ! isset( $inner_block['innerBlocks'] ) ) {
// This block doesnt have any inner blocks, so just add it to the flattened blocks.
array_push( $flattened_blocks, $inner_block );
} else {
// This block is has inner blocks, so go through the inner blocks recursively.
$inner_blocks_copy = $inner_block['innerBlocks'];
unset( $inner_block['innerBlocks'] );

// First add the current block to the flattened blocks, and then go through the inner blocks recursively.
array_push( $flattened_blocks, $inner_block );
$flattened_blocks = self::flatten_inner_blocks( $inner_blocks_copy, $inner_block['id'], $flattened_blocks );
}
}

return $flattened_blocks;
}

/**
* Register types and fields graphql integration.
*
* @return void
*/
public static function register_types() {
/**
* Filter to enable/disable the graphQL API. By default, it is enabled.
*
* @param bool $is_graphql_to_be_enabled Whether the graphQL API should be enabled or not.
*/
$is_graphql_to_be_enabled = apply_filters( 'vip_block_data_api__is_graphql_enabled', true );

if ( ! $is_graphql_to_be_enabled ) {
return;
}

// Register the type corresponding to the attributes of each individual block.
register_graphql_object_type(
'BlockAttribute',
[
'description' => 'Block attribute',
'fields' => [
'name' => [
'type' => [ 'non_null' => 'String' ],
'description' => 'Block data attribute name',
],
'value' => [
'type' => [ 'non_null' => 'String' ],
'description' => 'Block data attribute value',
],
],
],
);

// Register the type corresponding to the individual block, with the above attribute.
register_graphql_type(
'BlockData',
[
'description' => 'Block data',
'fields' => [
'id' => [
'type' => [ 'non_null' => 'ID' ],
'description' => 'ID of the block',
],
'parentId' => [
'type' => 'ID',
'description' => 'ID of the parent for this inner block, if it is an inner block. Otherwise, it will be null.',
],
'name' => [
'type' => [ 'non_null' => 'String' ],
'description' => 'Block name',
],
'attributes' => [
'type' => [
'list_of' => 'BlockAttribute',
],
'description' => 'Block attributes',
],
'innerBlocks' => [
'type' => [ 'list_of' => 'BlockData' ],
'description' => 'Flattened list of inner blocks of this block',
],
],
],
);

// 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',
'args' => [
'flatten' => [
'type' => 'Boolean',
'description' => 'Collate the inner blocks under each root block into a single list with a parent-child relationship. This is set to true by default, and setting it to false will preserve the original block hierarchy, but will require nested inner block queries to the desired depth. Default: true',
],
],
'resolve' => function ( $blocks, $args ) {
if ( ! isset( $args['flatten'] ) || true === $args['flatten'] ) {
$blocks['blocks'] = array_map( function ( $block ) {
// Flatten the inner blocks, if any.
if ( isset( $block['innerBlocks'] ) ) {
$block['innerBlocks'] = self::flatten_inner_blocks( $block['innerBlocks'], $block['id'] );
}

return $block;
}, $blocks['blocks'] );
}

return $blocks['blocks'];
},
],
'warnings' => [
'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',
'blocksData',
[
'type' => 'BlocksData',
'description' => 'A block representation of post content',
'resolve' => [ __CLASS__, 'get_blocks_data' ],
]
);
}
}

GraphQLApi::init();
10 changes: 4 additions & 6 deletions src/parser/block-additions/core-image.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,22 +19,20 @@ 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', [ __CLASS__, 'add_image_metadata' ], 5, 2 );
}

/**
* Add size metadata to core/image blocks
*
* @param array $sourced_block Sourced block result.
* @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 $sourced_block Sourced block result.
* @param string $block_name Name of the block.
*
* @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 ) {
if ( 'core/image' !== $block_name ) {
return $sourced_block;
}
Expand Down
2 changes: 2 additions & 0 deletions src/parser/content-parser.php
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@ public function should_block_be_included( $block, $block_name, $filter_options )
* @return array|WP_Error
*/
public function parse( $post_content, $post_id = null, $filter_options = [] ) {
Analytics::record_usage();

if ( isset( $filter_options['exclude'] ) && isset( $filter_options['include'] ) ) {
return new WP_Error( 'vip-block-data-api-invalid-params', 'Cannot provide blocks to exclude and include at the same time', [ 'status' => 400 ] );
}
Expand Down
2 changes: 0 additions & 2 deletions src/rest/rest-api.php
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,6 @@ public static function get_block_content( $params ) {
$post_id = $params['id'];
$post = get_post( $post_id );

Analytics::record_usage();

$parse_time_start = microtime( true );

$content_parser = new ContentParser();
Expand Down
2 changes: 2 additions & 0 deletions tests/bootstrap.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,5 @@ function _manually_load_plugin() {

// Add custom test classes
require_once __DIR__ . '/registry-test-case.php';

require_once __DIR__ . '/mocks/graphql-relay-mock.php';
Loading

0 comments on commit 76b143b

Please sign in to comment.