diff --git a/composer.json b/composer.json index c67c5c57..e2b3f2a7 100644 --- a/composer.json +++ b/composer.json @@ -6,8 +6,7 @@ "minimum-stability": "dev", "require": { "php": ">=7.4", - "imangazaliev/didom": "^2.0", - "cweagans/composer-patches": "^1" + "imangazaliev/didom": "^2.0" }, "require-dev": { "brain/monkey": "^2.6", @@ -37,8 +36,7 @@ "config": { "allow-plugins": { "dealerdirect/phpcodesniffer-composer-installer": true, - "phpstan/extension-installer": true, - "cweagans/composer-patches": true + "phpstan/extension-installer": true }, "platform": { "php": "7.4" diff --git a/composer.lock b/composer.lock index b70f43d3..97692c7b 100644 --- a/composer.lock +++ b/composer.lock @@ -4,56 +4,8 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "d1164a5bb34dceb30b89cd19ad1b6547", + "content-hash": "c7f329cbeaf904d21842b780196cf3b5", "packages": [ - { - "name": "cweagans/composer-patches", - "version": "1.7.3", - "source": { - "type": "git", - "url": "https://github.com/cweagans/composer-patches.git", - "reference": "e190d4466fe2b103a55467dfa83fc2fecfcaf2db" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/cweagans/composer-patches/zipball/e190d4466fe2b103a55467dfa83fc2fecfcaf2db", - "reference": "e190d4466fe2b103a55467dfa83fc2fecfcaf2db", - "shasum": "" - }, - "require": { - "composer-plugin-api": "^1.0 || ^2.0", - "php": ">=5.3.0" - }, - "require-dev": { - "composer/composer": "~1.0 || ~2.0", - "phpunit/phpunit": "~4.6" - }, - "type": "composer-plugin", - "extra": { - "class": "cweagans\\Composer\\Patches" - }, - "autoload": { - "psr-4": { - "cweagans\\Composer\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Cameron Eagans", - "email": "me@cweagans.net" - } - ], - "description": "Provides a way to patch Composer packages.", - "support": { - "issues": "https://github.com/cweagans/composer-patches/issues", - "source": "https://github.com/cweagans/composer-patches/tree/1.7.3" - }, - "time": "2022-12-20T22:53:13+00:00" - }, { "name": "imangazaliev/didom", "version": "2.0.1", @@ -156,60 +108,6 @@ }, "time": "2024-02-06T09:26:11+00:00" }, - { - "name": "appsero/client", - "version": "v2.0.4", - "source": { - "type": "git", - "url": "https://github.com/Appsero/client.git", - "reference": "12ff65b9770286d21edf314e7acfcd26fdde3315" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Appsero/client/zipball/12ff65b9770286d21edf314e7acfcd26fdde3315", - "reference": "12ff65b9770286d21edf314e7acfcd26fdde3315", - "shasum": "" - }, - "require": { - "php": ">=5.6" - }, - "require-dev": { - "dealerdirect/phpcodesniffer-composer-installer": "^0.7.2", - "phpcompatibility/phpcompatibility-wp": "dev-master", - "phpunit/phpunit": "^8.5.31", - "squizlabs/php_codesniffer": "^3.7", - "tareq1988/wp-php-cs-fixer": "dev-master", - "wp-coding-standards/wpcs": "dev-develop" - }, - "type": "library", - "autoload": { - "psr-4": { - "Appsero\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Tareq Hasan", - "email": "tareq@appsero.com" - } - ], - "description": "Appsero Client", - "keywords": [ - "analytics", - "plugin", - "theme", - "wordpress" - ], - "support": { - "issues": "https://github.com/Appsero/client/issues", - "source": "https://github.com/Appsero/client/tree/v2.0.4" - }, - "time": "2024-11-25T05:58:23+00:00" - }, { "name": "automattic/vipwpcs", "version": "3.0.1", @@ -643,51 +541,6 @@ }, "time": "2020-07-09T08:09:16+00:00" }, - { - "name": "ivome/graphql-relay-php", - "version": "v0.7.0", - "source": { - "type": "git", - "url": "https://github.com/ivome/graphql-relay-php.git", - "reference": "06bd176103618d896197d85d04a3a17c91e39698" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/ivome/graphql-relay-php/zipball/06bd176103618d896197d85d04a3a17c91e39698", - "reference": "06bd176103618d896197d85d04a3a17c91e39698", - "shasum": "" - }, - "require": { - "php": "^7.1 || ^8.0", - "webonyx/graphql-php": "^14.0 || ^15.0" - }, - "require-dev": { - "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", - "satooshi/php-coveralls": "~1.0" - }, - "type": "library", - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "description": "A PHP port of GraphQL Relay reference implementation", - "homepage": "https://github.com/ivome/graphql-relay-php", - "keywords": [ - "Relay", - "api", - "graphql" - ], - "support": { - "issues": "https://github.com/ivome/graphql-relay-php/issues", - "source": "https://github.com/ivome/graphql-relay-php/tree/v0.7.0" - }, - "time": "2023-10-20T15:43:03+00:00" - }, { "name": "mockery/mockery", "version": "1.6.12", @@ -4238,80 +4091,6 @@ ], "time": "2024-03-03T12:36:25+00:00" }, - { - "name": "webonyx/graphql-php", - "version": "v15.19.1", - "source": { - "type": "git", - "url": "https://github.com/webonyx/graphql-php.git", - "reference": "fa01712b1a170ddc1d92047011b2f4c2bdfa8234" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/webonyx/graphql-php/zipball/fa01712b1a170ddc1d92047011b2f4c2bdfa8234", - "reference": "fa01712b1a170ddc1d92047011b2f4c2bdfa8234", - "shasum": "" - }, - "require": { - "ext-json": "*", - "ext-mbstring": "*", - "php": "^7.4 || ^8" - }, - "require-dev": { - "amphp/amp": "^2.6", - "amphp/http-server": "^2.1", - "dms/phpunit-arraysubset-asserts": "dev-master", - "ergebnis/composer-normalize": "^2.28", - "friendsofphp/php-cs-fixer": "3.65.0", - "mll-lab/php-cs-fixer-config": "^5.9.2", - "nyholm/psr7": "^1.5", - "phpbench/phpbench": "^1.2", - "phpstan/extension-installer": "^1.1", - "phpstan/phpstan": "1.12.12", - "phpstan/phpstan-phpunit": "1.4.1", - "phpstan/phpstan-strict-rules": "1.6.1", - "phpunit/phpunit": "^9.5 || ^10.5.21 || ^11", - "psr/http-message": "^1 || ^2", - "react/http": "^1.6", - "react/promise": "^2.0 || ^3.0", - "rector/rector": "^1.0", - "symfony/polyfill-php81": "^1.23", - "symfony/var-exporter": "^5 || ^6 || ^7", - "thecodingmachine/safe": "^1.3 || ^2" - }, - "suggest": { - "amphp/http-server": "To leverage async resolving with webserver on AMPHP platform", - "psr/http-message": "To use standard GraphQL server", - "react/promise": "To leverage async resolving on React PHP platform" - }, - "type": "library", - "autoload": { - "psr-4": { - "GraphQL\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "A PHP port of GraphQL reference implementation", - "homepage": "https://github.com/webonyx/graphql-php", - "keywords": [ - "api", - "graphql" - ], - "support": { - "issues": "https://github.com/webonyx/graphql-php/issues", - "source": "https://github.com/webonyx/graphql-php/tree/v15.19.1" - }, - "funding": [ - { - "url": "https://opencollective.com/webonyx-graphql-php", - "type": "open_collective" - } - ], - "time": "2024-12-19T10:52:18+00:00" - }, { "name": "wp-coding-standards/wpcs", "version": "3.1.0", @@ -4378,89 +4157,6 @@ ], "time": "2024-03-25T16:39:00+00:00" }, - { - "name": "wp-graphql/wp-graphql", - "version": "v2.2.0", - "source": { - "type": "git", - "url": "https://github.com/wp-graphql/wp-graphql.git", - "reference": "3ebdea9681e6901d7ad6c88b7683cee12daa6028" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/wp-graphql/wp-graphql/zipball/3ebdea9681e6901d7ad6c88b7683cee12daa6028", - "reference": "3ebdea9681e6901d7ad6c88b7683cee12daa6028", - "shasum": "" - }, - "require": { - "appsero/client": "2.0.4", - "ivome/graphql-relay-php": "0.7.0", - "php": "^7.4 || ^8.0", - "webonyx/graphql-php": "15.19.1" - }, - "require-dev": { - "automattic/vipwpcs": "^3.0", - "codeception/module-asserts": "^1.0", - "codeception/module-cli": "^1.0", - "codeception/module-db": "^1.0", - "codeception/module-filesystem": "^1.0", - "codeception/module-phpbrowser": "^1.0", - "codeception/module-rest": "^1.2", - "codeception/module-webdriver": "^1.0", - "codeception/util-universalframework": "^1.0", - "composer/semver": "^3.0", - "dealerdirect/phpcodesniffer-composer-installer": "^1.0", - "lucatume/wp-browser": "<3.5", - "phpcompatibility/php-compatibility": "dev-develop as 9.9.9", - "phpcompatibility/phpcompatibility-wp": "^2.1", - "phpstan/extension-installer": "^1.1", - "phpstan/phpstan": "~2.1.2", - "phpstan/phpstan-deprecation-rules": "^2.0.1", - "phpunit/phpunit": "^9.5", - "slevomat/coding-standard": "^8.9", - "szepeviktor/phpstan-wordpress": "~2.0.1", - "wp-cli/wp-cli-bundle": "^2.8", - "wp-graphql/wp-graphql-testcase": "^3.0" - }, - "type": "wordpress-plugin", - "autoload": { - "files": [], - "psr-4": { - "WPGraphQL\\": "src/" - }, - "classmap": [ - "src/WPGraphQL.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "GPL-3.0-or-later" - ], - "authors": [ - { - "name": "Jason Bahl", - "email": "jasonbahl@mac.com" - }, - { - "name": "Edwin Cromley" - }, - { - "name": "Ryan Kanner" - }, - { - "name": "Hughie Devore" - }, - { - "name": "Chris Zarate" - } - ], - "description": "GraphQL API for WordPress", - "support": { - "issues": "https://github.com/wp-graphql/wp-graphql/issues", - "source": "https://github.com/wp-graphql/wp-graphql/tree/v2.2.0" - }, - "time": "2025-03-19T03:14:36+00:00" - }, { "name": "yoast/phpunit-polyfills", "version": "1.1.2", @@ -4535,7 +4231,7 @@ "platform": { "php": ">=7.4" }, - "platform-dev": [], + "platform-dev": {}, "platform-overrides": { "php": "7.4" }, diff --git a/includes/Blocks/Block.php b/includes/Blocks/Block.php index fb95ef92..19e9393a 100644 --- a/includes/Blocks/Block.php +++ b/includes/Blocks/Block.php @@ -8,6 +8,7 @@ namespace WPGraphQL\ContentBlocks\Blocks; use WPGraphQL\ContentBlocks\Data\BlockAttributeResolver; +use WPGraphQL\ContentBlocks\GraphQL\WPGraphQLRegisterConfig; use WPGraphQL\ContentBlocks\Registry\Registry; use WPGraphQL\ContentBlocks\Type\Scalar\Scalar; use WPGraphQL\ContentBlocks\Utilities\WPGraphQLHelpers; @@ -119,32 +120,41 @@ private function register_block_attributes_as_fields(): void { // 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'; + + // Stash the type name so we can use it statically. + $type_name = $this->type_name; register_graphql_object_type( $block_attribute_type_name, - [ - 'description' => sprintf( + // @TODO - Remove when WPGraphQL min version is 2.3.0 + WPGraphQLRegisterConfig::resolve_graphql_config( + [ + 'description' => static fn () => 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, - ] + __( 'Attributes of the %s Block Type', 'wp-graphql-content-blocks' ), + $type_name + ), + 'interfaces' => $this->get_block_attributes_interfaces(), + 'fields' => $block_attribute_fields, + ] + ) ); register_graphql_field( - $this->type_name, + $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; - }, - ] + // @TODO - Remove when WPGraphQL min version is 2.3.0 + WPGraphQLRegisterConfig::resolve_graphql_config( + [ + 'type' => $block_attribute_type_name, + 'description' => static fn () => sprintf( + // translators: %s is the block type name. + __( 'Attributes of the %s Block Type', 'wp-graphql-content-blocks' ), + $type_name + ), + 'resolve' => static function ( $block ) { + return $block; + }, + ] + ) ); } @@ -262,7 +272,7 @@ private function get_block_attribute_fields( ?array $block_attributes, string $p // Create the field config. $fields[ Utils::format_field_name( $attribute_name ) ] = [ 'type' => $graphql_type, - 'description' => sprintf( + 'description' => static fn () => sprintf( // translators: %1$s is the attribute name, %2$s is the block name. __( 'The "%1$s" field on the "%2$s" block or block attributes', 'wp-graphql-content-blocks' ), $attribute_name, @@ -295,15 +305,18 @@ private function register_inner_object_type( string $name, array $config, string register_graphql_object_type( $type, - [ - 'fields' => $fields, - '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' ), - $type, - $prefix - ), - ] + // @TODO - Remove when WPGraphQL min version is 2.3.0 + WPGraphQLRegisterConfig::resolve_graphql_config( + [ + 'fields' => $fields, + 'description' => static fn () => 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' ), + $type, + $prefix + ), + ] + ) ); return $type; @@ -345,7 +358,7 @@ private function create_attributes_fields( $attributes, $prefix ): array { $fields[ Utils::format_field_name( $name ) ] = [ 'type' => $type, - 'description' => sprintf( + 'description' => static fn () => 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, @@ -402,20 +415,23 @@ private function normalize_attribute_value( $value, $type ) { private function register_type(): void { register_graphql_object_type( $this->type_name, - [ - 'description' => __( 'A block used for editing the site', 'wp-graphql-content-blocks' ), - 'interfaces' => $this->get_block_interfaces(), - 'eagerlyLoadType' => true, - 'fields' => [ - 'name' => [ - 'type' => 'String', - 'description' => __( 'The name of the block', 'wp-graphql-content-blocks' ), - 'resolve' => static function ( $block ) { - return isset( $block['blockName'] ) ? (string) $block['blockName'] : null; - }, + // @TODO - Remove when WPGraphQL min version is 2.3.0 + WPGraphQLRegisterConfig::resolve_graphql_config( + [ + 'description' => static fn () => __( 'A block used for editing the site', 'wp-graphql-content-blocks' ), + 'interfaces' => $this->get_block_interfaces(), + 'eagerlyLoadType' => true, + 'fields' => [ + 'name' => [ + 'type' => 'String', + 'description' => static fn () => __( 'The name of the block', 'wp-graphql-content-blocks' ), + 'resolve' => static function ( $block ) { + return isset( $block['blockName'] ) ? (string) $block['blockName'] : null; + }, + ], ], - ], - ] + ] + ) ); } diff --git a/includes/Blocks/CoreImage.php b/includes/Blocks/CoreImage.php index caf6c81a..0be1914b 100644 --- a/includes/Blocks/CoreImage.php +++ b/includes/Blocks/CoreImage.php @@ -7,6 +7,7 @@ namespace WPGraphQL\ContentBlocks\Blocks; +use WPGraphQL\ContentBlocks\GraphQL\WPGraphQLRegisterConfig; use WPGraphQL\ContentBlocks\Registry\Registry; use WP_Block_Type; @@ -44,30 +45,36 @@ class CoreImage extends Block { public function __construct( WP_Block_Type $block, Registry $block_registry ) { parent::__construct( $block, $block_registry ); + // Stash type name for static methods. + $type_name = $this->type_name; + register_graphql_field( - $this->type_name, + $type_name, 'mediaDetails', - [ - 'type' => 'MediaDetails', - 'description' => sprintf( - // translators: %s is the block type name. - __( 'Media Details of the %s Block Type', 'wp-graphql-content-blocks' ), - $this->type_name - ), - 'resolve' => static function ( $block ) { - $attrs = $block['attrs']; - $id = $attrs['id'] ?? null; - if ( $id ) { - $media_details = wp_get_attachment_metadata( $id ); - if ( ! empty( $media_details ) ) { - $media_details['ID'] = $id; + // @TODO - Remove when WPGraphQL min version is 2.3.0 + WPGraphQLRegisterConfig::resolve_graphql_config( + [ + 'type' => 'MediaDetails', + 'description' => static fn () => sprintf( + // translators: %s is the block type name. + __( 'Media Details of the %s Block Type', 'wp-graphql-content-blocks' ), + $type_name + ), + 'resolve' => static function ( $block ) { + $attrs = $block['attrs']; + $id = $attrs['id'] ?? null; + if ( $id ) { + $media_details = wp_get_attachment_metadata( $id ); + if ( ! empty( $media_details ) ) { + $media_details['ID'] = $id; - return $media_details; + return $media_details; + } } - } - return null; - }, - ] + return null; + }, + ] + ) ); } } diff --git a/includes/Blocks/CorePostTerms.php b/includes/Blocks/CorePostTerms.php index 86db4293..6292525b 100644 --- a/includes/Blocks/CorePostTerms.php +++ b/includes/Blocks/CorePostTerms.php @@ -8,6 +8,7 @@ namespace WPGraphQL\ContentBlocks\Blocks; use WPGraphQL\AppContext; +use WPGraphQL\ContentBlocks\GraphQL\WPGraphQLRegisterConfig; use WPGraphQL\ContentBlocks\Registry\Registry; use WPGraphQL\Data\Connection\TaxonomyConnectionResolver; use WPGraphQL\Data\Connection\TermObjectConnectionResolver; @@ -34,16 +35,22 @@ private function register_fields(): void { register_graphql_fields( $this->type_name, [ - 'prefix' => [ - 'type' => 'String', - 'description' => __( 'Prefix to display before the post terms', 'wp-graphql-content-blocks' ), - 'resolve' => static fn ( $block ) => isset( $block['attrs']['prefix'] ) ? (string) $block['attrs']['prefix'] : null, - ], - 'suffix' => [ - 'type' => 'String', - 'description' => __( 'Suffix to display after the post terms', 'wp-graphql-content-blocks' ), - 'resolve' => static fn ( $block ) => isset( $block['attrs']['suffix'] ) ? (string) $block['attrs']['suffix'] : null, - ], + // @TODO - Remove when WPGraphQL min version is 2.3.0 + 'prefix' => WPGraphQLRegisterConfig::resolve_graphql_config( + [ + 'type' => 'String', + 'description' => static fn () => __( 'Prefix to display before the post terms', 'wp-graphql-content-blocks' ), + 'resolve' => static fn ( $block ) => isset( $block['attrs']['prefix'] ) ? (string) $block['attrs']['prefix'] : null, + ], + ), + // @TODO - Remove when WPGraphQL min version is 2.3.0 + 'suffix' => WPGraphQLRegisterConfig::resolve_graphql_config( + [ + 'type' => 'String', + 'description' => static fn () => __( 'Suffix to display after the post terms', 'wp-graphql-content-blocks' ), + 'resolve' => static fn ( $block ) => isset( $block['attrs']['suffix'] ) ? (string) $block['attrs']['suffix'] : null, + ], + ), ] ); } @@ -57,48 +64,54 @@ private function register_fields(): void { protected function register_connections() { // Register connection to terms. register_graphql_connection( - [ - 'fromType' => $this->type_name, - 'toType' => 'TermNode', - 'fromFieldName' => 'terms', - 'resolve' => static function ( $block, array $args, AppContext $context, $info ) { - $taxonomy = $block['attrs']['term'] ?? null; - if ( empty( $taxonomy ) ) { - return null; - } + // @TODO - Remove when WPGraphQL min version is 2.3.0 + WPGraphQLRegisterConfig::resolve_graphql_config( + [ + 'fromType' => $this->type_name, + 'toType' => 'TermNode', + 'fromFieldName' => 'terms', + 'resolve' => static function ( $block, array $args, AppContext $context, $info ) { + $taxonomy = $block['attrs']['term'] ?? null; + if ( empty( $taxonomy ) ) { + return null; + } - $post_id = get_the_ID(); - if ( ! $post_id ) { - return null; - } + $post_id = get_the_ID(); + if ( ! $post_id ) { + return null; + } - $args['where']['objectIds'] = $post_id; - $resolver = new TermObjectConnectionResolver( $block, $args, $context, $info, $taxonomy ); + $args['where']['objectIds'] = $post_id; + $resolver = new TermObjectConnectionResolver( $block, $args, $context, $info, $taxonomy ); - return $resolver->get_connection(); - }, - ] + return $resolver->get_connection(); + }, + ] + ) ); // Register connection to the taxonomy. register_graphql_connection( - [ - 'fromType' => $this->type_name, - 'toType' => 'Taxonomy', - 'fromFieldName' => 'taxonomy', - 'oneToOne' => true, - 'resolve' => static function ( $block, array $args, AppContext $context, $info ) { - $taxonomy = $block['attrs']['term'] ?? null; - if ( empty( $taxonomy ) ) { - return null; - } + // @TODO - Remove when WPGraphQL min version is 2.3.0 + WPGraphQLRegisterConfig::resolve_graphql_config( + [ + 'fromType' => $this->type_name, + 'toType' => 'Taxonomy', + 'fromFieldName' => 'taxonomy', + 'oneToOne' => true, + 'resolve' => static function ( $block, array $args, AppContext $context, $info ) { + $taxonomy = $block['attrs']['term'] ?? null; + if ( empty( $taxonomy ) ) { + return null; + } - $resolver = new TaxonomyConnectionResolver( $block, $args, $context, $info ); - $resolver->set_query_arg( 'name', $taxonomy ); + $resolver = new TaxonomyConnectionResolver( $block, $args, $context, $info ); + $resolver->set_query_arg( 'name', $taxonomy ); - return $resolver->one_to_one()->get_connection(); - }, - ] + return $resolver->one_to_one()->get_connection(); + }, + ] + ) ); } } diff --git a/includes/Data/BlockAttributeResolver.php b/includes/Data/BlockAttributeResolver.php index cd8e0dc7..e0dbca60 100644 --- a/includes/Data/BlockAttributeResolver.php +++ b/includes/Data/BlockAttributeResolver.php @@ -25,59 +25,63 @@ final class BlockAttributeResolver { * @return mixed */ public static function resolve_block_attribute( $attribute, string $html, $attribute_value ) { + + $source = $attribute['source'] ?? null; + // Return at the earliest point to reduce overhead + if ( ! $source ) { + return self::get_default_attribute_value( $attribute, $attribute_value ); + } + $value = null; - if ( isset( $attribute['source'] ) ) { - // @todo parse remaining sources: https://github.com/WordPress/gutenberg/blob/trunk/packages/blocks/src/api/parser/get-block-attributes.js#L198 - switch ( $attribute['source'] ) { - case 'attribute': - $value = self::parse_attribute_source( $html, $attribute ); - break; - case 'html': - case 'rich-text': - // If there is no selector, the source is the node's innerHTML. - if ( ! isset( $attribute['selector'] ) ) { - $value = ! empty( $html ) ? DOMHelpers::find_nodes( $html )->innerHTML() : null; - 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 ); + // @todo parse remaining sources: https://github.com/WordPress/gutenberg/blob/trunk/packages/blocks/src/api/parser/get-block-attributes.js#L198 + switch ( $source ) { + case 'attribute': + $value = self::parse_attribute_source( $html, $attribute ); + break; + case 'html': + case 'rich-text': + // If there is no selector, the source is the node's innerHTML. + if ( ! isset( $attribute['selector'] ) ) { + $value = ! empty( $html ) ? DOMHelpers::find_nodes( $html )->innerHTML() : null; break; - case 'tag': - $value = self::parse_tag_source( $html ); - break; - case 'meta': - $value = self::parse_meta_source( $attribute ); - break; - } - - // Sanitize the value type. - if ( isset( $attribute['type'] ) ) { - switch ( $attribute['type'] ) { - case 'integer': - $value = intval( $value ); - break; - case 'boolean': - // Boolean attributes can be an empty string. - $value = ( ! is_array( $value ) && isset( $value ) ) || ! empty( $value ); - 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; + case 'tag': + $value = self::parse_tag_source( $html ); + break; + case 'meta': + $value = self::parse_meta_source( $attribute ); + break; } - // Fallback to the attributes or default value if the result is empty. - if ( null === $value ) { - $default = $attribute['default'] ?? null; + $type = $attribute['type'] ?? null; + if ( ! $type ) { + return ( null === $value ) + ? self::get_default_attribute_value( $attribute, $attribute_value ) + : $value; + } - $value = $attribute_value ?? $default; + switch ( $type ) { + case 'integer': + $value = intval( $value ); + break; + case 'boolean': + // Boolean attributes can be an empty string. + $value = ( ! is_array( $value ) && isset( $value ) ) || ! empty( $value ); + break; } - return $value; + return ( null === $value ) + ? self::get_default_attribute_value( $attribute, $attribute_value ) + : $value; } /** @@ -198,4 +202,18 @@ private static function parse_meta_source( array $config ): ?string { private static function parse_tag_source( string $html ): ?string { return DOMHelpers::get_first_node_tag_name( $html ); } + + /** + * Get fallback value which is either attributes or default value + * + * @param array $attribute The configuration for the specific attribute. + * @param mixed $attribute_value The value from the parsed block attributes. + * + * @return mixed + */ + public static function get_default_attribute_value( $attribute, $attribute_value ) { + $default = $attribute['default'] ?? null; + + return $attribute_value ?? $default; + } } diff --git a/includes/Field/BlockSupports/Anchor.php b/includes/Field/BlockSupports/Anchor.php index 4ca1ba02..c97fed19 100644 --- a/includes/Field/BlockSupports/Anchor.php +++ b/includes/Field/BlockSupports/Anchor.php @@ -7,6 +7,7 @@ namespace WPGraphQL\ContentBlocks\Field\BlockSupports; +use WPGraphQL\ContentBlocks\GraphQL\WPGraphQLRegisterConfig; use WPGraphQL\ContentBlocks\Utilities\DOMHelpers; use WPGraphQL\ContentBlocks\Utilities\WPGraphQLHelpers; @@ -20,22 +21,25 @@ class Anchor { public static function register(): void { register_graphql_interface_type( 'BlockWithSupportsAnchor', - [ - 'description' => __( 'Block that supports Anchor field', 'wp-graphql-content-blocks' ), - 'fields' => [ - 'anchor' => [ - 'type' => 'string', - 'description' => __( 'The anchor field for the block.', 'wp-graphql-content-blocks' ), - 'resolve' => static function ( $block ) { - $rendered_block = wp_unslash( render_block( $block ) ); - if ( empty( $rendered_block ) ) { - return null; - } - return DOMHelpers::parse_first_node_attribute( $rendered_block, 'id' ); - }, + // @TODO - Remove when WPGraphQL min version is 2.3.0 + WPGraphQLRegisterConfig::resolve_graphql_config( + [ + 'description' => static fn () => __( 'Block that supports Anchor field', 'wp-graphql-content-blocks' ), + 'fields' => [ + 'anchor' => [ + 'type' => 'string', + 'description' => static fn () => __( 'The anchor field for the block.', 'wp-graphql-content-blocks' ), + 'resolve' => static function ( $block ) { + $rendered_block = wp_unslash( WPGraphQLHelpers::get_rendered_block( $block ) ); + if ( empty( $rendered_block ) ) { + return null; + } + return DOMHelpers::parse_first_node_attribute( $rendered_block, 'id' ); + }, + ], ], - ], - ] + ] + ) ); } diff --git a/includes/GraphQL/WPGraphQLRegisterConfig.php b/includes/GraphQL/WPGraphQLRegisterConfig.php new file mode 100644 index 00000000..aa7d6884 --- /dev/null +++ b/includes/GraphQL/WPGraphQLRegisterConfig.php @@ -0,0 +1,94 @@ +=' ) ) { + return $config; + } + + /** + * Recursively resolve nested configuration arrays. + * Some keys contain arrays of configurations that might also contain lazy-loaded values. + */ + $nested_configs = [ + 'args', + 'connections', + 'connectionArgs', + 'connectionFields', + 'edgeFields', + 'fields', + 'inputFields', + 'outputFields', + 'values', + ]; + + foreach ( $nested_configs as $nested_key ) { + // Skip if the key doesn't exist or isn't an array. + if ( ! isset( $config[ $nested_key ] ) || ! is_array( $config[ $nested_key ] ) ) { + continue; + } + + foreach ( $config[ $nested_key ] as $key => $value ) { + // If the value is an array, it might be a nested config requiring resolution. + if ( is_array( $value ) ) { + $config[ $nested_key ][ $key ] = self::resolve_graphql_config( $value ); + } + } + } + + /** + * Resolve the keys that cant be lazy-loaded in < 2.3.0. + * + * Mock \WPGraphQL\TypeRegistry::get_introspection_keys(). + * + * @see https://github.com/wp-graphql/wp-graphql/blob/f0988f9d70c592ae34902e6cd0a0ecf91774608e/src/Registry/TypeRegistry.php#L823-L836 + */ + $introspection_keys = [ 'description', 'deprecationReason' ]; + + // @phpstan-ignore function.alreadyNarrowedType (`WPGraphQL::is_introspection_query()` is only available in WPGraphQL 1.28.0+) + $has_introspection_check = method_exists( \WPGraphQL::class, 'is_introspection_query' ); + $is_introspection_query = $has_introspection_check ? \WPGraphQL::is_introspection_query() : false; + + foreach ( $introspection_keys as $key ) { + // Skip if the key doesn't need to be resolved. + if ( ! isset( $config[ $key ] ) || ! is_callable( $config[ $key ] ) ) { + continue; + } + + // If we 're _sure_ we are not introspecting, we can safely set the value to null. + if ( $has_introspection_check && ! $is_introspection_query ) { + $config[ $key ] = null; + continue; + } + + $config[ $key ] = $config[ $key ](); + } + + return $config; + } +} diff --git a/includes/Type/InterfaceType/EditorBlockInterface.php b/includes/Type/InterfaceType/EditorBlockInterface.php index 5aa8e760..84ce4510 100644 --- a/includes/Type/InterfaceType/EditorBlockInterface.php +++ b/includes/Type/InterfaceType/EditorBlockInterface.php @@ -8,6 +8,7 @@ namespace WPGraphQL\ContentBlocks\Type\InterfaceType; use WPGraphQL\ContentBlocks\Data\ContentBlocksResolver; +use WPGraphQL\ContentBlocks\GraphQL\WPGraphQLRegisterConfig; use WPGraphQL\ContentBlocks\Utilities\WPGraphQLHelpers; use WP_Block_Type_Registry; @@ -42,114 +43,120 @@ public static function get_block( array $block ) { public static function register_type(): void { register_graphql_interface_type( 'NodeWithEditorBlocks', - [ - 'description' => __( 'Node that has content blocks associated with it', 'wp-graphql-content-blocks' ), - 'eagerlyLoadType' => true, - 'fields' => [ - 'editorBlocks' => [ - 'type' => [ - 'list_of' => 'EditorBlock', - ], - 'args' => [ - 'flat' => [ - 'description' => __( 'Returns the list of blocks as a flat list if true', 'wp-graphql-content-blocks' ), - 'type' => 'Boolean', + // @TODO - Remove when WPGraphQL min version is 2.3.0 + WPGraphQLRegisterConfig::resolve_graphql_config( + [ + 'description' => static fn () => __( 'Node that has content blocks associated with it', 'wp-graphql-content-blocks' ), + 'eagerlyLoadType' => true, + 'fields' => [ + 'editorBlocks' => [ + 'type' => [ + 'list_of' => 'EditorBlock', + ], + 'args' => [ + 'flat' => [ + 'description' => static fn () => __( 'Returns the list of blocks as a flat list if true', 'wp-graphql-content-blocks' ), + 'type' => 'Boolean', + ], ], + 'description' => static fn () => __( 'List of editor blocks', 'wp-graphql-content-blocks' ), + 'resolve' => static function ( $node, $args ) { + return ContentBlocksResolver::resolve_content_blocks( $node, $args ); + }, ], - 'description' => __( 'List of editor blocks', 'wp-graphql-content-blocks' ), - 'resolve' => static function ( $node, $args ) { - return ContentBlocksResolver::resolve_content_blocks( $node, $args ); - }, ], - ], - ] + ] + ) ); // Register the EditorBlock Interface register_graphql_interface_type( 'EditorBlock', - [ - 'eagerlyLoadType' => true, - 'description' => __( 'Blocks that can be edited to create content and layouts', 'wp-graphql-content-blocks' ), - 'fields' => [ - 'clientId' => [ - 'type' => 'String', - 'description' => __( 'The id of the Block', 'wp-graphql-content-blocks' ), - 'resolve' => static function ( $block ) { - return isset( $block['clientId'] ) ? $block['clientId'] : uniqid(); - }, - ], - 'parentClientId' => [ - 'type' => 'String', - 'description' => __( 'The parent id of the Block', 'wp-graphql-content-blocks' ), - 'resolve' => static function ( $block ) { - return isset( $block['parentClientId'] ) ? $block['parentClientId'] : null; - }, - ], - 'name' => [ - 'type' => 'String', - 'description' => __( 'The name of the Block', 'wp-graphql-content-blocks' ), - ], - 'blockEditorCategoryName' => [ - 'type' => 'String', - 'description' => __( 'The name of the category the Block belongs to', 'wp-graphql-content-blocks' ), - 'resolve' => static function ( $block ) { - return isset( self::get_block( $block )->category ) ? self::get_block( $block )->category : null; - }, - ], - 'isDynamic' => [ - 'type' => [ 'non_null' => 'Boolean' ], - 'description' => __( 'Whether the block is Dynamic (server rendered)', 'wp-graphql-content-blocks' ), - 'resolve' => static function ( $block ) { - return isset( self::get_block( $block )->render_callback ); - }, - ], - 'apiVersion' => [ - 'type' => 'Integer', - 'description' => __( 'The API version of the Gutenberg Block', 'wp-graphql-content-blocks' ), - 'resolve' => static function ( $block ) { - return isset( self::get_block( $block )->api_version ) && absint( self::get_block( $block )->api_version ) ? absint( self::get_block( $block )->api_version ) : 2; - }, - ], - 'innerBlocks' => [ - 'type' => [ - 'list_of' => 'EditorBlock', + // @TODO - Remove when WPGraphQL min version is 2.3.0 + WPGraphQLRegisterConfig::resolve_graphql_config( + [ + 'eagerlyLoadType' => true, + 'description' => static fn () => __( 'Blocks that can be edited to create content and layouts', 'wp-graphql-content-blocks' ), + 'fields' => [ + 'clientId' => [ + 'type' => 'String', + 'description' => static fn () => __( 'The id of the Block', 'wp-graphql-content-blocks' ), + 'resolve' => static function ( $block ) { + return isset( $block['clientId'] ) ? $block['clientId'] : uniqid(); + }, ], - 'description' => __( 'The inner blocks of the Block', 'wp-graphql-content-blocks' ), - 'resolve' => static function ( $block ) { - return isset( $block['innerBlocks'] ) && is_array( $block['innerBlocks'] ) ? $block['innerBlocks'] : []; - }, - ], - 'cssClassNames' => [ - 'type' => [ 'list_of' => 'String' ], - 'description' => __( 'CSS Classnames to apply to the block', 'wp-graphql-content-blocks' ), - 'resolve' => static function ( $block ) { - if ( isset( $block['attrs']['className'] ) ) { - return explode( ' ', $block['attrs']['className'] ); - } + 'parentClientId' => [ + 'type' => 'String', + 'description' => static fn () => __( 'The parent id of the Block', 'wp-graphql-content-blocks' ), + 'resolve' => static function ( $block ) { + return isset( $block['parentClientId'] ) ? $block['parentClientId'] : null; + }, + ], + 'name' => [ + 'type' => 'String', + 'description' => static fn () => __( 'The name of the Block', 'wp-graphql-content-blocks' ), + ], + 'blockEditorCategoryName' => [ + 'type' => 'String', + 'description' => static fn () => __( 'The name of the category the Block belongs to', 'wp-graphql-content-blocks' ), + 'resolve' => static function ( $block ) { + return isset( self::get_block( $block )->category ) ? self::get_block( $block )->category : null; + }, + ], + 'isDynamic' => [ + 'type' => [ 'non_null' => 'Boolean' ], + 'description' => static fn () => __( 'Whether the block is Dynamic (server rendered)', 'wp-graphql-content-blocks' ), + 'resolve' => static function ( $block ) { + return isset( self::get_block( $block )->render_callback ); + }, + ], + 'apiVersion' => [ + 'type' => 'Integer', + 'description' => static fn () => __( 'The API version of the Gutenberg Block', 'wp-graphql-content-blocks' ), + 'resolve' => static function ( $block ) { + return isset( self::get_block( $block )->api_version ) && absint( self::get_block( $block )->api_version ) ? absint( self::get_block( $block )->api_version ) : 2; + }, + ], + 'innerBlocks' => [ + 'type' => [ + 'list_of' => 'EditorBlock', + ], + 'description' => static fn () => __( 'The inner blocks of the Block', 'wp-graphql-content-blocks' ), + 'resolve' => static function ( $block ) { + return isset( $block['innerBlocks'] ) && is_array( $block['innerBlocks'] ) ? $block['innerBlocks'] : []; + }, + ], + 'cssClassNames' => [ + 'type' => [ 'list_of' => 'String' ], + 'description' => static fn () => __( 'CSS Classnames to apply to the block', 'wp-graphql-content-blocks' ), + 'resolve' => static function ( $block ) { + if ( isset( $block['attrs']['className'] ) ) { + return explode( ' ', $block['attrs']['className'] ); + } - return null; - }, - ], - 'renderedHtml' => [ - 'type' => 'String', - 'description' => __( 'The rendered HTML for the block', 'wp-graphql-content-blocks' ), - 'resolve' => static function ( $block ) { - return render_block( $block ); - }, - ], - 'type' => [ - 'type' => 'String', - 'description' => __( 'The (GraphQL) type of the block', 'wp-graphql-content-blocks' ), - 'resolve' => static function ( $block ) { - return WPGraphQLHelpers::get_type_name_for_block( $block['blockName'] ?? null ); - }, + return null; + }, + ], + 'renderedHtml' => [ + 'type' => 'String', + 'description' => static fn () => __( 'The rendered HTML for the block', 'wp-graphql-content-blocks' ), + 'resolve' => static function ( $block ) { + return WPGraphQLHelpers::get_rendered_block( $block ); + }, + ], + 'type' => [ + 'type' => 'String', + 'description' => static fn () => __( 'The (GraphQL) type of the block', 'wp-graphql-content-blocks' ), + 'resolve' => static function ( $block ) { + return WPGraphQLHelpers::get_type_name_for_block( $block['blockName'] ?? null ); + }, + ], ], - ], - 'resolveType' => static function ( $block ) { - return WPGraphQLHelpers::get_type_name_for_block( $block['blockName'] ?? null ); - }, - ] + 'resolveType' => static function ( $block ) { + return WPGraphQLHelpers::get_type_name_for_block( $block['blockName'] ?? null ); + }, + ] + ) ); } } diff --git a/includes/Type/InterfaceType/PostTypeBlockInterface.php b/includes/Type/InterfaceType/PostTypeBlockInterface.php index f65e4d88..588ff99c 100644 --- a/includes/Type/InterfaceType/PostTypeBlockInterface.php +++ b/includes/Type/InterfaceType/PostTypeBlockInterface.php @@ -8,6 +8,7 @@ namespace WPGraphQL\ContentBlocks\Type\InterfaceType; use WPGraphQL\ContentBlocks\Data\ContentBlocksResolver; +use WPGraphQL\ContentBlocks\GraphQL\WPGraphQLRegisterConfig; use WPGraphQL\ContentBlocks\Utilities\WPGraphQLHelpers; /** @@ -23,50 +24,56 @@ final class PostTypeBlockInterface { public static function register_type( string $post_type, array $block_names = [] ): void { register_graphql_interface_type( ucfirst( $post_type ) . 'EditorBlock', - [ - 'description' => sprintf( - // translators: EditorBlock Interface for %s Block Type. - __( 'EditorBlock Interface for %s Block Type', 'wp-graphql-content-blocks' ), - ucfirst( $post_type ) - ), - 'interfaces' => [ 'EditorBlock' ], - 'fields' => [ - 'name' => [ - 'type' => 'String', + // @TODO - Remove when WPGraphQL min version is 2.3.0 + WPGraphQLRegisterConfig::resolve_graphql_config( + [ + 'description' => static fn () => sprintf( + // translators: EditorBlock Interface for %s Block Type. + __( 'EditorBlock Interface for %s Block Type', 'wp-graphql-content-blocks' ), + ucfirst( $post_type ) + ), + 'interfaces' => [ 'EditorBlock' ], + 'fields' => [ + 'name' => [ + 'type' => 'String', + ], ], - ], - 'resolveType' => static function ( $block ) { - return WPGraphQLHelpers::get_type_name_for_block( $block['blockName'] ?? null ); - }, - ] + 'resolveType' => static function ( $block ) { + return WPGraphQLHelpers::get_type_name_for_block( $block['blockName'] ?? null ); + }, + ] + ) ); register_graphql_interface_type( 'NodeWith' . ucfirst( $post_type ) . 'EditorBlocks', - [ - 'description' => sprintf( - // translators: %s is the post type. - __( 'Node that has %s content blocks associated with it', 'wp-graphql-content-blocks' ), - $post_type - ), - 'eagerlyLoadType' => true, - 'interfaces' => [ 'NodeWithEditorBlocks' ], - 'fields' => [ - 'editorBlocks' => [ - 'type' => [ - 'list_of' => ucfirst( $post_type ) . 'EditorBlock', + // @TODO - Remove when WPGraphQL min version is 2.3.0 + WPGraphQLRegisterConfig::resolve_graphql_config( + [ + 'description' => static fn () => sprintf( + // translators: %s is the post type. + __( 'Node that has %s content blocks associated with it', 'wp-graphql-content-blocks' ), + $post_type + ), + 'eagerlyLoadType' => true, + 'interfaces' => [ 'NodeWithEditorBlocks' ], + 'fields' => [ + 'editorBlocks' => [ + 'type' => [ + 'list_of' => ucfirst( $post_type ) . 'EditorBlock', + ], + 'description' => static fn () => sprintf( + // translators: %s is the post type. + __( 'List of %s editor blocks', 'wp-graphql-content-blocks' ), + $post_type + ), + 'resolve' => static function ( $node, $args ) use ( $block_names ) { + return ContentBlocksResolver::resolve_content_blocks( $node, $args, $block_names ); + }, ], - 'description' => sprintf( - // translators: %s is the post type. - __( 'List of %s editor blocks', 'wp-graphql-content-blocks' ), - $post_type - ), - 'resolve' => static function ( $node, $args ) use ( $block_names ) { - return ContentBlocksResolver::resolve_content_blocks( $node, $args, $block_names ); - }, ], - ], - ] + ] + ) ); } } diff --git a/includes/Type/Scalar/Scalar.php b/includes/Type/Scalar/Scalar.php index 010a5cf1..0ed1d7a2 100644 --- a/includes/Type/Scalar/Scalar.php +++ b/includes/Type/Scalar/Scalar.php @@ -7,6 +7,8 @@ namespace WPGraphQL\ContentBlocks\Type\Scalar; +use WPGraphQL\ContentBlocks\GraphQL\WPGraphQLRegisterConfig; + /** * Class Scalar */ @@ -17,21 +19,27 @@ final class Scalar { public function init(): void { register_graphql_scalar( 'BlockAttributesObject', - [ - 'description' => __( 'Generic Object Scalar Type', 'wp-graphql-content-blocks' ), - 'serialize' => static function ( $value ) { - return wp_json_encode( $value ); - }, - ] + // @TODO - Remove when WPGraphQL min version is 2.3.0 + WPGraphQLRegisterConfig::resolve_graphql_config( + [ + 'description' => static fn () => __( 'Generic Object Scalar Type', 'wp-graphql-content-blocks' ), + 'serialize' => static function ( $value ) { + return wp_json_encode( $value ); + }, + ] + ) ); register_graphql_scalar( 'BlockAttributesArray', - [ - 'description' => __( 'Generic Array Scalar Type', 'wp-graphql-content-blocks' ), - 'serialize' => static function ( $value ) { - return wp_json_encode( $value ); - }, - ] + // @TODO - Remove when WPGraphQL min version is 2.3.0 + WPGraphQLRegisterConfig::resolve_graphql_config( + [ + 'description' => static fn () => __( 'Generic Array Scalar Type', 'wp-graphql-content-blocks' ), + 'serialize' => static function ( $value ) { + return wp_json_encode( $value ); + }, + ] + ) ); } diff --git a/includes/Utilities/WPGraphQLHelpers.php b/includes/Utilities/WPGraphQLHelpers.php index 9cac2635..28981495 100644 --- a/includes/Utilities/WPGraphQLHelpers.php +++ b/includes/Utilities/WPGraphQLHelpers.php @@ -13,6 +13,13 @@ * Class WPGraphQLHelpers */ final class WPGraphQLHelpers { + /** + * Array of rendered blocks. + * + * @var array + */ + public static array $rendered_blocks = []; + /** * Formats the name of the block for the GraphQL registry * @@ -49,4 +56,34 @@ public static function get_type_name_for_block( ?string $block_name ): string { return self::format_type_name( $type_name ); } + + /** + * Gets the rendered block. + * + * @param mixed|array{blockName: array, attrs: array, innerBlocks: string, innerHTML: string, innerContent: string, clientId: ?string} $block The block being resolved. + */ + public static function get_rendered_block( $block ): ?string { + + // As the parent method does not have block as an array and might be a breaking change + // we are just ensuring this is an array + if ( ! is_array( $block ) ) { + // @phpstan-ignore-next-line + return render_block( $block ); + } + + $key = $block['clientId'] ?? null; + if ( ! is_string( $key ) ) { + // Client ID is expected but bail if it doesn't exist + return render_block( $block ); + } + + if ( array_key_exists( $key, self::$rendered_blocks ) ) { + return self::$rendered_blocks[ $key ]; + } + + $content = render_block( $block ); + self::$rendered_blocks[ $key ] = $content; + + return $content; + } }