PluginProbe
WPGraphQL / 2.22.1
WPGraphQL v2.22.1
2.22.3 2.22.2 2.22.1 2.22.0 2.21.1 2.21.0 2.20.0 2.19.0 2.18.0 2.17.0 2.16.0 2.15.1 2.15.0 2.14.1 2.14.0 2.13.0 2.2.0 2.3.0 2.3.3 2.3.6 2.3.8 2.5.0 2.5.1 2.5.2 2.5.3 All 177 releases
wp-graphql / src / AppContext.php

AppContext.php in WPGraphQL 2.22.1, at src/AppContext.php

508 lines 16.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace WPGraphQL;
4
5 use GraphQL\Error\UserError;
6 use WPGraphQL\Data\Loader\CommentAuthorLoader;
7 use WPGraphQL\Data\Loader\CommentLoader;
8 use WPGraphQL\Data\Loader\EnqueuedScriptLoader;
9 use WPGraphQL\Data\Loader\EnqueuedStylesheetLoader;
10 use WPGraphQL\Data\Loader\PluginLoader;
11 use WPGraphQL\Data\Loader\PostObjectLoader;
12 use WPGraphQL\Data\Loader\PostTypeLoader;
13 use WPGraphQL\Data\Loader\SettingGroupLoader;
14 use WPGraphQL\Data\Loader\TaxonomyLoader;
15 use WPGraphQL\Data\Loader\TermObjectLoader;
16 use WPGraphQL\Data\Loader\ThemeLoader;
17 use WPGraphQL\Data\Loader\UserLoader;
18 use WPGraphQL\Data\Loader\UserRoleLoader;
19 use WPGraphQL\Data\NodeResolver;
20
21 /**
22 * Class AppContext
23 * Creates an object that contains all of the context for the GraphQL query
24 * This class gets instantiated and populated in the main WPGraphQL class.
25 *
26 * The context is passed to each resolver during execution.
27 *
28 * Resolvers have the ability to read and write to context to pass info to nested resolvers.
29 *
30 * @package WPGraphQL
31 */
32 #[\AllowDynamicProperties]
33 class AppContext {
34 /**
35 * The default loaders for the AppContext.
36 */
37 private const DEFAULT_LOADERS = [
38 'comment_author' => CommentAuthorLoader::class,
39 'comment' => CommentLoader::class,
40 'enqueued_script' => EnqueuedScriptLoader::class,
41 'enqueued_stylesheet' => EnqueuedStylesheetLoader::class,
42 'plugin' => PluginLoader::class,
43 'nav_menu_item' => PostObjectLoader::class,
44 'post' => PostObjectLoader::class,
45 'post_type' => PostTypeLoader::class,
46 'setting_group' => SettingGroupLoader::class,
47 'taxonomy' => TaxonomyLoader::class,
48 'term' => TermObjectLoader::class,
49 'theme' => ThemeLoader::class,
50 'user' => UserLoader::class,
51 'user_role' => UserRoleLoader::class,
52 ];
53
54 /**
55 * Stores the class to use for the connection query.
56 *
57 * @var \WP_Query|null
58 */
59 public $connection_query_class = null;
60
61 /**
62 * Stores the url string for the current site
63 *
64 * @var string $root_url
65 */
66 public $root_url;
67
68 /**
69 * Stores the WP_User object of the current user
70 *
71 * @var \WP_User $viewer
72 */
73 public $viewer;
74
75 /**
76 * @var \WPGraphQL\Registry\TypeRegistry
77 */
78 public $type_registry;
79
80 /**
81 * Stores everything from the $_REQUEST global
82 *
83 * @var mixed $request
84 */
85 public $request;
86
87 /**
88 * Stores the normalized preview context for the request, parsed from the
89 * `X-GraphQL-Preview` header or the `preview` object in the request `extensions`.
90 * Null when the request does not carry preview context.
91 *
92 * @var array{databaseId:int,revisionDatabaseId:int,featuredImageDatabaseId:?int,nonce:?string}|null
93 */
94 public $preview = null;
95
96 /**
97 * Stores additional $config properties
98 *
99 * @var mixed $config
100 */
101 public $config;
102
103 /**
104 * Passes context about the current connection being resolved
105 *
106 * @todo These properties and methods are unused. We should consider deprecating/removing them.
107 *
108 * @var mixed|string|null
109 */
110 public $currentConnection = null;
111
112 /**
113 * Passes context about the current connection
114 *
115 * @todo These properties and methods are unused. We should consider deprecating/removing them.
116 *
117 * @var array<string,mixed>
118 */
119 public $connectionArgs = [];
120
121 /**
122 * Stores the loaders for the class
123 *
124 * @var array<string,\WPGraphQL\Data\Loader\AbstractDataLoader>
125 *
126 * phpcs:disable SlevomatCodingStandard.Namespaces.FullyQualifiedClassNameInAnnotation, -- For phpstan type hinting
127 *
128 * @template T of key-of<self::DEFAULT_LOADERS>
129 *
130 * @phpstan-var array<T, new<self::DEFAULT_LOADERS[T]>>|array<string,\WPGraphQL\Data\Loader\AbstractDataLoader>
131 *
132 * phpcs:enable
133 */
134 public $loaders = [];
135
136 /**
137 * Instance of the NodeResolver class to resolve nodes by URI
138 *
139 * @var \WPGraphQL\Data\NodeResolver
140 */
141 public $node_resolver;
142
143 /**
144 * The loader classes, before they are instantiated.
145 *
146 * @var array<string,class-string<\WPGraphQL\Data\Loader\AbstractDataLoader>>
147 */
148 private $loader_classes = self::DEFAULT_LOADERS;
149
150 /**
151 * Stores custom data with namespace isolation.
152 *
153 * This is a key-value store where data is organized by namespace to prevent collisions
154 * between different plugins/extensions.
155 *
156 * INTENDED USE: Store temporary, request-scoped state that needs to be passed between
157 * different phases of GraphQL execution (e.g., directive hooks, middleware, resolver chains).
158 *
159 * NOT INTENDED: Storing permanent configuration or replacing existing AppContext properties.
160 * For configuration, use the 'graphql_app_context_config' filter instead.
161 *
162 * @var array<string,array<string,mixed>>
163 */
164 private $store = [];
165
166 /**
167 * AppContext constructor.
168 */
169 public function __construct() {
170
171 // Prime the loader classes (and their instances) for the AppContext.
172 $this->prepare_data_loaders();
173
174 /**
175 * This sets up the NodeResolver to allow nodes to be resolved by URI
176 */
177 $this->node_resolver = new NodeResolver( $this );
178
179 /**
180 * This filters the config for the AppContext.
181 *
182 * This can be used to store additional context config, which is available to resolvers
183 * throughout the resolution of a GraphQL request.
184 *
185 * @param mixed[] $config The config array of the AppContext object
186 *
187 * @hookGroup request-lifecycle
188 * @since 0.0.5
189 */
190 $this->config = apply_filters( 'graphql_app_context_config', $this->config );
191 }
192
193 /**
194 * Prepares the data loaders for the AppContext.
195 *
196 * This method instantiates the loader classes and prepares them for use in the AppContext.
197 * It also applies filters to allow customization of the loader classes.
198 *
199 * @uses graphql_data_loader_classes filter.
200 * @uses graphql_data_loaders filter (deprecated).
201 */
202 private function prepare_data_loaders(): void {
203 /**
204 * Filter to change the data loader classes.
205 *
206 * This allows for additional loaders to be added to the AppContext or replaced as needed.
207 *
208 * @param array<string,class-string<\WPGraphQL\Data\Loader\AbstractDataLoader>> $loader_classes The loader classes accessible in the AppContext
209 * @param \WPGraphQL\AppContext $context The AppContext
210 *
211 * @hookGroup request-lifecycle
212 * @since 0.0.5
213 */
214 $this->loader_classes = apply_filters( 'graphql_data_loader_classes', $this->loader_classes, $this );
215
216 /**
217 * Prime the loaders if needed
218 *
219 * @todo Remove this when the loaders are instantiated on demand.
220 */
221 if ( has_filter( 'graphql_data_loaders' ) ) {
222 $loaders = array_map(
223 function ( $loader_class ) {
224 return new $loader_class( $this );
225 },
226 $this->loader_classes
227 );
228
229 /**
230 * @deprecated 2.3.2 in favor of graphql_data_loader_classes.
231 * @todo Remove in a future version.
232 *
233 * @param array<string,\WPGraphQL\Data\Loader\AbstractDataLoader> $loaders The loaders accessible in the AppContext
234 * @param \WPGraphQL\AppContext $context The AppContext
235 */
236 $this->loaders = apply_filters_deprecated(
237 'graphql_data_loaders',
238 [ $loaders, $this ],
239 '2.3.2',
240 'graphql_data_loader_classes',
241 esc_html__( 'The graphql_data_loaders filter is deprecated and will be removed in a future version. Instead, use the graphql_data_loader_classes filter to add/change data loader classes before they are instantiated.', 'wp-graphql' ),
242 );
243 }
244 }
245
246 /**
247 * Retrieves loader assigned to $key
248 *
249 * @param string $key The name of the loader to get
250 *
251 * @return \WPGraphQL\Data\Loader\AbstractDataLoader
252 *
253 * @deprecated Use get_loader instead.
254 */
255 public function getLoader( $key ) {
256 _deprecated_function( __METHOD__, '0.8.4', self::class . '::get_loader()' );
257 return $this->get_loader( $key );
258 }
259
260 /**
261 * Retrieves loader assigned to $key
262 *
263 * @template T of key-of<self::DEFAULT_LOADERS>
264 *
265 * @param T|string $key The name of the loader to get.
266 *
267 * @return \WPGraphQL\Data\Loader\AbstractDataLoader
268 * @throws \GraphQL\Error\UserError If the loader is not found.
269 *
270 * @phpstan-return ( $key is T ? new<self::DEFAULT_LOADERS[T]> : \WPGraphQL\Data\Loader\AbstractDataLoader )
271 */
272 public function get_loader( $key ) {
273 // @todo: Remove the isset() when `graphql_data_loaders` is removed.
274 if ( ! array_key_exists( $key, $this->loader_classes ) && ! isset( $this->loaders[ $key ] ) ) {
275 // translators: %s is the key of the loader that was not found.
276 throw new UserError( esc_html( sprintf( __( 'No loader assigned to the key %s', 'wp-graphql' ), $key ) ) );
277 }
278
279 // If the loader is not instantiated, instantiate it.
280 if ( ! isset( $this->loaders[ $key ] ) ) {
281 try {
282 $this->loaders[ $key ] = new $this->loader_classes[ $key ]( $this );
283 } catch ( \Throwable $e ) {
284 // translators: %s is the key of the loader that failed to instantiate.
285 throw new UserError( esc_html( sprintf( __( 'Failed to instantiate %1$s: %2$s', 'wp-graphql' ), $this->loader_classes[ $key ], $e->getMessage() ) ) );
286 }
287 }
288
289 /** @var \WPGraphQL\Data\Loader\AbstractDataLoader $loader */
290 $loader = $this->loaders[ $key ];
291 return $loader;
292 }
293
294 /**
295 * Magic getter used to warn about accessing the loaders property directly.
296 *
297 * @todo Remove this when we change the property visibility.
298 *
299 * @param string $key The name of the property to get.
300 * @return mixed
301 */
302 public function __get( $key ) {
303 // Use default handling if the key is not a loader.
304 if ( 'loaders' !== $key ) {
305 return $this->$key;
306 }
307
308 // Warn about accessing the loaders property directly.
309 _doing_it_wrong(
310 __METHOD__,
311 esc_html__( 'Accessing the AppContext::$loaders property from outside the AppContext class is deprecated and will throw an error in a future version. Use AppContext::get_loader() instead.', 'wp-graphql' ),
312 '2.3.2'
313 );
314
315 // Return the actual loaders array.
316 return $this->loaders;
317 }
318
319 /**
320 * Returns the $args for the connection the field is a part of
321 *
322 * @deprecated use get_connection_args() instead
323 * @return mixed[]|mixed
324 */
325 public function getConnectionArgs() {
326 _deprecated_function( __METHOD__, '0.8.4', self::class . '::get_connection_args()' );
327 return $this->get_connection_args();
328 }
329
330 /**
331 * Returns the $args for the connection the field is a part of
332 *
333 * @todo These properties and methods are unused. We should consider deprecating/removing them.
334 *
335 * @return mixed[]|mixed
336 */
337 public function get_connection_args() {
338 return isset( $this->currentConnection ) && isset( $this->connectionArgs[ $this->currentConnection ] ) ? $this->connectionArgs[ $this->currentConnection ] : [];
339 }
340
341 /**
342 * Returns the current connection
343 *
344 * @todo These properties and methods are unused. We should consider deprecating/removing them.
345 *
346 * @return mixed|string|null
347 */
348 public function get_current_connection() {
349 return isset( $this->currentConnection ) ? $this->currentConnection : null;
350 }
351
352 /**
353 * @return mixed|string|null
354 * @deprecated use get_current_connection instead.
355 */
356 public function getCurrentConnection() {
357 _deprecated_function( __METHOD__, '0.8.4', self::class . '::get_current_connection()' );
358 return $this->get_current_connection();
359 }
360
361 /**
362 * Magic setter to warn about setting dynamic properties on AppContext.
363 *
364 * This maintains backward compatibility while warning developers to use the new set() method.
365 *
366 * @param string $name The name of the property being set.
367 * @param mixed $value The value being assigned to the property.
368 * @return void
369 */
370 public function __set( $name, $value ) {
371 // Only warn for truly dynamic properties, not existing defined properties
372 if ( ! property_exists( $this, $name ) ) {
373 _doing_it_wrong(
374 __METHOD__,
375 sprintf(
376 // translators: %s is the name of the property being set.
377 esc_html__( 'Setting dynamic properties on AppContext is deprecated. Use AppContext::set() instead. Attempted to set property: %s', 'wp-graphql' ),
378 esc_html( $name )
379 ),
380 '2.5.0'
381 );
382 }
383
384 // Still set the property for backward compatibility
385 $this->$name = $value;
386 }
387
388 /**
389 * Sets a value in the context store with namespace isolation.
390 *
391 * It's strongly recommended to use a unique namespace to avoid collisions with other plugins.
392 * A good practice is to use your plugin's text domain or a similar unique identifier.
393 *
394 * Example:
395 * ```php
396 * $context->set( 'my-plugin', 'user-language', 'fr' );
397 * $context->set( 'my-plugin', 'original-locale', get_locale() );
398 * ```
399 *
400 * @param string $namespace The namespace to store the value under (e.g., 'my-plugin').
401 * @param string $key The key to store the value under within the namespace.
402 * @param mixed $value The value to store.
403 * @since 2.3.8
404 */
405 public function set( string $namespace, string $key, $value ): void { // phpcs:ignore Universal.NamingConventions.NoReservedKeywordParameterNames.namespaceFound -- Namespace is semantically appropriate.
406 if ( ! isset( $this->store[ $namespace ] ) ) {
407 $this->store[ $namespace ] = [];
408 }
409
410 $this->store[ $namespace ][ $key ] = $value;
411 }
412
413 /**
414 * Gets a value from the context store.
415 *
416 * Example:
417 * ```php
418 * $language = $context->get( 'my-plugin', 'user-language', 'en' );
419 * $locale = $context->get( 'my-plugin', 'original-locale' );
420 * ```
421 *
422 * @param string $namespace The namespace to retrieve the value from.
423 * @param string $key The key to retrieve within the namespace.
424 * @param mixed $default Optional. The default value to return if the key doesn't exist. Default null.
425 * @return mixed The value if it exists, otherwise the default value.
426 * @since 2.3.8
427 */
428 public function get( string $namespace, string $key, $default = null ) { // phpcs:ignore Universal.NamingConventions.NoReservedKeywordParameterNames.namespaceFound, Universal.NamingConventions.NoReservedKeywordParameterNames.defaultFound -- Semantically appropriate.
429 return $this->store[ $namespace ][ $key ] ?? $default;
430 }
431
432 /**
433 * Checks if a key exists in the context store.
434 *
435 * Example:
436 * ```php
437 * if ( $context->has( 'my-plugin', 'user-language' ) ) {
438 * $language = $context->get( 'my-plugin', 'user-language' );
439 * }
440 * ```
441 *
442 * @param string $namespace The namespace to check.
443 * @param string $key The key to check within the namespace.
444 * @return bool True if the key exists, false otherwise.
445 * @since 2.3.8
446 */
447 public function has( string $namespace, string $key ): bool { // phpcs:ignore Universal.NamingConventions.NoReservedKeywordParameterNames.namespaceFound -- Namespace is semantically appropriate.
448 return isset( $this->store[ $namespace ] ) && array_key_exists( $key, $this->store[ $namespace ] );
449 }
450
451 /**
452 * Removes a specific key from the context store.
453 *
454 * Example:
455 * ```php
456 * $context->remove( 'my-plugin', 'temporary-data' );
457 * ```
458 *
459 * @param string $namespace The namespace containing the key.
460 * @param string $key The key to remove.
461 * @since 2.3.8
462 */
463 public function remove( string $namespace, string $key ): void { // phpcs:ignore Universal.NamingConventions.NoReservedKeywordParameterNames.namespaceFound -- Namespace is semantically appropriate.
464 if ( isset( $this->store[ $namespace ] ) ) {
465 unset( $this->store[ $namespace ][ $key ] );
466 }
467 }
468
469 /**
470 * Clears all data in a specific namespace.
471 *
472 * This removes all keys associated with the given namespace.
473 *
474 * Example:
475 * ```php
476 * // Clear all data for 'my-plugin' namespace
477 * $context->clear( 'my-plugin' );
478 * ```
479 *
480 * @param string $namespace The namespace to clear.
481 * @since 2.3.8
482 */
483 public function clear( string $namespace ): void { // phpcs:ignore Universal.NamingConventions.NoReservedKeywordParameterNames.namespaceFound -- Namespace is semantically appropriate.
484 unset( $this->store[ $namespace ] );
485 }
486
487 /**
488 * Gets all data stored in a specific namespace.
489 *
490 * Returns an associative array of all key-value pairs in the namespace.
491 *
492 * Example:
493 * ```php
494 * $all_data = $context->all( 'my-plugin' );
495 * foreach ( $all_data as $key => $value ) {
496 * // Process each key-value pair
497 * }
498 * ```
499 *
500 * @param string $namespace The namespace to retrieve data from.
501 * @return array<string,mixed> An array of all key-value pairs in the namespace, or empty array if namespace doesn't exist.
502 * @since 2.3.8
503 */
504 public function all( string $namespace ): array { // phpcs:ignore Universal.NamingConventions.NoReservedKeywordParameterNames.namespaceFound -- Namespace is semantically appropriate.
505 return $this->store[ $namespace ] ?? [];
506 }
507 }
508