Extending the infrastructure
This document is for maintainers of the dual-API infrastructure itself (the build tooling and the engine-integration layer), not for code-API authors. It is intentionally a high-level map; the code itself is the primary source of truth for the details. Key entry points:
- Build tooling:
plugins/woocommerce/bin/api-builder/(ApiBuilder.php, templates undercode-templates/,StalenessChecker.php). - Engine surface:
plugins/woocommerce/src/Api/Infrastructure/Schema/and itsREADME.md. - Runtime helpers:
plugins/woocommerce/src/Api/Infrastructure/(GraphQLControllerBase,ResolverHelpers,MetadataController,QueryInfoExtractor).
The engine-decoupling surface
The GraphQL engine (currently webonyx/graphql-php, vendored as Automattic\WooCommerce\Vendor\GraphQL\*) is treated as a replaceable implementation detail. The contract that makes this possible:
Generated code, and any public signature on an
Api\Infrastructure\*class, may reference theSchema\*surface but neverVendor\GraphQL\*directly.
src/Api/Infrastructure/Schema/ is the single point of contact with the engine. Generated resolvers, types, and root types import only from there. This matters because plugins commit their generated trees to their own repos: routing every engine reference through this surface means a future engine swap in WooCommerce doesn't break already-committed plugin code. Method bodies may touch vendor symbols: that's WooCommerce's concern when the engine changes, not the plugin's.
The surface uses three patterns (see Schema/README.md):
- Subclass (
Schema,ObjectType,InputObjectType,EnumType,InterfaceType,CustomScalarType,Error): empty subclasses of the engine class today; a future migration translates the config in the constructor. - Static facade (
Type): delegatesint(),string(),nonNull(),listOf(), etc.; return types intentionally omitted so the concrete class can change. - Class alias (
ResolveInfo,AST\StringValueNode): used where the engine constructs the instances; registered eagerly inaliases.php(wired viacomposer.json'sautoload.files).
Adding a symbol to the surface
- Add a subclass / facade method / alias in the matching style.
- Update the template that needs it to import from
Api\Infrastructure\Schema\*. - Regenerate core (
build:api) and the fixture (build:api:test); confirm theAutogenerated/diff is imports-only. - Add a row to the table in
Schema/README.md.
Versioning is implicit in the namespace. If a change would break already-committed plugin code, add a sibling namespace (e.g. Schema\V2) and teach the templates to emit against it; keep the current surface until the last dependent plugin migrates. An engine-migration checklist lives in Schema/README.md.
ApiBuilder (in brief)
ApiBuilder scans the code-API directory, reflects over each class (placement, type declarations, attributes), and renders the matching template into the output tree. It also:
- Detects the per-plugin convention classes (
ClassResolver,PrincipalResolver/its principal type,HttpStatusResolver) and wires them into the generated controller subclass. - Harvests authorization and
#[Metadata]attributes into the generated resolvers and the_apiMetadatadata. - Emits per-field authorization gates and the input-side "only if provided" gates.
- Warns at build time about unresolvable attribute references (e.g. a missing
useimport) and errors on duplicate metadata names.
It is not unit-tested directly; it's validated end-to-end against a comprehensive dummy code-API fixture under tests/php/src/Internal/Api/Fixtures/DummyApi/, whose generated output is committed alongside it. When you change the builder or templates, update the dummy API if needed and regenerate both core and the fixture (build:api + build:api:test), then run the wc-phpunit-graphql-infra test suite. Treat a non-imports-only diff in the generated trees as a signal to review.
Runtime helpers
GraphQLControllerBase: abstract base for the generated controller. Owns the request lifecycle: principal resolution, validation (depth/complexity), execution, error formatting, and HTTP status selection (pick_status(), optionally via a pluginHttpStatusResolver). Its publicbuild_schema()returns theSchema\Schemawrapper, never the engine type.ResolverHelpers: static helpers the generated resolvers call: exception translation, pagination construction, authorization checks, andcompute_preauthorized().MetadataController: contributes the hand-written_apiMetadatafield and its supporting types (which don't fit the standard templates).QueryInfoExtractor: turns the engine'sResolveInfointo the_query_infotree.
What stays internal
QueryCache, Settings, the endpoint registrar, and the query depth/complexity rules remain under Internal\Api\*. No external code references them; they're wired by Main through the DI container. Keep them there unless an external consumer genuinely needs them - at which point move only the public-facing surface, following the same engine-decoupling rule.