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
$query_cache
Query cache / APQ resolver.
private
QueryCache
$query_cache
$schema
Cached GraphQL schema instance.
private
Schema|null
$schema
= null
$schema_handle
Cached public-facing schema handle wrapping {@see self::$schema}.
private
SchemaHandle|null
$schema_handle
= null
$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 —register()
Register the GraphQL REST route.
public
register() : void
Return values
void —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
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
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
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.
