ResolverHelpers
in package
Shared utilities for the auto-generated GraphQL resolvers.
The public surface uses only {@see} (a stable subclass of the engine's Error) on throws/returns so generated code never imports an engine-specific symbol — a future engine switch can rewrite the bodies here without invalidating already-committed plugin trees.
Table of Contents
- authorize_command() : bool
- Invoke a command's authorize() method, translating any thrown exceptions into spec-compliant GraphQL errors.
- build_authorization_error() : Error
- Build the GraphQL error to throw when an authorization check fails.
- build_field_authorization_error() : Error
- Like {@see self::build_authorization_error()} but carries a structured `subject` payload identifying *what* was denied — the enclosing type, the field (when applicable), and the attribute class name driving the decision. Clients can branch on `extensions.subject.field` to tell a field-level deny apart from an operation-level one.
- complexity_from_pagination() : int
- Compute the complexity cost of a paginated connection field.
- compute_preauthorized() : bool
- Compute the value `_preauthorized` would carry for the given command and principal (the AND of the autodiscovered authorization attributes' authorize() outcomes).
- create_input() : mixed
- Invoke a factory callable, catching InvalidArgumentException and converting it to a client-visible GraphQL error.
- create_pagination_params() : PaginationParams
- Build a PaginationParams instance from the standard GraphQL pagination arguments (first, last, after, before).
- execute_command() : mixed
- Execute a command's execute() method, translating any thrown exceptions into spec-compliant GraphQL errors.
- translate_exceptions() : mixed
- Invoke a callable, translating any thrown exception into a spec-compliant GraphQL error with a machine-readable code.
- wp_filesystem() : WP_Filesystem_Base|null
- Lazy-initialize and return the WP_Filesystem global, or null when the direct method isn't available (e.g. credentials prompt would be needed).
- authorize_method_shape_is_valid() : bool
- Whether a method's shape matches the authorization-attribute contract: public, non-static, returns bool, and parameters drawn from the accepted set — at most one principal (any non-`_`-prefixed name, non-nullable typed) plus any subset of `$_metadata` (array), `$_args` (array), and `$_parent` (any type).
- build_authorize_call_args() : array<int|string, mixed>
- Build the positional/named argument list for an attribute's `authorize()` method based on which opt-in slots its signature declares.
- collect_authorization_instances() : array<int, object>
- Collect attribute instances declared on $source whose class declares an authorization-shaped `authorize()` method.
- harvest_class_metadata() : array<string, bool|int|float|string|null>
- Mirror of `ApiBuilder::harvest_metadata()` for the runtime path. Walks {@see \Automattic\WooCommerce\Api\Attributes\Metadata}-subclass attributes on a class reflector and returns `name => value`. Duplicate names are resolved last-wins — the build-time validator already errors on duplicates, so this is only relevant for in-process classes that never went through a build.
Methods
authorize_command()
Invoke a command's authorize() method, translating any thrown exceptions into spec-compliant GraphQL errors.
public
static authorize_command(object $command, array<string|int, mixed> $authorize_args) : bool
Mirror of execute_command() for the authorize step. Needed because an authorize() call can throw an ApiException (e.g. UnauthorizedException when a target record does not exist); without this wrapper the exception would propagate up to the engine and lose its error code and user-visible message on its way through the generic error formatter.
Parameters
- $command : object
-
The command instance (must have an authorize() method).
- $authorize_args : array<string|int, mixed>
-
Named arguments to pass to authorize().
Tags
Return values
bool — The return value of authorize().build_authorization_error()
Build the GraphQL error to throw when an authorization check fails.
public
static build_authorization_error(object $principal) : Error
Distinguishes the two HTTP-correct shapes:
- UNAUTHORIZED (401) when the principal is anonymous — the caller could plausibly fix it by authenticating, so the response invites re-auth.
- FORBIDDEN (403) otherwise — the principal is recognised but isn't allowed; re-authenticating wouldn't help.
The "anonymous" check is opt-in by convention: the principal's
is_authenticated(): bool method, when present, decides. Principals
that don't define it fall through to FORBIDDEN — generated resolvers
still emit a coded error, just without the 401/403 distinction.
Used for class-level denials (operation-level "you cannot call this
query/mutation"). For field-level denials that should carry a
structured subject payload (type / field / attribute), see
{@see}.
Parameters
- $principal : object
-
The resolved request principal.
Return values
Error —build_field_authorization_error()
Like {@see self::build_authorization_error()} but carries a structured `subject` payload identifying *what* was denied — the enclosing type, the field (when applicable), and the attribute class name driving the decision. Clients can branch on `extensions.subject.field` to tell a field-level deny apart from an operation-level one.
public
static build_field_authorization_error(object $principal, string $type, string|null $field, string $attribute_short) : Error
The error code (UNAUTHORIZED / FORBIDDEN) is preserved verbatim so existing client handlers continue to work; the subject payload is additive.
Parameters
- $principal : object
-
The resolved request principal.
- $type : string
-
GraphQL type name carrying the gate.
- $field : string|null
-
Field name when the deny is field-level; null for type/operation-level denies.
- $attribute_short : string
-
Short class name of the deciding authorization attribute (no namespace).
Return values
Error —complexity_from_pagination()
Compute the complexity cost of a paginated connection field.
public
static complexity_from_pagination(int $child_complexity, array<string|int, mixed> $args) : int
Used as the complexity callable on every generated resolver field
that returns a Connection. Runs during query validation (before
resolver execution, so before PaginationParams::validate_args() has
a chance to reject bad input) — so out-of-range / wrong-type values
are clamped to MAX_PAGE_SIZE here. Using MAX_PAGE_SIZE as the
fallback means a malicious attempt to shrink cost via e.g. a
negative first value only inflates the computed complexity,
closing the cost-bypass angle.
Parameters
- $child_complexity : int
-
The complexity of a single child node.
- $args : array<string|int, mixed>
-
The field arguments (expects
first/last).
Return values
int — The total complexity for this connection field.compute_preauthorized()
Compute the value `_preauthorized` would carry for the given command and principal (the AND of the autodiscovered authorization attributes' authorize() outcomes).
public
static compute_preauthorized(string $command_fqcn, object $principal) : bool
Lets code-API callers (and tests) ask "would this command's attribute-based authorization grant access to this principal?" without going through the GraphQL pipeline.
Note that it returns true when the command has no authorization attributes
(in that case the command's own authorize() method, if any, is the sole
guard; and consulting it requires running the command, which this helper
deliberately doesn't do).
Note: this provides the attribute-level authorization only. A command with
both attributes and an authorize() method composes the two via the
_preauthorized infrastructure parameter; this helper returns the value
that _preauthorized would carry, not the final authorize() outcome.
Scope is class-level (queries / mutations). Field-level authorization
lives on output-type / input-type properties and is enforced inside
the generated resolvers. To inspect a field's declared authorization
from code, walk {@see}
and read the authorization slice on each row.
Parameters
- $command_fqcn : string
-
Fully-qualified command class name.
- $principal : object
-
The resolved principal. Anonymous requests are represented by a sentinel principal (e.g. {@see} whose underlying WP_User has ID=0), not by null.
Tags
Return values
bool —create_input()
Invoke a factory callable, catching InvalidArgumentException and converting it to a client-visible GraphQL error.
public
static create_input(callable $factory) : mixed
Used to wrap construction of unrolled input types (PaginationParams, ProductFilterInput, etc.) whose constructors may validate their arguments and throw.
Parameters
- $factory : callable
-
A callable that returns the constructed object.
Tags
Return values
mixed — The return value of the factory.create_pagination_params()
Build a PaginationParams instance from the standard GraphQL pagination arguments (first, last, after, before).
public
static create_pagination_params(array<string|int, mixed> $args) : PaginationParams
Parameters
- $args : array<string|int, mixed>
-
The GraphQL field arguments.
Tags
Return values
PaginationParams —execute_command()
Execute a command's execute() method, translating any thrown exceptions into spec-compliant GraphQL errors.
public
static execute_command(object $command, array<string|int, mixed> $execute_args) : mixed
Parameters
- $command : object
-
The command instance (must have an execute() method).
- $execute_args : array<string|int, mixed>
-
Named arguments to pass to execute().
Tags
Return values
mixed — The return value of execute().translate_exceptions()
Invoke a callable, translating any thrown exception into a spec-compliant GraphQL error with a machine-readable code.
public
static translate_exceptions(callable $operation) : mixed
- ApiException → its own code + extensions, with the original message.
- InvalidArgumentException → INVALID_ARGUMENT, with the original message.
- Any other Throwable → INTERNAL_ERROR, with a generic message; the
original throwable is attached as
previousfor debug-mode surfacing.
Public so that generated resolvers can wrap Code-API calls that happen outside the execute()/authorize() pair (e.g. the Connection::slice() call emitted for nested paginated connection fields, which can throw InvalidArgumentException when pagination bounds are exceeded).
Parameters
- $operation : callable
-
Callable to invoke.
Tags
Return values
mixed — The return value of the callable.wp_filesystem()
Lazy-initialize and return the WP_Filesystem global, or null when the direct method isn't available (e.g. credentials prompt would be needed).
public
static wp_filesystem() : WP_Filesystem_Base|null
Return values
WP_Filesystem_Base|null —authorize_method_shape_is_valid()
Whether a method's shape matches the authorization-attribute contract: public, non-static, returns bool, and parameters drawn from the accepted set — at most one principal (any non-`_`-prefixed name, non-nullable typed) plus any subset of `$_metadata` (array), `$_args` (array), and `$_parent` (any type).
private
static authorize_method_shape_is_valid(ReflectionMethod $method) : bool
Mirrors the build-time ApiBuilder::validate_attribute_authorize_shape()
check so the runtime helper recognises the same set of attributes ApiBuilder
would have emitted into a resolver.
Parameters
- $method : ReflectionMethod
-
The method to inspect.
Return values
bool —build_authorize_call_args()
Build the positional/named argument list for an attribute's `authorize()` method based on which opt-in slots its signature declares.
private
static build_authorize_call_args(ReflectionMethod $method, object $principal, array<string|int, mixed> $metadata, array<string|int, mixed> $args, mixed $parent) : array<int|string, mixed>
The principal is always passed first (positionally) when the method
declares a non-_-prefixed parameter; infrastructure parameters
($_metadata, $_args, $_parent) are passed as named arguments so
the attribute can omit any subset without affecting the call shape.
Parameters
- $method : ReflectionMethod
-
The attribute's
authorize()method. - $principal : object
-
The resolved principal to pass when the method takes one.
- $metadata : array<string|int, mixed>
-
Value for
$_metadata(passed if the method declares it). - $args : array<string|int, mixed>
-
Value for
$_args(passed if the method declares it). - $parent : mixed
-
Value for
$_parent(passed if the method declares it).
Return values
array<int|string, mixed> — Positional principal first (if any), then named infra slots. Use with `...` spread.collect_authorization_instances()
Collect attribute instances declared on $source whose class declares an authorization-shaped `authorize()` method.
private
static collect_authorization_instances(ReflectionClass $source) : array<int, object>
Mirrors {@see} for the runtime path: same direct-then-inherited precedence, same "any class with a bool-returning authorize() method qualifies" rule.
Parameters
- $source : ReflectionClass
-
Class/trait/interface to read attributes from.
Return values
array<int, object> —harvest_class_metadata()
Mirror of `ApiBuilder::harvest_metadata()` for the runtime path. Walks {@see \Automattic\WooCommerce\Api\Attributes\Metadata}-subclass attributes on a class reflector and returns `name => value`. Duplicate names are resolved last-wins — the build-time validator already errors on duplicates, so this is only relevant for in-process classes that never went through a build.
private
static harvest_class_metadata(ReflectionClass $ref) : array<string, bool|int|float|string|null>
The per-target _apiMetadata opt-out (shows_in_metadata_query())
is not applied here: the $_metadata slot threaded into a class-
level attribute's authorize() is for policy input, not discovery,
so attribute authors see every entry regardless of how it surfaces
through _apiMetadata.
Parameters
- $ref : ReflectionClass
-
The class to read metadata from.
