Skip to main content

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 under code-templates/, StalenessChecker.php).
  • Engine surface: plugins/woocommerce/src/Api/Infrastructure/Schema/ and its README.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 the Schema\* surface but never Vendor\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): delegates int(), 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 in aliases.php (wired via composer.json's autoload.files).

Adding a symbol to the surface

  1. Add a subclass / facade method / alias in the matching style.
  2. Update the template that needs it to import from Api\Infrastructure\Schema\*.
  3. Regenerate core (build:api) and the fixture (build:api:test); confirm the Autogenerated/ diff is imports-only.
  4. 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 _apiMetadata data.
  • 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 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.

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 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.

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.