Repository: wp-graphql/wp-graphql (language: php)

Issue:
Eagerly Load Types should only be true for introspection requests
### What problem does this address?

When a type is defined as `eagerlyLoadType => true` there is a run-time performance cost for each query and this is typically only needed for Introspection queries. 

### What is your proposed solution?

We should consider "forcing" eagerlyLoadType to be false for non introspection queries and true for introspection. 

### What alternatives have you considered?

_No response_

### Additional Context

see: https://github.com/wpengine/wp-graphql-content-blocks/issues/134#issuecomment-1648475383


Discussion / hints:

Fix patch:
diff --git a/cli/wp-cli.php b/cli/wp-cli.php
index e2b9ade46..002e60c10 100644
--- a/cli/wp-cli.php
+++ b/cli/wp-cli.php
@@ -39,8 +39,16 @@ public function generate_static_schema( $args, $assoc_args ) {
 		 * Generate the Schema
 		 */
 		WP_CLI::line( 'Getting the Schema...' );
+
+		// Set the introspection query flag
+		WPGraphQL::set_is_introspection_query( true );
+
+		// Get the schema
 		$schema = WPGraphQL::get_schema();
 
+		// Reset the introspection query flag
+		WPGraphQL::set_is_introspection_query( false );
+
 		/**
 		 * Format the Schema
 		 */
diff --git a/src/Registry/TypeRegistry.php b/src/Registry/TypeRegistry.php
index 11ddfb838..2d8bc844c 100644
--- a/src/Registry/TypeRegistry.php
+++ b/src/Registry/TypeRegistry.php
@@ -4,6 +4,7 @@
 
 use GraphQL\Error\Error;
 use GraphQL\Type\Definition\Type;
+use WPGraphQL;
 use WPGraphQL\Data\DataSource;
 use WPGraphQL\Mutation\CommentCreate;
 use WPGraphQL\Mutation\CommentDelete;
@@ -407,10 +408,10 @@ public function init_type_registry( self $type_registry ) {
 		 *
 		 * @var \WP_Post_Type[] $allowed_post_types
 		 */
-		$allowed_post_types = \WPGraphQL::get_allowed_post_types( 'objects' );
+		$allowed_post_types = WPGraphQL::get_allowed_post_types( 'objects' );
 
 		/** @var \WP_Taxonomy[] $allowed_taxonomies */
-		$allowed_taxonomies = \WPGraphQL::get_allowed_taxonomies( 'objects' );
+		$allowed_taxonomies = WPGraphQL::get_allowed_taxonomies( 'objects' );
 
 		foreach ( $allowed_post_types as $post_type_object ) {
 			PostObject::register_types( $post_type_object );
@@ -714,7 +715,7 @@ public function register_type( string $type_name, $config ): void {
 			return $this->prepare_type( $type_name, $config );
 		};
 
-		if ( is_array( $config ) && isset( $config['eagerlyLoadType'] ) && true === $config['eagerlyLoadType'] && ! isset( $this->eager_type_map[ $this->format_key( $type_name ) ] ) ) {
+		if ( WPGraphQL::is_introspection_query() && is_array( $config ) && isset( $config['eagerlyLoadType'] ) && true === $config['eagerlyLoadType'] && ! isset( $this->eager_type_map[ $this->format_key( $type_name ) ] ) ) {
 			$this->eager_type_map[ $this->format_key( $type_name ) ] = $this->format_key( $type_name );
 		}
 	}
diff --git a/src/Request.php b/src/Request.php
index e2a4a5e54..699c2a91c 100644
--- a/src/Request.php
+++ b/src/Request.php
@@ -151,9 +151,6 @@ public function __construct( array $data = [] ) {
 		// Get the Type Registry
 		$this->type_registry = \WPGraphQL::get_type_registry();
 
-		// Get the Schema
-		$this->schema = \WPGraphQL::get_schema();
-
 		// Get the App Context
 		$this->app_context = \WPGraphQL::get_app_context();
 
@@ -292,6 +289,9 @@ private function before_execute(): void {
 			$this->do_action( $this->params );
 		}
 
+		// Get the Schema
+		$this->schema = \WPGraphQL::get_schema();
+
 		/**
 		 * This action runs before execution of a GraphQL request (regardless if it's a single or batch request)
 		 *
diff --git a/src/Utils/QueryAnalyzer.php b/src/Utils/QueryAnalyzer.php
index 94c032328..18b148071 100644
--- a/src/Utils/QueryAnalyzer.php
+++ b/src/Utils/QueryAnalyzer.php
@@ -32,7 +32,7 @@
 class QueryAnalyzer {
 
 	/**
-	 * @var \GraphQL\Type\Schema
+	 * @var \WPGraphQL\WPSchema|null
 	 */
 	protected $schema;
 
@@ -107,6 +107,17 @@ class QueryAnalyzer {
 	 */
 	protected $is_enabled_for_query;
 
+	/**
+	 * @param \WPGraphQL\Request $request The GraphQL request being executed
+	 */
+	public function __construct( Request $request ) {
+		$this->request       = $request;
+		$this->runtime_nodes = [];
+		$this->models        = [];
+		$this->list_types    = [];
+		$this->queried_types = [];
+	}
+
 	/**
 	 * Checks whether the Query Analyzer is enabled on the site.
 	 *
@@ -133,15 +144,17 @@ public static function is_enabled(): bool {
 	}
 
 	/**
-	 * @param \WPGraphQL\Request $request The GraphQL request being executed
+	 * Get the GraphQL Schema.
+	 * If the schema is not set, it will be set.
+	 *
+	 * @throws \Exception
 	 */
-	public function __construct( Request $request ) {
-		$this->request       = $request;
-		$this->schema        = $request->schema;
-		$this->runtime_nodes = [];
-		$this->models        = [];
-		$this->list_types    = [];
-		$this->queried_types = [];
+	public function get_schema(): ?WPSchema {
+		if ( ! $this->schema ) {
+			$this->schema = WPGraphQL::get_schema();
+		}
+
+		return $this->schema;
 	}
 
 	/**
@@ -256,9 +269,9 @@ public function determine_graphql_keys( ?string $query, ?string $operation, ?arr
 
 		// if there's a query (either saved or part of the request params)
 		// get the GraphQL Types being asked for by the query
-		$this->list_types    = $this->set_list_types( $this->schema, $query );
-		$this->queried_types = $this->set_query_types( $this->schema, $query );
-		$this->models        = $this->set_query_models( $this->schema, $query );
+		$this->list_types    = $this->set_list_types( $this->get_schema(), $query );
+		$this->queried_types = $this->set_query_types( $this->get_schema(), $query );
+		$this->models        = $this->set_query_models( $this->get_schema(), $query );
 
 		/**
 		 * @param \WPGraphQL\Utils\QueryAnalyzer $query_analyzer The instance of the query analyzer
@@ -352,7 +365,7 @@ public static function get_wrapped_field_type( Type $type, FieldDefinition $fiel
 		}
 
 		if ( $type instanceof NonNull || $type instanceof ListOfType ) {
-			if ( $type instanceof ListOfType && isset( $parent_type->name ) ) {
+			if ( $type instanceof ListOfType && ! empty( $parent_type->name ) ) {
 				$is_list_type = true;
 			}
 
diff --git a/src/WPGraphQL.php b/src/WPGraphQL.php
index 96eee7711..4cc10eef3 100644
--- a/src/WPGraphQL.php
+++ b/src/WPGraphQL.php
@@ -62,7 +62,12 @@ final class WPGraphQL {
 	/**
 	 * @var bool
 	 */
-	protected static $is_graphql_request;
+	protected static $is_graphql_request = false;
+
+	/**
+	 * @var bool
+	 */
+	protected static $is_introspection_query = false;
 
 	/**
 	 * The instance of the WPGraphQL object
@@ -140,12 +145,32 @@ public static function set_is_graphql_request( $is_graphql_request = false ) {
 	}
 
 	/**
-	 * @return bool
+	 * Whether the request is a graphql request or not
 	 */
-	public static function is_graphql_request() {
+	public static function is_graphql_request(): bool {
 		return self::$is_graphql_request;
 	}
 
+	/**
+	 * Set whether the request is an introspection query or not
+	 *
+	 * @param bool $is_introspection_query
+	 *
+	 * @since todo
+	 */
+	public static function set_is_introspection_query( bool $is_introspection_query = false ): void {
+		self::$is_introspection_query = $is_introspection_query;
+	}
+
+	/**
+	 * Whether the request is an introspection query or not (query for __type or __schema)
+	 *
+	 * @since todo
+	 */
+	public static function is_introspection_query(): bool {
+		return self::$is_introspection_query;
+	}
+
 	/**
 	 * Sets up actions to run at certain spots throughout WordPress and the WPGraphQL execution
 	 * cycle
@@ -203,6 +228,7 @@ static function () {
 
 		// Throw an exception
 		add_action( 'do_graphql_request', [ $this, 'min_php_version_check' ] );
+		add_action( 'do_graphql_request', [ $this, 'introspection_check' ], 10, 4 );
 
 		// Initialize Admin functionality
 		add_action( 'after_setup_theme', [ $this, 'init_admin' ] );
@@ -219,6 +245,41 @@ static function () {
 		);
 	}
 
+	/**
+	 * @param ?string                         $query     The GraphQL query
+	 * @param ?string                         $operation The name of the operation
+	 * @param ?array<mixed>                   $variables Variables to be passed to your GraphQL
+	 *                                                   request
+	 * @param \GraphQL\Server\OperationParams $params    The Operation Params. This includes any
+	 *                                                   extra params,
+	 *
+	 * @throws \GraphQL\Error\SyntaxError
+	 * @throws \Exception
+	 */
+	public function introspection_check( ?string $query, ?string $operation, ?array $variables, \GraphQL\Server\OperationParams $params ): void {
+
+		if ( empty( $query ) ) {
+			return;
+		}
+
+		$ast              = \GraphQL\Language\Parser::parse( $query );
+		$is_introspection = false;
+
+		\GraphQL\Language\Visitor::visit(
+			$ast,
+			[
+				'Field' => static function ( \GraphQL\Language\AST\FieldNode $node ) use ( &$is_introspection ) {
+					if ( '__schema' === $node->name->value || '__type' === $node->name->value ) {
+						$is_introspection = true;
+						return \GraphQL\Language\Visitor::stop();
+					}
+				},
+			]
+		);
+
+		self::set_is_introspection_query( $is_introspection );
+	}
+
 	/**
 	 * Check if the minimum PHP version requirement is met before execution begins.
 	 *
@@ -233,7 +294,7 @@ public function min_php_version_check() {
 			throw new \Exception(
 				esc_html(
 					sprintf(
-						// translators: %1$s is the current PHP version, %2$s is the minimum required PHP version.
+					// translators: %1$s is the current PHP version, %2$s is the minimum required PHP version.
 						__( 'The server\'s current PHP version %1$s is lower than the WPGraphQL minimum required version: %2$s', 'wp-graphql' ),
 						PHP_VERSION,
 						GRAPHQL_MIN_PHP_VERSION


Test patch:
diff --git a/tests/wpunit/AccessFunctionsTest.php b/tests/wpunit/AccessFunctionsTest.php
index 1441215d9..bf608fee4 100644
--- a/tests/wpunit/AccessFunctionsTest.php
+++ b/tests/wpunit/AccessFunctionsTest.php
@@ -2376,13 +2376,11 @@ public function testRegisterFieldWithNonExistingTypeReturnsErrorWhenFieldIsRefer
 			$this->expectedField( 'users', self::NOT_NULL ),
 		] );
 
-		// The third query should throw a GraphQL\Error calling out the fact that the field is referencing a non-existent type
 		$this->expectException( GraphQL\Error\Error::class );
 		$this->expectExceptionMessageMatches( "/non-existent/" );
 
 		$actual_three = $this->graphql([
 			'query' => $query_three
 		]);
-
 	}
 }
diff --git a/tests/wpunit/AssertValidSchemaTest.php b/tests/wpunit/AssertValidSchemaTest.php
index ac4960190..9cb02655b 100644
--- a/tests/wpunit/AssertValidSchemaTest.php
+++ b/tests/wpunit/AssertValidSchemaTest.php
@@ -27,7 +27,9 @@ public function testValidSchema() {
 	public function testSchema() {
 		try {
 			$request = new \WPGraphQL\Request();
-			$request->schema->assertValid();
+
+			$schema = WPGraphQL::get_schema();
+			$schema->assertValid();
 
 			// Assert true upon success.
 			$this->assertTrue( true );
diff --git a/tests/wpunit/CustomPostTypeTest.php b/tests/wpunit/CustomPostTypeTest.php
index 4fd8579c5..56ef74055 100644
--- a/tests/wpunit/CustomPostTypeTest.php
+++ b/tests/wpunit/CustomPostTypeTest.php
@@ -1690,15 +1690,19 @@ public function testRegisterCustomPostTypeWithUnderscoresInGraphqlNameHasValidSc
 			// register the post type with underscore in the graphql_single_name / graphql_plural_name
 			register_post_type( 'with_underscore', $args );
 
-			$request = new \WPGraphQL\Request();
+			new \WPGraphQL\Request();
 
-			// clean up
-			unregister_post_type( 'with_underscore' );
+			$schema = WPGraphQL::get_schema();
+
+			$this->assertTrue( $schema->hasType( 'With_underscore' ) );
 
-			$request->schema->assertValid();
+			$schema->assertValid();
 
 			// Assert true upon success.
 			$this->assertTrue( true );
+
+			// clean up
+			unregister_post_type( 'with_underscore' );
 	}
 
 	public function testRegisterPostTypeWithoutGraphqlPluralNameIsValid() {
@@ -1730,7 +1734,9 @@ public function testRegisterPostTypeWithoutGraphqlPluralNameIsValid() {
 		);
 
 		$request = new \WPGraphQL\Request();
-		$request->schema->assertValid();
+		$schema  = WPGraphQL::get_schema();
+		$schema->assertValid();
+		$this->assertTrue( $schema->hasType( 'NoPlural' ) );
 
 		self::assertQuerySuccessful(
 			$actual,
@@ -1757,12 +1763,18 @@ public function testRegisterPostTypeWithoutGraphqlSingleOrPluralNameDoesntInvali
 
 		// assert that the schema is still valid, even though the tax
 		// didn't provide the single/plural name (it will be left out of the schema)
-		$request = new \WPGraphQL\Request();
-		$request->schema->assertValid();
+		new \WPGraphQL\Request();
+		$schema = WPGraphQL::get_schema();
+		$schema->assertValid();
+
+		$map = array_keys( $schema->getTypeMap() );
+
+		$this->assertTrue( in_array( 'BootstrapPost', $map, true ) );
+		$this->assertTrue( ! in_array( 'CptNoSinglePlural', $map, true ) );
 
 		// Cleanup
 		unregister_post_type( 'cpt_no_single_plural' );
-		$this->clearSchema();
+
 	}
 
 	public function testRegisterPostTypeWithUnderscoresAsGraphqlSingleName() {
diff --git a/tests/wpunit/CustomTaxonomyTest.php b/tests/wpunit/CustomTaxonomyTest.php
index df6e82b22..f0862c643 100644
--- a/tests/wpunit/CustomTaxonomyTest.php
+++ b/tests/wpunit/CustomTaxonomyTest.php
@@ -1229,7 +1229,8 @@ public function testRegisterCustomPostTypeWithUnderscoresInGraphqlNameHasValidSc
 
 		unregister_taxonomy( 'with_underscore' );
 
-		$request->schema->assertValid();
+		$schema = WPGraphQL::get_schema();
+		$schema->assertValid();
 
 		// Assert true upon success.
 		$this->assertTrue( true );
@@ -1247,7 +1248,8 @@ public function testRegisterTaxonomyWithoutGraphqlPluralNameIsValid() {
 		);
 
 		$request = new \WPGraphQL\Request();
-		$request->schema->assertValid();
+		$schema = WPGraphQL::get_schema();
+		$schema->assertValid();
 
 		$query = '
 		{
@@ -1290,7 +1292,8 @@ public function testRegisterTaxonomyWithoutGraphqlSingleOrPluralNameDoesntInvali
 		// assert that the schema is still valid, even though the tax
 		// didn't provide the single/plural name (it will be left out of the schema)
 		$request = new \WPGraphQL\Request();
-		$request->schema->assertValid();
+		$schema = WPGraphQL::get_schema();
+		$schema->assertValid();
 
 		unregister_taxonomy( 'tax_no_single_plural' );
 	}
diff --git a/tests/wpunit/InterfaceTest.php b/tests/wpunit/InterfaceTest.php
index 9e3f564b5..4b6a3d68e 100644
--- a/tests/wpunit/InterfaceTest.php
+++ b/tests/wpunit/InterfaceTest.php
@@ -117,7 +117,8 @@ interfaceOnlyField
 	public function testSchemaIsValid() {
 		try {
 			$request = new \WPGraphQL\Request();
-			$request->schema->assertValid();
+			$schema  = WPGraphQL::get_schema();
+			$schema->assertValid();
 
 			// Assert true upon success.
 			$this->assertTrue( true );
diff --git a/tests/wpunit/PostObjectMutationsTest.php b/tests/wpunit/PostObjectMutationsTest.php
index 1095c06b2..3b6484e1e 100644
--- a/tests/wpunit/PostObjectMutationsTest.php
+++ b/tests/wpunit/PostObjectMutationsTest.php
@@ -787,12 +787,12 @@ public function testDeletePostOfAnotherType() {
 		$page_id = $this->factory()->post->create( $args );
 
 		$query = '
-		mutation deletePostWithPageIdShouldFail{
-			deletePost( $id:ID! ){
-				post{
-					id
-				}
-			}
+		mutation deletePostWithPageIdShouldFail($id: ID!) {
+		  deletePost(input: {id: $id}) {
+		    post {
+		      id
+		    }
+		  }
 		}
 		';
 
diff --git a/tests/wpunit/RequestTest.php b/tests/wpunit/RequestTest.php
index b471d94c9..446efa87f 100644
--- a/tests/wpunit/RequestTest.php
+++ b/tests/wpunit/RequestTest.php
@@ -94,11 +94,9 @@ public function testRequestError() {
 		$this->factory->post->create();
 
 		$request = new Request( [ 'query' => 'query {}' ] );
+		$this->expectException( Exception::class );
 		$results = $request->execute();
 
-		$this->assertArrayHasKey( 'errors', $results );
-		$this->assertArrayNotHasKey( 'data', $results );
-		$this->assertEquals( 1, count( $results['errors'] ) );
 	}
 
 	/**