WooCommerce Dual API

Dual API architecture

This document explains how the pieces fit together. For how to actually write classes, see Writing the code API.

Two halves, one source

A dual API has two halves:

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.php)──▶  src/Internal/Api/Autogenerated/  ──▶  /wp-json/<your-route>
(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 can enforce this in your CI pipeline.

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( 'event' )]
#[Description( 'Fetch a single event by id.' )]
#[PublicAccess]
class GetEvent {
    public function execute( int $id ): ?Event {
        // ...
    }
}

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:

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, which the WooCommerce Dual API plugin vendors and re-namespaces 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 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 engine maintainer, see Extending the infrastructure.

Where things live

In the WooCommerce Dual API plugin (the engine):

Path Contents Edit?
src/Api/Attributes/ The built-in attributes (Name, Description, RequiredCapability, Metadata, …) Engine maintainers only
src/Api/Infrastructure/ Public, engine-decoupled runtime surface and default convention classes (Main, Principal, ClassResolver, GraphQLControllerBase, the Schema\* wrappers, …) Engine maintainers only
src/Api/Pagination/ The Relay pagination building blocks (Connection, Edge, PageInfo, PaginationParams) Engine maintainers only
src/Api/*Exception.php The exception hierarchy Engine maintainers only
src/Internal/Api/ Internal runtime not referenced by external code (QueryCache, Settings, endpoint registrar, query rules, the plugin bootstrap) Engine maintainers only
bin/api-builder/ The build tooling (ApiBuilder, build-api.php, staleness checker, templates). Shipped with the plugin so code-API plugins can build against an installed copy Engine maintainers only
lib/packages/GraphQL/ The vendored GraphQL engine Never; re-vendored with Mozart

In a plugin that defines a dual API:

Path Contents Edit?
src/Api/ Your code API: queries, mutations, types, input types, enums, interfaces, scalars, custom attributes, convention classes, helpers Yes, this is the source
src/Internal/Api/Autogenerated/ Generated GraphQL resolvers and type definitions No, regenerate instead
bin/build-api.php Your build script, a thin wrapper around ApiBuilder::run_for_plugin() Rarely

The generated tree mirrors the role directories: Autogenerated/GraphQLQueries/, GraphQLMutations/, and GraphQLTypes/{Output,Input,Enums,Interfaces,Scalars,Pagination}/, plus a RootQueryType, RootMutationType, TypeRegistry, and the GraphQLController that the endpoint is attached to.

Request lifecycle (summarized)

When a GraphQL request hits an endpoint, the controller (a generated subclass of GraphQLControllerBase):

  1. Resolves a principal for the request (who is calling) via the configured PrincipalResolver, then decides whether the request is processed at all: the “Allow anonymous requests” setting and the woocommerce_graphql_request_allowed filter can refuse it with a 401 before the query is parsed.
  2. Rejects queries longer than the maximum query length, parses the query (optionally caching the parsed AST), checks the depth and complexity limits on their own, and only then validates it against the full rule set. Both limits are enforced in time proportional to the size of the document, so documents beyond them never reach the more expensive rules. The field-merge rule is a bounded variant of the stock one (OverlappingFieldsRule), and a response carries at most GraphQLControllerBase::MAX_ERRORS_PER_RESPONSE errors, with the number left out in extensions.omittedErrors.
  3. Runs the resolvers, which look up the corresponding command class through the ClassResolver, check authorization, and call execute().
  4. 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.

One engine, many endpoints

Every plugin with a dual API registers its own, dedicated endpoint; there is no shared or federated schema. The engine is shared, though: its settings, filters and caches apply to every endpoint on the site (see Settings and caching). See Creating a dual API in a plugin for the bootstrap.