Extending the code API
This guide covers how to add to or change the code API (the PHP classes the GraphQL layer is generated from). It applies both to WooCommerce core's own code API and to the ones implemented by plugins (see Creating a dual API in a plugin for the plugin-specific bootstrap).
Reminder: core's
coupons/productsAPI is a proof of concept and may change. Use it as a pattern, not a stable contract.
The workflow
- Add or edit classes under
src/Api/. - Regenerate the GraphQL layer:
pnpm --filter=@woocommerce/plugin-woocommerce build:api. - Run the tests and the staleness check.
- Commit the source change and the regenerated
Autogenerated/tree together.
You never edit the generated tree by hand. If a generated file looks wrong, fix the source class or the underlying templates and regenerate.
Queries and mutations
A query or mutation is a class with one public execute() method. Place it under Queries/ or Mutations/ (nested subdirectories are fine). The GraphQL field name defaults to the camelCase form of the class name; override with #[Name].
#[Name( 'coupon' )]
#[Description( 'Retrieve a single coupon by ID or code.' )]
#[RequiredCapability( 'read_private_shop_coupons' )]
class GetCoupon {
public function execute(
#[Description( 'The ID of the coupon to retrieve.' )]
?int $id = null,
#[Description( 'The coupon code to look up.' )]
?string $code = null,
): ?Coupon {
// ...
}
}
- Arguments come from
execute()parameters; their GraphQL types are inferred from the PHP type declarations. A non-nullable parameter (int $id) becomes a non-null argument (Int!); a nullable parameter (?int $id) becomes a nullable argument (Int). An argument is optional (the client may omit it) when it is nullable or has a default value; so?int $idis optional even without a default, and only a non-nullable parameter with no default is required. A default value is additionally exposed as the argument's GraphQL default. Add per-argument docs with#[Description]on the parameter. - Return type comes from the PHP return type. When
execute()returns a GraphQL interface - which in the code API is implemented as a PHP trait (see Enums, interfaces, scalars below), and a trait can't be used as a return type hint - declare it with#[ReturnType( SomeInterface::class )]and returnobject. - Errors: throw a plain
\InvalidArgumentExceptionfor malformed input (will be mapped toINVALID_ARGUMENT/ 400), or one of the exception classes to pin a specific error code and HTTP status.
Mutations are identical except for the directory. They typically take a single input-type argument and return an output type or a dedicated result type.
Output types
Classes under Types/ become GraphQL output types. Public properties become fields, named as-is (snake_case is preserved). Type mapping is inferred from the PHP property type.
#[Description( 'Represents a WooCommerce discount coupon.' )]
class Coupon {
use ObjectWithId; // contributes the `id` field
#[Description( 'The coupon code.' )]
public string $code;
#[Description( 'The type of discount.' )]
public DiscountType $discount_type; // enum
#[Description( 'The date the coupon was created.' )]
#[ScalarType( DateTime::class )]
public ?string $date_created; // custom scalar
#[Description( 'Product IDs the coupon can be applied to.' )]
#[ArrayOf( 'int' )]
public array $product_ids; // list type
}
Useful attributes on properties:
#[ArrayOf( 'int' )]/#[ArrayOf( SomeType::class )]: element type of anarrayproperty.#[ScalarType( DateTime::class )]: render a property through a custom scalar. The property is typically astringholding the scalar's raw form (e.g. an ISO date), but it isn't required to be: any value the scalar'sserialize()accepts works; the property's nullability still controls the field's nullability.#[ConnectionOf( SomeType::class )]on aConnection-typed property: a nested paginated connection field (see Relay-style pagination).#[Deprecated( 'reason' )]: mark the field as deprecated (will be visible as such in GraphQL introspection).#[Ignore]: exclude the property from the schema.#[Parameter( ... )]/#[ParameterDescription( ... )]: give a field computed arguments (e.g. aformattedflag on a price field).
See the Attributes reference for exact signatures.
Input types
Classes under InputTypes/ become GraphQL input types. A field is optional when its type is nullable or it has a default value; a non-nullable field with no default is required. (The example below uses nullable-with-default for optional fields, which is the common shape.)
Use the TracksProvidedFields trait to distinguish "field omitted" (leave unchanged) from "field explicitly set to null" (clear it) - essential for patch-style update mutations:
class CreateCouponInput {
use TracksProvidedFields;
public string $code; // required
public ?string $description = null; // optional
}
In the consuming execute(), call $input->was_provided( 'description' ) to check whether the client actually sent the field. This works on any input type that uses the trait, whether it's an argument to a mutation (the common case, for patch-style updates) or to a query - the operation resolver populates the tracker when it builds the input object. The exception is an #[Unroll]ed input parameter: its fields are flattened into separate arguments and the object is rebuilt through a different path, so was_provided() isn't populated there.
Enums, interfaces, scalars
- Enums (
Enums/) are backed PHP enums. Case names convert from PascalCase toSCREAMING_SNAKE_CASE(e.g.FixedCart→FIXED_CART); override with#[Name]. Add#[Description]to the enum and each case. A common pattern is anOthercase plus araw_*field on the type, so plugin-added values don't break the enum. - Interfaces (
Interfaces/) are PHP traits marked with#[Name]/#[Description]. A type thatuses the trait implements the interface. Traits can compose other traits (e.g.ProductusesObjectWithId). - Scalars (
Scalars/) are classes with staticserialize( mixed $value ): string(PHP → transport) andparse( string $value ): mixed(client → PHP, throwing\InvalidArgumentExceptionon bad input). Apply one to a field with#[ScalarType].
Why interfaces are PHP traits
GraphQL interfaces are modeled as PHP traits rather than PHP interfaces for a concrete reason: in the code API a type's fields are its public properties, and a PHP interface can only declare methods, not properties. A trait, by contrast, can declare the shared properties and inject them into every type that uses it; so a single trait both defines the interface's field set and physically contributes those fields to each implementer. The builder treats a trait placed under Interfaces/ as a GraphQL interface and registers every output type that uses it as an implementer. (This is also why a query/mutation returning an interface can't type-hint it directly - a trait isn't a usable return type - and instead uses #[ReturnType]; see Queries and mutations.)
A trait that lives outside Interfaces/ is just an ordinary code-sharing mixin: the builder does not turn it into a GraphQL type. This matters for input types: an input type may use traits to share fields or behavior (for example TracksProvidedFields, or a shared base of common input fields), but doing so never produces an "input interface". GraphQL defines interfaces only for output object types (there is no input-interface concept in the GraphQL specification) so there is nothing for the builder to generate. Interface modeling applies to output types only.
Pagination (connections)
List queries handle pagination with Relay-style cursor connections: return a Connection and declare the node type with #[ConnectionOf( <NodeType>::class )], taking a PaginationParams argument (which #[Unroll]s into first / last / after / before).
#[Name( 'coupons' )]
#[RequiredCapability( 'read_private_shop_coupons' )]
class ListCoupons {
#[ConnectionOf( Coupon::class )]
public function execute( PaginationParams $pagination, ?CouponStatus $status = null ): Connection {
// build Edge[] with cursors, a PageInfo, and a total_count
}
}
This is a whole topic of its own: cursors, PageInfo semantics, the page-size cap, nested connections, and the two ways to build a Connection. See Relay-style pagination.
Infrastructure parameters
execute() and authorize() can declare specially named, underscore-prefixed parameters that the framework injects. They are optional, detected by name, and may appear in any order; declare only the ones you need:
?array $_query_info: the selection tree of the current query, for resolve-time optimization (e.g. skipping expensive joins for unrequested fields).<PrincipalType> $_principal: the resolved principal for the request.bool $_preauthorized: inauthorize(), whether the attribute-based gates already grant access (lets you compose custom logic on top).array $_metadata,array $_args,mixed $_parent: context forauthorize()in granular (type/field) authorization.
See Recognized methods and parameters for the full contract, and Authentication and authorization for how authorization is wired.
After you change anything
Regenerate and commit the generated tree. The CI staleness check fails any PR whose src/Api/ source doesn't match its committed Autogenerated/ output.