WooCommerce Dual API

Metadata and discovery

The dual API can attach machine-readable metadata to schema elements (types, fields, arguments, enum values) and expose it for discovery. The first built-in uses are marking elements as internal or experimental, but the mechanism is general: plugins ship their own categories without engine changes.

Attaching metadata

The base #[Metadata( name, value )] attribute attaches one name/value entry. It is repeatable and targets classes, properties, parameters, and enum cases. Values are restricted to bool|int|float|string|null.

#[Metadata( 'owner', 'payments-team' )]
#[Metadata( 'beta', true )]
class SomeType { /* ... */ }

Two convenience subclasses are built in:

A plugin defines its own category by subclassing Metadata; the reference plugin’s #[Team( 'platform' )] is a one-liner:

#[Attribute( Attribute::TARGET_CLASS | Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE )]
final class Team extends Metadata {
    public function __construct( string $team ) {
        parent::__construct( 'team', $team );
    }
}

Duplicate names on the same target are a build-time error (no silent merge or last-wins). Type-level metadata is not auto-propagated to fields; consumers apply the “subfields inherit” rule themselves if they want it.

Description mirroring

A metadata subclass can mirror its marking into the human-readable description, so it’s visible in tools (like stock GraphiQL) that don’t know about the discovery channel. Override transform_description():

When several transforming attributes apply to one element, their transforms chain in PHP source order (last-in-source wraps outermost), and the text flows through the standard __() translation pipeline with your plugin’s text domain. The plain #[Metadata] base does not modify descriptions. To define your own description-mirroring category, subclass Metadata and override transform_description(); see the Attributes reference.

Discovery via GraphQL: _apiMetadata

Every generated schema gains a root field:

_apiMetadata(name: String, type: String, field: String, attribute: String): [MetadataTarget!]!

Each MetadataTarget carries two parallel slices: the collected metadata entries, and an authorization slice describing the authorization gates on that target. Arguments narrow independently (combined with AND): name trims surviving rows to the matching metadata entry, and attribute trims the authorization slice to a specific attribute short name.

Access is gated

_apiMetadata is gated like introspection, see Authentication and authorization. The resolver consults can_query_metadata() on the principal if present, otherwise falls back to can_introspect(), otherwise denies; the woocommerce_graphql_can_query_metadata filter can override. This prevents anonymous callers from enumerating the schema’s authorization gates.

Opting a target out

Apply #[HiddenFromMetadataQuery] to a class or property to omit it (and its descriptors) from _apiMetadata. This is recognized by a duck-typed shows_in_metadata_query(): bool returning false; a target’s visibility is the AND of that method across all its attributes. It does not affect native introspection or the runtime authorization gates: an attribute hidden from discovery still runs its authorize(). (The reference plugin composes it with #[RequiresScope] on Event::$internal_notes: gated at runtime, absent from discovery.)

Discovery via PHP: SchemaHandle

For in-process inspection, GraphQLControllerBase::get_schema() returns an opaque SchemaHandle (Automattic\WooCommerce\Api\Utils\SchemaHandle) with:

The handle never exposes the underlying engine type in its public signature, so PHP callers don’t depend on the GraphQL engine. It’s the natural home for future schema-inspection operations.