Dual API architecture
This document explains how the pieces fit together. For how to actually write classes, see Extending the code API.
Two halves, one source
The dual API has two halves:
- The code API: plain PHP classes under
src/Api/. They are GraphQL-agnostic: they import nothing from any GraphQL library and work as a standalone, in-process PHP API. This is the authoritative, manually maintained source. - The autogenerated GraphQL layer: code under
src/Internal/Api/Autogenerated/produced by a build script from the code API. It is committed to source control but never hand-edited. It powers a GraphQL endpoint (by defaultPOST|GET /wp-json/wc/graphql).
The build script reads the code API and (re)generates the GraphQL layer. The relationship is one-directional: you change PHP classes, then regenerate.
src/Api/ ──(build:api)──▶ src/Internal/Api/Autogenerated/ ──▶ /wp-json/wc/graphql
(you edit this) (generated, committed, never edited) (GraphQL endpoint)
Because the generated tree is committed to source code, regenerating it after a source change is mandatory; a staleness check enforces this in GitHub's CI pipeline for pull requests.
Code-first and the command pattern
The code API is organized around the command pattern: each query or mutation is a class with a single execute() method (plus an optional authorize() method). Output types, input types, enums, interfaces, and scalars are likewise plain classes/enums.
#[Name( 'product' )]
#[Description( 'Retrieve a single product by ID.' )]
#[RequiredCapability( 'read_product' )]
class GetProduct {
#[ReturnType( Product::class )]
public function execute( int $id ): ?object {
// ...
}
}
The build script infers as much as it can from code structure and uses PHP 8 attributes only where structure is not enough.
Convention over configuration
Two conventions drive most behavior:
- Directory placement determines role. A class in
Queries/becomes a GraphQL query; one inTypes/becomes an output type; one inEnums/becomes an enum; and so on. Arbitrary nested subdirectories are allowed for organization (e.g.Queries/Coupons/GetCoupon.php) - nesting does not change the role. See Recognized directories. - Names are derived, then overridable. GraphQL type names default to the PHP class name; query/mutation names to its camelCase form; fields to property names as-is; enum values from PascalCase to
SCREAMING_SNAKE_CASE. Any of these can be overridden with#[Name( '...' )].
Attributes fill the gaps that conventions cannot: descriptions, authorization, type shaping (arrays, connections, custom scalars), deprecation, and metadata. See the Attributes reference.
The GraphQL engine is an implementation detail
The GraphQL endpoint is currently powered by the webonyx/graphql-php package, vendored and re-namespaced to Automattic\WooCommerce\Vendor\GraphQL\* to avoid version conflicts with other plugins.
This is deliberately hidden from code-API authors. The autogenerated code never references Vendor\GraphQL\* directly: it references only a thin, WooCommerce-owned schema surface under Api\Infrastructure\Schema\*. That surface is the single point of contact with the engine, so the engine could be replaced in the future without breaking already-committed generated code in plugins. As a code-API author you never see GraphQL types at all; as an infrastructure maintainer, see Extending the infrastructure.
Where things live
| Path | Contents | Edit? |
|---|---|---|
src/Api/ | The code API: attributes, queries, mutations, types, input types, enums, interfaces, scalars, pagination, utils | Yes, this is the source |
src/Api/Infrastructure/ | Public, engine-decoupled runtime surface and convention classes (Principal, ClassResolver, GraphQLControllerBase, the Schema\* wrappers, ...) | Rarely; infrastructure only |
src/Internal/Api/Autogenerated/ | Generated GraphQL resolvers and type definitions | No, regenerate instead |
src/Internal/Api/ | Internal runtime not referenced by external code (QueryCache, Settings, endpoint registrar, query rules) | Rarely; core only |
bin/api-builder/ | The build tooling (ApiBuilder, build-api.php, staleness checker, templates). Not shipped in release builds | Rarely; infrastructure only |
The generated tree mirrors the role directories: Autogenerated/GraphQLQueries/, GraphQLMutations/, and GraphQLTypes/{Output,Input,Enums,Interfaces,Scalars,Pagination}/, plus a RootQueryType, RootMutationType, and TypeRegistry.
Request lifecycle (summarized)
When a GraphQL request hits the endpoint, the controller (a generated subclass of GraphQLControllerBase):
- Resolves a principal for the request (who is calling) via the configured
PrincipalResolver. - Parses and validates the query (depth and complexity limits; optional caching of the parsed AST).
- Runs the resolvers, which look up the corresponding command class through the
ClassResolver, check authorization, and callexecute(). - Formats the result (or errors) and picks an HTTP status code (optionally via a plugin-supplied
HttpStatusResolver).
Each of these steps is a documented extension point; see Authentication and authorization, Settings and caching, and Infrastructure classes.
Reusable by plugins
Everything above applies to a plugin that wants its own dual API. A plugin defines its own src/Api/ tree, runs the same builder against it, commits the generated output to its own repo, and registers a dedicated GraphQL endpoint. It reuses WooCommerce's infrastructure and can supply its own convention classes (authentication, class resolution, status codes) and attributes where it needs to diverge from the defaults. See Creating a dual API in a plugin.