Skip to content

feat: RFC — Centralized registry for mapping user options to the GraphQL schema #3562

Description

@izzygld

Summary

This RFC proposes a centralized registry for mapping WordPress user options to the WPGraphQL schema, enabling both core and plugins to declaratively expose user profile fields without manually writing resolvers and input handlers.

This was suggested by @jasonbahl in the review of #3551:

Similar to our discussion about options, I'd love at some point to have a more formal way to map registered user options to the schema more programmatically (i.e. when a plugin adds fields to the user profile, I'd love for those fields to automatically map to the schema)

Problem

Currently, exposing user options in the GraphQL schema requires:

  1. Adding a resolver closure in Model/User.php
  2. Registering the field definition in Type/ObjectType/User.php
  3. Adding mutation input handling in the mutation classes
  4. Handling type coercion manually (e.g., WordPress stores booleans as 'true'/'false' strings)
  5. Handling defaults manually (e.g., get_user_option() returns false when not set)

Each plugin that adds user profile fields must repeat this boilerplate via register_graphql_field(). There's no standardized way to:

  • Define the WordPress option key to GraphQL field mapping
  • Specify type coercion rules
  • Declare defaults
  • Automatically generate both query fields AND mutation inputs from a single registration

Proposed Solution

register_graphql_user_option()

A new registration function that maps a WordPress user option to a GraphQL field on the User type, with automatic resolver generation, type coercion, default handling, and optional mutation support.

register_graphql_user_option( 'adminColor', [
    'option_key'   => 'admin_color',
    'type'         => 'String',
    'description'  => __( 'The admin color scheme preference', 'wp-graphql' ),
    'default'      => 'fresh',
    'show_in_graphql' => true,
    'mutatable'    => true, // Auto-generate updateUser input
    'sanitize_callback' => function( $value ) {
        // Optional validation/sanitization for mutations
        return sanitize_text_field( $value );
    },
] );

Boolean Convenience

For the common pattern of WordPress string-boolean options:

register_graphql_user_option( 'hasRichEditingEnabled', [
    'option_key'   => 'rich_editing',
    'type'         => 'Boolean',
    'description'  => __( 'Whether rich editing is enabled', 'wp-graphql' ),
    'default'      => true,
    'mutatable'    => true,
    // Type coercion handled automatically:
    // - Query: 'true'/'false' string -> Boolean
    // - Mutation: Boolean -> 'true'/'false' string for storage
] );

Plugin Usage

Plugins that add user profile fields could register them in a single call:

add_action( 'graphql_register_types', function() {
    register_graphql_user_option( 'darkMode', [
        'option_key'  => 'my_plugin_dark_mode',
        'type'        => 'Boolean',
        'description' => __( 'Whether dark mode is enabled', 'my-plugin' ),
        'default'     => false,
        'mutatable'   => true,
    ] );
} );

Architecture

Registry Class

A new UserOptionRegistry class (similar to TypeRegistry) that:

  1. Stores registered user option mappings
  2. Auto-generates Model/User.php field resolvers at runtime
  3. Auto-generates Type/ObjectType/User.php field definitions
  4. Optionally auto-generates mutation input fields for updateUser/createUser
  5. Handles type coercion between WordPress storage format and GraphQL types
  6. Applies sanitize_callback during mutations

Core Dogfooding

WPGraphQL core would use this registry for its own fields, replacing the current manual implementation:

// In a new file like src/Registry/UserOptions.php
register_graphql_user_option( 'adminColor', [
    'option_key'  => 'admin_color',
    'type'        => 'String',
    'description' => __( 'The admin color scheme preference', 'wp-graphql' ),
    'default'     => 'fresh',
] );

register_graphql_user_option( 'hasRichEditingEnabled', [
    'option_key'  => 'rich_editing',
    'type'        => 'Boolean',
    'description' => __( 'Whether rich editing is enabled', 'wp-graphql' ),
    'default'     => true,
] );

// etc.

Type Coercion Map

GraphQL Type WP Storage Query Coercion Mutation Coercion
Boolean 'true'/'false' 'true' === $val $val ? 'true' : 'false'
String string passthrough sanitize_text_field()
Int string/int absint() absint()
Float string/float floatval() floatval()

Scope

Phase 1 (MVP)

  • register_graphql_user_option() function
  • Auto-generated query field resolvers
  • Type coercion for Boolean and String
  • Core fields migrated to use registry

Phase 2

  • Auto-generated mutation inputs (updateUser, createUser)
  • sanitize_callback support
  • auth_callback for field-level permission control

Phase 3

  • Filter hooks for extensibility (graphql_user_option_value, etc.)
  • Admin UI integration (optional)
  • Similar pattern for post meta, term meta (register_graphql_post_option(), etc.)

Open Questions

  1. Should this use get_user_option() or get_user_meta() under the hood? (Leaning get_user_option() for multisite compatibility, per @jasonbahl's feedback on feat: add core user admin preferences fields to User type #3551)
  2. Should the registry support complex types (objects, lists) or stick to scalars?
  3. Should there be a filter to allow plugins to modify registered user options before schema generation?
  4. Naming: register_graphql_user_option() vs register_graphql_user_meta() vs register_graphql_user_field()?

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    component: architectureRelating to fundamental architectural decisionseffort: highMore than a weekimpact: highUnblocks new use cases, substantial improvement to existing feature, fixes a major bugneeds: discussionRequires a discussion to proceedscope: apiIssues related to access functions, actions, and filterstype: featureNew functionality being added

    Type

    No type
    No fields configured for issues without a type.

    Projects

    Status
    🆕 New

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions