WooCommerce Code Reference

GraphQLControllerBase
in package

Handles incoming GraphQL requests over the WooCommerce REST API.

Abstract: the autogenerated GraphQLController subclass emitted by ApiBuilder (both for WooCommerce core and for sibling plugins reusing this infrastructure) is the concrete class. The public surface plugins extend is engine-decoupled — the abstract {@see} returns {@see}, a stable subclass of the underlying engine's schema type, so a future engine swap doesn't break already-committed autogen trees.

Table of Contents

DEFAULT_ENDPOINT_URL  = 'wc/graphql'
Default path (relative to /wp-json/) at which the GraphQL route is registered.
DEFAULT_MAX_QUERY_COMPLEXITY  = 1000
Default complexity-score limit applied when the option is unset or non-positive.
DEFAULT_MAX_QUERY_DEPTH  = 15
Default nesting-depth limit applied when the option is unset or non-positive.
ENDPOINT_URL_SEGMENT_PATTERN  = '/^[A-Za-z0-9_\-]+$/'
Regex matching one valid path segment of the endpoint URL.
ERROR_STATUS_MAP  = array('UNAUTHORIZED' => 401, 'INVALID_TOKEN' => 401, 'FORBIDDEN' => 403, 'NOT_FOUND' => 404, 'METHOD_NOT_ALLOWED' => 405, 'INVALID_ARGUMENT' => 400, 'BAD_USER_INPUT' => 400, 'GRAPHQL_PARSE_ERROR' => 400, 'GRAPHQL_PARSE_FAILED' => 400, 'GRAPHQL_VALIDATION_FAILED' => 400, 'VALIDATION_ERROR' => 422, 'INTERNAL_ERROR' => 500)
Mapping from machine-readable error codes to HTTP status codes.
$query_cache  : QueryCache
Query cache / APQ resolver.
$schema  : Schema|null
Cached GraphQL schema instance.
$schema_handle  : SchemaHandle|null
Cached public-facing schema handle wrapping {@see self::$schema}.
$status_resolver  : object|null
Optional plugin-supplied HTTP status resolver.
get_endpoint_url()  : string
The path (relative to /wp-json/) at which the GraphQL route is registered.
get_max_query_complexity()  : int
The maximum computed complexity score allowed for a GraphQL query.
get_max_query_depth()  : int
The maximum nesting depth allowed in a GraphQL query.
get_schema()  : SchemaHandle
Public handle to the live GraphQL schema for runtime inspection.
handle_request()  : WP_REST_Response
Handle an incoming GraphQL request.
register()  : void
Register the GraphQL REST route.
build_schema()  : Schema
Construct the GraphQL schema.
get_class_resolver_fqcn()  : string|null
FQCN of the user-provided ClassResolver, or null when none was detected.
get_principal_resolver_fqcn()  : string|null
FQCN of the user-provided PrincipalResolver, or null when none was detected.
get_status_resolver()  : object|null
Return the HTTP status resolver instance to use for this controller, or null to opt out. Default: null (use the framework defaults).
principal_resolver_takes_request()  : bool
Whether the configured PrincipalResolver's `resolve_principal()` declares the \WP_REST_Request parameter (true) or omits it (false).
build_resolver_failure_response()  : WP_REST_Response
Build the canonical 500 response used when the HTTP status resolver throws or returns an out-of-range value. Body shape matches an unhandled internal error so callers don't need a separate path for "resolver blew up".
compute_query_depth()  : array{tree_only: int, in_depth: int}
Compute the maximum nesting depth of the executing operation, under two different metrics:
decode_json_param()  : array<string|int, mixed>
Decode an optional JSON-object param (`variables` / `extensions`) into an array.
document_has_mutation()  : bool
Check whether the parsed document contains a mutation operation.
extract_previous_chain()  : array<int, array{class: string, message: string, file: string, line: int, trace: string[]}>
Walk the `getPrevious()` chain of a Throwable and return one entry per wrapped exception. Used in debug mode so that resolver-level wrappers (which bury the real cause behind a generic "INTERNAL_ERROR") still surface the underlying class/message/file/line/trace.
format_exception()  : array<string|int, mixed>
Format a caught exception into a GraphQL error array.
get_debug_flags()  : int
Determine debug flags for the request, based on {@see self::is_debug_mode()}.
get_engine_schema()  : Schema
Build and cache the engine-typed GraphQL schema used internally to serve requests. Kept private to keep the engine type out of the controller's public surface; consumers should reach {@see SchemaHandle} through {@see self::get_schema()} instead.
get_error_status()  : int
Determine the HTTP status code from an array of GraphQL errors.
get_resolve_error_status()  : int
Determine the HTTP status code for an error returned by QueryCache::resolve().
is_debug_mode()  : bool
Check if debug mode is active.
is_introspection_allowed()  : bool
Check whether GraphQL introspection is allowed for this request.
is_valid_endpoint_url()  : bool
Whether a value is a valid endpoint URL.
pick_status()  : int
Filter the framework-computed default HTTP status through the optional plugin-supplied status resolver.
process_request()  : WP_REST_Response
Process the GraphQL request. Extracted so that handle_request() can wrap everything in a single try/catch that respects debug mode.
resolve_class()  : object
Resolve a class to an instance via the configured ClassResolver, or `new`.
resolve_request_principal()  : object
Resolve the request principal once per HTTP request.
split_endpoint_url()  : array{0: string, 1: string}
Split the endpoint URL into the `[namespace, route]` pair that register_rest_route() expects.
walk_depth_in_depth()  : int
Walk a selection set counting every field in the deepest chain, leaves included. Produces the "shape" metric surfaced alongside the enforcement metric in debug output.
walk_depth_tree_only()  : int
Walk a selection set counting only fields with child selections, matching webonyx's QueryDepth rule so the returned number is directly comparable to the configured "Maximum query depth" limit.

Constants

DEFAULT_ENDPOINT_URL

Default path (relative to /wp-json/) at which the GraphQL route is registered.

public mixed DEFAULT_ENDPOINT_URL = 'wc/graphql'

Used as the fallback when the {@see} option is unset or was stored in an invalid form. See {@see} for the accessor.

DEFAULT_MAX_QUERY_COMPLEXITY

Default complexity-score limit applied when the option is unset or non-positive.

public mixed DEFAULT_MAX_QUERY_COMPLEXITY = 1000

Complexity is the sum of per-field scores; connection fields multiply their child score by the requested page size. Queries exceeding the configured limit are rejected during validation. See {@see} for the accessor.

DEFAULT_MAX_QUERY_DEPTH

Default nesting-depth limit applied when the option is unset or non-positive.

public mixed DEFAULT_MAX_QUERY_DEPTH = 15

Queries exceeding the configured limit are rejected during validation, before any resolver runs. See {@see} for the accessor.

ENDPOINT_URL_SEGMENT_PATTERN

Regex matching one valid path segment of the endpoint URL.

public mixed ENDPOINT_URL_SEGMENT_PATTERN = '/^[A-Za-z0-9_\-]+$/'

Constrained to the character class WordPress REST routes accept (alphanumerics, underscores, hyphens). Shared with {@see} so the UI sanitizer and the controller-side fallback stay in lockstep.

ERROR_STATUS_MAP

Mapping from machine-readable error codes to HTTP status codes.

private mixed ERROR_STATUS_MAP = array('UNAUTHORIZED' => 401, 'INVALID_TOKEN' => 401, 'FORBIDDEN' => 403, 'NOT_FOUND' => 404, 'METHOD_NOT_ALLOWED' => 405, 'INVALID_ARGUMENT' => 400, 'BAD_USER_INPUT' => 400, 'GRAPHQL_PARSE_ERROR' => 400, 'GRAPHQL_PARSE_FAILED' => 400, 'GRAPHQL_VALIDATION_FAILED' => 400, 'VALIDATION_ERROR' => 422, 'INTERNAL_ERROR' => 500)

Any code not listed here defaults to 500, so unknown/unrecognised codes from third-party resolvers stay on the safe side. The error formatter installed in process_request() guarantees every error carries a code from this table before get_error_status() inspects it.

Properties

$status_resolver

Optional plugin-supplied HTTP status resolver.

private object|null $status_resolver = null

Populated from {@see} during {@see}. Stays null when neither this controller nor its subclass supplies one, in which case {@see} short-circuits to the default status without ever calling a resolver.

Typed as ?object rather than a WooCommerce-defined interface so that sibling plugins do not have to import a WooCommerce type for what is structurally a single duck-typed method.

Methods

get_endpoint_url()

The path (relative to /wp-json/) at which the GraphQL route is registered.

public static get_endpoint_url() : string

Reads the {@see} store option; falls back to {@see} when the option is unset, empty, or fails {@see}. The UI already validates on save, so this defense-in-depth guard only fires for CLI-set option values.

Return values
string

get_max_query_complexity()

The maximum computed complexity score allowed for a GraphQL query.

public static get_max_query_complexity() : int

Reads the {@see} store option; falls back to {@see} when the option is unset, empty, or non-positive.

Return values
int

get_max_query_depth()

The maximum nesting depth allowed in a GraphQL query.

public static get_max_query_depth() : int

Reads the {@see} store option; falls back to {@see} when the option is unset, empty, or non-positive.

Return values
int

get_schema()

Public handle to the live GraphQL schema for runtime inspection.

public get_schema() : SchemaHandle

Returns an opaque {@see}; callers reach metadata (and any future schema-inspection operations) through methods on that object rather than touching the underlying engine type. The handle is cached and wraps the same engine schema this controller uses to serve real requests.

Return values
SchemaHandle

handle_request()

Handle an incoming GraphQL request.

public handle_request(WP_REST_Request $request) : WP_REST_Response

Resolves the principal first so debug-mode / introspection checks can consult it from inside both process_request() and the top-level exception formatter. When resolve_request_principal() itself throws (e.g. an InvalidTokenException from a plugin's PrincipalResolver), $principal stays null and the resulting error response carries no debug info — by design, since the caller failed to authenticate.

Parameters
$request : WP_REST_Request

The REST request.

Return values
WP_REST_Response

build_schema()

Construct the GraphQL schema.

protected abstract build_schema() : Schema

Implemented by the autogenerated subclass emitted by ApiBuilder (both for WooCommerce core and for sibling plugins that reuse this infrastructure) so the base class stays agnostic to any specific autogenerated namespace.

Return values
Schema

get_class_resolver_fqcn()

FQCN of the user-provided ClassResolver, or null when none was detected.

protected get_class_resolver_fqcn() : string|null

The autogenerated subclass overrides this to return its plugin's <api_namespace>\Infrastructure\ClassResolver when ApiBuilder detected one. When null, classes are instantiated with new $class().

Return values
string|null

get_principal_resolver_fqcn()

FQCN of the user-provided PrincipalResolver, or null when none was detected.

protected get_principal_resolver_fqcn() : string|null

The autogenerated subclass overrides this to return its plugin's <api_namespace>\Infrastructure\PrincipalResolver when ApiBuilder detected one. When null, the controller falls back to {@see} (anonymous → null) to populate the request principal.

Return values
string|null

get_status_resolver()

Return the HTTP status resolver instance to use for this controller, or null to opt out. Default: null (use the framework defaults).

protected get_status_resolver() : object|null

Autogenerated subclasses override this when the plugin ships a <plugin-api-namespace>\Infrastructure\HttpStatusResolver convention class. The returned object is duck-typed: it must expose public function resolve_status( int $default_status, array $output, \WP_REST_Request $request ): int, must return an int, and must not throw. A throw is treated as a plugin bug and produces a fixed 500 INTERNAL_ERROR response — see {@see} and {@see}.

Return values
object|null

principal_resolver_takes_request()

Whether the configured PrincipalResolver's `resolve_principal()` declares the \WP_REST_Request parameter (true) or omits it (false).

protected principal_resolver_takes_request() : bool

Captured at build time and emitted as an override on the autogenerated controller subclass, so the call below uses the right arity without runtime reflection. Default is irrelevant when {@see} returns null (the inline wp_get_current_user() fallback applies instead).

Return values
bool

build_resolver_failure_response()

Build the canonical 500 response used when the HTTP status resolver throws or returns an out-of-range value. Body shape matches an unhandled internal error so callers don't need a separate path for "resolver blew up".

private build_resolver_failure_response(StatusResolverFailedException $e, WP_REST_Request $request, object|null $principal) : WP_REST_Response

In debug mode, attaches extensions.debug (message, file, line, trace) for the wrapper exception, plus an extensions.previous chain when the resolver itself threw — mirroring the shape that {@see} produces for the generic-Throwable path. Outside debug mode the body stays purely generic so resolver internals never leak to anonymous callers.

Parameters
$e : StatusResolverFailedException

The wrapper exception thrown by {@see}.

$request : WP_REST_Request

The originating REST request.

$principal : object|null

The resolved principal, or null when resolution failed.

Return values
WP_REST_Response

compute_query_depth()

Compute the maximum nesting depth of the executing operation, under two different metrics:

private compute_query_depth(DocumentNode $document, string|null $operation_name) : array{tree_only: int, in_depth: int}
  • tree_only: only fields whose own selection set is non-empty count toward depth; leaves are excluded. This is the number directly comparable to the "Maximum query depth" setting's limit, and matches what webonyx's QueryDepth validation rule measures for the enforcement decision.
  • in_depth: counts every field in the deepest chain, leaves included. Useful as a shape metric when inspecting a query.

Inline fragments pass through without incrementing either metric. Named-fragment spreads are not expanded here, so both numbers are lower bounds when spreads are present. The webonyx QueryDepth validation rule (which does expand spreads) remains the authoritative gate.

Parameters
$document : DocumentNode

The parsed GraphQL document.

$operation_name : string|null

The requested operation name, if any.

Return values
array{tree_only: int, in_depth: int}

decode_json_param()

Decode an optional JSON-object param (`variables` / `extensions`) into an array.

private decode_json_param(mixed $value, string $name) : array<string|int, mixed>

WP_REST_Request delivers POST-body params as already-decoded arrays, but GET query-string equivalents arrive as raw JSON strings. This helper unifies the two and rejects malformed JSON or non-object payloads with an InvalidArgumentException — which handle_request() surfaces as HTTP 400 INVALID_ARGUMENT, rather than letting a null decode slip through as "no variables" or a scalar decode trigger a downstream TypeError / HTTP 500.

Parameters
$value : mixed

The param value from WP_REST_Request::get_param().

$name : string

The param name, used in error messages.

Tags
throws
InvalidArgumentException

When the payload is not a JSON object or not valid JSON.

Return values
array<string|int, mixed>The decoded object, or an empty array when the param is omitted / empty / JSON null.

document_has_mutation()

Check whether the parsed document contains a mutation operation.

private document_has_mutation(DocumentNode $document, string|null $operation_name) : bool

When an operation name is given, only that operation is checked; otherwise any mutation definition in the document triggers a match.

Parameters
$document : DocumentNode

The parsed GraphQL document.

$operation_name : string|null

The requested operation name, if any.

Return values
bool

extract_previous_chain()

Walk the `getPrevious()` chain of a Throwable and return one entry per wrapped exception. Used in debug mode so that resolver-level wrappers (which bury the real cause behind a generic "INTERNAL_ERROR") still surface the underlying class/message/file/line/trace.

private extract_previous_chain(Throwable $e) : array<int, array{class: string, message: string, file: string, line: int, trace: string[]}>
Parameters
$e : Throwable

The outermost exception.

Return values
array<int, array{class: string, message: string, file: string, line: int, trace: string[]}>

format_exception()

Format a caught exception into a GraphQL error array.

private format_exception(Throwable $e, WP_REST_Request $request, object|null $principal) : array<string|int, mixed>
Parameters
$e : Throwable

The caught exception.

$request : WP_REST_Request

The REST request.

$principal : object|null

The resolved principal, or null when the exception came from principal resolution itself.

Return values
array<string|int, mixed>

get_debug_flags()

Determine debug flags for the request, based on {@see self::is_debug_mode()}.

private get_debug_flags(WP_REST_Request $request, object|null $principal) : int
Parameters
$request : WP_REST_Request

The REST request.

$principal : object|null

The resolved principal, or null if resolution itself failed.

Return values
int

get_engine_schema()

Build and cache the engine-typed GraphQL schema used internally to serve requests. Kept private to keep the engine type out of the controller's public surface; consumers should reach {@see SchemaHandle} through {@see self::get_schema()} instead.

private get_engine_schema() : Schema
Return values
Schema

get_error_status()

Determine the HTTP status code from an array of GraphQL errors.

private get_error_status(array<string|int, mixed> $errors) : int

Applies the code-to-status lookup to each error and returns the worst (highest) status seen. A single genuine 5xx among mixed errors surfaces as 500, which is the more useful signal for monitoring and logs.

Parameters
$errors : array<string|int, mixed>

The GraphQL errors array.

Return values
int

get_resolve_error_status()

Determine the HTTP status code for an error returned by QueryCache::resolve().

private get_resolve_error_status(array<string|int, mixed> $response) : int

PERSISTED_QUERY_NOT_FOUND uses 200 per the Apollo APQ convention (protocol signal, not error).

Parameters
$response : array<string|int, mixed>

The error response array from resolve().

Return values
int

is_debug_mode()

Check if debug mode is active.

private is_debug_mode(object|null $principal, WP_REST_Request $request) : bool

Debug mode is gated on _debug=1 being set on the request: when absent, debug mode is off regardless of any other signal. When present, the principal opts in via a can_use_debug_mode(): bool method (principals that don't declare it are denied by default), and the decision is then passed through the {@see 'woocommerce_graphql_can_use_debug_mode'} filter.

Fail-closed contract: the principal must be non-null (principal-resolution failures deny outright, before the filter is consulted), the principal method's return value is treated with === true, and any throw from either the principal method or the filter callback denies. The filter must likewise return strictly true to allow; any other value denies.

Parameters
$principal : object|null

The resolved principal, or null when principal resolution failed.

$request : WP_REST_Request

The REST request.

Return values
bool

is_introspection_allowed()

Check whether GraphQL introspection is allowed for this request.

private is_introspection_allowed(object|null $principal, WP_REST_Request $request) : bool

The principal opts in via a can_introspect(): bool method; principals that don't declare it are denied by default. The decision is then passed through the {@see 'woocommerce_graphql_can_introspect'} filter so sites can grant or revoke access without subclassing the principal — useful for per-request rules (specific IPs, headers, query parameters, etc.).

Fail-closed contract: the principal must be non-null (principal-resolution failures deny outright, before the filter is consulted), the principal method's return value is treated with === true, and any throw from either the principal method or the filter callback denies. The filter must likewise return strictly true to allow; any other value denies.

Parameters
$principal : object|null

The resolved principal, or null when principal resolution failed.

$request : WP_REST_Request

The REST request.

Return values
bool

is_valid_endpoint_url()

Whether a value is a valid endpoint URL.

private static is_valid_endpoint_url(string $value) : bool

Requires at least two non-empty path segments (so register_rest_route() has both a namespace and a route), each matching {@see}. Mirrors the rules enforced on save by {@see}, so values that bypass the UI (e.g. CLI-set options) get the same treatment.

Parameters
$value : string

Endpoint URL with surrounding slashes already stripped.

Return values
bool

pick_status()

Filter the framework-computed default HTTP status through the optional plugin-supplied status resolver.

private pick_status(int $default, array<string|int, mixed> $output, WP_REST_Request $request) : int

When no resolver is configured this returns the default verbatim. When a resolver is configured its return value (an int) is returned in place of the default — which the resolver may also pass through unchanged for cases it does not want to override.

Resolver-thrown exceptions are converted into an internal {@see} for {@see} to handle, so a plugin bug never corrupts or duplicates a response.

Parameters
$default : int

The framework-computed default status.

$output : array<string|int, mixed>

The response body about to be sent (may include errors/data).

$request : WP_REST_Request

The originating request.

Tags
throws
StatusResolverFailedException

When the resolver throws or returns a status code outside the 100..599 HTTP range.

Return values
int

process_request()

Process the GraphQL request. Extracted so that handle_request() can wrap everything in a single try/catch that respects debug mode.

private process_request(WP_REST_Request $request, object $principal) : WP_REST_Response
Parameters
$request : WP_REST_Request

The REST request.

$principal : object

The principal resolved by handle_request(); never null when this is reached.

Return values
WP_REST_Response

resolve_class()

Resolve a class to an instance via the configured ClassResolver, or `new`.

private resolve_class(string $class_name) : object

Used internally to instantiate the PrincipalResolver per request. The autogenerated resolver classes use the detected ClassResolver directly (the FQCN is baked in at build time), so this helper is only the runtime path for infrastructure classes that the controller itself has to load.

Parameters
$class_name : string

Fully-qualified name of the class to resolve.

Return values
object

resolve_request_principal()

Resolve the request principal once per HTTP request.

private resolve_request_principal(WP_REST_Request $request) : object

Invoked eagerly at the top of {@see}, so a principal resolver throwing {@see} fails the request before any resolver runs (single coded error in the response, no data).

The principal is never null — anonymous requests are signalled by a principal whose authentication state is unauthenticated (for the default {@see}, that's Principal::$user->ID === 0). Plugin resolvers can also signal "invalid credentials" by throwing ApiException.

The configured resolver's resolve_principal() may declare its \WP_REST_Request parameter or omit it; the autogenerated subclass overrides {@see} so this call uses the right arity without runtime reflection.

Parameters
$request : WP_REST_Request

The incoming REST request.

Tags
throws
ApiException

When the configured PrincipalResolver rejects the request.

Return values
object

split_endpoint_url()

Split the endpoint URL into the `[namespace, route]` pair that register_rest_route() expects.

private static split_endpoint_url() : array{0: string, 1: string}

The last path segment becomes the route; everything before it becomes the namespace. E.g. wc/v4/graphql['wc/v4', '/graphql'].

Return values
array{0: string, 1: string}

walk_depth_in_depth()

Walk a selection set counting every field in the deepest chain, leaves included. Produces the "shape" metric surfaced alongside the enforcement metric in debug output.

private walk_depth_in_depth(SelectionSetNode|null $selection_set, int $depth) : int
Parameters
$selection_set : SelectionSetNode|null

The selection set to walk, or null for a leaf.

$depth : int

The depth of the selection set's parent.

Return values
int

walk_depth_tree_only()

Walk a selection set counting only fields with child selections, matching webonyx's QueryDepth rule so the returned number is directly comparable to the configured "Maximum query depth" limit.

private walk_depth_tree_only(SelectionSetNode|null $selection_set, int $depth) : int
Parameters
$selection_set : SelectionSetNode|null

The selection set to walk.

$depth : int

The depth at which fields in this selection set sit.

Return values
int