-
Notifications
You must be signed in to change notification settings - Fork 466
Description
Summary
This RFC proposes the introduction of a formal API for setting and getting arbitrary data on the AppContext object. This will provide a more stable and predictable way for developers, especially those creating extensions or using features like directives, to pass data through the GraphQL execution lifecycle.
Motivation
The AppContext object is a powerful feature of WPGraphQL, as it is passed to every resolver. This allows it to be a vehicle for passing contextual information throughout a request.
Features like the new Directive API (#3391) often will need to store state or pass data between different hooks of the execution process (e.g., from a before_execute hook to an after_execute hook).
Currently, developers can add arbitrary properties directly to the AppContext object (e.g., $context->my_custom_property = 'value';).
This is possible because the class is marked with #[AllowDynamicProperties].
This approach has several downsides:
- Lack of predictability: There's no clear, defined API for this functionality.
- Risk of collisions: Different plugins or parts of the codebase might unknowingly use the same property name, leading to conflicts and hard-to-debug issues.
- Pollution of the AppContext object: The AppContext object's public properties are mixed with arbitrary, dynamic data, making it harder to understand its core interface.
- Forward-compatibility issues: Dynamic properties are deprecated in PHP 8.2 and will be removed in PHP 9.0.
While #[AllowDynamicProperties] suppresses this, it's a signal that we should move towards more structured classes.
By introducing a formal get() and set() API, we can address these issues and provide a robust mechanism for custom data storage within the AppContext.
Proposed API
We will add the following public methods to the WPGraphQL\AppContext class. A private $store (naming tbd) array will store the key/values.
Something like:
class AppContext {
/**
* @var array<string,mixed>
*/
private $store = [];
/**
* Sets a value on the context.
*
* It's recommended to use a namespace-prefixed key to avoid collisions.
* e.g., 'my-plugin/my-key'
*
* @param string $key The key to set.
* @param mixed $value The value to set.
* @return void
*/
public function set( string $key, $value ): void {
$this->store[ $key ] = $value;
}
/**
* Gets a value from the context.
*
* @param string $key The key to get.
* @param mixed $default The default value to return if the key is not set.
* @return mixed
*/
public function get( string $key, $default = null ) {
return $this->store[ $key ] ?? $default;
}
/**
* Checks if a key is set in the context.
*
* @param string $key The key to check.
* @return bool
*/
public function has( string $key ): bool {
return array_key_exists( $key, $this->store );
}
/**
* Removes a value from the context.
*
* @param string $key The key to remove.
* @return void
*/
public function remove( string $key ): void {
unset( $this->store[ $key ] );
}
}This approach should maintain backward compatibility while clearly communicating the new, preferred way of interacting with the AppContext for custom data.
Key Naming Conventions
To prevent key collisions between different plugins and themes, we will strongly recommend in the documentation that developers prefix their keys with a unique namespace. A good practice is to use the plugin's text domain or a similar identifier.
// Good: Prefixed with a namespace
$context->set( 'my-awesome-plugin/user-language', 'fr' );
// Bad: Generic key, prone to collision
$context->set( 'language', 'fr' );We could/should alternatively enforce it in the API, using a namespace as an argument on get/set, i.e.:
$context->set( $namespace, $key, $value );
$context->get( $namespace, $key );Example Usage in a Directive
In #3391 I shared an example directive that allows for the locale to be set and reset before and after query execution. Below is a POC (not necessarily functional) example of how this directive could possibly be re-written with the new AppContext key/value store API:
register_graphql_directive( 'setLocale', [
'description' => 'Executes the query or field in the specified language.',
'args' => [ /* ... */ ],
'locations' => [
'QUERY' => [
'before_execute' => function ( $request, $operation, $directive_args ) {
$locale = $directive_args['locale'] ?? null;
if ( ! empty( $locale ) ) {
// Store the original locale using the new API
$request->app_context->set( 'setLocale/original_locale', get_locale() );
switch_to_locale( $locale );
}
},
'after_execute' => function ( $request ) {
// Restore the locale if it was changed
$original_locale = $request->app_context->get('setLocale/original_locale');
if ( null !== $original_locale ) {
restore_previous_locale();
$request->app_context->remove( 'setLocale/original_locale' );
}
},
],
// ... (similar changes for the 'FIELD' location)
],
]);Metadata
Metadata
Assignees
Labels
Type
Projects
Status