docs/apis/dual-api/extending-the-infrastructure.md
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:
plugins/woocommerce/bin/api-builder/ (ApiBuilder.php, templates under code-templates/, StalenessChecker.php).plugins/woocommerce/src/Api/Infrastructure/Schema/ and its README.md.plugins/woocommerce/src/Api/Infrastructure/ (GraphQLControllerBase, ResolverHelpers, MetadataController, QueryInfoExtractor).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):
Schema, ObjectType, InputObjectType, EnumType, InterfaceType, CustomScalarType, Error): empty subclasses of the engine class today; a future migration translates the config in the constructor.Type): delegates int(), string(), nonNull(), listOf(), etc.; return types intentionally omitted so the concrete class can change.ResolveInfo, AST\StringValueNode): used where the engine constructs the instances; registered eagerly in aliases.php (wired via composer.json's autoload.files).Api\Infrastructure\Schema\*.build:api) and the fixture (build:api:test); confirm the Autogenerated/ diff is imports-only.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 scans the code-API directory, reflects over each class (placement, type declarations, attributes), and renders the matching template into the output tree. It also:
ClassResolver, PrincipalResolver/its principal type, HttpStatusResolver) and wires them into the generated controller subclass.#[Metadata] attributes into the generated resolvers and the _apiMetadata data.use import) 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.
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 plugin HttpStatusResolver). Its public build_schema() returns the Schema\Schema wrapper, never the engine type.ResolverHelpers: static helpers the generated resolvers call: exception translation, pagination construction, authorization checks, and compute_preauthorized().MetadataController: contributes the hand-written _apiMetadata field and its supporting types (which don't fit the standard templates).QueryInfoExtractor: turns the engine's ResolveInfo into the _query_info tree.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.