Back to Woocommerce

Creating a dual API in a plugin

docs/apis/dual-api/creating-a-dual-api-in-a-plugin.md

10.9.0-dev6.1 KB
Original Source

Creating a dual API in a plugin

A plugin can define its own code API and get a matching GraphQL endpoint using WooCommerce's infrastructure. The plugin writes its own classes under src/Api/, runs the same builder against them, commits the generated tree to its own repo, and registers a dedicated endpoint.

The full, runnable reference for everything here is the woocommerce-simple-events plugin. The snippets below are condensed; see that repo for complete files.

Prerequisites

  • WooCommerce installed with the dual_code_graphql_api feature flag enabled, on PHP 8.1+.
  • The plugin's own Composer autoloader (PSR-4) and a vendor/autoload.php.

The endpoint is dedicated: each plugin registers its own REST route. You cannot federate into core's /wc/graphql.

1. Lay out the code API

Use the same directory conventions as core, under your plugin's namespace:

text
my-plugin/
├── bin/build-api.php
├── src/Api/
│   ├── Queries/        Mutations/   Types/   InputTypes/
│   ├── Enums/          Interfaces/  Scalars/
│   ├── Attributes/         ← custom attributes (optional)
│   └── Infrastructure/     ← custom convention classes (optional)
└── src/Internal/Api/Autogenerated/   ← generated; committed

Writing the code-API classes is identical to core, see Extending the code API.

2. Add the build script

A plugin's bin/build-api.php is a thin wrapper around ApiBuilder::run_for_plugin():

php
<?php
require_once $wc_path . '/vendor/autoload.php'; // dev-mode WC autoloader

use Automattic\WooCommerce\Api\Infrastructure\DesignTime\ApiBuilder;

ApiBuilder::run_for_plugin( dirname( __DIR__ ), 'Automattic\\MyPlugin' );

run_for_plugin( $plugin_root, $namespace_prefix ) derives the conventional dirs/namespaces: source at $plugin_root/src/Api (namespace <prefix>\Api), output at $plugin_root/src/Internal/Api/Autogenerated (namespace <prefix>\Internal\Api\Autogenerated). Run it with WC_PATH=<path-to-woocommerce> php bin/build-api.php (or a package.json script), and commit the generated tree. See Building and staleness checks, and add an equivalent staleness check to your CI.

ApiBuilder lives under WooCommerce's bin/api-builder/ and is registered via autoload-dev, so it is only resolvable from a dev-mode WooCommerce install. It is not shipped in release builds.

3. Register the endpoint

In your plugin bootstrap, register the route through core's Main:

php
use Automattic\WooCommerce\Api\Infrastructure\Main as WooCommerceApiMain;

add_action( 'plugins_loaded', static function () {
    if ( ! method_exists( WooCommerceApiMain::class, 'register_graphql_endpoint' ) ) {
        return; // WooCommerce too old, or feature/PHP unavailable
    }
    WooCommerceApiMain::register_graphql_endpoint( __DIR__, 'my-plugin', '/graphql' );
} );

The first argument may be your plugin directory (the controller class is resolved by convention) or the fully-qualified controller class name. This is a no-op when the feature flag is off or PHP is < 8.1. Your endpoint goes through the same request pipeline as core's and inherits the core GraphQL settings.

4. Reuse or replace the convention classes

ApiBuilder detects a small set of convention classes at <your-namespace>\Api\Infrastructure\*. Ship one only when you need to diverge from the default; otherwise core's default applies. The same overriding mechanism is what core itself uses.

ClassDefaultShip your own to…
ClassResolverwc_get_container()->get()Instantiate commands through your own DI container.
PrincipalResolverwraps wp_get_current_user()Authenticate against something other than WP users. Its return type declares your principal type.
Principalwraps WP_UserCarry your own identity/permission data. Add is_authenticated(), and can_introspect()/can_query_metadata()/can_use_debug_mode() to opt into those surfaces.
HttpStatusResolvernone (per-error-code map)Override response HTTP status, e.g. always return 200.

See Infrastructure classes for exact signatures.

Custom authentication example (HTTP basic against a fixed credential, role in a header):

php
namespace Automattic\MyPlugin\Api\Infrastructure;

use Automattic\WooCommerce\Api\InvalidTokenException;

final class PrincipalResolver {
    public function resolve_principal( \WP_REST_Request $request ): EventsPrincipal {
        $user = $_SERVER['PHP_AUTH_USER'] ?? null;
        $pass = $_SERVER['PHP_AUTH_PW'] ?? null;
        if ( null === $user || null === $pass ) {
            return EventsPrincipal::anonymous();
        }
        if ( 'password' !== $pass || ! isset( EventsPrincipal::SCOPES_BY_ROLE[ $user ] ) ) {
            throw new InvalidTokenException();
        }
        return new EventsPrincipal( $user, $user, EventsPrincipal::SCOPES_BY_ROLE[ $user ] );
    }
}

5. Define custom attributes and exceptions (optional)

  • Attributes: a class in your Api/Attributes/ becomes an authorization attribute by declaring authorize( <PrincipalType> $principal ): bool, a metadata attribute by extending Metadata, and so on. See Attributes reference. Authorization attributes can gate operations, types, fields, and arguments.
  • Exceptions: extend ApiException (or a subclass) to pin your own (error code, HTTP status). See Exceptions reference.

6. Engine-decoupling guarantee

Your committed generated tree references only WooCommerce's public Api\Infrastructure\* surface, never the underlying GraphQL engine (Vendor\GraphQL\*). If WooCommerce ever swaps engines, that surface absorbs the change and your already-committed generated code keeps working. The flip side: never write code (generated or hand-written) that imports from Vendor\GraphQL\* or from Internal\Api\*. See Architecture and Extending the infrastructure.